Compare commits

..

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

110 changed files with 3585 additions and 6965 deletions

230
.circleci/config.yml Normal file
View file

@ -0,0 +1,230 @@
# 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
- run:
name: Run Go tests
command: |
go test ./...
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-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;
- 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;
- 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-other
- upload:
requires:
- build-linux
- build-macos
- 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

View file

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

File diff suppressed because it is too large Load diff

View file

@ -1,6 +1,7 @@
# Yggdrasil # 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 ## 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 other Yggdrasil nodes. Yggdrasil does not require you to have IPv6 Internet
connectivity - it also works over IPv4. 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 ## Supported Platforms
Yggdrasil works on a number of platforms, including Linux, macOS, Ubiquiti We actively support the following platforms, and packages are available for
EdgeRouter, VyOS, Windows, FreeBSD, OpenBSD and OpenWrt. some of the below:
Please see our [Installation](https://yggdrasil-network.github.io/installation.html) - Linux
page for more information. You may also find other platform-specific wrappers, scripts - `.deb` and `.rpm` packages are built by CI for Debian and Red Hat-based
or tools in the `contrib` folder. 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 ## Building
If you want to build from source, as opposed to installing one of the pre-built If you want to build from source, as opposed to installing one of the pre-built
packages: 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. Clone this repository
2. Run `./build` 2. Run `./build`
@ -56,7 +80,6 @@ other configuration such as listen addresses or multicast addresses, etc.
### Run Yggdrasil ### Run Yggdrasil
To run with the generated static configuration: To run with the generated static configuration:
``` ```
./yggdrasil -useconffile /path/to/yggdrasil.conf ./yggdrasil -useconffile /path/to/yggdrasil.conf
``` ```
@ -74,18 +97,21 @@ by giving the Yggdrasil binary the `CAP_NET_ADMIN` capability.
## Documentation ## 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) - [Configuration file options](https://yggdrasil-network.github.io/configuration.html)
- [Configuring Yggdrasil](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) - [Frequently asked questions](https://yggdrasil-network.github.io/faq.html)
- [Admin API documentation](https://yggdrasil-network.github.io/admin.html)
- [Version changelog](CHANGELOG.md) - [Version changelog](CHANGELOG.md)
## Community ## Community
Feel free to join us on our [Matrix Feel free to join us on our [Matrix
channel](https://matrix.to/#/#yggdrasil:matrix.org) at `#yggdrasil:matrix.org` 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 ## License

20
appveyor.yml Normal file
View file

@ -0,0 +1,20 @@
version: '{build}'
pull_requests:
do_not_increment_build_number: true
os: Visual Studio 2019
shallow_clone: false
environment:
MSYS2_PATH_TYPE: inherit
CHERE_INVOKING: enabled_from_arguments
build_script:
- cmd: >-
cd %APPVEYOR_BUILD_FOLDER%
- c:\msys64\usr\bin\bash -lc "./contrib/msi/build-msi.sh x64"
- c:\msys64\usr\bin\bash -lc "./contrib/msi/build-msi.sh x86"
test: off
artifacts:
- path: '*.msi'

32
build
View file

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

View file

@ -1,4 +1,5 @@
/* /*
This file generates crypto keys. This file generates crypto keys.
It prints out a new set of keys each time if finds a "better" one. 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). By default, "better" means a higher NodeID (-> higher IP address).
@ -7,6 +8,7 @@ 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. If run with the "-sig" flag, it generates signing keys instead.
A "better" signing key means one with a higher TreeID. 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. This only matters if it's high enough to make you the root of the tree.
*/ */
package main package main
@ -16,9 +18,6 @@ import (
"fmt" "fmt"
"net" "net"
"runtime" "runtime"
"time"
"suah.dev/protect"
"github.com/yggdrasil-network/yggdrasil-go/src/address" "github.com/yggdrasil-network/yggdrasil-go/src/address"
) )
@ -26,19 +25,10 @@ import (
type keySet struct { type keySet struct {
priv ed25519.PrivateKey priv ed25519.PrivateKey
pub ed25519.PublicKey pub ed25519.PublicKey
count uint64
} }
func main() { func main() {
if err := protect.Pledge("stdio"); err != nil {
panic(err)
}
threads := runtime.GOMAXPROCS(0) threads := runtime.GOMAXPROCS(0)
fmt.Println("Threads:", threads)
start := time.Now()
var totalKeys uint64
totalKeys = 0
var currentBest ed25519.PublicKey var currentBest ed25519.PublicKey
newKeys := make(chan keySet, threads) newKeys := make(chan keySet, threads)
for i := 0; i < threads; i++ { for i := 0; i < threads; i++ {
@ -47,9 +37,8 @@ func main() {
for { for {
newKey := <-newKeys newKey := <-newKeys
if isBetter(currentBest, newKey.pub) || len(currentBest) == 0 { if isBetter(currentBest, newKey.pub) || len(currentBest) == 0 {
totalKeys += newKey.count
currentBest = newKey.pub currentBest = newKey.pub
fmt.Println("-----", time.Since(start), "---", totalKeys, "keys tried") fmt.Println("-----")
fmt.Println("Priv:", hex.EncodeToString(newKey.priv)) fmt.Println("Priv:", hex.EncodeToString(newKey.priv))
fmt.Println("Pub:", hex.EncodeToString(newKey.pub)) fmt.Println("Pub:", hex.EncodeToString(newKey.pub))
addr := address.AddrForKey(newKey.pub) addr := address.AddrForKey(newKey.pub)
@ -72,14 +61,11 @@ func isBetter(oldPub, newPub ed25519.PublicKey) bool {
func doKeys(out chan<- keySet) { func doKeys(out chan<- keySet) {
bestKey := make(ed25519.PublicKey, ed25519.PublicKeySize) bestKey := make(ed25519.PublicKey, ed25519.PublicKeySize)
var count uint64
count = 0
for idx := range bestKey { for idx := range bestKey {
bestKey[idx] = 0xff bestKey[idx] = 0xff
} }
for { for {
pub, priv, err := ed25519.GenerateKey(nil) pub, priv, err := ed25519.GenerateKey(nil)
count++
if err != nil { if err != nil {
panic(err) panic(err)
} }
@ -87,7 +73,6 @@ func doKeys(out chan<- keySet) {
continue continue
} }
bestKey = pub bestKey = pub
out <- keySet{priv, pub, count} out <- keySet{priv, pub}
count = 0
} }
} }

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,157 @@
package main package main
import ( import (
"context" "bytes"
"crypto/ed25519" "crypto/ed25519"
"encoding/hex" "encoding/hex"
"encoding/json" "encoding/json"
"flag" "flag"
"fmt" "fmt"
"io/ioutil"
"net" "net"
"os" "os"
"os/signal" "os/signal"
"regexp"
"strings" "strings"
"syscall" "syscall"
"suah.dev/protect" "golang.org/x/text/encoding/unicode"
"github.com/gologme/log" "github.com/gologme/log"
gsyslog "github.com/hashicorp/go-syslog" gsyslog "github.com/hashicorp/go-syslog"
"github.com/hjson/hjson-go/v4" "github.com/hjson/hjson-go"
"github.com/kardianos/minwinsvc" "github.com/kardianos/minwinsvc"
"github.com/mitchellh/mapstructure"
"github.com/yggdrasil-network/yggdrasil-go/src/address" "github.com/yggdrasil-network/yggdrasil-go/src/address"
"github.com/yggdrasil-network/yggdrasil-go/src/admin" "github.com/yggdrasil-network/yggdrasil-go/src/admin"
"github.com/yggdrasil-network/yggdrasil-go/src/config" "github.com/yggdrasil-network/yggdrasil-go/src/config"
"github.com/yggdrasil-network/yggdrasil-go/src/ipv6rwc" "github.com/yggdrasil-network/yggdrasil-go/src/defaults"
"github.com/yggdrasil-network/yggdrasil-go/src/core" "github.com/yggdrasil-network/yggdrasil-go/src/core"
"github.com/yggdrasil-network/yggdrasil-go/src/multicast" "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/version"
) )
type node struct { type node struct {
core *core.Core core core.Core
tun *tun.TunAdapter config *config.NodeConfig
tuntap *tuntap.TunAdapter
multicast *multicast.Multicast multicast *multicast.Multicast
admin *admin.AdminSocket admin *admin.AdminSocket
} }
// The main function is responsible for configuring and starting Yggdrasil. func readConfig(log *log.Logger, useconf *bool, useconffile *string, normaliseconf *bool) *config.NodeConfig {
func main() { // Use a configuration file. If -useconf, the configuration will be read
// Not all operations are coverable with pledge(2), so immediately // from stdin. If -useconffile, the configuration will be read from the
// limit file system access with unveil(2), effectively preventing // filesystem.
// "proc exec" promises right from the start: var conf []byte
//
// - 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()
var err error var err error
switch { if *useconffile != "" {
case *ver: // Read the file from the filesystem
fmt.Println("Build name:", version.BuildName()) conf, err = ioutil.ReadFile(*useconffile)
fmt.Println("Build version:", version.BuildVersion()) } else {
return // Read the file from stdin.
conf, err = ioutil.ReadAll(os.Stdin)
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 err != nil {
privateKey := ed25519.PrivateKey(cfg.PrivateKey) panic(err)
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 there's a byte order mark - which Windows 10 is now incredibly fond of
n := &node{} // 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
// Set up the Yggdrasil node itself. // because hjson doesn't know what to do with UTF-16 and will panic
{ if bytes.Equal(conf[0:2], []byte{0xFF, 0xFE}) ||
iprange := net.IPNet{ bytes.Equal(conf[0:2], []byte{0xFE, 0xFF}) {
IP: net.ParseIP("200::"), utf := unicode.UTF16(unicode.BigEndian, unicode.UseBOM)
Mask: net.CIDRMask(7, 128), decoder := utf.NewDecoder()
conf, err = decoder.Bytes(conf)
if err != nil {
panic(err)
} }
options := []core.SetupOption{ }
core.NodeInfo(cfg.NodeInfo), // Generate a new configuration - this gives us a set of sane defaults -
core.NodeInfoPrivacy(cfg.NodeInfoPrivacy), // then parse the configuration we loaded above on top of it. The effect
core.PeerFilter(func(ip net.IP) bool { // of this is that any configuration item that is missing from the provided
return !iprange.Contains(ip) // configuration will use a sane default.
}), cfg := defaults.GenerateConfig()
} var dat map[string]interface{}
for _, addr := range cfg.Listen { if err := hjson.Unmarshal(conf, &dat); err != nil {
options = append(options, core.ListenAddress(addr)) panic(err)
} }
for _, peer := range cfg.Peers { // Check if we have old field names
options = append(options, core.Peer{URI: peer}) if _, ok := dat["TunnelRouting"]; ok {
} log.Warnln("WARNING: Tunnel routing is no longer supported")
for intf, peers := range cfg.InterfacePeers { }
for _, peer := range peers { if old, ok := dat["SigningPrivateKey"]; ok {
options = append(options, core.Peer{URI: peer, SourceInterface: intf}) log.Warnln("WARNING: The \"SigningPrivateKey\" configuration option has been renamed to \"PrivateKey\"")
if _, ok := dat["PrivateKey"]; !ok {
if privstr, err := hex.DecodeString(old.(string)); err == nil {
priv := ed25519.PrivateKey(privstr)
pub := priv.Public().(ed25519.PublicKey)
dat["PrivateKey"] = hex.EncodeToString(priv[:])
dat["PublicKey"] = hex.EncodeToString(pub[:])
} else {
log.Warnln("WARNING: The \"SigningPrivateKey\" configuration option contains an invalid value and will be ignored")
} }
} }
for _, allowed := range cfg.AllowedPublicKeys { }
k, err := hex.DecodeString(allowed) if oldmc, ok := dat["MulticastInterfaces"]; ok {
if err != nil { if oldmcvals, ok := oldmc.([]interface{}); ok {
panic(err) var newmc []config.MulticastInterfaceConfig
for _, oldmcval := range oldmcvals {
if str, ok := oldmcval.(string); ok {
newmc = append(newmc, config.MulticastInterfaceConfig{
Regex: str,
Beacon: true,
Listen: true,
})
}
}
if newmc != nil {
if oldport, ok := dat["LinkLocalTCPPort"]; ok {
// numbers parse to float64 by default
if port, ok := oldport.(float64); ok {
for idx := range newmc {
newmc[idx].Port = uint16(port)
}
}
}
dat["MulticastInterfaces"] = newmc
} }
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()
} }
} }
// Sanitise the config
// Set up the multicast module. confJson, err := json.Marshal(dat)
{ if err != nil {
options := []multicast.SetupOption{} panic(err)
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)
}
} }
if err := json.Unmarshal(confJson, &cfg); err != nil {
// Set up the TUN module. panic(err)
{
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)
}
} }
// Overlay our newly mapped configuration onto the autoconf node config that
//Windows service shutdown // we generated above.
minwinsvc.SetOnExit(func() { if err = mapstructure.Decode(dat, &cfg); err != nil {
logger.Infof("Shutting down service ...") panic(err)
cancel()
// Wait for all parts to shutdown properly
<-done
})
// Change user if requested
if *chuserto != "" {
err = chuser(*chuserto)
if err != nil {
panic(err)
}
} }
return cfg
}
// Promise final modes of operation. At this point, if at all: // Generates a new configuration and returns it in HJSON format. This is used
// - raw socket is created/open // with -genconf.
// - admin socket is created/open func doGenconf(isjson bool) string {
// - privileges are dropped to non-root user cfg := defaults.GenerateConfig()
// var bs []byte
// Peers, InterfacePeers, Listen can be UNIX sockets; var err error
// Go's net.Listen.Close() deletes files on shutdown. if isjson {
promises := []string{"stdio", "cpath", "inet", "unix", "dns"} bs, err = json.MarshalIndent(cfg, "", " ")
if len(cfg.MulticastInterfaces) > 0 { } else {
promises = append(promises, "mcast") bs, err = hjson.Marshal(cfg)
} }
if err := protect.Pledge(strings.Join(promises, " ")); err != nil { if err != nil {
panic(fmt.Sprintf("pledge: %v: %v", promises, err)) panic(err)
} }
return string(bs)
// 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()
} }
func setLogLevel(loglevel string, logger *log.Logger) { func setLogLevel(loglevel string, logger *log.Logger) {
@ -362,3 +179,175 @@ 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()
// 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")
}
if *normaliseconf {
setLogLevel("error", logger)
} else {
setLogLevel(*loglevel, logger)
}
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 = defaults.GenerateConfig()
case *useconffile != "" || *useconf:
// Read the configuration from either stdin or from the filesystem
cfg = readConfig(logger, 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.
getNodeKey := func() ed25519.PublicKey {
if pubkey, err := hex.DecodeString(cfg.PrivateKey); err == nil {
return ed25519.PrivateKey(pubkey).Public().(ed25519.PublicKey)
}
return nil
}
switch {
case *getaddr:
if key := getNodeKey(); key != nil {
addr := address.AddrForKey(key)
ip := net.IP(addr[:])
fmt.Println(ip.String())
}
return
case *getsnet:
if key := getNodeKey(); key != nil {
snet := address.SubnetForKey(key)
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:
}
// Setup the Yggdrasil node itself. The node{} type includes a Core, so we
// don't need to create this manually.
n := node{config: cfg}
// Now start Yggdrasil - this starts the DHT, router, switch and other core
// components needed for Yggdrasil to operate
if err = n.core.Start(cfg, logger); err != nil {
logger.Errorln("An error occurred during startup")
panic(err)
}
// Register the session firewall gatekeeper function
// Allocate our modules
n.admin = &admin.AdminSocket{}
n.multicast = &multicast.Multicast{}
n.tuntap = &tuntap.TunAdapter{}
// Start the admin socket
if err := n.admin.Init(&n.core, cfg, logger, nil); err != nil {
logger.Errorln("An error occurred initialising admin socket:", err)
} else if err := n.admin.Start(); err != nil {
logger.Errorln("An error occurred starting admin socket:", err)
}
n.admin.SetupAdminHandlers(n.admin)
// Start the multicast interface
if err := n.multicast.Init(&n.core, cfg, logger, nil); err != nil {
logger.Errorln("An error occurred initialising multicast:", err)
} else if err := n.multicast.Start(); err != nil {
logger.Errorln("An error occurred starting multicast:", err)
}
n.multicast.SetupAdminHandlers(n.admin)
// Start the TUN/TAP interface
if err := n.tuntap.Init(&n.core, cfg, logger, nil); err != nil {
logger.Errorln("An error occurred initialising TUN/TAP:", err)
} else if err := n.tuntap.Start(); err != nil {
logger.Errorln("An error occurred starting TUN/TAP:", err)
}
n.tuntap.SetupAdminHandlers(n.admin)
// 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()
public := n.core.GetSelf().Key
logger.Infof("Your public key is %s", hex.EncodeToString(public[:]))
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)
signal.Notify(c, os.Interrupt, syscall.SIGTERM)
// Capture the service being stopped on Windows.
<-c
minwinsvc.SetOnExit(n.shutdown)
n.shutdown()
}
func (n *node) shutdown() {
_ = n.admin.Stop()
_ = n.multicast.Stop()
_ = n.tuntap.Stop()
n.core.Stop()
}

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" "errors"
"flag" "flag"
"fmt" "fmt"
"io/ioutil"
"log" "log"
"net" "net"
"net/url" "net/url"
"os" "os"
"sort"
"strconv"
"strings" "strings"
"time"
"suah.dev/protect" "golang.org/x/text/encoding/unicode"
"github.com/olekukonko/tablewriter" "github.com/hjson/hjson-go"
"github.com/yggdrasil-network/yggdrasil-go/src/admin" "github.com/yggdrasil-network/yggdrasil-go/src/defaults"
"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/yggdrasil-network/yggdrasil-go/src/version" "github.com/yggdrasil-network/yggdrasil-go/src/version"
) )
func main() { type admin_info map[string]interface{}
// read config, speak DNS/TCP and/or over a UNIX socket
if err := protect.Pledge("stdio rpath inet unix dns"); err != nil {
panic(err)
}
func main() {
// makes sure we can use defer and still return an error code to the OS // makes sure we can use defer and still return an error code to the OS
os.Exit(run()) os.Exit(run())
} }
@ -36,7 +32,6 @@ func main() {
func run() int { func run() int {
logbuffer := &bytes.Buffer{} logbuffer := &bytes.Buffer{}
logger := log.New(logbuffer, "", log.Flags()) logger := log.New(logbuffer, "", log.Flags())
defer func() int { defer func() int {
if r := recover(); r != nil { if r := recover(); r != nil {
logger.Println("Fatal error:", r) logger.Println("Fatal error:", r)
@ -46,30 +41,83 @@ func run() int {
return 0 return 0
}() }()
cmdLineEnv := newCmdLineEnv() endpoint := defaults.GetDefaults().DefaultAdminListen
cmdLineEnv.parseFlagsAndArgs()
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()
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], "-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 name:", version.BuildName())
fmt.Println("Build version:", version.BuildVersion()) fmt.Println("Build version:", version.BuildVersion())
fmt.Println("To get the version number of the running Yggdrasil node, run", os.Args[0], "getSelf") fmt.Println("To get the version number of the running Yggdrasil node, run", os.Args[0], "getSelf")
return 0 return 0
} }
if len(cmdLineEnv.args) == 0 { if len(args) == 0 {
flag.Usage() flag.Usage()
return 0 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 var conn net.Conn
u, err := url.Parse(cmdLineEnv.endpoint) u, err := url.Parse(endpoint)
if err == nil { if err == nil {
switch strings.ToLower(u.Scheme) { switch strings.ToLower(u.Scheme) {
case "unix": case "unix":
logger.Println("Connecting to UNIX socket", cmdLineEnv.endpoint[7:]) logger.Println("Connecting to UNIX socket", endpoint[7:])
conn, err = net.Dial("unix", cmdLineEnv.endpoint[7:]) conn, err = net.Dial("unix", endpoint[7:])
case "tcp": case "tcp":
logger.Println("Connecting to TCP socket", u.Host) logger.Println("Connecting to TCP socket", u.Host)
conn, err = net.Dial("tcp", u.Host) conn, err = net.Dial("tcp", u.Host)
@ -79,255 +127,326 @@ func run() int {
} }
} else { } else {
logger.Println("Connecting to TCP socket", u.Host) logger.Println("Connecting to TCP socket", u.Host)
conn, err = net.Dial("tcp", cmdLineEnv.endpoint) conn, err = net.Dial("tcp", endpoint)
} }
if err != nil { if err != nil {
panic(err) panic(err)
} }
// config and socket are done, work without unprivileges
if err := protect.Pledge("stdio"); err != nil {
panic(err)
}
logger.Println("Connected") logger.Println("Connected")
defer conn.Close() defer conn.Close()
decoder := json.NewDecoder(conn) decoder := json.NewDecoder(conn)
encoder := json.NewEncoder(conn) encoder := json.NewEncoder(conn)
send := &admin.AdminSocketRequest{} send := make(admin_info)
recv := &admin.AdminSocketResponse{} recv := make(admin_info)
args := map[string]string{}
for c, a := range cmdLineEnv.args { for c, a := range args {
if c == 0 { if c == 0 {
if strings.HasPrefix(a, "-") { if strings.HasPrefix(a, "-") {
logger.Printf("Ignoring flag %s as it should be specified before other parameters\n", a) logger.Printf("Ignoring flag %s as it should be specified before other parameters\n", a)
continue continue
} }
logger.Printf("Sending request: %v\n", a) logger.Printf("Sending request: %v\n", a)
send.Name = a send["request"] = a
continue continue
} }
tokens := strings.SplitN(a, "=", 2) tokens := strings.Split(a, "=")
switch { if len(tokens) == 1 {
case len(tokens) == 1: send[tokens[0]] = true
logger.Println("Ignoring invalid argument:", a) } else if len(tokens) > 2 {
default: send[tokens[0]] = strings.Join(tokens[1:], "=")
args[tokens[0]] = 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 { if err := encoder.Encode(&send); err != nil {
panic(err) panic(err)
} }
logger.Printf("Request sent") logger.Printf("Request sent")
if err := decoder.Decode(&recv); err != nil { if err := decoder.Decode(&recv); err == nil {
panic(err) logger.Printf("Response received")
} if recv["status"] == "error" {
if recv.Status == "error" { if err, ok := recv["error"]; ok {
if err := recv.Error; err != "" { fmt.Println("Admin socket returned an error:", err)
fmt.Println("Admin socket returned an error:", err) } else {
} else { fmt.Println("Admin socket returned an error but didn't specify any error text")
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 boxSigKey, ok := v.(map[string]interface{})["key"].(string); ok {
fmt.Println("Public key:", boxSigKey)
}
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 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 return 0
} }

BIN
contrib/.DS_Store vendored

Binary file not shown.

View file

@ -1,5 +1,7 @@
/* /*
This file generates crypto keys for [ansible-yggdrasil](https://github.com/jcgruenhage/ansible-yggdrasil/) This file generates crypto keys for [ansible-yggdrasil](https://github.com/jcgruenhage/ansible-yggdrasil/)
*/ */
package main package main

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

View file

@ -15,10 +15,6 @@ command -v mkbom >/dev/null 2>&1 || (
sudo make install || (echo "Failed to build mkbom"; exit 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 # Check if we can find the files we need - they should
# exist if you are running this script from the root of # exist if you are running this script from the root of
# the yggdrasil-go repo and you have ran ./build # 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) PKGVERSION=$(sh contrib/semver/version.sh --bare)
PKGARCH=${PKGARCH-amd64} PKGARCH=${PKGARCH-amd64}
PAYLOADSIZE=$(( $(wc -c pkgbuild/flat/base.pkg/Payload | awk '{ print $1 }') / 1024 )) PAYLOADSIZE=$(( $(wc -c pkgbuild/flat/base.pkg/Payload | awk '{ print $1 }') / 1024 ))
[ "$PKGARCH" = "amd64" ] && PKGHOSTARCH="x86_64" || PKGHOSTARCH=${PKGARCH}
# Create the PackageInfo file # Create the PackageInfo file
cat > pkgbuild/flat/base.pkg/PackageInfo << EOF cat > pkgbuild/flat/base.pkg/PackageInfo << EOF
@ -99,7 +94,7 @@ cat > pkgbuild/flat/Distribution << EOF
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<installer-script minSpecVersion="1.000000" authoringTool="com.apple.PackageMaker" authoringToolVersion="3.0.3" authoringToolBuild="174"> <installer-script minSpecVersion="1.000000" authoringTool="com.apple.PackageMaker" authoringToolVersion="3.0.3" authoringToolBuild="174">
<title>Yggdrasil (${PKGNAME}-${PKGVERSION})</title> <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"/> <domains enable_anywhere="true"/>
<installation-check script="pm_install_check();"/> <installation-check script="pm_install_check();"/>
<script> <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,9 @@
#!/bin/sh #!/bin/sh
# This script generates an MSI file for Yggdrasil for a given architecture. It # 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 # needs to run on Windows within MSYS2 and Go 1.13 or later must be installed on
# the system and within the PATH. This is ran currently by GitHub Actions (see # the system and within the PATH. This is ran currently by Appveyor (see
# the workflows in the repository). # appveyor.yml in the repository root) for both x86 and x64.
# #
# Author: Neil Alexander <neilalexander@users.noreply.github.com> # Author: Neil Alexander <neilalexander@users.noreply.github.com>
@ -11,18 +11,47 @@
PKGARCH=$1 PKGARCH=$1
if [ "${PKGARCH}" == "" ]; if [ "${PKGARCH}" == "" ];
then then
echo "tell me the architecture: x86, x64, arm or arm64" echo "tell me the architecture: x86, x64 or arm"
exit 1 exit 1
fi fi
# 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
# Install prerequisites within MSYS2
pacman -S --needed --noconfirm unzip git curl
# Download the wix tools! # Download the wix tools!
dotnet tool install --global wix --version 5.0.0 if [ ! -d wixbin ];
then
curl -LO https://github.com/wixtoolset/wix3/releases/download/wix3112rtm/wix311-binaries.zip
if [ `md5sum wix311-binaries.zip | cut -f 1 -d " "` != "47a506f8ab6666ee3cc502fb07d0ee2a" ];
then
echo "wix package didn't match expected checksum"
exit 1
fi
mkdir -p wixbin
unzip -o wix311-binaries.zip -d wixbin || (
echo "failed to unzip WiX"
exit 1
)
fi
# Build Yggdrasil! # Build Yggdrasil!
[ "${PKGARCH}" == "x64" ] && GOOS=windows GOARCH=amd64 CGO_ENABLED=0 ./build [ "${PKGARCH}" == "x64" ] && GOOS=windows GOARCH=amd64 CGO_ENABLED=0 ./build
[ "${PKGARCH}" == "x86" ] && GOOS=windows GOARCH=386 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}" == "arm" ] && GOOS=windows GOARCH=arm CGO_ENABLED=0 ./build
[ "${PKGARCH}" == "arm64" ] && GOOS=windows GOARCH=arm64 CGO_ENABLED=0 ./build #[ "${PKGARCH}" == "arm64" ] && GOOS=windows GOARCH=arm64 CGO_ENABLED=0 ./build
# Create the postinstall script # Create the postinstall script
cat > updateconfig.bat << EOF cat > updateconfig.bat << EOF
@ -40,19 +69,14 @@ EOF
PKGNAME=$(sh contrib/semver/name.sh) PKGNAME=$(sh contrib/semver/name.sh)
PKGVERSION=$(sh contrib/msi/msversion.sh --bare) PKGVERSION=$(sh contrib/msi/msversion.sh --bare)
PKGVERSIONMS=$(echo $PKGVERSION | tr - .) PKGVERSIONMS=$(echo $PKGVERSION | tr - .)
([ "${PKGARCH}" == "x64" ] || [ "${PKGARCH}" == "arm64" ]) && \ [ "${PKGARCH}" == "x64" ] && \
PKGGUID="77757838-1a23-40a5-a720-c3b43e0260cc" PKGINSTFOLDER="ProgramFiles64Folder" || \ PKGGUID="77757838-1a23-40a5-a720-c3b43e0260cc" PKGINSTFOLDER="ProgramFiles64Folder" || \
PKGGUID="54a3294e-a441-4322-aefb-3bb40dd022bb" PKGINSTFOLDER="ProgramFilesFolder" PKGGUID="54a3294e-a441-4322-aefb-3bb40dd022bb" PKGINSTFOLDER="ProgramFilesFolder"
# Download the Wintun driver # Download the Wintun driver
if [ ! -d wintun ]; if [ ! -d wintun ];
then then
curl -o wintun.zip https://www.wintun.net/builds/wintun-0.14.1.zip curl -o wintun.zip https://www.wintun.net/builds/wintun-0.11.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 unzip wintun.zip
fi fi
if [ $PKGARCH = "x64" ]; then if [ $PKGARCH = "x64" ]; then
@ -61,8 +85,8 @@ elif [ $PKGARCH = "x86" ]; then
PKGWINTUNDLL=wintun/bin/x86/wintun.dll PKGWINTUNDLL=wintun/bin/x86/wintun.dll
elif [ $PKGARCH = "arm" ]; then elif [ $PKGARCH = "arm" ]; then
PKGWINTUNDLL=wintun/bin/arm/wintun.dll PKGWINTUNDLL=wintun/bin/arm/wintun.dll
elif [ $PKGARCH = "arm64" ]; then #elif [ $PKGARCH = "arm64" ]; then
PKGWINTUNDLL=wintun/bin/arm64/wintun.dll # PKGWINTUNDLL=wintun/bin/arm64/wintun.dll
else else
echo "wasn't sure which architecture to get wintun for" echo "wasn't sure which architecture to get wintun for"
exit 1 exit 1
@ -93,7 +117,7 @@ cat > wix.xml << EOF
Description="Yggdrasil Network Installer" Description="Yggdrasil Network Installer"
Comments="Yggdrasil Network standalone router for Windows." Comments="Yggdrasil Network standalone router for Windows."
Manufacturer="github.com/yggdrasil-network" Manufacturer="github.com/yggdrasil-network"
InstallerVersion="500" InstallerVersion="200"
InstallScope="perMachine" InstallScope="perMachine"
Languages="1033" Languages="1033"
Compressed="yes" Compressed="yes"
@ -197,5 +221,5 @@ EOF
# Generate the MSI # Generate the MSI
CANDLEFLAGS="-nologo" CANDLEFLAGS="-nologo"
LIGHTFLAGS="-nologo -spdb -sice:ICE71 -sice:ICE61" LIGHTFLAGS="-nologo -spdb -sice:ICE71 -sice:ICE61"
candle $CANDLEFLAGS -out ${PKGNAME}-${PKGVERSION}-${PKGARCH}.wixobj -arch ${PKGARCH} wix.xml && \ wixbin/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 wixbin/light $LIGHTFLAGS -ext WixUtilExtension.dll -out ${PKGNAME}-${PKGVERSION}-${PKGARCH}.msi ${PKGNAME}-${PKGVERSION}-${PKGARCH}.wixobj

View file

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

View file

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

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

63
go.mod
View file

@ -1,49 +1,26 @@
module github.com/yggdrasil-network/yggdrasil-go 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/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/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 ( require (
github.com/Arceliar/ironwood v0.0.0-20210619124114-6ad55cae5031
github.com/Arceliar/phony v0.0.0-20210209235338-dde1a8dca979
github.com/VividCortex/ewma v1.2.0 // indirect github.com/VividCortex/ewma v1.2.0 // indirect
github.com/fatih/color v1.18.0 // indirect github.com/cheggaaa/pb/v3 v3.0.8
github.com/mattn/go-isatty v0.0.20 // indirect github.com/fatih/color v1.12.0 // indirect
github.com/mattn/go-runewidth v0.0.16 // indirect github.com/gologme/log v1.2.0
github.com/olekukonko/tablewriter v0.0.5 github.com/hashicorp/go-syslog v1.0.0
github.com/vishvananda/netns v0.0.5 // indirect github.com/hjson/hjson-go v3.1.0+incompatible
suah.dev/protect v1.2.4 github.com/kardianos/minwinsvc v1.0.0
github.com/mattn/go-isatty v0.0.13 // indirect
github.com/mattn/go-runewidth v0.0.13 // indirect
github.com/mitchellh/mapstructure v1.4.1
github.com/vishvananda/netlink v1.1.0
github.com/vishvananda/netns v0.0.0-20210104183010-2eb08e3e575f // indirect
golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a // indirect
golang.org/x/net v0.0.0-20210610132358-84b48f89b13b
golang.org/x/sys v0.0.0-20210611083646-a4fc73990273
golang.org/x/text v0.3.7-0.20210503195748-5c7c50ebbd4f
golang.zx2c4.com/wireguard v0.0.0-20210604143328-f9b48a961cd2
golang.zx2c4.com/wireguard/windows v0.3.14
) )

186
go.sum
View file

@ -1,119 +1,79 @@
github.com/Arceliar/ironwood v0.0.0-20241213013129-743fe2fccbd3 h1:d8N0z+udAnbU5PdjpLSNPTWlqeU/nnYsQ42B6+879aw= github.com/Arceliar/ironwood v0.0.0-20210619124114-6ad55cae5031 h1:DZVDfYhVdu+0wAiRHoY1olyNkKxIot9UjBnbQFzuUlM=
github.com/Arceliar/ironwood v0.0.0-20241213013129-743fe2fccbd3/go.mod h1:SrrElc3FFMpYCODSr11jWbLFeOM8WsY+DbDY/l2AXF0= github.com/Arceliar/ironwood v0.0.0-20210619124114-6ad55cae5031/go.mod h1:RP72rucOFm5udrnEzTmIWLRVGQiV/fSUAQXJ0RST/nk=
github.com/Arceliar/phony v0.0.0-20220903101357-530938a4b13d h1:UK9fsWbWqwIQkMCz1CP+v5pGbsGoWAw6g4AyvMpm1EM= github.com/Arceliar/phony v0.0.0-20210209235338-dde1a8dca979 h1:WndgpSW13S32VLQ3ugUxx2EnnWmgba1kCqPkd4Gk1yQ=
github.com/Arceliar/phony v0.0.0-20220903101357-530938a4b13d/go.mod h1:BCnxhRf47C/dy/e/D2pmB8NkB3dQVIrkD98b220rx5Q= github.com/Arceliar/phony v0.0.0-20210209235338-dde1a8dca979/go.mod h1:6Lkn+/zJilRMsKmbmG1RPoamiArC6HS73xbwRyp3UyI=
github.com/VividCortex/ewma v1.1.1/go.mod h1:2Tkkvm3sRDVXaiyucHiACn4cqf7DpdyLvmxzcbUokwA=
github.com/VividCortex/ewma v1.2.0 h1:f58SaIzcDXrSy3kWaHNvuJgJ3Nmz59Zji6XoJR/q1ow= github.com/VividCortex/ewma v1.2.0 h1:f58SaIzcDXrSy3kWaHNvuJgJ3Nmz59Zji6XoJR/q1ow=
github.com/VividCortex/ewma v1.2.0/go.mod h1:nz4BbCtbLyFDeC9SUHbtcT5644juEuWfUAUnGx7j5l4= 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/cheggaaa/pb/v3 v3.0.8 h1:bC8oemdChbke2FHIIGy9mn4DPJ2caZYQnfbRqwmdCoA=
github.com/bits-and-blooms/bitset v1.13.0 h1:bAQ9OPNFYbGHV6Nez0tmNI0RiEu7/hxlYJRUA0wFAVE= github.com/cheggaaa/pb/v3 v3.0.8/go.mod h1:UICbiLec/XO6Hw6k+BHEtHeQFzzBH4i2/qk/ow1EJTA=
github.com/bits-and-blooms/bitset v1.13.0/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8= github.com/fatih/color v1.10.0/go.mod h1:ELkj/draVOlAH/xkhN6mQ50Qd0MPOk5AAr3maGEBuJM=
github.com/bits-and-blooms/bloom/v3 v3.7.0 h1:VfknkqV4xI+PsaDIsoHueyxVDZrfvMn56jeWUzvzdls= github.com/fatih/color v1.12.0 h1:mRhaKNwANqRgUBGKmnI5ZxEk7QXmjQeCcuYFMX2bfcc=
github.com/bits-and-blooms/bloom/v3 v3.7.0/go.mod h1:VKlUSvp0lFIYqxJjzdnSsZEw4iHb1kOL2tfHTgyJBHg= github.com/fatih/color v1.12.0/go.mod h1:ELkj/draVOlAH/xkhN6mQ50Qd0MPOk5AAr3maGEBuJM=
github.com/cheggaaa/pb/v3 v3.1.5 h1:QuuUzeM2WsAqG2gMqtzaWithDJv0i+i6UlnwSCI4QLk= github.com/gologme/log v1.2.0 h1:Ya5Ip/KD6FX7uH0S31QO87nCCSucKtF44TLbTtO7V4c=
github.com/cheggaaa/pb/v3 v3.1.5/go.mod h1:CrxkeghYTXi1lQBEI7jSn+3svI3cuc19haAj6jM60XI= github.com/gologme/log v1.2.0/go.mod h1:gq31gQ8wEHkR+WekdWsqDuf8pXTUZA9BnnzTuPz1Y9U=
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/hashicorp/go-syslog v1.0.0 h1:KaodqZuhUoZereWVIYmpUgZysurB1kBLX2j0MwMrUAE= github.com/hashicorp/go-syslog v1.0.0 h1:KaodqZuhUoZereWVIYmpUgZysurB1kBLX2j0MwMrUAE=
github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4= 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 v3.1.0+incompatible h1:DY/9yE8ey8Zv22bY+mHV1uk2yRy0h8tKhZ77hEdi0Aw=
github.com/hjson/hjson-go/v4 v4.4.0/go.mod h1:KaYt3bTw3zhBjYqnXkYywcYctk0A2nxeEFTse3rH13E= github.com/hjson/hjson-go v3.1.0+incompatible/go.mod h1:qsetwF8NlsTsOTwZTApNlTCerV+b2GjYRRcIk4JMFio=
github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/kardianos/minwinsvc v1.0.0 h1:+JfAi8IBJna0jY2dJGZqi7o15z13JelFIklJCAENALA=
github.com/kardianos/minwinsvc v1.0.2 h1:JmZKFJQrmTGa/WiW+vkJXKmfzdjabuEW4Tirj5lLdR0= github.com/kardianos/minwinsvc v1.0.0/go.mod h1:Bgd0oc+D0Qo3bBytmNtyRKVlp85dAloLKhfxanPFFRc=
github.com/kardianos/minwinsvc v1.0.2/go.mod h1:LUZNYhNmxujx2tR7FbdxqYJ9XDDoCd3MQcl1o//FWl4= github.com/lxn/walk v0.0.0-20210112085537-c389da54e794/go.mod h1:E23UucZGqpuUANJooIbHWCufXvOcT6E7Stq81gU+CSQ=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= github.com/lxn/win v0.0.0-20210218163916-a377121e959e/go.mod h1:KxxjdtRkfNoYDCUP5ryK7XJJNTnpC8atvtmTheChOtk=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= github.com/mattn/go-colorable v0.1.8 h1:c1ghPdyEDarC70ftn0y+A/Ee++9zz8ljHG1b13eJ0s8=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/mattn/go-colorable v0.1.8/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-isatty v0.0.13 h1:qdl+GuBjcsKKDco5BsxPJlId98mSWNKqYA+Co0SC1yA=
github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= github.com/mattn/go-isatty v0.0.13/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/mattn/go-runewidth v0.0.13 h1:lTGmDsbAYt5DmK6OnoV7EuIF1wEIFAcxld6ypU4OSgU=
github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec= github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY= github.com/mitchellh/mapstructure v1.4.1 h1:CpVNEelQCZBooIPDn+AR3NpivK/TIKU8bDxdASFVQag=
github.com/onsi/ginkgo/v2 v2.9.5 h1:+6Hr4uxzP4XIUyAkg61dWBw8lb/gc4/X5luuxN/EC+Q= github.com/mitchellh/mapstructure v1.4.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/onsi/ginkgo/v2 v2.9.5/go.mod h1:tvAoo1QUJwNEU2ITftXTpR7R1RbCzoZUOs3RonqW57k= github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
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/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= 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/vishvananda/netlink v1.1.0 h1:1iyaYNBLmP6L0220aDnYQpo1QEV4t4hJ+xEEhhJH8j0=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/vishvananda/netlink v1.1.0/go.mod h1:cTgwzPIzzgDAYoQrMm0EdrjRUBkTqKYppBueQtXaqoE=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= github.com/vishvananda/netns v0.0.0-20191106174202-0a2b9b5464df/go.mod h1:JP3t17pCcGlemwknint6hfoeCVQrEMVwxRLRjXpq+BU=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/vishvananda/netns v0.0.0-20210104183010-2eb08e3e575f h1:p4VB7kIXpOQvVn1ZaTIVp+3vuYAXFe3OJEvjbUYJLaA=
github.com/twmb/murmur3 v1.1.6 h1:mqrRot1BRxm+Yct+vavLMou2/iJt0tNVTTC0QoIjaZg= github.com/vishvananda/netns v0.0.0-20210104183010-2eb08e3e575f/go.mod h1:DD4vA1DwXk04H54A1oHXtwZmA0grkVMdPxx/VGLCah0=
github.com/twmb/murmur3 v1.1.6/go.mod h1:Qq/R7NUyOfr65zD+6Q5IHKsJLwP7exErjN6lyyq3OSQ= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
github.com/vishvananda/netlink v1.3.0 h1:X7l42GfcV4S6E4vHTsw48qbrV+9PVojNfIhZcwQdrZk= golang.org/x/crypto v0.0.0-20210220033148-5ea612d1eb83/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
github.com/vishvananda/netlink v1.3.0/go.mod h1:i6NetklAujEcC6fK0JPjT8qSwWyO0HLn4UKG+hGqeJs= golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
github.com/vishvananda/netns v0.0.4/go.mod h1:SpkAiCQRtJ6TvvxPnOSyH3BMl6unz3xZlaprSwhNNJM= golang.org/x/crypto v0.0.0-20210506145944-38f3c27a63bf/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8=
github.com/vishvananda/netns v0.0.5 h1:DfiHV+j8bA32MFM7bfEunvT8IAqQ/NzSJHtcmW5zdEY= golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a h1:kr2P4QFmQr29mSLA43kwrOcgcReGTfbE9N577tCTuBc=
github.com/vishvananda/netns v0.0.5/go.mod h1:SpkAiCQRtJ6TvvxPnOSyH3BMl6unz3xZlaprSwhNNJM= golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8=
github.com/wlynxg/anet v0.0.5 h1:J3VJGi1gvo0JwZ/P1/Yc/8p63SoW98B5dHkYDmpgvvU= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
github.com/wlynxg/anet v0.0.5/go.mod h1:eay5PRQr7fIVAMbTbchTnO9gG65Hg/uYGdc7mguHxoA= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
go.uber.org/mock v0.5.0 h1:KAMbZvZPyBPWgD14IrIQ38QCyjwpvVVV6K/bHl1IwQU= golang.org/x/net v0.0.0-20210510120150-4163338589ed/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
go.uber.org/mock v0.5.0/go.mod h1:ge71pBPLYDk7QIi1LupWxdAykm7KIEFchiOqd6z7qMM= golang.org/x/net v0.0.0-20210610132358-84b48f89b13b h1:k+E048sYJHyVnsr1GDrRZWQ32D2C7lWs9JRc0bel53A=
golang.org/x/crypto v0.33.0 h1:IOBPskki6Lysi0lo9qQvbxiQ+FvsCC/YWOecCHAixus= golang.org/x/net v0.0.0-20210610132358-84b48f89b13b/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 h1:2dVuKD2vS7b0QIHQbpyTISPd0LeHDbnYEryqj5Q1ug8= golang.org/x/sys v0.0.0-20190606203320-7fc4e5ec1444/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY= golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/mod v0.19.0 h1:fEdghXQSo20giMthA7cd28ZC+jts4amQ3YMXiP5oMQ8= golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/mod v0.19.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/sys v0.0.0-20200217220822-9197077df867/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8= golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk= golang.org/x/sys v0.0.0-20200602225109-6fdc65e7d980/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sync v0.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w= golang.org/x/sys v0.0.0-20201018230417-eeed37f84f13/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210309040221-94ec62e08169/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210403161142-5e06dd20ab57/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210611083646-a4fc73990273 h1:faDu4veV+8pcThn4fewv6TVlNCezafGoC1gM/mxQLbQ=
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.0.0-20210611083646-a4fc73990273/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= golang.org/x/text v0.3.7-0.20210503195748-5c7c50ebbd4f h1:yQJrRE0hDxDFmZLlRaw+3vusO4fwNHgHIjUOMO7bHYI=
golang.org/x/tools v0.23.0 h1:SGsXPZ+2l4JsgaCKkx+FQ9YZ5XEtA1GZYuoDjenLjvg= golang.org/x/text v0.3.7-0.20210503195748-5c7c50ebbd4f/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/tools v0.23.0/go.mod h1:pnu6ufv6vQkll6szChhK3C3L/ruaIv5eBeztNG8wtsI= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 h1:B82qJJgjvYKsXS9jeunTOisW56dUokqW/FOteYJJ/yg= golang.zx2c4.com/wireguard v0.0.0-20210510202332-9844c74f67ec/go.mod h1:a057zjmoc00UN7gVkaJt2sXVK523kMJcogDTEvPIasg=
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2/go.mod h1:deeaetjYA+DHMHg+sMSMI58GrEteJUUzzw7en6TJQcI= golang.zx2c4.com/wireguard v0.0.0-20210604143328-f9b48a961cd2 h1:wfOOSvHgIzTZ9h5Vb6yUFZNn7uf3bT7PeYsHOO7tYDM=
golang.zx2c4.com/wireguard v0.0.0-20231211153847-12269c276173 h1:/jFs0duh4rdb8uIfPMv78iAJGcPKDeqAFnaLBropIC4= golang.zx2c4.com/wireguard v0.0.0-20210604143328-f9b48a961cd2/go.mod h1:laHzsbfMhGSobUmruXWAyMKKHSqvIcrqZJMyHD+/3O8=
golang.zx2c4.com/wireguard v0.0.0-20231211153847-12269c276173/go.mod h1:tkCQ4FQXmpAgYVh++1cq16/dH4QJtmvpRv19DWGAHSA= golang.zx2c4.com/wireguard/windows v0.3.14 h1:5yIDYyrQyGkLqV+tzY4ilMNeIvQeMXAz0glZz9u179A=
golang.zx2c4.com/wireguard/windows v0.5.3 h1:On6j2Rpn3OEMXqBq00QEDC7bWSZrPIHKIus8eIuExIE= golang.zx2c4.com/wireguard/windows v0.3.14/go.mod h1:3P4IEAsb+BjlKZmpUXgy74c0iX9AVwwr3WcVJ8nPgME=
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=

View file

@ -64,7 +64,7 @@ func AddrForKey(publicKey ed25519.PublicKey) *Address {
buf[idx] = ^buf[idx] buf[idx] = ^buf[idx]
} }
var addr Address var addr Address
var temp = make([]byte, 0, 32) var temp []byte
done := false done := false
ones := byte(0) ones := byte(0)
bits := byte(0) bits := byte(0)
@ -108,16 +108,16 @@ func SubnetForKey(publicKey ed25519.PublicKey) *Subnet {
} }
var snet Subnet var snet Subnet
copy(snet[:], addr[:]) copy(snet[:], addr[:])
prefix := GetPrefix() // nolint:staticcheck prefix := GetPrefix()
snet[len(prefix)-1] |= 0x01 snet[len(prefix)-1] |= 0x01
return &snet return &snet
} }
// GetKey returns the partial ed25519.PublicKey for the Address. // GetKet returns the partial ed25519.PublicKey for the Address.
// This is used for key lookup. // This is used for key lookup.
func (a *Address) GetKey() ed25519.PublicKey { func (a *Address) GetKey() ed25519.PublicKey {
var key [ed25519.PublicKeySize]byte var key [ed25519.PublicKeySize]byte
prefix := GetPrefix() // nolint:staticcheck prefix := GetPrefix()
ones := int(a[len(prefix)]) ones := int(a[len(prefix)])
for idx := 0; idx < ones; idx++ { for idx := 0; idx < ones; idx++ {
key[idx/8] |= 0x80 >> byte(idx%8) key[idx/8] |= 0x80 >> byte(idx%8)
@ -129,11 +129,7 @@ func (a *Address) GetKey() ed25519.PublicKey {
bits <<= byte(idx % 8) bits <<= byte(idx % 8)
keyIdx := keyOffset + (idx - addrOffset) keyIdx := keyOffset + (idx - addrOffset)
bits >>= byte(keyIdx % 8) bits >>= byte(keyIdx % 8)
idx := keyIdx / 8 key[keyIdx/8] |= bits
if idx >= len(key) {
break
}
key[idx] |= bits
} }
for idx := range key { for idx := range key {
key[idx] = ^key[idx] key[idx] = ^key[idx]
@ -141,7 +137,7 @@ func (a *Address) GetKey() ed25519.PublicKey {
return ed25519.PublicKey(key[:]) return ed25519.PublicKey(key[:])
} }
// GetKey returns the partial ed25519.PublicKey for the Subnet. // GetKet returns the partial ed25519.PublicKey for the Subnet.
// This is used for key lookup. // This is used for key lookup.
func (s *Subnet) GetKey() ed25519.PublicKey { func (s *Subnet) GetKey() ed25519.PublicKey {
var addr Address var addr Address

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

@ -7,63 +7,55 @@ import (
"net" "net"
"net/url" "net/url"
"os" "os"
"sort"
"strings" "strings"
"time" "time"
"github.com/gologme/log"
"github.com/yggdrasil-network/yggdrasil-go/src/config"
"github.com/yggdrasil-network/yggdrasil-go/src/core" "github.com/yggdrasil-network/yggdrasil-go/src/core"
) )
// TODO: Add authentication // TODO: Add authentication
type AdminSocket struct { type AdminSocket struct {
core *core.Core core *core.Core
log core.Logger log *log.Logger
listener net.Listener listenaddr string
handlers map[string]handler listener net.Listener
done chan struct{} handlers map[string]handler
config struct { done chan struct{}
listenaddr ListenAddress
}
}
type AdminSocketRequest struct {
Name string `json:"request"`
Arguments json.RawMessage `json:"arguments,omitempty"`
KeepAlive bool `json:"keepalive,omitempty"`
} }
type AdminSocketResponse struct { type AdminSocketResponse struct {
Status string `json:"status"` Status string `json:"status"`
Error string `json:"error,omitempty"` Request struct {
Request AdminSocketRequest `json:"request"` Name string `json:"request"`
Response json.RawMessage `json:"response"` KeepAlive bool `json:"keepalive"`
} `json:"request"`
Response interface{} `json:"response"`
} }
type handler struct { type handler struct {
desc string // What does the endpoint do? args []string // List of human-readable argument names
args []string // List of human-readable argument names handler func(json.RawMessage) (interface{}, error) // First is input map, second is output
handler core.AddHandlerFunc // First is input map, second is output
} }
type ListResponse struct { type ListResponse struct {
List []ListEntry `json:"list"` List map[string]ListEntry `json:"list"`
} }
type ListEntry struct { type ListEntry struct {
Command string `json:"command"` Fields []string `json:"fields"`
Description string `json:"description"`
Fields []string `json:"fields,omitempty"`
} }
// AddHandler is called for each admin function to add the handler and help documentation to the API. // 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(json.RawMessage) (interface{}, error)) error {
if _, ok := a.handlers[strings.ToLower(name)]; ok { if _, ok := a.handlers[strings.ToLower(name)]; ok {
return errors.New("handler already exists") return errors.New("handler already exists")
} }
a.handlers[strings.ToLower(name)] = handler{ a.handlers[strings.ToLower(name)] = handler{
desc: desc,
args: args, args: args,
handler: handlerfunc, handler: handlerfunc,
} }
@ -71,182 +63,99 @@ func (a *AdminSocket) AddHandler(name, desc string, args []string, handlerfunc c
} }
// Init runs the initial admin setup. // Init runs the initial admin setup.
func New(c *core.Core, log core.Logger, opts ...SetupOption) (*AdminSocket, error) { func (a *AdminSocket) Init(c *core.Core, nc *config.NodeConfig, log *log.Logger, options interface{}) error {
a := &AdminSocket{ a.core = c
core: c, a.log = log
log: log, a.handlers = make(map[string]handler)
handlers: make(map[string]handler), nc.RLock()
} a.listenaddr = nc.AdminListen
for _, opt := range opts { nc.RUnlock()
a._applyOption(opt) a.done = make(chan struct{})
} close(a.done) // Start in a done / not-started state
if a.config.listenaddr == "none" || a.config.listenaddr == "" { _ = a.AddHandler("list", []string{}, func(_ json.RawMessage) (interface{}, error) {
return nil, nil res := &ListResponse{
} List: map[string]ListEntry{},
listenaddr := string(a.config.listenaddr)
u, err := url.Parse(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")
os.Exit(1)
} else {
if err := os.Remove(u.Path); err == nil {
a.log.Debugln(u.Path, "was cleaned up")
} else {
a.log.Errorln(u.Path, "already exists and was not cleaned up:", err)
os.Exit(1)
}
}
}
a.listener, err = net.Listen("unix", u.Path)
if err == nil {
switch u.Path[:1] {
case "@": // maybe abstract namespace
default:
if err := os.Chmod(u.Path, 0660); err != nil {
a.log.Warnln("WARNING:", u.Path, "may have unsafe permissions!")
}
}
}
case "tcp":
a.listener, err = net.Listen("tcp", u.Host)
default:
a.listener, err = net.Listen("tcp", listenaddr)
} }
} else {
a.listener, err = net.Listen("tcp", listenaddr)
}
if err != nil {
a.log.Errorf("Admin socket failed to listen: %v", err)
os.Exit(1)
}
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 { for name, handler := range a.handlers {
res.List = append(res.List, ListEntry{ res.List[name] = ListEntry{
Command: name, Fields: handler.args,
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 return res, nil
}) })
a.done = make(chan struct{}) a.core.SetAdmin(a)
go a.listen() return nil
return a, a.core.SetAdmin(a)
} }
func (a *AdminSocket) SetupAdminHandlers() { func (a *AdminSocket) SetupAdminHandlers(na *AdminSocket) {
_ = a.AddHandler( _ = a.AddHandler("getSelf", []string{}, func(in json.RawMessage) (interface{}, error) {
"getSelf", "Show details about this node", []string{}, req := &GetSelfRequest{}
func(in json.RawMessage) (interface{}, error) { res := &GetSelfResponse{}
req := &GetSelfRequest{} if err := json.Unmarshal(in, &req); err != nil {
res := &GetSelfResponse{} return nil, err
if err := json.Unmarshal(in, &req); err != nil { }
return nil, err if err := a.getSelfHandler(req, res); err != nil {
} return nil, err
if err := a.getSelfHandler(req, res); err != nil { }
return nil, err return res, nil
} })
return res, nil _ = a.AddHandler("getPeers", []string{}, func(in json.RawMessage) (interface{}, error) {
}, req := &GetPeersRequest{}
) res := &GetPeersResponse{}
_ = a.AddHandler( if err := json.Unmarshal(in, &req); err != nil {
"getPeers", "Show directly connected peers", []string{}, return nil, err
func(in json.RawMessage) (interface{}, error) { }
req := &GetPeersRequest{} if err := a.getPeersHandler(req, res); err != nil {
res := &GetPeersResponse{} return nil, err
if err := json.Unmarshal(in, &req); err != nil { }
return nil, err return res, nil
} })
if err := a.getPeersHandler(req, res); err != nil { _ = a.AddHandler("getDHT", []string{}, func(in json.RawMessage) (interface{}, error) {
return nil, err req := &GetDHTRequest{}
} res := &GetDHTResponse{}
return res, nil if err := json.Unmarshal(in, &req); err != nil {
}, return nil, err
) }
_ = a.AddHandler( if err := a.getDHTHandler(req, res); err != nil {
"getTree", "Show known Tree entries", []string{}, return nil, err
func(in json.RawMessage) (interface{}, error) { }
req := &GetTreeRequest{} return res, nil
res := &GetTreeResponse{} })
if err := json.Unmarshal(in, &req); err != nil { _ = a.AddHandler("getPaths", []string{}, func(in json.RawMessage) (interface{}, error) {
return nil, err req := &GetPathsRequest{}
} res := &GetPathsResponse{}
if err := a.getTreeHandler(req, res); err != nil { if err := json.Unmarshal(in, &req); err != nil {
return nil, err return nil, err
} }
return res, nil if err := a.getPathsHandler(req, res); err != nil {
}, return nil, err
) }
_ = a.AddHandler( return res, nil
"getPaths", "Show established paths through this node", []string{}, })
func(in json.RawMessage) (interface{}, error) { _ = a.AddHandler("getSessions", []string{}, func(in json.RawMessage) (interface{}, error) {
req := &GetPathsRequest{} req := &GetSessionsRequest{}
res := &GetPathsResponse{} res := &GetSessionsResponse{}
if err := json.Unmarshal(in, &req); err != nil { if err := json.Unmarshal(in, &req); err != nil {
return nil, err return nil, err
} }
if err := a.getPathsHandler(req, res); err != nil { if err := a.getSessionsHandler(req, res); err != nil {
return nil, err return nil, err
} }
return res, nil return res, nil
}, })
) //_ = a.AddHandler("getNodeInfo", []string{"key"}, t.proto.nodeinfo.nodeInfoAdminHandler)
_ = a.AddHandler( //_ = a.AddHandler("debug_remoteGetSelf", []string{"key"}, t.proto.getSelfHandler)
"getSessions", "Show established traffic sessions with remote nodes", []string{}, //_ = a.AddHandler("debug_remoteGetPeers", []string{"key"}, t.proto.getPeersHandler)
func(in json.RawMessage) (interface{}, error) { //_ = a.AddHandler("debug_remoteGetDHT", []string{"key"}, t.proto.getDHTHandler)
req := &GetSessionsRequest{} }
res := &GetSessionsResponse{}
if err := json.Unmarshal(in, &req); err != nil { // Start runs the admin API socket to listen for / respond to admin API calls.
return nil, err func (a *AdminSocket) Start() error {
} if a.listenaddr != "none" && a.listenaddr != "" {
if err := a.getSessionsHandler(req, res); err != nil { a.done = make(chan struct{})
return nil, err go a.listen()
} }
return res, nil return 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. // IsStarted returns true if the module has been started.
@ -263,9 +172,6 @@ func (a *AdminSocket) IsStarted() bool {
// Stop will stop the admin API and close the socket. // Stop will stop the admin API and close the socket.
func (a *AdminSocket) Stop() error { func (a *AdminSocket) Stop() error {
if a == nil {
return nil
}
if a.listener != nil { if a.listener != nil {
select { select {
case <-a.done: case <-a.done:
@ -279,6 +185,50 @@ func (a *AdminSocket) Stop() error {
// listen is run by start and manages API connections. // listen is run by start and manages API connections.
func (a *AdminSocket) listen() { func (a *AdminSocket) listen() {
u, err := url.Parse(a.listenaddr)
if err == nil {
switch strings.ToLower(u.Scheme) {
case "unix":
if _, err := os.Stat(a.listenaddr[7:]); err == nil {
a.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(a.listenaddr[7:]); err == nil {
a.log.Debugln(a.listenaddr[7:], "was cleaned up")
} else {
a.log.Errorln(a.listenaddr[7:], "already exists and was not cleaned up:", err)
os.Exit(1)
}
}
}
a.listener, err = net.Listen("unix", a.listenaddr[7:])
if err == nil {
switch a.listenaddr[7:8] {
case "@": // maybe abstract namespace
default:
if err := os.Chmod(a.listenaddr[7:], 0660); err != nil {
a.log.Warnln("WARNING:", a.listenaddr[:7], "may have unsafe permissions!")
}
}
}
case "tcp":
a.listener, err = net.Listen("tcp", u.Host)
default:
// err = errors.New(fmt.Sprint("protocol not supported: ", u.Scheme))
a.listener, err = net.Listen("tcp", a.listenaddr)
}
} else {
a.listener, err = net.Listen("tcp", a.listenaddr)
}
if err != nil {
a.log.Errorf("Admin socket failed to listen: %v", err)
os.Exit(1)
}
a.log.Infof("%s admin socket listening on %s",
strings.ToUpper(a.listener.Addr().Network()),
a.listener.Addr().String())
defer a.listener.Close() defer a.listener.Close()
for { for {
conn, err := a.listener.Accept() conn, err := a.listener.Accept()
@ -306,65 +256,53 @@ func (a *AdminSocket) handleRequest(conn net.Conn) {
defer conn.Close() defer conn.Close()
defer func() {
r := recover()
if r != nil {
a.log.Debugln("Admin socket error:", r)
if err := encoder.Encode(&ErrorResponse{
Error: "Check your syntax and input types",
}); err != nil {
a.log.Debugln("Admin socket JSON encode error:", err)
}
conn.Close()
}
}()
for { for {
var err error var err error
var buf json.RawMessage var buf json.RawMessage
var req AdminSocketRequest _ = decoder.Decode(&buf)
var resp AdminSocketResponse var resp AdminSocketResponse
req.Arguments = []byte("{}") resp.Status = "success"
if err := func() error { if err = json.Unmarshal(buf, &resp.Request); err == nil {
if err = decoder.Decode(&buf); err != nil { if resp.Request.Name == "" {
return fmt.Errorf("Failed to find request") resp.Status = "error"
resp.Response = &ErrorResponse{
Error: "No request specified",
}
} else if h, ok := a.handlers[strings.ToLower(resp.Request.Name)]; ok {
resp.Response, err = h.handler(buf)
if err != nil {
resp.Status = "error"
resp.Response = &ErrorResponse{
Error: err.Error(),
}
}
} else {
resp.Status = "error"
resp.Response = &ErrorResponse{
Error: fmt.Sprintf("Unknown action '%s', try 'list' for help", resp.Request.Name),
}
} }
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)
if err != nil {
return err
}
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 { if err = encoder.Encode(resp); err != nil {
a.log.Debugln("Encode error:", err) a.log.Debugln("Encode error:", err)
} }
if !req.KeepAlive { if !resp.Request.KeepAlive {
break break
} else { } else {
continue continue
} }
} }
} }
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)
}
}

34
src/admin/getdht.go Normal file
View file

@ -0,0 +1,34 @@
package admin
import (
"encoding/hex"
"net"
"github.com/yggdrasil-network/yggdrasil-go/src/address"
)
type GetDHTRequest struct{}
type GetDHTResponse struct {
DHT map[string]DHTEntry `json:"dht"`
}
type DHTEntry struct {
PublicKey string `json:"key"`
Port uint64 `json:"port"`
Rest uint64 `json:"rest"`
}
func (a *AdminSocket) getDHTHandler(req *GetDHTRequest, res *GetDHTResponse) error {
res.DHT = map[string]DHTEntry{}
for _, d := range a.core.GetDHT() {
addr := address.AddrForKey(d.Key)
so := net.IP(addr[:]).String()
res.DHT[so] = DHTEntry{
PublicKey: hex.EncodeToString(d.Key[:]),
Port: d.Port,
Rest: d.Rest,
}
}
return nil
}

View file

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

View file

@ -3,9 +3,6 @@ package admin
import ( import (
"encoding/hex" "encoding/hex"
"net" "net"
"slices"
"strings"
"time"
"github.com/yggdrasil-network/yggdrasil-go/src/address" "github.com/yggdrasil-network/yggdrasil-go/src/address"
) )
@ -14,78 +11,27 @@ type GetPeersRequest struct {
} }
type GetPeersResponse struct { type GetPeersResponse struct {
Peers []PeerEntry `json:"peers"` Peers map[string]PeerEntry `json:"peers"`
} }
type PeerEntry struct { type PeerEntry struct {
URI string `json:"remote,omitempty"` PublicKey string `json:"key"`
Up bool `json:"up"` Port uint64 `json:"port"`
Inbound bool `json:"inbound"` Coords []uint64 `json:"coords"`
IPAddress string `json:"address,omitempty"` Remote string `json:"remote"`
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 { func (a *AdminSocket) getPeersHandler(req *GetPeersRequest, res *GetPeersResponse) error {
peers := a.core.GetPeers() res.Peers = map[string]PeerEntry{}
res.Peers = make([]PeerEntry, 0, len(peers)) for _, p := range a.core.GetPeers() {
for _, p := range peers { addr := address.AddrForKey(p.Key)
peer := PeerEntry{ so := net.IP(addr[:]).String()
Port: p.Port, res.Peers[so] = PeerEntry{
Up: p.Up, PublicKey: hex.EncodeToString(p.Key),
Inbound: p.Inbound, Port: p.Port,
Priority: uint64(p.Priority), // can't be uint8 thanks to gobind Coords: p.Coords,
Cost: p.Cost, Remote: p.Remote,
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 return nil
} }

View file

@ -9,22 +9,28 @@ import (
type GetSelfRequest struct{} type GetSelfRequest struct{}
type GetSelfResponse struct { type GetSelfResponse struct {
BuildName string `json:"build_name"` Self map[string]SelfEntry `json:"self"`
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 { type SelfEntry struct {
BuildName string `json:"build_name"`
BuildVersion string `json:"build_version"`
PublicKey string `json:"key"`
Coords []uint64 `json:"coords"`
Subnet string `json:"subnet"`
}
func (a *AdminSocket) getSelfHandler(req *GetSelfRequest, res *GetSelfResponse) error {
res.Self = make(map[string]SelfEntry)
self := a.core.GetSelf() self := a.core.GetSelf()
addr := a.core.Address().String()
snet := a.core.Subnet() snet := a.core.Subnet()
res.BuildName = version.BuildName() res.Self[addr] = SelfEntry{
res.BuildVersion = version.BuildVersion() BuildName: version.BuildName(),
res.PublicKey = hex.EncodeToString(self.Key[:]) BuildVersion: version.BuildVersion(),
res.IPAddress = a.core.Address().String() PublicKey: hex.EncodeToString(self.Key[:]),
res.Subnet = snet.String() Subnet: snet.String(),
res.RoutingEntries = self.RoutingEntries Coords: self.Coords,
}
return nil return nil
} }

View file

@ -3,8 +3,6 @@ package admin
import ( import (
"encoding/hex" "encoding/hex"
"net" "net"
"slices"
"strings"
"github.com/yggdrasil-network/yggdrasil-go/src/address" "github.com/yggdrasil-network/yggdrasil-go/src/address"
) )
@ -12,32 +10,21 @@ import (
type GetSessionsRequest struct{} type GetSessionsRequest struct{}
type GetSessionsResponse struct { type GetSessionsResponse struct {
Sessions []SessionEntry `json:"sessions"` Sessions map[string]SessionEntry `json:"sessions"`
} }
type SessionEntry struct { type SessionEntry struct {
IPAddress string `json:"address"` PublicKey string `json:"key"`
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 { func (a *AdminSocket) getSessionsHandler(req *GetSessionsRequest, res *GetSessionsResponse) error {
sessions := a.core.GetSessions() res.Sessions = map[string]SessionEntry{}
res.Sessions = make([]SessionEntry, 0, len(sessions)) for _, s := range a.core.GetSessions() {
for _, s := range sessions {
addr := address.AddrForKey(s.Key) addr := address.AddrForKey(s.Key)
res.Sessions = append(res.Sessions, SessionEntry{ so := net.IP(addr[:]).String()
IPAddress: net.IP(addr[:]).String(), res.Sessions[so] = SessionEntry{
PublicKey: hex.EncodeToString(s.Key[:]), 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 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,45 @@ configuration option that is not provided.
package config package config
import ( import (
"bytes"
"crypto/ed25519" "crypto/ed25519"
"crypto/rand"
"crypto/tls"
"crypto/x509"
"crypto/x509/pkix"
"encoding/hex" "encoding/hex"
"encoding/json" "sync"
"encoding/pem"
"fmt"
"io"
"math/big"
"os"
"time"
"github.com/hjson/hjson-go/v4"
"golang.org/x/text/encoding/unicode"
) )
// NodeConfig is the main configuration structure, containing configuration // NodeConfig is the main configuration structure, containing configuration
// options that are necessary for an Yggdrasil node to run. You will need to // 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. // supply one of these structs to the Yggdrasil core when starting a node.
type NodeConfig struct { type NodeConfig struct {
PrivateKey KeyBytes `json:",omitempty" comment:"Your private key. DO NOT share this with anyone!"` sync.RWMutex `json:"-"`
PrivateKeyPath string `json:",omitempty" comment:"The path to your private key file in PEM format."` Peers []string `comment:"List of connection strings for outbound peer connections in URI format,\ne.g. tls://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."`
Certificate *tls.Certificate `json:"-"` 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 ] }.\nNote that SOCKS peerings will NOT be affected by this option and should\ngo in the \"Peers\" section instead."`
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."` 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.\ntls://0.0.0.0:0 or tls://[::]:0 to listen on all interfaces."`
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\"."` 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."`
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."` MulticastInterfaces []MulticastInterfaceConfig `comment:"Configuration for which interfaces multicast peer discovery should be\nenabled on. Each entry in the list should be a json object which may\ncontain Regex, Beacon, Listen, and Port. Regex is a regular expression\nwhich is matched against an interface name, and interfaces use the\nfirst configuration that they match gainst. Beacon configures whether\nor not the node should send link-local multicast beacons to advertise\ntheir presence, while listening for incoming connections on Port.\nListen controls whether or not the node listens for multicast beacons\nand opens outgoing connections."`
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."` 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."`
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."` PublicKey string `comment:"Your public key. Your peers may ask you for this to put\ninto their AllowedPublicKeys configuration."`
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!"` PrivateKey string `comment:"Your private key. DO NOT share this with anyone!"`
IfName string `comment:"Local network interface name for TUN adapter, or \"auto\" to select\nan interface automatically, or \"none\" to run without TUN."` 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."` 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."` 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."` 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 { type MulticastInterfaceConfig struct {
Regex string Regex string
Beacon bool Beacon bool
Listen bool Listen bool
Port uint16 `json:",omitempty"` Port uint16
Priority uint64 `json:",omitempty"` // really uint8, but gobind won't export it
Password string
} }
// Generates default configuration and returns a pointer to the resulting // NewSigningKeys replaces the signing keypair in the NodeConfig with a new
// NodeConfig. This is used when outputting the -genconf parameter and also when // signing keypair. The signing keys are used by the switch to derive the
// using -autoconf. // structure of the spanning tree.
func GenerateConfig() *NodeConfig { func (cfg *NodeConfig) NewKeys() {
// Get the defaults for the platform. spub, spriv, err := ed25519.GenerateKey(nil)
defaults := GetDefaults()
// Create a node configuration and populate it.
cfg := new(NodeConfig)
cfg.NewPrivateKey()
cfg.Listen = []string{}
cfg.AdminListen = defaults.DefaultAdminListen
cfg.Peers = []string{}
cfg.InterfacePeers = map[string][]string{}
cfg.AllowedPublicKeys = []string{}
cfg.MulticastInterfaces = defaults.DefaultMulticastInterfaces
cfg.IfName = defaults.DefaultIfName
cfg.IfMTU = defaults.DefaultIfMTU
cfg.NodeInfoPrivacy = false
if err := cfg.postprocessConfig(); err != nil {
panic(err)
}
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
}
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 { if err != nil {
panic(err) panic(err)
} }
cfg.PrivateKey = KeyBytes(spriv) cfg.PublicKey = hex.EncodeToString(spub[:])
} cfg.PrivateKey = hex.EncodeToString(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
} }

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

@ -2,149 +2,114 @@ package core
import ( import (
"crypto/ed25519" "crypto/ed25519"
//"encoding/hex"
"encoding/json" "encoding/json"
//"errors"
//"fmt"
"net" "net"
"net/url" "net/url"
"sync/atomic" //"sort"
"time" //"time"
"github.com/Arceliar/phony" "github.com/gologme/log"
"github.com/Arceliar/ironwood/network"
"github.com/yggdrasil-network/yggdrasil-go/src/address" "github.com/yggdrasil-network/yggdrasil-go/src/address"
//"github.com/yggdrasil-network/yggdrasil-go/src/crypto"
//"github.com/Arceliar/phony"
) )
type SelfInfo struct { type Self struct {
Key ed25519.PublicKey Key ed25519.PublicKey
RoutingEntries uint64 Root ed25519.PublicKey
Coords []uint64
} }
type PeerInfo struct { type Peer struct {
URI string Key ed25519.PublicKey
Up bool Root ed25519.PublicKey
Inbound bool Coords []uint64
LastError error Port uint64
LastErrorTime time.Time Remote string
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 { type DHTEntry struct {
Key ed25519.PublicKey Key ed25519.PublicKey
Parent ed25519.PublicKey Port uint64
Sequence uint64 Rest uint64
//Port uint64
//Rest uint64
} }
type PathEntryInfo struct { type PathEntry struct {
Key ed25519.PublicKey Key ed25519.PublicKey
Path []uint64 Path []uint64
Sequence uint64
} }
type SessionInfo struct { type Session struct {
Key ed25519.PublicKey Key ed25519.PublicKey
RXBytes uint64
TXBytes uint64
Uptime time.Duration
} }
func (c *Core) GetSelf() SelfInfo { func (c *Core) GetSelf() Self {
var self SelfInfo var self Self
s := c.PacketConn.PacketConn.Debug.GetSelf() s := c.pc.PacketConn.Debug.GetSelf()
self.Key = s.Key self.Key = s.Key
self.RoutingEntries = s.RoutingEntries self.Root = s.Root
self.Coords = s.Coords
return self return self
} }
func (c *Core) GetPeers() []PeerInfo { func (c *Core) GetPeers() []Peer {
peers := []PeerInfo{} var peers []Peer
conns := map[net.Conn]network.DebugPeerInfo{} names := make(map[net.Conn]string)
iwpeers := c.PacketConn.PacketConn.Debug.GetPeers() c.links.mutex.Lock()
for _, p := range iwpeers { for _, info := range c.links.links {
conns[p.Conn] = p names[info.conn] = info.lname
} }
c.links.mutex.Unlock()
phony.Block(&c.links, func() { ps := c.pc.PacketConn.Debug.GetPeers()
for info, state := range c.links._links { for _, p := range ps {
var peerinfo PeerInfo var info Peer
var conn net.Conn info.Key = p.Key
peerinfo.URI = info.uri info.Root = p.Root
peerinfo.LastError = state._err info.Coords = p.Coords
peerinfo.LastErrorTime = state._errtime info.Port = p.Port
if c := state._conn; c != nil { info.Remote = p.Conn.RemoteAddr().String()
conn = c if name := names[p.Conn]; name != "" {
peerinfo.Up = true info.Remote = name
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)
} }
}) peers = append(peers, info)
}
return peers return peers
} }
func (c *Core) GetTree() []TreeEntryInfo { func (c *Core) GetDHT() []DHTEntry {
var trees []TreeEntryInfo var dhts []DHTEntry
ts := c.PacketConn.PacketConn.Debug.GetTree() ds := c.pc.PacketConn.Debug.GetDHT()
for _, t := range ts { for _, d := range ds {
var info TreeEntryInfo var info DHTEntry
info.Key = t.Key info.Key = d.Key
info.Parent = t.Parent info.Port = d.Port
info.Sequence = t.Sequence info.Rest = d.Rest
//info.Port = d.Port dhts = append(dhts, info)
//info.Rest = d.Rest
trees = append(trees, info)
} }
return trees return dhts
} }
func (c *Core) GetPaths() []PathEntryInfo { func (c *Core) GetPaths() []PathEntry {
var paths []PathEntryInfo var paths []PathEntry
ps := c.PacketConn.PacketConn.Debug.GetPaths() ps := c.pc.PacketConn.Debug.GetPaths()
for _, p := range ps { for _, p := range ps {
var info PathEntryInfo var info PathEntry
info.Key = p.Key info.Key = p.Key
info.Sequence = p.Sequence
info.Path = p.Path info.Path = p.Path
paths = append(paths, info) paths = append(paths, info)
} }
return paths return paths
} }
func (c *Core) GetSessions() []SessionInfo { func (c *Core) GetSessions() []Session {
var sessions []SessionInfo var sessions []Session
ss := c.PacketConn.Debug.GetSessions() ss := c.pc.Debug.GetSessions()
for _, s := range ss { for _, s := range ss {
var info SessionInfo var info Session
info.Key = s.Key info.Key = s.Key
info.RXBytes = s.RX
info.TXBytes = s.TX
info.Uptime = s.Uptime
sessions = append(sessions, info) sessions = append(sessions, info)
} }
return sessions return sessions
@ -153,15 +118,8 @@ func (c *Core) GetSessions() []SessionInfo {
// Listen starts a new listener (either TCP or TLS). The input should be a url.URL // 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 // 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. // link-local address, the interface should be provided as the second argument.
func (c *Core) Listen(u *url.URL, sintf string) (*Listener, error) { func (c *Core) Listen(u *url.URL, sintf string) (*TcpListener, error) {
return c.links.listen(u, sintf, false) return c.links.tcp.listenURL(u, sintf)
}
// 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 gets the IPv6 address of the Yggdrasil node. This is always a /128
@ -189,76 +147,154 @@ func (c *Core) Subnet() net.IPNet {
// may be useful if you want to redirect the output later. Note that 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 // expects a Logger from the github.com/gologme/log package and not from Go's
// built-in log package. // built-in log package.
func (c *Core) SetLogger(log Logger) { func (c *Core) SetLogger(log *log.Logger) {
c.log = log c.log = log
} }
// AddPeer adds a peer. This should be specified in the peer URI format, e.g.: // AddPeer adds a peer. This should be specified in the peer URI format, e.g.:
// // tcp://a.b.c.d:e
// tcp://a.b.c.d:e // socks://a.b.c.d:e/f.g.h.i:j
// 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 // This adds the peer to the peer list, so that they will be called again if the
// connection drops. // connection drops.
func (c *Core) AddPeer(u *url.URL, sintf string) error { /*
return c.links.add(u, sintf, linkTypePersistent) func (c *Core) AddPeer(addr string, sintf string) error {
if err := c.CallPeer(addr, sintf); err != nil {
// TODO: We maybe want this to write the peer to the persistent
// configuration even if a connection attempt fails, but first we'll need to
// move the code to check the peer URI so that we don't deliberately save a
// peer with a known bad URI. Loading peers from config should really do the
// same thing too but I don't think that happens today
return err
}
c.config.Mutex.Lock()
defer c.config.Mutex.Unlock()
if sintf == "" {
for _, peer := range c.config.Current.Peers {
if peer == addr {
return errors.New("peer already added")
}
}
c.config.Current.Peers = append(c.config.Current.Peers, addr)
} else {
if _, ok := c.config.Current.InterfacePeers[sintf]; ok {
for _, peer := range c.config.Current.InterfacePeers[sintf] {
if peer == addr {
return errors.New("peer already added")
}
}
}
if _, ok := c.config.Current.InterfacePeers[sintf]; !ok {
c.config.Current.InterfacePeers[sintf] = []string{addr}
} else {
c.config.Current.InterfacePeers[sintf] = append(c.config.Current.InterfacePeers[sintf], addr)
}
}
return nil
} }
*/
// RemovePeer removes a peer. The peer should be specified in URI format, see AddPeer. /*
// The peer is not disconnected immediately. func (c *Core) RemovePeer(addr string, sintf string) error {
func (c *Core) RemovePeer(u *url.URL, sintf string) error { if sintf == "" {
return c.links.remove(u, sintf, linkTypePersistent) for i, peer := range c.config.Current.Peers {
if peer == addr {
c.config.Current.Peers = append(c.config.Current.Peers[:i], c.config.Current.Peers[i+1:]...)
break
}
}
} else if _, ok := c.config.Current.InterfacePeers[sintf]; ok {
for i, peer := range c.config.Current.InterfacePeers[sintf] {
if peer == addr {
c.config.Current.InterfacePeers[sintf] = append(c.config.Current.InterfacePeers[sintf][:i], c.config.Current.InterfacePeers[sintf][i+1:]...)
break
}
}
}
panic("TODO") // Get the net.Conn to this peer (if any) and close it
c.peers.Act(nil, func() {
ports := c.peers.ports
for _, peer := range ports {
if addr == peer.intf.name() {
c.peers._removePeer(peer)
}
}
})
return nil
} }
*/
// CallPeer calls a peer once. This should be specified in the peer URI format, // CallPeer calls a peer once. This should be specified in the peer URI format,
// e.g.: // e.g.:
// // tcp://a.b.c.d:e
// tcp://a.b.c.d:e // socks://a.b.c.d:e/f.g.h.i:j
// 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 // This does not add the peer to the peer list, so if the connection drops, the
// peer will not be called again automatically. // peer will not be called again automatically.
func (c *Core) CallPeer(u *url.URL, sintf string) error { func (c *Core) CallPeer(u *url.URL, sintf string) error {
return c.links.add(u, sintf, linkTypeEphemeral) return c.links.call(u, sintf)
} }
func (c *Core) PublicKey() ed25519.PublicKey { func (c *Core) PublicKey() ed25519.PublicKey {
return c.public return c.public
} }
func (c *Core) MaxMTU() uint64 {
return c.store.maxSessionMTU()
}
func (c *Core) SetMTU(mtu uint64) {
if mtu < 1280 {
mtu = 1280
}
c.store.mutex.Lock()
c.store.mtu = mtu
c.store.mutex.Unlock()
}
func (c *Core) MTU() uint64 {
c.store.mutex.Lock()
mtu := c.store.mtu
c.store.mutex.Unlock()
return mtu
}
// Implement io.ReadWriteCloser
func (c *Core) Read(p []byte) (n int, err error) {
n, err = c.store.readPC(p)
return
}
func (c *Core) Write(p []byte) (n int, err error) {
n, err = c.store.writePC(p)
return
}
func (c *Core) Close() error {
c.Stop()
return nil
}
// Hack to get the admin stuff working, TODO something cleaner // Hack to get the admin stuff working, TODO something cleaner
type AddHandler interface { type AddHandler interface {
AddHandler(name, desc string, args []string, handlerfunc AddHandlerFunc) error AddHandler(name string, args []string, handlerfunc func(json.RawMessage) (interface{}, error)) error
} }
type AddHandlerFunc func(json.RawMessage) (interface{}, error)
// SetAdmin must be called after Init and before Start. // SetAdmin must be called after Init and before Start.
// It sets the admin handler for NodeInfo and the Debug admin functions. // It sets the admin handler for NodeInfo and the Debug admin functions.
func (c *Core) SetAdmin(a AddHandler) error { func (c *Core) SetAdmin(a AddHandler) error {
if err := a.AddHandler( if err := a.AddHandler("getNodeInfo", []string{"key"}, c.proto.nodeinfo.nodeInfoAdminHandler); err != nil {
"getNodeInfo", "Request nodeinfo from a remote node by its public key", []string{"key"},
c.proto.nodeinfo.nodeInfoAdminHandler,
); err != nil {
return err return err
} }
if err := a.AddHandler( if err := a.AddHandler("debug_remoteGetSelf", []string{"key"}, c.proto.getSelfHandler); err != nil {
"debug_remoteGetSelf", "Debug use only", []string{"key"},
c.proto.getSelfHandler,
); err != nil {
return err return err
} }
if err := a.AddHandler( if err := a.AddHandler("debug_remoteGetPeers", []string{"key"}, c.proto.getPeersHandler); err != nil {
"debug_remoteGetPeers", "Debug use only", []string{"key"},
c.proto.getPeersHandler,
); err != nil {
return err return err
} }
if err := a.AddHandler( if err := a.AddHandler("debug_remoteGetDHT", []string{"key"}, c.proto.getDHTHandler); err != nil {
"debug_remoteGetTree", "Debug use only", []string{"key"},
c.proto.getTreeHandler,
); err != nil {
return err return err
} }
return nil return nil

View file

@ -3,20 +3,19 @@ package core
import ( import (
"context" "context"
"crypto/ed25519" "crypto/ed25519"
"crypto/tls" "encoding/hex"
"errors"
"fmt" "fmt"
"io" "io/ioutil"
"net"
"net/url" "net/url"
"time" "time"
iwe "github.com/Arceliar/ironwood/encrypted" iw "github.com/Arceliar/ironwood/encrypted"
iwn "github.com/Arceliar/ironwood/network"
iwt "github.com/Arceliar/ironwood/types"
"github.com/Arceliar/phony" "github.com/Arceliar/phony"
"github.com/gologme/log" "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/version" "github.com/yggdrasil-network/yggdrasil-go/src/version"
) )
@ -27,36 +26,113 @@ type Core struct {
// We're going to keep our own copy of the provided config - that way we can // 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 // guarantee that it will be covered by the mutex
phony.Inbox phony.Inbox
*iwe.PacketConn pc *iw.PacketConn
ctx context.Context config *config.NodeConfig // Config
cancel context.CancelFunc
secret ed25519.PrivateKey secret ed25519.PrivateKey
public ed25519.PublicKey public ed25519.PublicKey
links links links links
proto protoHandler proto protoHandler
log Logger store keyStore
log *log.Logger
addPeerTimer *time.Timer addPeerTimer *time.Timer
config struct { ctx context.Context
tls *tls.Config // immutable after startup ctxCancel context.CancelFunc
//_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) { func (c *Core) _init() error {
c := &Core{ // TODO separate init and start functions
log: logger, // Init sets up structs
} // Start launches goroutines that depend on structs being set up
c.ctx, c.cancel = context.WithCancel(context.Background()) // This is pretty much required to completely avoid race conditions
c.config.RLock()
defer c.config.RUnlock()
if c.log == nil { if c.log == nil {
c.log = log.New(io.Discard, "", 0) c.log = log.New(ioutil.Discard, "", 0)
} }
sigPriv, err := hex.DecodeString(c.config.PrivateKey)
if err != nil {
return err
}
if len(sigPriv) < ed25519.PrivateKeySize {
return errors.New("PrivateKey is incorrect length")
}
c.secret = ed25519.PrivateKey(sigPriv)
c.public = c.secret.Public().(ed25519.PublicKey)
// TODO check public against current.PublicKey, error if they don't match
c.pc, err = iw.NewPacketConn(c.secret)
c.ctx, c.ctxCancel = context.WithCancel(context.Background())
c.store.init(c)
c.proto.init(c)
if err := c.proto.nodeinfo.setNodeInfo(c.config.NodeInfo, c.config.NodeInfoPrivacy); err != nil {
return fmt.Errorf("setNodeInfo: %w", err)
}
return err
}
// If any static peers were provided in the configuration above then we should
// configure them. The loop ensures that disconnected peers will eventually
// be reconnected with.
func (c *Core) _addPeerLoop() {
c.config.RLock()
defer c.config.RUnlock()
if c.addPeerTimer == nil {
return
}
// Add peers from the Peers section
for _, peer := range c.config.Peers {
go func(peer string, intf string) {
u, err := url.Parse(peer)
if err != nil {
c.log.Errorln("Failed to parse peer url:", peer, err)
}
if err := c.CallPeer(u, intf); err != nil {
c.log.Errorln("Failed to add peer:", err)
}
}(peer, "") // TODO: this should be acted and not in a goroutine?
}
// Add peers from the InterfacePeers section
for intf, intfpeers := range c.config.InterfacePeers {
for _, peer := range intfpeers {
go func(peer string, intf string) {
u, err := url.Parse(peer)
if err != nil {
c.log.Errorln("Failed to parse peer url:", peer, err)
}
if err := c.CallPeer(u, intf); err != nil {
c.log.Errorln("Failed to add peer:", err)
}
}(peer, intf) // TODO: this should be acted and not in a goroutine?
}
}
c.addPeerTimer = time.AfterFunc(time.Minute, func() {
c.Act(nil, c._addPeerLoop)
})
}
// Start starts up Yggdrasil using the provided config.NodeConfig, and outputs
// debug logging through the provided log.Logger. The started stack will include
// TCP and UDP sockets, a multicast discovery socket, an admin socket, router,
// switch and DHT node. A config.NodeState is returned which contains both the
// current and previous configurations (from reconfigures).
func (c *Core) Start(nc *config.NodeConfig, log *log.Logger) (err error) {
phony.Block(c, func() {
err = c._start(nc, log)
})
return
}
// This function is unsafe and should only be ran by the core actor.
func (c *Core) _start(nc *config.NodeConfig, log *log.Logger) error {
c.log = log
c.config = nc
if name := version.BuildName(); name != "unknown" { if name := version.BuildName(); name != "unknown" {
c.log.Infoln("Build name:", name) c.log.Infoln("Build name:", name)
} }
@ -64,189 +140,44 @@ func New(cert *tls.Certificate, logger Logger, opts ...SetupOption) (*Core, erro
c.log.Infoln("Build version:", version) c.log.Infoln("Build version:", version)
} }
var err error c.log.Infoln("Starting up...")
c.config._listeners = map[ListenAddress]struct{}{} if err := c._init(); err != nil {
c.config._allowedPublicKeys = map[[32]byte]struct{}{} c.log.Errorln("Failed to initialize core")
for _, opt := range opts { return err
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 { if err := c.links.init(c); err != nil {
return nil, fmt.Errorf("error initialising links: %w", err) c.log.Errorln("Failed to start link interfaces")
return 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() { c.addPeerTimer = time.AfterFunc(0, func() {
phony.Block(&c.links, func() { c.Act(nil, c._addPeerLoop)
for _, l := range c.links._links {
select {
case l.kick <- struct{}{}:
default:
}
}
}) })
c.log.Infoln("Startup complete")
return nil
} }
// Stop shuts down the Yggdrasil node. // Stop shuts down the Yggdrasil node.
func (c *Core) Stop() { func (c *Core) Stop() {
phony.Block(c, func() { phony.Block(c, c._stop)
c.log.Infoln("Stopping...")
_ = c._close()
c.log.Infoln("Stopped")
})
} }
// This function is unsafe and should only be ran by the core actor. // This function is unsafe and should only be ran by the core actor.
func (c *Core) _close() error { func (c *Core) _stop() {
c.cancel() c.log.Infoln("Stopping...")
c.links.shutdown() c.ctxCancel()
err := c.PacketConn.Close() c.pc.Close()
if c.addPeerTimer != nil { if c.addPeerTimer != nil {
c.addPeerTimer.Stop() c.addPeerTimer.Stop()
c.addPeerTimer = nil c.addPeerTimer = nil
} }
return err _ = c.links.stop()
} /* FIXME this deadlocks, need a waitgroup or something to coordinate shutdown
for _, peer := range c.GetPeers() {
func (c *Core) MTU() uint64 { c.DisconnectPeer(peer.Port)
const sessionTypeOverhead = 1
MTU := c.PacketConn.MTU() - sessionTypeOverhead
if MTU > 65535 {
MTU = 65535
} }
return MTU */
} c.log.Infoln("Stopped")
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

@ -2,16 +2,28 @@ package core
import ( import (
"bytes" "bytes"
"crypto/rand" "math/rand"
"net/url" "net/url"
"os" "os"
"testing" "testing"
"time" "time"
"github.com/gologme/log" "github.com/gologme/log"
"github.com/yggdrasil-network/yggdrasil-go/src/config" "github.com/yggdrasil-network/yggdrasil-go/src/config"
"github.com/yggdrasil-network/yggdrasil-go/src/defaults"
) )
// GenerateConfig produces default configuration with suitable modifications for tests.
func GenerateConfig() *config.NodeConfig {
cfg := defaults.GenerateConfig()
cfg.AdminListen = "none"
cfg.Listen = []string{"tcp://127.0.0.1:0"}
cfg.IfName = "none"
return cfg
}
// GetLoggerWithPrefix creates a new logger instance with prefix. // GetLoggerWithPrefix creates a new logger instance with prefix.
// If verbose is set to true, three log levels are enabled: "info", "warn", "error". // If verbose is set to true, three log levels are enabled: "info", "warn", "error".
func GetLoggerWithPrefix(prefix string, verbose bool) *log.Logger { func GetLoggerWithPrefix(prefix string, verbose bool) *log.Logger {
@ -25,65 +37,29 @@ func GetLoggerWithPrefix(prefix string, verbose bool) *log.Logger {
return l 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. // CreateAndConnectTwo creates two nodes. nodeB connects to nodeA.
// Verbosity flag is passed to logger. // Verbosity flag is passed to logger.
func CreateAndConnectTwo(t testing.TB, verbose bool) (nodeA *Core, nodeB *Core) { func CreateAndConnectTwo(t testing.TB, verbose bool) (nodeA *Core, nodeB *Core) {
var err error nodeA = new(Core)
if err := nodeA.Start(GenerateConfig(), GetLoggerWithPrefix("A: ", verbose)); err != nil {
cfgA, cfgB := config.GenerateConfig(), config.GenerateConfig()
if err = cfgA.GenerateSelfSignedCertificate(); err != nil {
t.Fatal(err) t.Fatal(err)
} }
if err = cfgB.GenerateSelfSignedCertificate(); err != nil { nodeA.SetMTU(1500)
nodeB = new(Core)
if err := nodeB.Start(GenerateConfig(), GetLoggerWithPrefix("B: ", verbose)); err != nil {
t.Fatal(err) t.Fatal(err)
} }
nodeB.SetMTU(1500)
logger := GetLoggerWithPrefix("", false) u, err := url.Parse("tcp://" + nodeA.links.tcp.getAddr().String())
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 { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
nodeAListener, err := nodeA.Listen(nodeAListenURL, "") err = nodeB.CallPeer(u, "")
if err != nil { if err != nil {
t.Fatal(err) 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) time.Sleep(100 * time.Millisecond)
@ -102,13 +78,7 @@ func WaitConnected(nodeA, nodeB *Core) bool {
// It may take up to 3 seconds, but let's wait 5. // It may take up to 3 seconds, but let's wait 5.
for i := 0; i < 50; i++ { for i := 0; i < 50; i++ {
time.Sleep(100 * time.Millisecond) time.Sleep(100 * time.Millisecond)
/* if len(nodeA.GetPeers()) > 0 && len(nodeB.GetPeers()) > 0 {
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 true
} }
} }
@ -124,7 +94,7 @@ func CreateEchoListener(t testing.TB, nodeA *Core, bufLen int, repeats int) chan
buf := make([]byte, bufLen) buf := make([]byte, bufLen)
res := make([]byte, bufLen) res := make([]byte, bufLen)
for i := 0; i < repeats; i++ { for i := 0; i < repeats; i++ {
n, from, err := nodeA.ReadFrom(buf) n, err := nodeA.Read(buf)
if err != nil { if err != nil {
t.Error(err) t.Error(err)
return return
@ -136,7 +106,7 @@ func CreateEchoListener(t testing.TB, nodeA *Core, bufLen int, repeats int) chan
copy(res, buf) copy(res, buf)
copy(res[8:24], buf[24:40]) copy(res[8:24], buf[24:40])
copy(res[24:40], buf[8:24]) copy(res[24:40], buf[8:24])
_, err = nodeA.WriteTo(res, from) _, err = nodeA.Write(res)
if err != nil { if err != nil {
t.Error(err) t.Error(err)
} }
@ -167,16 +137,16 @@ func TestCore_Start_Transfer(t *testing.T) {
// Send // Send
msg := make([]byte, msgLen) msg := make([]byte, msgLen)
_, _ = rand.Read(msg[40:]) rand.Read(msg[40:])
msg[0] = 0x60 msg[0] = 0x60
copy(msg[8:24], nodeB.Address()) copy(msg[8:24], nodeB.Address())
copy(msg[24:40], nodeA.Address()) copy(msg[24:40], nodeA.Address())
_, err := nodeB.WriteTo(msg, nodeA.LocalAddr()) _, err := nodeB.Write(msg)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
buf := make([]byte, msgLen) buf := make([]byte, msgLen)
_, _, err = nodeB.ReadFrom(buf) _, err = nodeB.Read(buf)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@ -199,7 +169,7 @@ func BenchmarkCore_Start_Transfer(b *testing.B) {
// Send // Send
msg := make([]byte, msgLen) msg := make([]byte, msgLen)
_, _ = rand.Read(msg[40:]) rand.Read(msg[40:])
msg[0] = 0x60 msg[0] = 0x60
copy(msg[8:24], nodeB.Address()) copy(msg[8:24], nodeB.Address())
copy(msg[24:40], nodeA.Address()) copy(msg[24:40], nodeA.Address())
@ -209,82 +179,15 @@ func BenchmarkCore_Start_Transfer(b *testing.B) {
b.SetBytes(int64(msgLen)) b.SetBytes(int64(msgLen))
b.ResetTimer() b.ResetTimer()
addr := nodeA.LocalAddr()
for i := 0; i < b.N; i++ { for i := 0; i < b.N; i++ {
_, err := nodeB.WriteTo(msg, addr) _, err := nodeB.Write(msg)
if err != nil { if err != nil {
b.Fatal(err) b.Fatal(err)
} }
_, _, err = nodeB.ReadFrom(buf) _, err = nodeB.Read(buf)
if err != nil { if err != nil {
b.Fatal(err) b.Fatal(err)
} }
} }
<-done <-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 +1,33 @@
// +build debug
package core package core
import ( import "fmt"
"fmt"
"net/http"
_ "net/http/pprof"
"os"
)
// Start the profiler if the required environment variable is set. import _ "net/http/pprof"
import "net/http"
import "runtime"
import "os"
import "github.com/gologme/log"
// Start the profiler in debug builds, if the required environment variable is set.
func init() { func init() {
envVarName := "PPROFLISTEN" envVarName := "PPROFLISTEN"
if hostPort := os.Getenv(envVarName); hostPort != "" { hostPort := os.Getenv(envVarName)
switch {
case hostPort == "":
fmt.Fprintf(os.Stderr, "DEBUG: %s not set, profiler not started.\n", envVarName)
default:
fmt.Fprintf(os.Stderr, "DEBUG: Starting pprof on %s\n", hostPort) fmt.Fprintf(os.Stderr, "DEBUG: Starting pprof on %s\n", hostPort)
go func() { go func() { fmt.Println(http.ListenAndServe(hostPort, nil)) }()
fmt.Fprintf(os.Stderr, "DEBUG: %s", http.ListenAndServe(hostPort, nil))
}()
} }
} }
// Starts the function profiler. This is only supported when built with
// '-tags build'.
func StartProfiler(log *log.Logger) error {
runtime.SetBlockProfileRate(1)
go func() { log.Println(http.ListenAndServe("localhost:6060", nil)) }()
return nil
}

View file

@ -1,4 +1,4 @@
package ipv6rwc package core
// The ICMPv6 module implements functions to easily create ICMPv6 // The ICMPv6 module implements functions to easily create ICMPv6
// packets. These functions, when mixed with the built-in Go IPv6 // packets. These functions, when mixed with the built-in Go IPv6

View file

@ -1,4 +1,4 @@
package ipv6rwc package core
import ( import (
"crypto/ed25519" "crypto/ed25519"
@ -14,24 +14,14 @@ import (
iwt "github.com/Arceliar/ironwood/types" iwt "github.com/Arceliar/ironwood/types"
"github.com/yggdrasil-network/yggdrasil-go/src/address" "github.com/yggdrasil-network/yggdrasil-go/src/address"
"github.com/yggdrasil-network/yggdrasil-go/src/core"
) )
const keyStoreTimeout = 2 * time.Minute const keyStoreTimeout = 2 * time.Minute
/*
// Out-of-band packet types
const (
typeKeyDummy = iota // nolint:deadcode,varcheck
typeKeyLookup
typeKeyResponse
)
*/
type keyArray [ed25519.PublicKeySize]byte type keyArray [ed25519.PublicKeySize]byte
type keyStore struct { type keyStore struct {
core *core.Core core *Core
address address.Address address address.Address
subnet address.Subnet subnet address.Subnet
mutex sync.Mutex mutex sync.Mutex
@ -55,17 +45,14 @@ type buffer struct {
timeout *time.Timer timeout *time.Timer
} }
func (k *keyStore) init(c *core.Core) { func (k *keyStore) init(core *Core) {
k.core = c k.core = core
k.address = *address.AddrForKey(k.core.PublicKey()) k.address = *address.AddrForKey(k.core.public)
k.subnet = *address.SubnetForKey(k.core.PublicKey()) k.subnet = *address.SubnetForKey(k.core.public)
/*if err := k.core.SetOutOfBandHandler(k.oobHandler); err != nil { if err := k.core.pc.SetOutOfBandHandler(k.oobHandler); err != nil {
err = fmt.Errorf("tun.core.SetOutOfBandHander: %w", err) err = fmt.Errorf("tun.core.SetOutOfBandHander: %w", err)
panic(err) panic(err)
}*/ }
k.core.SetPathNotify(func(key ed25519.PublicKey) {
k.update(key)
})
k.keyToInfo = make(map[keyArray]*keyInfo) k.keyToInfo = make(map[keyArray]*keyInfo)
k.addrToInfo = make(map[address.Address]*keyInfo) k.addrToInfo = make(map[address.Address]*keyInfo)
k.addrBuffer = make(map[address.Address]*buffer) k.addrBuffer = make(map[address.Address]*buffer)
@ -79,7 +66,7 @@ func (k *keyStore) sendToAddress(addr address.Address, bs []byte) {
if info := k.addrToInfo[addr]; info != nil { if info := k.addrToInfo[addr]; info != nil {
k.resetTimeout(info) k.resetTimeout(info)
k.mutex.Unlock() k.mutex.Unlock()
_, _ = k.core.WriteTo(bs, iwt.Addr(info.key[:])) _, _ = k.core.pc.WriteTo(bs, iwt.Addr(info.key[:]))
} else { } else {
var buf *buffer var buf *buffer
if buf = k.addrBuffer[addr]; buf == nil { if buf = k.addrBuffer[addr]; buf == nil {
@ -108,7 +95,7 @@ func (k *keyStore) sendToSubnet(subnet address.Subnet, bs []byte) {
if info := k.subnetToInfo[subnet]; info != nil { if info := k.subnetToInfo[subnet]; info != nil {
k.resetTimeout(info) k.resetTimeout(info)
k.mutex.Unlock() k.mutex.Unlock()
_, _ = k.core.WriteTo(bs, iwt.Addr(info.key[:])) _, _ = k.core.pc.WriteTo(bs, iwt.Addr(info.key[:]))
} else { } else {
var buf *buffer var buf *buffer
if buf = k.subnetBuffer[subnet]; buf == nil { if buf = k.subnetBuffer[subnet]; buf == nil {
@ -137,7 +124,6 @@ func (k *keyStore) update(key ed25519.PublicKey) *keyInfo {
var kArray keyArray var kArray keyArray
copy(kArray[:], key) copy(kArray[:], key)
var info *keyInfo var info *keyInfo
var packets [][]byte
if info = k.keyToInfo[kArray]; info == nil { if info = k.keyToInfo[kArray]; info == nil {
info = new(keyInfo) info = new(keyInfo)
info.key = kArray info.key = kArray
@ -146,19 +132,19 @@ func (k *keyStore) update(key ed25519.PublicKey) *keyInfo {
k.keyToInfo[info.key] = info k.keyToInfo[info.key] = info
k.addrToInfo[info.address] = info k.addrToInfo[info.address] = info
k.subnetToInfo[info.subnet] = info k.subnetToInfo[info.subnet] = info
k.resetTimeout(info)
k.mutex.Unlock()
if buf := k.addrBuffer[info.address]; buf != nil { if buf := k.addrBuffer[info.address]; buf != nil {
packets = append(packets, buf.packet) k.core.pc.WriteTo(buf.packet, iwt.Addr(info.key[:]))
delete(k.addrBuffer, info.address) delete(k.addrBuffer, info.address)
} }
if buf := k.subnetBuffer[info.subnet]; buf != nil { if buf := k.subnetBuffer[info.subnet]; buf != nil {
packets = append(packets, buf.packet) k.core.pc.WriteTo(buf.packet, iwt.Addr(info.key[:]))
delete(k.subnetBuffer, info.subnet) delete(k.subnetBuffer, info.subnet)
} }
} } else {
k.resetTimeout(info) k.resetTimeout(info)
k.mutex.Unlock() k.mutex.Unlock()
for _, packet := range packets {
_, _ = k.core.WriteTo(packet, iwt.Addr(info.key[:]))
} }
return info return info
} }
@ -182,8 +168,7 @@ func (k *keyStore) resetTimeout(info *keyInfo) {
}) })
} }
/* func (k *keyStore) oobHandler(fromKey, toKey ed25519.PublicKey, data []byte) {
func (k *keyStore) oobHandler(fromKey, toKey ed25519.PublicKey, data []byte) { // nolint:unused
if len(data) != 1+ed25519.SignatureSize { if len(data) != 1+ed25519.SignatureSize {
return return
} }
@ -204,39 +189,48 @@ func (k *keyStore) oobHandler(fromKey, toKey ed25519.PublicKey, data []byte) { /
} }
} }
} }
*/
func (k *keyStore) sendKeyLookup(partial ed25519.PublicKey) { func (k *keyStore) sendKeyLookup(partial ed25519.PublicKey) {
/* sig := ed25519.Sign(k.core.secret, partial[:])
sig := ed25519.Sign(k.core.PrivateKey(), partial[:]) bs := append([]byte{typeKeyLookup}, sig...)
bs := append([]byte{typeKeyLookup}, sig...) _ = k.core.pc.SendOutOfBand(partial, bs)
//_ = k.core.SendOutOfBand(partial, bs)
_ = bs
*/
k.core.SendLookup(partial)
} }
/* func (k *keyStore) sendKeyResponse(dest ed25519.PublicKey) {
func (k *keyStore) sendKeyResponse(dest ed25519.PublicKey) { // nolint:unused sig := ed25519.Sign(k.core.secret, dest[:])
sig := ed25519.Sign(k.core.PrivateKey(), dest[:])
bs := append([]byte{typeKeyResponse}, sig...) bs := append([]byte{typeKeyResponse}, sig...)
//_ = k.core.SendOutOfBand(dest, bs) _ = k.core.pc.SendOutOfBand(dest, bs)
_ = bs }
func (k *keyStore) maxSessionMTU() uint64 {
const sessionTypeOverhead = 1
return k.core.pc.MTU() - sessionTypeOverhead
} }
*/
func (k *keyStore) readPC(p []byte) (int, error) { func (k *keyStore) readPC(p []byte) (int, error) {
buf := make([]byte, k.core.MTU(), 65535) buf := make([]byte, k.core.pc.MTU(), 65535)
for { for {
bs := buf bs := buf
n, from, err := k.core.ReadFrom(bs) n, from, err := k.core.pc.ReadFrom(bs)
if err != nil { if err != nil {
return n, err return n, err
} }
if n == 0 { if n == 0 {
continue continue
} }
bs = bs[:n] 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]...)
k.core.proto.handleProto(nil, key, data)
continue
default:
continue
}
bs = bs[1:n]
if len(bs) == 0 { if len(bs) == 0 {
continue continue
} }
@ -251,11 +245,11 @@ func (k *keyStore) readPC(p []byte) (int, error) {
k.mutex.Unlock() k.mutex.Unlock()
if len(bs) > mtu { if len(bs) > mtu {
// Using bs would make it leak off the stack, so copy to buf // Using bs would make it leak off the stack, so copy to buf
buf := make([]byte, 512) buf := make([]byte, 40)
cn := copy(buf, bs) copy(buf, bs)
ptb := &icmp.PacketTooBig{ ptb := &icmp.PacketTooBig{
MTU: mtu, MTU: mtu,
Data: buf[:cn], Data: buf[:40],
} }
if packet, err := CreateICMPv6(buf[8:24], buf[24:40], ipv6.ICMPTypePacketTooBig, 0, ptb); err == nil { if packet, err := CreateICMPv6(buf[8:24], buf[24:40], ipv6.ICMPTypePacketTooBig, 0, ptb); err == nil {
_, _ = k.writePC(packet) _, _ = k.writePC(packet)
@ -300,69 +294,15 @@ func (k *keyStore) writePC(bs []byte) (int, error) {
strErr := fmt.Sprint("incorrect source address: ", net.IP(srcAddr[:]).String()) strErr := fmt.Sprint("incorrect source address: ", net.IP(srcAddr[:]).String())
return 0, errors.New(strErr) return 0, errors.New(strErr)
} }
buf := make([]byte, 1+len(bs), 65535)
buf[0] = typeSessionTraffic
copy(buf[1:], bs)
if dstAddr.IsValid() { if dstAddr.IsValid() {
k.sendToAddress(dstAddr, bs) k.sendToAddress(dstAddr, buf)
} else if dstSubnet.IsValid() { } else if dstSubnet.IsValid() {
k.sendToSubnet(dstSubnet, bs) k.sendToSubnet(dstSubnet, buf)
} else { } else {
return 0, errors.New("invalid destination address") return 0, errors.New("invalid destination address")
} }
return len(bs), nil 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
}

View file

@ -1,767 +1,251 @@
package core package core
import ( import (
"bytes" "crypto/ed25519"
"context"
"encoding/hex" "encoding/hex"
"errors" "errors"
"fmt" "fmt"
"io" "io"
"net" "net"
"net/url" "net/url"
"strconv"
"strings" "strings"
"sync/atomic" "sync"
//"sync/atomic"
"time" "time"
"github.com/Arceliar/phony"
"github.com/yggdrasil-network/yggdrasil-go/src/address" "github.com/yggdrasil-network/yggdrasil-go/src/address"
"golang.org/x/crypto/blake2b" "github.com/yggdrasil-network/yggdrasil-go/src/util"
"golang.org/x/net/proxy"
//"github.com/Arceliar/phony" // TODO? use instead of mutexes
) )
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 { type links struct {
phony.Inbox core *Core
core *Core mutex sync.RWMutex // protects links below
tcp *linkTCP // TCP interface support links map[linkInfo]*link
tls *linkTLS // TLS interface support tcp tcp // TCP interface support
unix *linkUNIX // UNIX interface support stopped chan struct{}
socks *linkSOCKS // SOCKS interface support // TODO timeout (to remove from switch), read from config.ReadTimeout
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 // linkInfo is used as a map key
type linkInfo struct { type linkInfo struct {
uri string // Peering URI in complete form key keyArray
sintf string // Peering source interface (i.e. from InterfacePeers) linkType string // Type of link, e.g. TCP, AWDL
local string // Local name or address
remote string // Remote name or address
} }
// link tracks the state of a connection, either persistent or non-persistent
type link struct { type link struct {
ctx context.Context // Connection context lname string
cancel context.CancelFunc // Stop future redial attempts (when peer removed) links *links
kick chan struct{} // Attempt to reconnect now, if backing off conn net.Conn
linkType linkType // Type of link, i.e. outbound/inbound, persistent/ephemeral options linkOptions
linkProto string // Protocol carrier of link, e.g. TCP, AWDL info linkInfo
// The remaining fields can only be modified safely from within the links actor incoming bool
_conn *linkConn // Connected link, if any, nil if not connected force bool
_err error // Last error on the connection, if any closed chan struct{}
_errtime time.Time // Last time an error occurred
} }
type linkOptions struct { type linkOptions struct {
pinnedEd25519Keys map[keyArray]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 { func (l *links) init(c *Core) error {
l.core = c l.core = c
l.tcp = l.newLinkTCP() l.mutex.Lock()
l.tls = l.newLinkTLS(l.tcp) l.links = make(map[linkInfo]*link)
l.unix = l.newLinkUNIX() l.mutex.Unlock()
l.socks = l.newLinkSOCKS() l.stopped = make(chan struct{})
l.quic = l.newLinkQUIC()
l.ws = l.newLinkWS() if err := l.tcp.init(l); err != nil {
l.wss = l.newLinkWSS() c.log.Errorln("Failed to start TCP interface")
l._links = make(map[linkInfo]*link) return err
l._listeners = make(map[*Listener]context.CancelFunc) }
l.Act(nil, l._updateAverages)
return nil return nil
} }
func (l *links) _updateAverages() { func (l *links) call(u *url.URL, sintf string) error {
select { //u, err := url.Parse(uri)
case <-l.core.ctx.Done(): //if err != nil {
return // return fmt.Errorf("peer %s is not correctly formatted (%s)", uri, err)
default: //}
tcpOpts := tcpOptions{}
if pubkeys, ok := u.Query()["key"]; ok && len(pubkeys) > 0 {
tcpOpts.pinnedEd25519Keys = make(map[keyArray]struct{})
for _, pubkey := range pubkeys {
if sigPub, err := hex.DecodeString(pubkey); err == nil {
var sigPubKey keyArray
copy(sigPubKey[:], sigPub)
tcpOpts.pinnedEd25519Keys[sigPubKey] = struct{}{}
}
}
} }
switch u.Scheme {
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": case "tcp":
protocol = l.tcp l.tcp.call(u.Host, tcpOpts, sintf)
case "socks":
tcpOpts.socksProxyAddr = u.Host
if u.User != nil {
tcpOpts.socksProxyAuth = &proxy.Auth{}
tcpOpts.socksProxyAuth.User = u.User.Username()
tcpOpts.socksProxyAuth.Password, _ = u.User.Password()
}
tcpOpts.upgrade = l.tcp.tls.forDialer // TODO make this configurable
pathtokens := strings.Split(strings.Trim(u.Path, "/"), "/")
l.tcp.call(pathtokens[0], tcpOpts, sintf)
case "tls": case "tls":
protocol = l.tls tcpOpts.upgrade = l.tcp.tls.forDialer
case "unix": l.tcp.call(u.Host, tcpOpts, sintf)
protocol = l.unix
case "quic":
protocol = l.quic
case "ws":
protocol = l.ws
case "wss":
protocol = l.wss
default: default:
ctxcancel() return errors.New("unknown call scheme: " + u.Scheme)
return nil, ErrLinkUnrecognisedSchema }
return nil
}
func (l *links) create(conn net.Conn, name, linkType, local, remote string, incoming, force bool, options linkOptions) (*link, error) {
// Technically anything unique would work for names, but let's pick something human readable, just for debugging
intf := link{
conn: conn,
lname: name,
links: l,
options: options,
info: linkInfo{
linkType: linkType,
local: local,
remote: remote,
},
incoming: incoming,
force: force,
}
return &intf, nil
}
func (l *links) stop() error {
close(l.stopped)
if err := l.tcp.stop(); err != nil {
return err
}
return nil
}
func (intf *link) handler() (chan struct{}, error) {
// TODO split some of this into shorter functions, so it's easier to read, and for the FIXME duplicate peer issue mentioned later
defer intf.conn.Close()
meta := version_getBaseMetadata()
meta.key = intf.links.core.public
metaBytes := meta.encode()
// TODO timeouts on send/recv (goroutine for send/recv, channel select w/ timer)
var err error
if !util.FuncTimeout(30*time.Second, func() {
var n int
n, err = intf.conn.Write(metaBytes)
if err == nil && n != len(metaBytes) {
err = errors.New("incomplete metadata send")
}
}) {
return nil, errors.New("timeout on metadata send")
} }
listener, err := protocol.listen(ctx, u, sintf)
if err != nil { if err != nil {
ctxcancel()
return nil, err return nil, err
} }
addr := listener.Addr() if !util.FuncTimeout(30*time.Second, func() {
cancel := func() { var n int
ctxcancel() n, err = io.ReadFull(intf.conn, metaBytes)
if err := listener.Close(); err != nil && !errors.Is(err, net.ErrClosed) { if err == nil && n != len(metaBytes) {
l.core.log.Warnf("Error closing %s listener %s: %s", strings.ToUpper(u.Scheme), addr, err) err = errors.New("incomplete metadata recv")
} }
}) {
return nil, errors.New("timeout on metadata recv")
} }
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 { if err != nil {
return fmt.Errorf("failed to generate handshake: %w", err) return nil, 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{} meta = version_metadata{}
base := version_getBaseMetadata() base := version_getBaseMetadata()
if err := meta.decode(conn, options.password); err != nil { if !meta.decode(metaBytes) {
_ = conn.Close() return nil, errors.New("failed to decode metadata")
return err
} }
if !meta.check() { if !meta.check() {
return fmt.Errorf("remote node incompatible version (local %s, remote %s)", intf.links.core.log.Errorf("Failed to connect to node: %s is incompatible version (local %s, remote %s)",
fmt.Sprintf("%d.%d", base.majorVer, base.minorVer), intf.lname,
fmt.Sprintf("%d.%d", meta.majorVer, meta.minorVer), fmt.Sprintf("%d.%d", base.ver, base.minorVer),
fmt.Sprintf("%d.%d", meta.ver, meta.minorVer),
) )
} return nil, errors.New("remote node is incompatible version")
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 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. // check - in future versions we really should check a signature or something like that.
if pinned := options.pinnedEd25519Keys; len(pinned) > 0 { if pinned := intf.options.pinnedEd25519Keys; pinned != nil {
var key keyArray var key keyArray
copy(key[:], meta.publicKey) copy(key[:], meta.key)
if _, allowed := pinned[key]; !allowed { if _, allowed := pinned[key]; !allowed {
return fmt.Errorf("node public key that does not match pinned keys") intf.links.core.log.Errorf("Failed to connect to node: %q sent ed25519 key that does not match pinned keys", intf.name())
return nil, fmt.Errorf("failed to connect: host sent ed25519 key that does not match pinned keys")
} }
} }
// Check if we're authorized to connect to this key / IP // Check if we're authorized to connect to this key / IP
if !local { intf.links.core.config.RLock()
var allowed map[[32]byte]struct{} allowed := intf.links.core.config.AllowedPublicKeys
phony.Block(l.core, func() { intf.links.core.config.RUnlock()
allowed = l.core.config._allowedPublicKeys isallowed := len(allowed) == 0
}) for _, k := range allowed {
isallowed := len(allowed) == 0 if k == hex.EncodeToString(meta.key) { // TODO: this is yuck
for k := range allowed { isallowed = true
if bytes.Equal(k[:], meta.publicKey) { break
isallowed = true
break
}
}
if linkType == linkTypeIncoming && !isallowed {
return fmt.Errorf("node public key %q is not in AllowedPublicKeys", hex.EncodeToString(meta.publicKey))
} }
} }
if intf.incoming && !intf.force && !isallowed {
dir := "outbound" intf.links.core.log.Warnf("%s connection from %s forbidden: AllowedEncryptionPublicKeys does not contain key %s",
if linkType == linkTypeIncoming { strings.ToUpper(intf.info.linkType), intf.info.remote, hex.EncodeToString(meta.key))
dir = "inbound" intf.close()
return nil, nil
} }
remoteAddr := net.IP(address.AddrForKey(meta.publicKey)[:]).String() // Check if we already have a link to this node
remoteStr := fmt.Sprintf("%s@%s", remoteAddr, conn.RemoteAddr()) copy(intf.info.key[:], meta.key)
localStr := conn.LocalAddr() intf.links.mutex.Lock()
priority := options.priority if oldIntf, isIn := intf.links.links[intf.info]; isIn {
if meta.priority > priority { intf.links.mutex.Unlock()
priority = meta.priority // FIXME we should really return an error and let the caller block instead
// That lets them do things like close connections on its own, avoid printing a connection message in the first place, etc.
intf.links.core.log.Debugln("DEBUG: found existing interface for", intf.name())
return oldIntf.closed, nil
} else {
intf.closed = make(chan struct{})
intf.links.links[intf.info] = intf
defer func() {
intf.links.mutex.Lock()
delete(intf.links.links, intf.info)
intf.links.mutex.Unlock()
close(intf.closed)
}()
intf.links.core.log.Debugln("DEBUG: registered interface for", intf.name())
} }
l.core.log.Infof("Connected %s: %s, source %s", intf.links.mutex.Unlock()
dir, remoteStr, localStr) themAddr := address.AddrForKey(ed25519.PublicKey(intf.info.key[:]))
if success != nil { themAddrString := net.IP(themAddr[:]).String()
success() themString := fmt.Sprintf("%s@%s", themAddrString, intf.info.remote)
} intf.links.core.log.Infof("Connected %s: %s, source %s",
strings.ToUpper(intf.info.linkType), themString, intf.info.local)
err = l.core.HandleConn(meta.publicKey, conn, priority) // Run the handler
switch err { err = intf.links.core.pc.HandleConn(ed25519.PublicKey(intf.info.key[:]), intf.conn)
case io.EOF, net.ErrClosed, nil: // TODO don't report an error if it's just a 'use of closed network connection'
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 { if err != nil {
return nil, err intf.links.core.log.Infof("Disconnected %s: %s, source %s; error: %s",
} strings.ToUpper(intf.info.linkType), themString, intf.info.local, err)
port, err := strconv.Atoi(p) } else {
if err != nil { intf.links.core.log.Infof("Disconnected %s: %s, source %s",
return nil, err strings.ToUpper(intf.info.linkType), themString, intf.info.local)
}
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 return nil, err
} }
func urlForLinkInfo(u url.URL) url.URL { func (intf *link) close() {
u.RawQuery = "" intf.conn.Close()
return u
} }
type linkConn struct { func (intf *link) name() string {
// tx and rx are at the beginning of the struct to ensure 64-bit alignment return intf.lname
// 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

@ -4,24 +4,31 @@ import (
"encoding/hex" "encoding/hex"
"encoding/json" "encoding/json"
"errors" "errors"
"fmt" "net"
"runtime" "runtime"
"strings"
"time" "time"
iwt "github.com/Arceliar/ironwood/types" iwt "github.com/Arceliar/ironwood/types"
"github.com/Arceliar/phony" "github.com/Arceliar/phony"
//"github.com/yggdrasil-network/yggdrasil-go/src/crypto"
"github.com/yggdrasil-network/yggdrasil-go/src/address"
"github.com/yggdrasil-network/yggdrasil-go/src/version" "github.com/yggdrasil-network/yggdrasil-go/src/version"
) )
// NodeInfoPayload represents a RequestNodeInfo response, in bytes.
type NodeInfoPayload []byte
type nodeinfo struct { type nodeinfo struct {
phony.Inbox phony.Inbox
proto *protoHandler proto *protoHandler
myNodeInfo json.RawMessage myNodeInfo NodeInfoPayload
callbacks map[keyArray]nodeinfoCallback callbacks map[keyArray]nodeinfoCallback
} }
type nodeinfoCallback struct { type nodeinfoCallback struct {
call func(nodeinfo json.RawMessage) call func(nodeinfo NodeInfoPayload)
created time.Time created time.Time
} }
@ -50,7 +57,7 @@ func (m *nodeinfo) _cleanup() {
}) })
} }
func (m *nodeinfo) _addCallback(sender keyArray, call func(nodeinfo json.RawMessage)) { func (m *nodeinfo) _addCallback(sender keyArray, call func(nodeinfo NodeInfoPayload)) {
m.callbacks[sender] = nodeinfoCallback{ m.callbacks[sender] = nodeinfoCallback{
created: time.Now(), created: time.Now(),
call: call, call: call,
@ -58,59 +65,71 @@ func (m *nodeinfo) _addCallback(sender keyArray, call func(nodeinfo json.RawMess
} }
// Handles the callback, if there is one // Handles the callback, if there is one
func (m *nodeinfo) _callback(sender keyArray, nodeinfo json.RawMessage) { func (m *nodeinfo) _callback(sender keyArray, nodeinfo NodeInfoPayload) {
if callback, ok := m.callbacks[sender]; ok { if callback, ok := m.callbacks[sender]; ok {
callback.call(nodeinfo) callback.call(nodeinfo)
delete(m.callbacks, sender) delete(m.callbacks, sender)
} }
} }
func (m *nodeinfo) _getNodeInfo() json.RawMessage { func (m *nodeinfo) _getNodeInfo() NodeInfoPayload {
return m.myNodeInfo return m.myNodeInfo
} }
// Set the current node's nodeinfo // Set the current node's nodeinfo
func (m *nodeinfo) setNodeInfo(given map[string]interface{}, privacy bool) (err error) { func (m *nodeinfo) setNodeInfo(given interface{}, privacy bool) (err error) {
phony.Block(m, func() { phony.Block(m, func() {
err = m._setNodeInfo(given, privacy) err = m._setNodeInfo(given, privacy)
}) })
return return
} }
func (m *nodeinfo) _setNodeInfo(given map[string]interface{}, privacy bool) error { func (m *nodeinfo) _setNodeInfo(given interface{}, privacy bool) error {
newnodeinfo := make(map[string]interface{}, len(given)) defaults := map[string]interface{}{
for k, v := range given { "buildname": version.BuildName(),
newnodeinfo[k] = v "buildversion": version.BuildVersion(),
"buildplatform": runtime.GOOS,
"buildarch": runtime.GOARCH,
} }
newnodeinfo := make(map[string]interface{})
if !privacy { if !privacy {
newnodeinfo["buildname"] = version.BuildName() for k, v := range defaults {
newnodeinfo["buildversion"] = version.BuildVersion() newnodeinfo[k] = v
newnodeinfo["buildplatform"] = runtime.GOOS }
newnodeinfo["buildarch"] = runtime.GOARCH }
if nodeinfomap, ok := given.(map[string]interface{}); ok {
for key, value := range nodeinfomap {
if _, ok := defaults[key]; ok {
if strvalue, strok := value.(string); strok && strings.EqualFold(strvalue, "null") || value == nil {
delete(newnodeinfo, key)
}
continue
}
newnodeinfo[key] = value
}
} }
newjson, err := json.Marshal(newnodeinfo) newjson, err := json.Marshal(newnodeinfo)
switch { if err == nil {
case err != nil: if len(newjson) > 16384 {
return fmt.Errorf("NodeInfo marshalling failed: %w", err) return errors.New("NodeInfo exceeds max length of 16384 bytes")
case len(newjson) > 16384: }
return fmt.Errorf("NodeInfo exceeds max length of 16384 bytes")
default:
m.myNodeInfo = newjson m.myNodeInfo = newjson
return nil return nil
} }
return err
} }
func (m *nodeinfo) sendReq(from phony.Actor, key keyArray, callback func(nodeinfo json.RawMessage)) { func (m *nodeinfo) sendReq(from phony.Actor, key keyArray, callback func(nodeinfo NodeInfoPayload)) {
m.Act(from, func() { m.Act(from, func() {
m._sendReq(key, callback) m._sendReq(key, callback)
}) })
} }
func (m *nodeinfo) _sendReq(key keyArray, callback func(nodeinfo json.RawMessage)) { func (m *nodeinfo) _sendReq(key keyArray, callback func(nodeinfo NodeInfoPayload)) {
if callback != nil { if callback != nil {
m._addCallback(key, callback) m._addCallback(key, callback)
} }
_, _ = m.proto.core.PacketConn.WriteTo([]byte{typeSessionProto, typeProtoNodeInfoRequest}, iwt.Addr(key[:])) _, _ = m.proto.core.pc.WriteTo([]byte{typeSessionProto, typeProtoNodeInfoRequest}, iwt.Addr(key[:]))
} }
func (m *nodeinfo) handleReq(from phony.Actor, key keyArray) { func (m *nodeinfo) handleReq(from phony.Actor, key keyArray) {
@ -119,7 +138,7 @@ func (m *nodeinfo) handleReq(from phony.Actor, key keyArray) {
}) })
} }
func (m *nodeinfo) handleRes(from phony.Actor, key keyArray, info json.RawMessage) { func (m *nodeinfo) handleRes(from phony.Actor, key keyArray, info NodeInfoPayload) {
m.Act(from, func() { m.Act(from, func() {
m._callback(key, info) m._callback(key, info)
}) })
@ -127,7 +146,7 @@ func (m *nodeinfo) handleRes(from phony.Actor, key keyArray, info json.RawMessag
func (m *nodeinfo) _sendRes(key keyArray) { func (m *nodeinfo) _sendRes(key keyArray) {
bs := append([]byte{typeSessionProto, typeProtoNodeInfoResponse}, m._getNodeInfo()...) bs := append([]byte{typeSessionProto, typeProtoNodeInfoResponse}, m._getNodeInfo()...)
_, _ = m.proto.core.PacketConn.WriteTo(bs, iwt.Addr(key[:])) _, _ = m.proto.core.pc.WriteTo(bs, iwt.Addr(key[:]))
} }
// Admin socket stuff // Admin socket stuff
@ -135,39 +154,36 @@ func (m *nodeinfo) _sendRes(key keyArray) {
type GetNodeInfoRequest struct { type GetNodeInfoRequest struct {
Key string `json:"key"` Key string `json:"key"`
} }
type GetNodeInfoResponse map[string]json.RawMessage type GetNodeInfoResponse map[string]interface{}
func (m *nodeinfo) nodeInfoAdminHandler(in json.RawMessage) (interface{}, error) { func (m *nodeinfo) nodeInfoAdminHandler(in json.RawMessage) (interface{}, error) {
var req GetNodeInfoRequest var req GetNodeInfoRequest
if err := json.Unmarshal(in, &req); err != nil { if err := json.Unmarshal(in, &req); err != nil {
return nil, err return nil, err
} }
if req.Key == "" {
return nil, fmt.Errorf("No remote public key supplied")
}
var key keyArray var key keyArray
var kbs []byte var kbs []byte
var err error var err error
if kbs, err = hex.DecodeString(req.Key); err != nil { if kbs, err = hex.DecodeString(req.Key); err != nil {
return nil, fmt.Errorf("Failed to decode public key: %w", err) return nil, err
} }
copy(key[:], kbs) copy(key[:], kbs)
ch := make(chan []byte, 1) ch := make(chan []byte, 1)
m.sendReq(nil, key, func(info json.RawMessage) { m.sendReq(nil, key, func(info NodeInfoPayload) {
ch <- info ch <- info
}) })
timer := time.NewTimer(6 * time.Second) timer := time.NewTimer(6 * time.Second)
defer timer.Stop() defer timer.Stop()
select { select {
case <-timer.C: case <-timer.C:
return nil, errors.New("Timed out waiting for response") return nil, errors.New("timeout")
case info := <-ch: case info := <-ch:
var msg json.RawMessage var msg json.RawMessage
if err := msg.UnmarshalJSON(info); err != nil { if err := msg.UnmarshalJSON(info); err != nil {
return nil, err return nil, err
} }
key := hex.EncodeToString(kbs[:]) ip := net.IP(address.AddrForKey(kbs)[:])
res := GetNodeInfoResponse{key: msg} res := GetNodeInfoResponse{ip.String(): msg}
return res, nil 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,7 +1,6 @@
package core package core
import ( import (
"crypto/ed25519"
"encoding/hex" "encoding/hex"
"encoding/json" "encoding/json"
"errors" "errors"
@ -21,8 +20,8 @@ const (
typeDebugGetSelfResponse typeDebugGetSelfResponse
typeDebugGetPeersRequest typeDebugGetPeersRequest
typeDebugGetPeersResponse typeDebugGetPeersResponse
typeDebugGetTreeRequest typeDebugGetDHTRequest
typeDebugGetTreeResponse typeDebugGetDHTResponse
) )
type reqInfo struct { type reqInfo struct {
@ -30,30 +29,23 @@ type reqInfo struct {
timer *time.Timer // time.AfterFunc cleanup timer *time.Timer // time.AfterFunc cleanup
} }
type keyArray [ed25519.PublicKeySize]byte
type protoHandler struct { type protoHandler struct {
phony.Inbox phony.Inbox
core *Core core *Core
nodeinfo nodeinfo nodeinfo nodeinfo
sreqs map[keyArray]*reqInfo
selfRequests map[keyArray]*reqInfo preqs map[keyArray]*reqInfo
peersRequests map[keyArray]*reqInfo dreqs map[keyArray]*reqInfo
treeRequests map[keyArray]*reqInfo
} }
func (p *protoHandler) init(core *Core) { func (p *protoHandler) init(core *Core) {
p.core = core p.core = core
p.nodeinfo.init(p) p.nodeinfo.init(p)
p.sreqs = make(map[keyArray]*reqInfo)
p.selfRequests = make(map[keyArray]*reqInfo) p.preqs = make(map[keyArray]*reqInfo)
p.peersRequests = make(map[keyArray]*reqInfo) p.dreqs = make(map[keyArray]*reqInfo)
p.treeRequests = make(map[keyArray]*reqInfo)
} }
// Common functions
func (p *protoHandler) handleProto(from phony.Actor, key keyArray, bs []byte) { func (p *protoHandler) handleProto(from phony.Actor, key keyArray, bs []byte) {
if len(bs) == 0 { if len(bs) == 0 {
return return
@ -65,16 +57,10 @@ func (p *protoHandler) handleProto(from phony.Actor, key keyArray, bs []byte) {
case typeProtoNodeInfoResponse: case typeProtoNodeInfoResponse:
p.nodeinfo.handleRes(p, key, bs[1:]) p.nodeinfo.handleRes(p, key, bs[1:])
case typeProtoDebug: case typeProtoDebug:
p.handleDebug(from, key, bs[1:]) p._handleDebug(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) { func (p *protoHandler) _handleDebug(key keyArray, bs []byte) {
if len(bs) == 0 { if len(bs) == 0 {
return return
@ -89,36 +75,29 @@ func (p *protoHandler) _handleDebug(key keyArray, bs []byte) {
p._handleGetPeersRequest(key) p._handleGetPeersRequest(key)
case typeDebugGetPeersResponse: case typeDebugGetPeersResponse:
p._handleGetPeersResponse(key, bs[1:]) p._handleGetPeersResponse(key, bs[1:])
case typeDebugGetTreeRequest: case typeDebugGetDHTRequest:
p._handleGetTreeRequest(key) p._handleGetDHTRequest(key)
case typeDebugGetTreeResponse: case typeDebugGetDHTResponse:
p._handleGetTreeResponse(key, bs[1:]) p._handleGetDHTResponse(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)) { func (p *protoHandler) sendGetSelfRequest(key keyArray, callback func([]byte)) {
p.Act(nil, func() { p.Act(nil, func() {
if info := p.selfRequests[key]; info != nil { if info := p.sreqs[key]; info != nil {
info.timer.Stop() info.timer.Stop()
delete(p.selfRequests, key) delete(p.sreqs, key)
} }
info := new(reqInfo) info := new(reqInfo)
info.callback = callback info.callback = callback
info.timer = time.AfterFunc(time.Minute, func() { info.timer = time.AfterFunc(time.Minute, func() {
p.Act(nil, func() { p.Act(nil, func() {
if p.selfRequests[key] == info { if p.sreqs[key] == info {
delete(p.selfRequests, key) delete(p.sreqs, key)
} }
}) })
}) })
p.selfRequests[key] = info p.sreqs[key] = info
p._sendDebug(key, typeDebugGetSelfRequest, nil) p._sendDebug(key, typeDebugGetSelfRequest, nil)
}) })
} }
@ -126,8 +105,8 @@ func (p *protoHandler) sendGetSelfRequest(key keyArray, callback func([]byte)) {
func (p *protoHandler) _handleGetSelfRequest(key keyArray) { func (p *protoHandler) _handleGetSelfRequest(key keyArray) {
self := p.core.GetSelf() self := p.core.GetSelf()
res := map[string]string{ res := map[string]string{
"key": hex.EncodeToString(self.Key[:]), "key": hex.EncodeToString(self.Key[:]),
"routing_entries": fmt.Sprintf("%v", self.RoutingEntries), "coords": fmt.Sprintf("%v", self.Coords),
} }
bs, err := json.Marshal(res) // FIXME this puts keys in base64, not hex bs, err := json.Marshal(res) // FIXME this puts keys in base64, not hex
if err != nil { if err != nil {
@ -137,31 +116,29 @@ func (p *protoHandler) _handleGetSelfRequest(key keyArray) {
} }
func (p *protoHandler) _handleGetSelfResponse(key keyArray, bs []byte) { func (p *protoHandler) _handleGetSelfResponse(key keyArray, bs []byte) {
if info := p.selfRequests[key]; info != nil { if info := p.sreqs[key]; info != nil {
info.timer.Stop() info.timer.Stop()
info.callback(bs) info.callback(bs)
delete(p.selfRequests, key) delete(p.sreqs, key)
} }
} }
// Get peers
func (p *protoHandler) sendGetPeersRequest(key keyArray, callback func([]byte)) { func (p *protoHandler) sendGetPeersRequest(key keyArray, callback func([]byte)) {
p.Act(nil, func() { p.Act(nil, func() {
if info := p.peersRequests[key]; info != nil { if info := p.preqs[key]; info != nil {
info.timer.Stop() info.timer.Stop()
delete(p.peersRequests, key) delete(p.preqs, key)
} }
info := new(reqInfo) info := new(reqInfo)
info.callback = callback info.callback = callback
info.timer = time.AfterFunc(time.Minute, func() { info.timer = time.AfterFunc(time.Minute, func() {
p.Act(nil, func() { p.Act(nil, func() {
if p.peersRequests[key] == info { if p.preqs[key] == info {
delete(p.peersRequests, key) delete(p.preqs, key)
} }
}) })
}) })
p.peersRequests[key] = info p.preqs[key] = info
p._sendDebug(key, typeDebugGetPeersRequest, nil) p._sendDebug(key, typeDebugGetPeersRequest, nil)
}) })
} }
@ -172,7 +149,7 @@ func (p *protoHandler) _handleGetPeersRequest(key keyArray) {
for _, pinfo := range peers { for _, pinfo := range peers {
tmp := append(bs, pinfo.Key[:]...) tmp := append(bs, pinfo.Key[:]...)
const responseOverhead = 2 // 1 debug type, 1 getpeers type const responseOverhead = 2 // 1 debug type, 1 getpeers type
if uint64(len(tmp))+responseOverhead > p.core.MTU() { if uint64(len(tmp))+responseOverhead > p.core.store.maxSessionMTU() {
break break
} }
bs = tmp bs = tmp
@ -181,58 +158,61 @@ func (p *protoHandler) _handleGetPeersRequest(key keyArray) {
} }
func (p *protoHandler) _handleGetPeersResponse(key keyArray, bs []byte) { func (p *protoHandler) _handleGetPeersResponse(key keyArray, bs []byte) {
if info := p.peersRequests[key]; info != nil { if info := p.preqs[key]; info != nil {
info.timer.Stop() info.timer.Stop()
info.callback(bs) info.callback(bs)
delete(p.peersRequests, key) delete(p.preqs, key)
} }
} }
// Get Tree func (p *protoHandler) sendGetDHTRequest(key keyArray, callback func([]byte)) {
func (p *protoHandler) sendGetTreeRequest(key keyArray, callback func([]byte)) {
p.Act(nil, func() { p.Act(nil, func() {
if info := p.treeRequests[key]; info != nil { if info := p.dreqs[key]; info != nil {
info.timer.Stop() info.timer.Stop()
delete(p.treeRequests, key) delete(p.dreqs, key)
} }
info := new(reqInfo) info := new(reqInfo)
info.callback = callback info.callback = callback
info.timer = time.AfterFunc(time.Minute, func() { info.timer = time.AfterFunc(time.Minute, func() {
p.Act(nil, func() { p.Act(nil, func() {
if p.treeRequests[key] == info { if p.dreqs[key] == info {
delete(p.treeRequests, key) delete(p.dreqs, key)
} }
}) })
}) })
p.treeRequests[key] = info p.dreqs[key] = info
p._sendDebug(key, typeDebugGetTreeRequest, nil) p._sendDebug(key, typeDebugGetDHTRequest, nil)
}) })
} }
func (p *protoHandler) _handleGetTreeRequest(key keyArray) { func (p *protoHandler) _handleGetDHTRequest(key keyArray) {
dinfos := p.core.GetTree() dinfos := p.core.GetDHT()
var bs []byte var bs []byte
for _, dinfo := range dinfos { for _, dinfo := range dinfos {
tmp := append(bs, dinfo.Key[:]...) tmp := append(bs, dinfo.Key[:]...)
const responseOverhead = 2 // 1 debug type, 1 gettree type const responseOverhead = 2 // 1 debug type, 1 getdht type
if uint64(len(tmp))+responseOverhead > p.core.MTU() { if uint64(len(tmp))+responseOverhead > p.core.store.maxSessionMTU() {
break break
} }
bs = tmp bs = tmp
} }
p._sendDebug(key, typeDebugGetTreeResponse, bs) p._sendDebug(key, typeDebugGetDHTResponse, bs)
} }
func (p *protoHandler) _handleGetTreeResponse(key keyArray, bs []byte) { func (p *protoHandler) _handleGetDHTResponse(key keyArray, bs []byte) {
if info := p.treeRequests[key]; info != nil { if info := p.dreqs[key]; info != nil {
info.timer.Stop() info.timer.Stop()
info.callback(bs) info.callback(bs)
delete(p.treeRequests, key) delete(p.dreqs, key)
} }
} }
// Admin socket stuff for "Get self" func (p *protoHandler) _sendDebug(key keyArray, dType uint8, data []byte) {
bs := append([]byte{typeSessionProto, typeProtoDebug, dType}, data...)
_, _ = p.core.pc.WriteTo(bs, iwt.Addr(key[:]))
}
// Admin socket stuff
type DebugGetSelfRequest struct { type DebugGetSelfRequest struct {
Key string `json:"key"` Key string `json:"key"`
@ -251,16 +231,15 @@ func (p *protoHandler) getSelfHandler(in json.RawMessage) (interface{}, error) {
if kbs, err = hex.DecodeString(req.Key); err != nil { if kbs, err = hex.DecodeString(req.Key); err != nil {
return nil, err return nil, err
} }
if len(kbs) != ed25519.PublicKeySize {
return nil, fmt.Errorf("invalid public key length")
}
copy(key[:], kbs) copy(key[:], kbs)
ch := make(chan []byte, 1) ch := make(chan []byte, 1)
p.sendGetSelfRequest(key, func(info []byte) { p.sendGetSelfRequest(key, func(info []byte) {
ch <- info ch <- info
}) })
timer := time.NewTimer(6 * time.Second)
defer timer.Stop()
select { select {
case <-time.After(6 * time.Second): case <-timer.C:
return nil, errors.New("timeout") return nil, errors.New("timeout")
case info := <-ch: case info := <-ch:
var msg json.RawMessage var msg json.RawMessage
@ -273,8 +252,6 @@ func (p *protoHandler) getSelfHandler(in json.RawMessage) (interface{}, error) {
} }
} }
// Admin socket stuff for "Get peers"
type DebugGetPeersRequest struct { type DebugGetPeersRequest struct {
Key string `json:"key"` Key string `json:"key"`
} }
@ -292,16 +269,15 @@ func (p *protoHandler) getPeersHandler(in json.RawMessage) (interface{}, error)
if kbs, err = hex.DecodeString(req.Key); err != nil { if kbs, err = hex.DecodeString(req.Key); err != nil {
return nil, err return nil, err
} }
if len(kbs) != ed25519.PublicKeySize {
return nil, fmt.Errorf("invalid public key length")
}
copy(key[:], kbs) copy(key[:], kbs)
ch := make(chan []byte, 1) ch := make(chan []byte, 1)
p.sendGetPeersRequest(key, func(info []byte) { p.sendGetPeersRequest(key, func(info []byte) {
ch <- info ch <- info
}) })
timer := time.NewTimer(6 * time.Second)
defer timer.Stop()
select { select {
case <-time.After(6 * time.Second): case <-timer.C:
return nil, errors.New("timeout") return nil, errors.New("timeout")
case info := <-ch: case info := <-ch:
ks := make(map[string][]string) ks := make(map[string][]string)
@ -324,16 +300,14 @@ func (p *protoHandler) getPeersHandler(in json.RawMessage) (interface{}, error)
} }
} }
// Admin socket stuff for "Get Tree" type DebugGetDHTRequest struct {
type DebugGetTreeRequest struct {
Key string `json:"key"` Key string `json:"key"`
} }
type DebugGetTreeResponse map[string]interface{} type DebugGetDHTResponse map[string]interface{}
func (p *protoHandler) getTreeHandler(in json.RawMessage) (interface{}, error) { func (p *protoHandler) getDHTHandler(in json.RawMessage) (interface{}, error) {
var req DebugGetTreeRequest var req DebugGetDHTRequest
if err := json.Unmarshal(in, &req); err != nil { if err := json.Unmarshal(in, &req); err != nil {
return nil, err return nil, err
} }
@ -343,16 +317,15 @@ func (p *protoHandler) getTreeHandler(in json.RawMessage) (interface{}, error) {
if kbs, err = hex.DecodeString(req.Key); err != nil { if kbs, err = hex.DecodeString(req.Key); err != nil {
return nil, err return nil, err
} }
if len(kbs) != ed25519.PublicKeySize {
return nil, fmt.Errorf("invalid public key length")
}
copy(key[:], kbs) copy(key[:], kbs)
ch := make(chan []byte, 1) ch := make(chan []byte, 1)
p.sendGetTreeRequest(key, func(info []byte) { p.sendGetDHTRequest(key, func(info []byte) {
ch <- info ch <- info
}) })
timer := time.NewTimer(6 * time.Second)
defer timer.Stop()
select { select {
case <-time.After(6 * time.Second): case <-timer.C:
return nil, errors.New("timeout") return nil, errors.New("timeout")
case info := <-ch: case info := <-ch:
ks := make(map[string][]string) ks := make(map[string][]string)
@ -370,7 +343,7 @@ func (p *protoHandler) getTreeHandler(in json.RawMessage) (interface{}, error) {
return nil, err return nil, err
} }
ip := net.IP(address.AddrForKey(kbs)[:]) ip := net.IP(address.AddrForKey(kbs)[:])
res := DebugGetTreeResponse{ip.String(): msg} res := DebugGetDHTResponse{ip.String(): msg}
return res, nil return res, nil
} }
} }

417
src/core/tcp.go Normal file
View file

@ -0,0 +1,417 @@
package core
// This sends packets to peers using TCP as a transport
// It's generally better tested than the UDP implementation
// Using it regularly is insane, but I find TCP easier to test/debug with it
// Updating and optimizing the UDP version is a higher priority
// TODO:
// Something needs to make sure we're getting *valid* packets
// Could be used to DoS (connect, give someone else's keys, spew garbage)
// I guess the "peer" part should watch for link packets, disconnect?
// TCP connections start with a metadata exchange.
// It involves exchanging version numbers and crypto keys
// See version.go for version metadata format
import (
"context"
"fmt"
"math/rand"
"net"
"net/url"
"strings"
"sync"
"time"
"golang.org/x/net/proxy"
"github.com/yggdrasil-network/yggdrasil-go/src/address"
//"github.com/yggdrasil-network/yggdrasil-go/src/util"
)
const default_timeout = 6 * time.Second
// The TCP listener and information about active TCP connections, to avoid duplication.
type tcp struct {
links *links
waitgroup sync.WaitGroup
mutex sync.Mutex // Protecting the below
listeners map[string]*TcpListener
calls map[string]struct{}
conns map[linkInfo](chan struct{})
tls tcptls
}
// TcpListener is a stoppable TCP listener interface. These are typically
// returned from calls to the ListenTCP() function and are also used internally
// to represent listeners created by the "Listen" configuration option and for
// multicast interfaces.
type TcpListener struct {
Listener net.Listener
opts tcpOptions
stop chan struct{}
}
type TcpUpgrade struct {
upgrade func(c net.Conn, o *tcpOptions) (net.Conn, error)
name string
}
type tcpOptions struct {
linkOptions
upgrade *TcpUpgrade
socksProxyAddr string
socksProxyAuth *proxy.Auth
socksPeerAddr string
}
func (l *TcpListener) Stop() {
defer func() { _ = recover() }()
close(l.stop)
}
// Wrapper function to set additional options for specific connection types.
func (t *tcp) setExtraOptions(c net.Conn) {
switch sock := c.(type) {
case *net.TCPConn:
_ = sock.SetNoDelay(true)
// TODO something for socks5
default:
}
}
// Returns the address of the listener.
func (t *tcp) getAddr() *net.TCPAddr {
// TODO: Fix this, because this will currently only give a single address
// to multicast.go, which obviously is not great, but right now multicast.go
// doesn't have the ability to send more than one address in a packet either
t.mutex.Lock()
defer t.mutex.Unlock()
for _, l := range t.listeners {
return l.Listener.Addr().(*net.TCPAddr)
}
return nil
}
// Initializes the struct.
func (t *tcp) init(l *links) error {
t.links = l
t.tls.init(t)
t.mutex.Lock()
t.calls = make(map[string]struct{})
t.conns = make(map[linkInfo](chan struct{}))
t.listeners = make(map[string]*TcpListener)
t.mutex.Unlock()
t.links.core.config.RLock()
defer t.links.core.config.RUnlock()
for _, listenaddr := range t.links.core.config.Listen {
u, err := url.Parse(listenaddr)
if err != nil {
t.links.core.log.Errorln("Failed to parse listener: listener", listenaddr, "is not correctly formatted, ignoring")
}
if _, err := t.listenURL(u, ""); err != nil {
return err
}
}
return nil
}
func (t *tcp) stop() error {
t.mutex.Lock()
for _, listener := range t.listeners {
listener.Stop()
}
t.mutex.Unlock()
t.waitgroup.Wait()
return nil
}
func (t *tcp) listenURL(u *url.URL, sintf string) (*TcpListener, error) {
var listener *TcpListener
var err error
hostport := u.Host // Used for tcp and tls
if len(sintf) != 0 {
host, port, err := net.SplitHostPort(hostport)
if err == nil {
hostport = fmt.Sprintf("[%s%%%s]:%s", host, sintf, port)
}
}
switch u.Scheme {
case "tcp":
listener, err = t.listen(hostport, nil)
case "tls":
listener, err = t.listen(hostport, t.tls.forListener)
default:
t.links.core.log.Errorln("Failed to add listener: listener", u.String(), "is not correctly formatted, ignoring")
}
return listener, err
}
func (t *tcp) listen(listenaddr string, upgrade *TcpUpgrade) (*TcpListener, error) {
var err error
ctx := t.links.core.ctx
lc := net.ListenConfig{
Control: t.tcpContext,
}
listener, err := lc.Listen(ctx, "tcp", listenaddr)
if err == nil {
l := TcpListener{
Listener: listener,
opts: tcpOptions{upgrade: upgrade},
stop: make(chan struct{}),
}
t.waitgroup.Add(1)
go t.listener(&l, listenaddr)
return &l, nil
}
return nil, err
}
// Runs the listener, which spawns off goroutines for incoming connections.
func (t *tcp) listener(l *TcpListener, listenaddr string) {
defer t.waitgroup.Done()
if l == nil {
return
}
// Track the listener so that we can find it again in future
t.mutex.Lock()
if _, isIn := t.listeners[listenaddr]; isIn {
t.mutex.Unlock()
l.Listener.Close()
return
}
callproto := "TCP"
if l.opts.upgrade != nil {
callproto = strings.ToUpper(l.opts.upgrade.name)
}
t.listeners[listenaddr] = l
t.mutex.Unlock()
// And here we go!
defer func() {
t.links.core.log.Infoln("Stopping", callproto, "listener on:", l.Listener.Addr().String())
l.Listener.Close()
t.mutex.Lock()
delete(t.listeners, listenaddr)
t.mutex.Unlock()
}()
t.links.core.log.Infoln("Listening for", callproto, "on:", l.Listener.Addr().String())
go func() {
<-l.stop
l.Listener.Close()
}()
defer l.Stop()
for {
sock, err := l.Listener.Accept()
if err != nil {
t.links.core.log.Errorln("Failed to accept connection:", err)
select {
case <-l.stop:
return
default:
}
time.Sleep(time.Second) // So we don't busy loop
continue
}
t.waitgroup.Add(1)
options := l.opts
go t.handler(sock, true, options)
}
}
// Checks if we already are calling this address
func (t *tcp) startCalling(saddr string) bool {
t.mutex.Lock()
defer t.mutex.Unlock()
_, isIn := t.calls[saddr]
t.calls[saddr] = struct{}{}
return !isIn
}
// Checks if a connection already exists.
// If not, it adds it to the list of active outgoing calls (to block future attempts) and dials the address.
// If the dial is successful, it launches the handler.
// When finished, it removes the outgoing call, so reconnection attempts can be made later.
// This all happens in a separate goroutine that it spawns.
func (t *tcp) call(saddr string, options tcpOptions, sintf string) {
go func() {
callname := saddr
callproto := "TCP"
if options.upgrade != nil {
callproto = strings.ToUpper(options.upgrade.name)
}
if sintf != "" {
callname = fmt.Sprintf("%s/%s/%s", callproto, saddr, sintf)
}
if !t.startCalling(callname) {
return
}
defer func() {
// Block new calls for a little while, to mitigate livelock scenarios
rand.Seed(time.Now().UnixNano())
delay := default_timeout + time.Duration(rand.Intn(10000))*time.Millisecond
time.Sleep(delay)
t.mutex.Lock()
delete(t.calls, callname)
t.mutex.Unlock()
}()
var conn net.Conn
var err error
if options.socksProxyAddr != "" {
if sintf != "" {
return
}
dialerdst, er := net.ResolveTCPAddr("tcp", options.socksProxyAddr)
if er != nil {
return
}
var dialer proxy.Dialer
dialer, err = proxy.SOCKS5("tcp", dialerdst.String(), options.socksProxyAuth, proxy.Direct)
if err != nil {
return
}
ctx, done := context.WithTimeout(t.links.core.ctx, default_timeout)
conn, err = dialer.(proxy.ContextDialer).DialContext(ctx, "tcp", saddr)
done()
if err != nil {
return
}
t.waitgroup.Add(1)
options.socksPeerAddr = saddr
if ch := t.handler(conn, false, options); ch != nil {
<-ch
}
} else {
dst, err := net.ResolveTCPAddr("tcp", saddr)
if err != nil {
return
}
if dst.IP.IsLinkLocalUnicast() {
dst.Zone = sintf
if dst.Zone == "" {
return
}
}
dialer := net.Dialer{
Control: t.tcpContext,
}
if sintf != "" {
dialer.Control = t.getControl(sintf)
ief, err := net.InterfaceByName(sintf)
if err != nil {
return
}
if ief.Flags&net.FlagUp == 0 {
return
}
addrs, err := ief.Addrs()
if err == nil {
for addrindex, addr := range addrs {
src, _, err := net.ParseCIDR(addr.String())
if err != nil {
continue
}
if src.Equal(dst.IP) {
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
}
}
}
ctx, done := context.WithTimeout(t.links.core.ctx, default_timeout)
conn, err = dialer.DialContext(ctx, "tcp", dst.String())
done()
if err != nil {
t.links.core.log.Debugf("Failed to dial %s: %s", callproto, err)
return
}
t.waitgroup.Add(1)
if ch := t.handler(conn, false, options); ch != nil {
<-ch
}
}
}()
}
func (t *tcp) handler(sock net.Conn, incoming bool, options tcpOptions) chan struct{} {
defer t.waitgroup.Done() // Happens after sock.close
defer sock.Close()
t.setExtraOptions(sock)
var upgraded bool
if options.upgrade != nil {
var err error
if sock, err = options.upgrade.upgrade(sock, &options); err != nil {
t.links.core.log.Errorln("TCP handler upgrade failed:", err)
return nil
}
upgraded = true
}
var name, proto, local, remote string
if options.socksProxyAddr != "" {
name = "socks://" + sock.RemoteAddr().String() + "/" + options.socksPeerAddr
proto = "socks"
local, _, _ = net.SplitHostPort(sock.LocalAddr().String())
remote, _, _ = net.SplitHostPort(options.socksPeerAddr)
} else {
if upgraded {
proto = options.upgrade.name
name = proto + "://" + sock.RemoteAddr().String()
} else {
proto = "tcp"
name = proto + "://" + sock.RemoteAddr().String()
}
local, _, _ = net.SplitHostPort(sock.LocalAddr().String())
remote, _, _ = net.SplitHostPort(sock.RemoteAddr().String())
}
localIP := net.ParseIP(local)
if localIP = localIP.To16(); localIP != nil {
var laddr address.Address
var lsubnet address.Subnet
copy(laddr[:], localIP)
copy(lsubnet[:], localIP)
if laddr.IsValid() || lsubnet.IsValid() {
// The local address is with the network address/prefix range
// This would route ygg over ygg, which we don't want
// FIXME ideally this check should happen outside of the core library
// Maybe dial/listen at the application level
// Then pass a net.Conn to the core library (after these kinds of checks are done)
t.links.core.log.Debugln("Dropping ygg-tunneled connection", local, remote)
return nil
}
}
force := net.ParseIP(strings.Split(remote, "%")[0]).IsLinkLocalUnicast()
link, err := t.links.create(sock, name, proto, local, remote, incoming, force, options.linkOptions)
if err != nil {
t.links.core.log.Println(err)
panic(err)
}
t.links.core.log.Debugln("DEBUG: starting handler for", name)
ch, err := link.handler()
t.links.core.log.Debugln("DEBUG: stopped handler for", name, err)
return ch
}

View file

@ -1,4 +1,3 @@
//go:build darwin
// +build darwin // +build darwin
package core package core
@ -11,7 +10,7 @@ import (
// WARNING: This context is used both by net.Dialer and net.Listen in tcp.go // 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 { func (t *tcp) tcpContext(network, address string, c syscall.RawConn) error {
var control error var control error
var recvanyif error var recvanyif error
@ -28,6 +27,6 @@ func (t *linkTCP) tcpContext(network, address string, c syscall.RawConn) error {
} }
} }
func (t *linkTCP) getControl(_ string) func(string, string, syscall.RawConn) error { func (t *tcp) getControl(sintf string) func(string, string, syscall.RawConn) error {
return t.tcpContext return t.tcpContext
} }

45
src/core/tcp_linux.go Normal file
View file

@ -0,0 +1,45 @@
// +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 *tcp) tcpContext(network, address string, c syscall.RawConn) error {
var control error
var bbr error
control = c.Control(func(fd uintptr) {
bbr = unix.SetsockoptString(int(fd), unix.IPPROTO_TCP, unix.TCP_CONGESTION, "bbr")
})
// Log any errors
if bbr != nil {
t.links.core.log.Debugln("Failed to set tcp_congestion_control to bbr for socket, SetsockoptString error:", bbr)
}
if control != nil {
t.links.core.log.Debugln("Failed to set tcp_congestion_control to bbr for socket, Control error:", control)
}
// Return nil because errors here are not considered fatal for the connection, it just means congestion control is suboptimal
return nil
}
func (t *tcp) 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)
}
}

17
src/core/tcp_other.go Normal file
View file

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

View file

@ -1,29 +1,125 @@
package core package core
import ( import (
"bytes"
"crypto/ed25519"
"crypto/rand"
"crypto/tls" "crypto/tls"
"crypto/x509" "crypto/x509"
"crypto/x509/pkix"
"encoding/hex"
"encoding/pem"
"errors"
"log"
"math/big"
"net"
"time"
) )
func (c *Core) generateTLSConfig(cert *tls.Certificate) (*tls.Config, error) { type tcptls struct {
config := &tls.Config{ tcp *tcp
Certificates: []tls.Certificate{*cert}, config *tls.Config
ClientAuth: tls.NoClientCert, forDialer *TcpUpgrade
GetClientCertificate: func(cri *tls.CertificateRequestInfo) (*tls.Certificate, error) { forListener *TcpUpgrade
return cert, nil }
},
VerifyPeerCertificate: c.verifyTLSCertificate, func (t *tcptls) init(tcp *tcp) {
VerifyConnection: c.verifyTLSConnection, t.tcp = tcp
InsecureSkipVerify: true, t.forDialer = &TcpUpgrade{
MinVersion: tls.VersionTLS13, upgrade: t.upgradeDialer,
name: "tls",
}
t.forListener = &TcpUpgrade{
upgrade: t.upgradeListener,
name: "tls",
}
edpriv := make(ed25519.PrivateKey, ed25519.PrivateKeySize)
copy(edpriv[:], tcp.links.core.secret[:])
certBuf := &bytes.Buffer{}
// TODO: because NotAfter is finite, we should add some mechanism to regenerate the certificate and restart the listeners periodically for nodes with very high uptimes. Perhaps regenerate certs and restart listeners every few months or so.
pubtemp := x509.Certificate{
SerialNumber: big.NewInt(1),
Subject: pkix.Name{
CommonName: hex.EncodeToString(tcp.links.core.public[:]),
},
NotBefore: time.Now(),
NotAfter: time.Now().Add(time.Hour * 24 * 365),
KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature,
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
BasicConstraintsValid: true,
}
derbytes, err := x509.CreateCertificate(rand.Reader, &pubtemp, &pubtemp, edpriv.Public(), edpriv)
if err != nil {
log.Fatalf("Failed to create certificate: %s", err)
}
if err := pem.Encode(certBuf, &pem.Block{Type: "CERTIFICATE", Bytes: derbytes}); err != nil {
panic("failed to encode certificate into PEM")
}
cpool := x509.NewCertPool()
cpool.AppendCertsFromPEM(derbytes)
t.config = &tls.Config{
RootCAs: cpool,
Certificates: []tls.Certificate{
{
Certificate: [][]byte{derbytes},
PrivateKey: edpriv,
},
},
InsecureSkipVerify: true,
MinVersion: tls.VersionTLS13,
} }
return config, nil
} }
func (c *Core) verifyTLSCertificate(_ [][]byte, _ [][]*x509.Certificate) error { func (t *tcptls) configForOptions(options *tcpOptions) *tls.Config {
return nil config := *t.config
config.VerifyPeerCertificate = func(rawCerts [][]byte, _ [][]*x509.Certificate) error {
if len(rawCerts) != 1 {
return errors.New("tls not exactly 1 cert")
}
cert, err := x509.ParseCertificate(rawCerts[0])
if err != nil {
return errors.New("tls failed to parse cert")
}
if cert.PublicKeyAlgorithm != x509.Ed25519 {
return errors.New("tls wrong cert algorithm")
}
pk := cert.PublicKey.(ed25519.PublicKey)
var key keyArray
copy(key[:], pk)
// If options does not have a pinned key, then pin one now
if options.pinnedEd25519Keys == nil {
options.pinnedEd25519Keys = make(map[keyArray]struct{})
options.pinnedEd25519Keys[key] = struct{}{}
}
if _, isIn := options.pinnedEd25519Keys[key]; !isIn {
return errors.New("tls key does not match pinned key")
}
return nil
}
return &config
} }
func (c *Core) verifyTLSConnection(_ tls.ConnectionState) error { func (t *tcptls) upgradeListener(c net.Conn, options *tcpOptions) (net.Conn, error) {
return nil config := t.configForOptions(options)
conn := tls.Server(c, config)
if err := conn.Handshake(); err != nil {
return c, err
}
return conn, nil
}
func (t *tcptls) upgradeDialer(c net.Conn, options *tcpOptions) (net.Conn, error) {
config := t.configForOptions(options)
conn := tls.Client(c, config)
if err := conn.Handshake(); err != nil {
return c, err
}
return conn, nil
} }

View file

@ -1,5 +1,12 @@
package core package core
// Out-of-band packet types
const (
typeKeyDummy = iota // nolint:deadcode,varcheck
typeKeyLookup
typeKeyResponse
)
// In-band packet types // In-band packet types
const ( const (
typeSessionDummy = iota // nolint:deadcode,varcheck typeSessionDummy = iota // nolint:deadcode,varcheck

View file

@ -4,166 +4,65 @@ package core
// Used in the initial connection setup and key exchange // Used in the initial connection setup and key exchange
// Some of this could arguably go in wire.go instead // Some of this could arguably go in wire.go instead
import ( import "crypto/ed25519"
"bytes"
"crypto/ed25519"
"encoding/binary"
"io"
"golang.org/x/crypto/blake2b"
)
// This is the version-specific metadata exchanged at the start of a connection. // 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. // 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. // 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 { type version_metadata struct {
majorVer uint16 meta [4]byte
minorVer uint16 ver uint8 // 1 byte in this version
publicKey ed25519.PublicKey // Everything after this point potentially depends on the version number, and is subject to change in future versions
priority uint8 minorVer uint8 // 1 byte in this version
key ed25519.PublicKey
} }
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. // Gets a base metadata with no keys set, but with the correct version numbers.
func version_getBaseMetadata() version_metadata { func version_getBaseMetadata() version_metadata {
return version_metadata{ return version_metadata{
majorVer: ProtocolVersionMajor, meta: [4]byte{'m', 'e', 't', 'a'},
minorVer: ProtocolVersionMinor, ver: 0,
minorVer: 4,
} }
} }
// Gets the length of the metadata for this version, used to know how many bytes to read from the start of a connection.
func version_getMetaLength() (mlen int) {
mlen += 4 // meta
mlen++ // ver, as long as it's < 127, which it is in this version
mlen++ // minorVer, as long as it's < 127, which it is in this version
mlen += ed25519.PublicKeySize // key
return
}
// Encodes version metadata into its wire format. // Encodes version metadata into its wire format.
func (m *version_metadata) encode(privateKey ed25519.PrivateKey, password []byte) ([]byte, error) { func (m *version_metadata) encode() []byte {
bs := make([]byte, 0, 64) bs := make([]byte, 0, version_getMetaLength())
bs = append(bs, 'm', 'e', 't', 'a') bs = append(bs, m.meta[:]...)
bs = append(bs, 0, 0) // Remaining message length bs = append(bs, m.ver)
bs = append(bs, m.minorVer)
bs = binary.BigEndian.AppendUint16(bs, metaVersionMajor) bs = append(bs, m.key[:]...)
bs = binary.BigEndian.AppendUint16(bs, 2) if len(bs) != version_getMetaLength() {
bs = binary.BigEndian.AppendUint16(bs, m.majorVer) panic("Inconsistent metadata length")
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) return bs
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. // Decodes version metadata from its wire format into the struct.
func (m *version_metadata) decode(r io.Reader, password []byte) error { func (m *version_metadata) decode(bs []byte) bool {
bh := [6]byte{} if len(bs) != version_getMetaLength() {
if _, err := io.ReadFull(r, bh[:]); err != nil { return false
return err
} }
meta := [4]byte{'m', 'e', 't', 'a'} offset := 0
if !bytes.Equal(bh[:4], meta[:]) { offset += copy(m.meta[:], bs[offset:])
return ErrHandshakeInvalidPreamble m.ver, offset = bs[offset], offset+1
} m.minorVer, offset = bs[offset], offset+1
hl := binary.BigEndian.Uint16(bh[4:6]) m.key = append([]byte(nil), bs[offset:]...)
if hl < ed25519.SignatureSize { return true
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. // Checks that the "meta" bytes and the version numbers are the expected values.
func (m *version_metadata) check() bool { func (m *version_metadata) check() bool {
switch { base := version_getBaseMetadata()
case m.majorVer != ProtocolVersionMajor: return base.meta == m.meta && base.ver == m.ver && base.minorVer == m.minorVer
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)
}
}
}
}

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

@ -0,0 +1,44 @@
package defaults
import "github.com/yggdrasil-network/yggdrasil-go/src/config"
type MulticastInterfaceConfig = config.MulticastInterfaceConfig
// 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/TAP
MaximumIfMTU uint64
DefaultIfMTU uint64
DefaultIfName string
}
// 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() *config.NodeConfig {
// Create a node configuration and populate it.
cfg := new(config.NodeConfig)
cfg.NewKeys()
cfg.Listen = []string{}
cfg.AdminListen = GetDefaults().DefaultAdminListen
cfg.Peers = []string{}
cfg.InterfacePeers = map[string][]string{}
cfg.AllowedPublicKeys = []string{}
cfg.MulticastInterfaces = GetDefaults().DefaultMulticastInterfaces
cfg.IfName = GetDefaults().DefaultIfName
cfg.IfMTU = GetDefaults().DefaultIfMTU
cfg.NodeInfoPrivacy = false
return cfg
}

View file

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

View file

@ -1,11 +1,10 @@
//go:build freebsd
// +build freebsd // +build freebsd
package config package defaults
// Sane defaults for the BSD platforms. The "default" options may be // Sane defaults for the BSD platforms. The "default" options may be
// may be replaced by the running configuration. // may be replaced by the running configuration.
func getDefaults() platformDefaultParameters { func GetDefaults() platformDefaultParameters {
return platformDefaultParameters{ return platformDefaultParameters{
// Admin // Admin
DefaultAdminListen: "unix:///var/run/yggdrasil.sock", DefaultAdminListen: "unix:///var/run/yggdrasil.sock",
@ -18,7 +17,7 @@ func getDefaults() platformDefaultParameters {
{Regex: ".*", Beacon: true, Listen: true}, {Regex: ".*", Beacon: true, Listen: true},
}, },
// TUN // TUN/TAP
MaximumIfMTU: 32767, MaximumIfMTU: 32767,
DefaultIfMTU: 32767, DefaultIfMTU: 32767,
DefaultIfName: "/dev/tun0", DefaultIfName: "/dev/tun0",

View file

@ -1,11 +1,10 @@
//go:build linux
// +build linux // +build linux
package config package defaults
// Sane defaults for the Linux platform. The "default" options may be // Sane defaults for the Linux platform. The "default" options may be
// may be replaced by the running configuration. // may be replaced by the running configuration.
func getDefaults() platformDefaultParameters { func GetDefaults() platformDefaultParameters {
return platformDefaultParameters{ return platformDefaultParameters{
// Admin // Admin
DefaultAdminListen: "unix:///var/run/yggdrasil.sock", DefaultAdminListen: "unix:///var/run/yggdrasil.sock",
@ -18,7 +17,7 @@ func getDefaults() platformDefaultParameters {
{Regex: ".*", Beacon: true, Listen: true}, {Regex: ".*", Beacon: true, Listen: true},
}, },
// TUN // TUN/TAP
MaximumIfMTU: 65535, MaximumIfMTU: 65535,
DefaultIfMTU: 65535, DefaultIfMTU: 65535,
DefaultIfName: "auto", DefaultIfName: "auto",

View file

@ -1,11 +1,10 @@
//go:build openbsd
// +build openbsd // +build openbsd
package config package defaults
// Sane defaults for the BSD platforms. The "default" options may be // Sane defaults for the BSD platforms. The "default" options may be
// may be replaced by the running configuration. // may be replaced by the running configuration.
func getDefaults() platformDefaultParameters { func GetDefaults() platformDefaultParameters {
return platformDefaultParameters{ return platformDefaultParameters{
// Admin // Admin
DefaultAdminListen: "unix:///var/run/yggdrasil.sock", DefaultAdminListen: "unix:///var/run/yggdrasil.sock",
@ -18,7 +17,7 @@ func getDefaults() platformDefaultParameters {
{Regex: ".*", Beacon: true, Listen: true}, {Regex: ".*", Beacon: true, Listen: true},
}, },
// TUN // TUN/TAP
MaximumIfMTU: 16384, MaximumIfMTU: 16384,
DefaultIfMTU: 16384, DefaultIfMTU: 16384,
DefaultIfName: "tun0", DefaultIfName: "tun0",

View file

@ -1,11 +1,10 @@
//go:build !linux && !darwin && !windows && !openbsd && !freebsd
// +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 // Sane defaults for the other platforms. The "default" options may be
// may be replaced by the running configuration. // may be replaced by the running configuration.
func getDefaults() platformDefaultParameters { func GetDefaults() platformDefaultParameters {
return platformDefaultParameters{ return platformDefaultParameters{
// Admin // Admin
DefaultAdminListen: "tcp://localhost:9001", DefaultAdminListen: "tcp://localhost:9001",
@ -18,7 +17,7 @@ func getDefaults() platformDefaultParameters {
{Regex: ".*", Beacon: true, Listen: true}, {Regex: ".*", Beacon: true, Listen: true},
}, },
// TUN // TUN/TAP
MaximumIfMTU: 65535, MaximumIfMTU: 65535,
DefaultIfMTU: 65535, DefaultIfMTU: 65535,
DefaultIfName: "none", DefaultIfName: "none",

View file

@ -1,11 +1,10 @@
//go:build windows
// +build windows // +build windows
package config package defaults
// Sane defaults for the Windows platform. The "default" options may be // Sane defaults for the Windows platform. The "default" options may be
// may be replaced by the running configuration. // may be replaced by the running configuration.
func getDefaults() platformDefaultParameters { func GetDefaults() platformDefaultParameters {
return platformDefaultParameters{ return platformDefaultParameters{
// Admin // Admin
DefaultAdminListen: "tcp://localhost:9001", DefaultAdminListen: "tcp://localhost:9001",
@ -18,7 +17,7 @@ func getDefaults() platformDefaultParameters {
{Regex: ".*", Beacon: true, Listen: true}, {Regex: ".*", Beacon: true, Listen: true},
}, },
// TUN // TUN/TAP
MaximumIfMTU: 65535, MaximumIfMTU: 65535,
DefaultIfMTU: 65535, DefaultIfMTU: 65535,
DefaultIfName: "Yggdrasil", DefaultIfName: "Yggdrasil",

View file

@ -2,63 +2,33 @@ package multicast
import ( import (
"encoding/json" "encoding/json"
"slices"
"strings"
"github.com/Arceliar/phony"
"github.com/yggdrasil-network/yggdrasil-go/src/admin" "github.com/yggdrasil-network/yggdrasil-go/src/admin"
) )
type GetMulticastInterfacesRequest struct{} type GetMulticastInterfacesRequest struct{}
type GetMulticastInterfacesResponse struct { type GetMulticastInterfacesResponse struct {
Interfaces []MulticastInterfaceState `json:"multicast_interfaces"` Interfaces []string `json:"multicast_interfaces"`
} }
type MulticastInterfaceState struct { func (m *Multicast) getMulticastInterfacesHandler(req *GetMulticastInterfacesRequest, res *GetMulticastInterfacesResponse) error {
Name string `json:"name"` res.Interfaces = []string{}
Address string `json:"address"` for _, v := range m.Interfaces() {
Beacon bool `json:"beacon"` res.Interfaces = append(res.Interfaces, v.Name)
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 return nil
} }
func (m *Multicast) SetupAdminHandlers(a *admin.AdminSocket) { func (m *Multicast) SetupAdminHandlers(a *admin.AdminSocket) {
_ = a.AddHandler( _ = a.AddHandler("getMulticastInterfaces", []string{}, func(in json.RawMessage) (interface{}, error) {
"getMulticastInterfaces", "Show which interfaces multicast is enabled on", []string{}, req := &GetMulticastInterfacesRequest{}
func(in json.RawMessage) (interface{}, error) { res := &GetMulticastInterfacesResponse{}
req := &GetMulticastInterfacesRequest{} if err := json.Unmarshal(in, &req); err != nil {
res := &GetMulticastInterfacesResponse{} return nil, err
if err := json.Unmarshal(in, &req); err != nil { }
return nil, err if err := m.getMulticastInterfacesHandler(req, res); err != nil {
} return nil, err
if err := m.getMulticastInterfacesHandler(req, res); err != nil { }
return nil, err return res, nil
} })
return res, 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
}

View file

@ -1,38 +0,0 @@
package multicast
import (
"crypto/ed25519"
"reflect"
"testing"
)
func TestMulticastAdvertisementRoundTrip(t *testing.T) {
pk, sk, err := ed25519.GenerateKey(nil)
if err != nil {
t.Fatal(err)
}
orig := multicastAdvertisement{
MajorVersion: 1,
MinorVersion: 2,
PublicKey: pk,
Port: 3,
Hash: sk, // any bytes will do
}
ob, err := orig.MarshalBinary()
if err != nil {
t.Fatal(err)
}
var new multicastAdvertisement
if err := new.UnmarshalBinary(ob); err != nil {
t.Fatal(err)
}
if !reflect.DeepEqual(orig, new) {
t.Logf("original: %+v", orig)
t.Logf("new: %+v", new)
t.Fatalf("differences found after round-trip")
}
}

View file

@ -4,20 +4,19 @@ import (
"bytes" "bytes"
"context" "context"
"crypto/ed25519" "crypto/ed25519"
"encoding/binary"
"encoding/hex" "encoding/hex"
"fmt" "fmt"
"math/rand"
"net" "net"
"net/url" "net/url"
"sync/atomic" "regexp"
"time" "time"
"github.com/Arceliar/phony" "github.com/Arceliar/phony"
"github.com/gologme/log" "github.com/gologme/log"
"github.com/wlynxg/anet"
"github.com/yggdrasil-network/yggdrasil-go/src/config"
"github.com/yggdrasil-network/yggdrasil-go/src/core" "github.com/yggdrasil-network/yggdrasil-go/src/core"
"golang.org/x/crypto/blake2b"
"golang.org/x/net/ipv6" "golang.org/x/net/ipv6"
) )
@ -28,75 +27,65 @@ import (
type Multicast struct { type Multicast struct {
phony.Inbox phony.Inbox
core *core.Core core *core.Core
config *config.NodeConfig
log *log.Logger log *log.Logger
sock *ipv6.PacketConn sock *ipv6.PacketConn
running atomic.Bool groupAddr string
_listeners map[string]*listenerInfo listeners map[string]*listenerInfo
_interfaces map[string]*interfaceInfo isOpen bool
_timer *time.Timer _interfaces map[string]interfaceInfo
config struct {
_groupAddr GroupAddress
_interfaces map[MulticastInterface]struct{}
}
} }
type interfaceInfo struct { type interfaceInfo struct {
iface net.Interface iface net.Interface
addrs []net.Addr addrs []net.Addr
beacon bool beacon bool
listen bool listen bool
port uint16 port uint16
priority uint8
password []byte
hash []byte
} }
type listenerInfo struct { type listenerInfo struct {
listener *core.Listener listener *core.TcpListener
time time.Time time time.Time
interval time.Duration interval time.Duration
port uint16 port uint16
} }
// Init prepares the multicast interface for use.
func (m *Multicast) Init(core *core.Core, nc *config.NodeConfig, log *log.Logger, options interface{}) error {
m.core = core
m.config = nc
m.log = log
m.listeners = make(map[string]*listenerInfo)
m._interfaces = make(map[string]interfaceInfo)
m.groupAddr = "[ff02::114]:9001"
return nil
}
// Start starts the multicast interface. This launches goroutines which will // Start starts the multicast interface. This launches goroutines which will
// listen for multicast beacons from other hosts and will advertise multicast // listen for multicast beacons from other hosts and will advertise multicast
// beacons out to the network. // beacons out to the network.
func New(core *core.Core, log *log.Logger, opts ...SetupOption) (*Multicast, error) { func (m *Multicast) Start() error {
m := &Multicast{
core: core,
log: log,
_listeners: make(map[string]*listenerInfo),
_interfaces: make(map[string]*interfaceInfo),
}
m.config._interfaces = map[MulticastInterface]struct{}{}
m.config._groupAddr = GroupAddress("[ff02::114]:9001")
for _, opt := range opts {
m._applyOption(opt)
}
var err error var err error
phony.Block(m, func() { phony.Block(m, func() {
err = m._start() err = m._start()
}) })
return m, err m.log.Debugln("Started multicast module")
return err
} }
func (m *Multicast) _start() error { func (m *Multicast) _start() error {
if !m.running.CompareAndSwap(false, true) { if m.isOpen {
return fmt.Errorf("multicast module is already started") return fmt.Errorf("multicast module is already started")
} }
var anyEnabled bool m.config.RLock()
for intf := range m.config._interfaces { defer m.config.RUnlock()
anyEnabled = anyEnabled || intf.Beacon || intf.Listen if len(m.config.MulticastInterfaces) == 0 {
}
if !anyEnabled {
m.running.Store(false)
return nil return nil
} }
m.log.Debugln("Starting multicast module") m.log.Infoln("Starting multicast module")
defer m.log.Debugln("Started multicast module") addr, err := net.ResolveUDPAddr("udp", m.groupAddr)
addr, err := net.ResolveUDPAddr("udp", string(m.config._groupAddr))
if err != nil { if err != nil {
m.running.Store(false)
return err return err
} }
listenString := fmt.Sprintf("[::]:%v", addr.Port) listenString := fmt.Sprintf("[::]:%v", addr.Port)
@ -105,7 +94,6 @@ func (m *Multicast) _start() error {
} }
conn, err := lc.ListenPacket(context.Background(), "udp6", listenString) conn, err := lc.ListenPacket(context.Background(), "udp6", listenString)
if err != nil { if err != nil {
m.running.Store(false)
return err return err
} }
m.sock = ipv6.NewPacketConn(conn) m.sock = ipv6.NewPacketConn(conn)
@ -113,6 +101,7 @@ func (m *Multicast) _start() error {
// Windows can't set this flag, so we need to handle it in other ways // Windows can't set this flag, so we need to handle it in other ways
} }
m.isOpen = true
go m.listen() go m.listen()
m.Act(nil, m._multicastStarted) m.Act(nil, m._multicastStarted)
m.Act(nil, m._announce) m.Act(nil, m._announce)
@ -122,7 +111,11 @@ func (m *Multicast) _start() error {
// IsStarted returns true if the module has been started. // IsStarted returns true if the module has been started.
func (m *Multicast) IsStarted() bool { func (m *Multicast) IsStarted() bool {
return m.running.Load() var isOpen bool
phony.Block(m, func() {
isOpen = m.isOpen
})
return isOpen
} }
// Stop stops the multicast module. // Stop stops the multicast module.
@ -136,10 +129,8 @@ func (m *Multicast) Stop() error {
} }
func (m *Multicast) _stop() error { func (m *Multicast) _stop() error {
if !m.running.CompareAndSwap(true, false) {
return nil
}
m.log.Infoln("Stopping multicast module") m.log.Infoln("Stopping multicast module")
m.isOpen = false
if m.sock != nil { if m.sock != nil {
m.sock.Close() m.sock.Close()
} }
@ -147,24 +138,16 @@ func (m *Multicast) _stop() error {
} }
func (m *Multicast) _updateInterfaces() { func (m *Multicast) _updateInterfaces() {
interfaces := m._getAllowedInterfaces() interfaces := m.getAllowedInterfaces()
for name, info := range interfaces { for name, info := range interfaces {
// 'anet' package is used here to avoid https://github.com/golang/go/issues/40569 addrs, err := info.iface.Addrs()
addrs, err := anet.InterfaceAddrsByInterface(&info.iface)
if err != nil { if err != nil {
m.log.Warnf("Failed up get addresses for interface %s: %s", name, err) m.log.Warnf("Failed up get addresses for interface %s: %s", name, err)
delete(interfaces, name) delete(interfaces, name)
continue continue
} }
for _, addr := range addrs { info.addrs = addrs
addrIP, _, err := net.ParseCIDR(addr.String())
if err != nil || addrIP.To4() != nil || !addrIP.IsLinkLocalUnicast() {
continue
}
info.addrs = append(info.addrs, addr)
}
interfaces[name] = info interfaces[name] = info
m.log.Debugf("Discovered addresses for interface %s: %s", name, addrs)
} }
m._interfaces = interfaces m._interfaces = interfaces
} }
@ -180,91 +163,75 @@ func (m *Multicast) Interfaces() map[string]net.Interface {
} }
// getAllowedInterfaces returns the currently known/enabled multicast interfaces. // getAllowedInterfaces returns the currently known/enabled multicast interfaces.
func (m *Multicast) _getAllowedInterfaces() map[string]*interfaceInfo { func (m *Multicast) getAllowedInterfaces() map[string]interfaceInfo {
interfaces := make(map[string]*interfaceInfo) interfaces := make(map[string]interfaceInfo)
// Get interface expressions from config
ifcfgs := m.config.MulticastInterfaces
// Ask the system for network interfaces // Ask the system for network interfaces
// 'anet' package is used here to avoid https://github.com/golang/go/issues/40569 allifaces, err := net.Interfaces()
allifaces, err := anet.Interfaces()
if err != nil { if err != nil {
// Don't panic, since this may be from e.g. too many open files (from too much connection spam) // Don't panic, since this may be from e.g. too many open files (from too much connection spam)
m.log.Debugf("Failed to get interfaces: %s", err) // TODO? log something
return nil return nil
} }
// Work out which interfaces to announce on // Work out which interfaces to announce on
pk := m.core.PublicKey()
for _, iface := range allifaces { for _, iface := range allifaces {
switch { if iface.Flags&net.FlagUp == 0 {
case iface.Flags&net.FlagUp == 0: // Ignore interfaces that are down
continue // Ignore interfaces that are down continue
case iface.Flags&net.FlagRunning == 0:
continue // Ignore interfaces that are not running
case iface.Flags&net.FlagMulticast == 0:
continue // Ignore non-multicast interfaces
case iface.Flags&net.FlagPointToPoint != 0:
continue // Ignore point-to-point interfaces
} }
for ifcfg := range m.config._interfaces { if iface.Flags&net.FlagMulticast == 0 {
// Ignore non-multicast interfaces
continue
}
if iface.Flags&net.FlagPointToPoint != 0 {
// Ignore point-to-point interfaces
continue
}
for _, ifcfg := range ifcfgs {
// Compile each regular expression // Compile each regular expression
// Does the interface match the regular expression? Store it if so e, err := regexp.Compile(ifcfg.Regex)
if !ifcfg.Beacon && !ifcfg.Listen {
continue
}
if !ifcfg.Regex.MatchString(iface.Name) {
continue
}
hasher, err := blake2b.New512([]byte(ifcfg.Password))
if err != nil { if err != nil {
continue panic(err)
} }
if n, err := hasher.Write(pk); err != nil { // Does the interface match the regular expression? Store it if so
continue if e.MatchString(iface.Name) {
} else if n != ed25519.PublicKeySize { if ifcfg.Beacon || ifcfg.Listen {
continue info := interfaceInfo{
iface: iface,
beacon: ifcfg.Beacon,
listen: ifcfg.Listen,
port: ifcfg.Port,
}
interfaces[iface.Name] = info
}
break
} }
interfaces[iface.Name] = &interfaceInfo{
iface: iface,
beacon: ifcfg.Beacon,
listen: ifcfg.Listen,
port: ifcfg.Port,
priority: ifcfg.Priority,
password: []byte(ifcfg.Password),
hash: hasher.Sum(nil),
}
break
} }
} }
return interfaces return interfaces
} }
func (m *Multicast) AnnounceNow() {
phony.Block(m, func() {
if m._timer != nil && !m._timer.Stop() {
<-m._timer.C
}
m.Act(nil, m._announce)
})
}
func (m *Multicast) _announce() { func (m *Multicast) _announce() {
if !m.running.Load() { if !m.isOpen {
return return
} }
m._updateInterfaces() m._updateInterfaces()
groupAddr, err := net.ResolveUDPAddr("udp6", string(m.config._groupAddr)) groupAddr, err := net.ResolveUDPAddr("udp6", m.groupAddr)
if err != nil { if err != nil {
panic(err) panic(err)
} }
destAddr, err := net.ResolveUDPAddr("udp6", string(m.config._groupAddr)) destAddr, err := net.ResolveUDPAddr("udp6", m.groupAddr)
if err != nil { if err != nil {
panic(err) panic(err)
} }
// There might be interfaces that we configured listeners for but are no // There might be interfaces that we configured listeners for but are no
// longer up - if that's the case then we should stop the listeners // longer up - if that's the case then we should stop the listeners
for name, info := range m._listeners { for name, info := range m.listeners {
// Prepare our stop function! // Prepare our stop function!
stop := func() { stop := func() {
info.listener.Cancel() info.listener.Stop()
delete(m._listeners, name) delete(m.listeners, name)
m.log.Debugln("No longer multicasting on", name) m.log.Debugln("No longer multicasting on", name)
} }
// If the interface is no longer visible on the system then stop the // If the interface is no longer visible on the system then stop the
@ -276,7 +243,7 @@ func (m *Multicast) _announce() {
// It's possible that the link-local listener address has changed so if // It's possible that the link-local listener address has changed so if
// that is the case then we should clean up the interface listener // that is the case then we should clean up the interface listener
found := false found := false
listenaddr, err := net.ResolveTCPAddr("tcp6", info.listener.Addr().String()) listenaddr, err := net.ResolveTCPAddr("tcp6", info.listener.Listener.Addr().String())
if err != nil { if err != nil {
stop() stop()
continue continue
@ -305,9 +272,13 @@ func (m *Multicast) _announce() {
for _, info := range m._interfaces { for _, info := range m._interfaces {
iface := info.iface iface := info.iface
for _, addr := range info.addrs { for _, addr := range info.addrs {
addrIP, _, err := net.ParseCIDR(addr.String()) addrIP, _, _ := net.ParseCIDR(addr.String())
// Ignore IPv4 addresses or non-link-local addresses // Ignore IPv4 addresses
if err != nil || addrIP.To4() != nil || !addrIP.IsLinkLocalUnicast() { if addrIP.To4() != nil {
continue
}
// Ignore non-link-local addresses
if !addrIP.IsLinkLocalUnicast() {
continue continue
} }
if info.listen { if info.listen {
@ -319,27 +290,24 @@ func (m *Multicast) _announce() {
} }
// Try and see if we already have a TCP listener for this interface // Try and see if we already have a TCP listener for this interface
var linfo *listenerInfo var linfo *listenerInfo
if _, ok := m._listeners[iface.Name]; !ok { if nfo, ok := m.listeners[iface.Name]; !ok || nfo.listener.Listener == nil {
// No listener was found - let's create one // No listener was found - let's create one
v := &url.Values{} urlString := fmt.Sprintf("tls://[%s]:%d", addrIP, info.port)
v.Add("priority", fmt.Sprintf("%d", info.priority)) u, err := url.Parse(urlString)
v.Add("password", string(info.password)) if err != nil {
u := &url.URL{ panic(err)
Scheme: "tls",
Host: net.JoinHostPort(addrIP.String(), fmt.Sprintf("%d", info.port)),
RawQuery: v.Encode(),
} }
if li, err := m.core.ListenLocal(u, iface.Name); err == nil { if li, err := m.core.Listen(u, iface.Name); err == nil {
m.log.Debugln("Started multicasting on", iface.Name) m.log.Debugln("Started multicasting on", iface.Name)
// Store the listener so that we can stop it later if needed // Store the listener so that we can stop it later if needed
linfo = &listenerInfo{listener: li, time: time.Now(), port: info.port} linfo = &listenerInfo{listener: li, time: time.Now()}
m._listeners[iface.Name] = linfo m.listeners[iface.Name] = linfo
} else { } else {
m.log.Warnln("Not multicasting on", iface.Name, "due to error:", err) m.log.Warnln("Not multicasting on", iface.Name, "due to error:", err)
} }
} else { } else {
// An existing listener was found // An existing listener was found
linfo = m._listeners[iface.Name] linfo = m.listeners[iface.Name]
} }
// Make sure nothing above failed for some reason // Make sure nothing above failed for some reason
if linfo == nil { if linfo == nil {
@ -348,47 +316,37 @@ func (m *Multicast) _announce() {
if time.Since(linfo.time) < linfo.interval { if time.Since(linfo.time) < linfo.interval {
continue continue
} }
addr := linfo.listener.Addr().(*net.TCPAddr) // Get the listener details and construct the multicast beacon
adv := multicastAdvertisement{ lladdr := linfo.listener.Listener.Addr().String()
MajorVersion: core.ProtocolVersionMajor, if a, err := net.ResolveTCPAddr("tcp6", lladdr); err == nil {
MinorVersion: core.ProtocolVersionMinor, a.Zone = ""
PublicKey: m.core.PublicKey(), destAddr.Zone = iface.Name
Port: uint16(addr.Port), msg := append([]byte(nil), m.core.GetSelf().Key...)
Hash: info.hash, msg = append(msg, a.IP...)
} pbs := make([]byte, 2)
msg, err := adv.MarshalBinary() binary.BigEndian.PutUint16(pbs, uint16(a.Port))
if err != nil { msg = append(msg, pbs...)
continue _, _ = m.sock.WriteTo(msg, nil, destAddr)
}
destAddr.Zone = iface.Name
if _, err = m.sock.WriteTo(msg, nil, destAddr); err != nil {
m.log.Warn("Failed to send multicast beacon:", err)
} }
if linfo.interval.Seconds() < 15 { if linfo.interval.Seconds() < 15 {
linfo.interval += time.Second linfo.interval += time.Second
} }
linfo.time = time.Now()
break break
} }
} }
annInterval := time.Second + time.Microsecond*(time.Duration(rand.Intn(1048576))) // Randomize delay time.AfterFunc(time.Second, func() {
m._timer = time.AfterFunc(annInterval, func() {
m.Act(nil, m._announce) m.Act(nil, m._announce)
}) })
} }
func (m *Multicast) listen() { func (m *Multicast) listen() {
groupAddr, err := net.ResolveUDPAddr("udp6", string(m.config._groupAddr)) groupAddr, err := net.ResolveUDPAddr("udp6", m.groupAddr)
if err != nil { if err != nil {
panic(err) panic(err)
} }
bs := make([]byte, 2048) bs := make([]byte, 2048)
hb := make([]byte, 0, blake2b.Size) // Reused to reduce hash allocations
for { for {
if !m.running.Load() { nBytes, rcm, fromAddr, err := m.sock.ReadFrom(bs)
return
}
n, rcm, fromAddr, err := m.sock.ReadFrom(bs)
if err != nil { if err != nil {
if !m.IsStarted() { if !m.IsStarted() {
return return
@ -406,45 +364,40 @@ func (m *Multicast) listen() {
continue continue
} }
} }
var adv multicastAdvertisement if nBytes < ed25519.PublicKeySize {
if err := adv.UnmarshalBinary(bs[:n]); err != nil {
continue continue
} }
switch { var key ed25519.PublicKey
case adv.MajorVersion != core.ProtocolVersionMajor: key = append(key, bs[:ed25519.PublicKeySize]...)
continue if bytes.Equal(key, m.core.GetSelf().Key) {
case adv.MinorVersion != core.ProtocolVersionMinor: continue // don't bother trying to peer with self
continue }
case adv.PublicKey.Equal(m.core.PublicKey()): begin := ed25519.PublicKeySize
end := nBytes - 2
if end <= begin {
continue // malformed address
}
ip := bs[begin:end]
port := binary.BigEndian.Uint16(bs[end:nBytes])
anAddr := net.TCPAddr{IP: ip, Port: int(port)}
addr, err := net.ResolveTCPAddr("tcp6", anAddr.String())
if err != nil {
continue continue
} }
from := fromAddr.(*net.UDPAddr) from := fromAddr.(*net.UDPAddr)
from.Port = int(adv.Port) if !from.IP.Equal(addr.IP) {
var interfaces map[string]*interfaceInfo continue
}
var interfaces map[string]interfaceInfo
phony.Block(m, func() { phony.Block(m, func() {
interfaces = m._interfaces interfaces = m._interfaces
}) })
if info, ok := interfaces[from.Zone]; ok && info.listen { if info, ok := interfaces[from.Zone]; ok && info.listen {
hasher, err := blake2b.New512(info.password) addr.Zone = ""
pin := fmt.Sprintf("/?key=%s", hex.EncodeToString(key))
u, err := url.Parse("tls://" + addr.String() + pin)
if err != nil { if err != nil {
continue m.log.Debugln("Call from multicast failed, parse error:", addr.String(), err)
}
if n, err := hasher.Write(adv.PublicKey); err != nil {
continue
} else if n != ed25519.PublicKeySize {
continue
}
if !bytes.Equal(hasher.Sum(hb[:0]), adv.Hash) {
continue
}
v := &url.Values{}
v.Add("key", hex.EncodeToString(adv.PublicKey))
v.Add("priority", fmt.Sprintf("%d", info.priority))
v.Add("password", string(info.password))
u := &url.URL{
Scheme: "tls",
Host: from.String(),
RawQuery: v.Encode(),
} }
if err := m.core.CallPeer(u, from.Zone); err != nil { if err := m.core.CallPeer(u, from.Zone); err != nil {
m.log.Debugln("Call from multicast failed:", err) m.log.Debugln("Call from multicast failed:", err)

View file

@ -1,17 +1,48 @@
//go:build !cgo && (darwin || ios) // +build darwin
// +build !cgo
// +build darwin ios
package multicast package multicast
/*
#cgo CFLAGS: -x objective-c
#cgo LDFLAGS: -framework Foundation
#import <Foundation/Foundation.h>
NSNetServiceBrowser *serviceBrowser;
void StartAWDLBrowsing() {
if (serviceBrowser == nil) {
serviceBrowser = [[NSNetServiceBrowser alloc] init];
serviceBrowser.includesPeerToPeer = YES;
}
[serviceBrowser searchForServicesOfType:@"_yggdrasil._tcp" inDomain:@""];
}
void StopAWDLBrowsing() {
if (serviceBrowser == nil) {
return;
}
[serviceBrowser stop];
}
*/
import "C"
import ( import (
"syscall" "syscall"
"time"
"golang.org/x/sys/unix" "golang.org/x/sys/unix"
) )
func (m *Multicast) _multicastStarted() { func (m *Multicast) _multicastStarted() {
if !m.isOpen {
return
}
C.StopAWDLBrowsing()
for intf := range m._interfaces {
if intf == "awdl0" {
C.StartAWDLBrowsing()
break
}
}
time.AfterFunc(time.Minute, func() {
m.Act(nil, m._multicastStarted)
})
} }
func (m *Multicast) multicastReuse(network string, address string, c syscall.RawConn) error { func (m *Multicast) multicastReuse(network string, address string, c syscall.RawConn) error {

View file

@ -1,69 +0,0 @@
//go:build (darwin && cgo) || (ios && cgo)
// +build darwin,cgo ios,cgo
package multicast
/*
#cgo CFLAGS: -x objective-c
#cgo LDFLAGS: -framework Foundation
#import <Foundation/Foundation.h>
NSNetServiceBrowser *serviceBrowser;
void StartAWDLBrowsing() {
if (serviceBrowser == nil) {
serviceBrowser = [[NSNetServiceBrowser alloc] init];
serviceBrowser.includesPeerToPeer = YES;
}
[serviceBrowser searchForServicesOfType:@"_yggdrasil._tcp" inDomain:@""];
}
void StopAWDLBrowsing() {
if (serviceBrowser == nil) {
return;
}
[serviceBrowser stop];
}
*/
import "C"
import (
"syscall"
"time"
"golang.org/x/sys/unix"
)
func (m *Multicast) _multicastStarted() {
if !m.running.Load() {
return
}
C.StopAWDLBrowsing()
for intf := range m._interfaces {
if intf == "awdl0" {
C.StartAWDLBrowsing()
break
}
}
time.AfterFunc(time.Minute, func() {
m.Act(nil, m._multicastStarted)
})
}
func (m *Multicast) multicastReuse(network string, address string, c syscall.RawConn) error {
var control error
var reuseport error
var recvanyif error
control = c.Control(func(fd uintptr) {
reuseport = unix.SetsockoptInt(int(fd), unix.SOL_SOCKET, unix.SO_REUSEPORT, 1)
// sys/socket.h: #define SO_RECV_ANYIF 0x1104
recvanyif = unix.SetsockoptInt(int(fd), syscall.SOL_SOCKET, 0x1104, 1)
})
switch {
case reuseport != nil:
return reuseport
case recvanyif != nil:
return recvanyif
default:
return control
}
}

View file

@ -1,5 +1,4 @@
//go:build !linux && !darwin && !ios && !netbsd && !freebsd && !openbsd && !dragonflybsd && !windows // +build !linux,!darwin,!netbsd,!freebsd,!openbsd,!dragonflybsd,!windows
// +build !linux,!darwin,!ios,!netbsd,!freebsd,!openbsd,!dragonflybsd,!windows
package multicast package multicast

View file

@ -1,13 +1,9 @@
//go:build linux || netbsd || freebsd || openbsd || dragonflybsd
// +build linux netbsd freebsd openbsd dragonflybsd // +build linux netbsd freebsd openbsd dragonflybsd
package multicast package multicast
import ( import "syscall"
"syscall" import "golang.org/x/sys/unix"
"golang.org/x/sys/unix"
)
func (m *Multicast) _multicastStarted() { func (m *Multicast) _multicastStarted() {
@ -15,19 +11,15 @@ func (m *Multicast) _multicastStarted() {
func (m *Multicast) multicastReuse(network string, address string, c syscall.RawConn) error { func (m *Multicast) multicastReuse(network string, address string, c syscall.RawConn) error {
var control error var control error
var reuseaddr error var reuseport error
control = c.Control(func(fd uintptr) { control = c.Control(func(fd uintptr) {
// Previously we used SO_REUSEPORT here, but that meant that machines running reuseport = unix.SetsockoptInt(int(fd), unix.SOL_SOCKET, unix.SO_REUSEPORT, 1)
// Yggdrasil nodes as different users would inevitably fail with EADDRINUSE.
// The behaviour for multicast is similar with both, so we'll use SO_REUSEADDR
// instead.
reuseaddr = unix.SetsockoptInt(int(fd), unix.SOL_SOCKET, unix.SO_REUSEADDR, 1)
}) })
switch { switch {
case reuseaddr != nil: case reuseport != nil:
return reuseaddr return reuseport
default: default:
return control return control
} }

View file

@ -1,13 +1,9 @@
//go:build windows
// +build windows // +build windows
package multicast package multicast
import ( import "syscall"
"syscall" import "golang.org/x/sys/windows"
"golang.org/x/sys/windows"
)
func (m *Multicast) _multicastStarted() { func (m *Multicast) _multicastStarted() {

View file

@ -1,30 +0,0 @@
package multicast
import "regexp"
func (m *Multicast) _applyOption(opt SetupOption) {
switch v := opt.(type) {
case MulticastInterface:
m.config._interfaces[v] = struct{}{}
case GroupAddress:
m.config._groupAddr = v
}
}
type SetupOption interface {
isSetupOption()
}
type MulticastInterface struct {
Regex *regexp.Regexp
Beacon bool
Listen bool
Port uint16
Priority uint8
Password string
}
type GroupAddress string
func (a MulticastInterface) isSetupOption() {}
func (a GroupAddress) isSetupOption() {}

View file

@ -1,45 +0,0 @@
package tun
import (
"encoding/json"
"github.com/yggdrasil-network/yggdrasil-go/src/admin"
)
type GetTUNRequest struct{}
type GetTUNResponse struct {
Enabled bool `json:"enabled"`
Name string `json:"name,omitempty"`
MTU uint64 `json:"mtu,omitempty"`
}
type TUNEntry struct {
MTU uint64 `json:"mtu"`
}
func (t *TunAdapter) getTUNHandler(req *GetTUNRequest, res *GetTUNResponse) error {
res.Enabled = t.isEnabled
if !t.isEnabled {
return nil
}
res.Name = t.Name()
res.MTU = t.MTU()
return nil
}
func (t *TunAdapter) SetupAdminHandlers(a *admin.AdminSocket) {
_ = a.AddHandler(
"getTun", "Show information about the node's TUN interface", []string{},
func(in json.RawMessage) (interface{}, error) {
req := &GetTUNRequest{}
res := &GetTUNResponse{}
if err := json.Unmarshal(in, &req); err != nil {
return nil, err
}
if err := t.getTUNHandler(req, res); err != nil {
return nil, err
}
return res, nil
},
)
}

View file

@ -1,75 +0,0 @@
package tun
import (
"errors"
wgtun "golang.zx2c4.com/wireguard/tun"
)
const TUN_OFFSET_BYTES = 80 // sizeof(virtio_net_hdr)
func (tun *TunAdapter) read() {
vs := tun.iface.BatchSize()
bufs := make([][]byte, vs)
sizes := make([]int, vs)
for i := range bufs {
bufs[i] = make([]byte, TUN_OFFSET_BYTES+65535)
}
for {
n, err := tun.iface.Read(bufs, sizes, TUN_OFFSET_BYTES)
if err != nil {
if errors.Is(err, wgtun.ErrTooManySegments) {
tun.log.Debugln("TUN segments dropped: %v", err)
continue
}
tun.log.Errorln("Error reading TUN:", err)
return
}
for i, b := range bufs[:n] {
if _, err := tun.rwc.Write(b[TUN_OFFSET_BYTES : TUN_OFFSET_BYTES+sizes[i]]); err != nil {
tun.log.Debugln("Unable to send packet:", err)
}
}
}
}
func (tun *TunAdapter) queue() {
for {
p := bufPool.Get().([]byte)[:bufPoolSize]
n, err := tun.rwc.Read(p)
if err != nil {
tun.log.Errorln("Exiting TUN writer due to core read error:", err)
return
}
tun.ch <- p[:n]
}
}
func (tun *TunAdapter) write() {
vs := cap(tun.ch)
bufs := make([][]byte, vs)
for i := range bufs {
bufs[i] = make([]byte, TUN_OFFSET_BYTES+65535)
}
for {
n := len(tun.ch)
if n == 0 {
n = 1 // Nothing queued up yet, wait for it instead
}
for i := 0; i < n; i++ {
msg := <-tun.ch
bufs[i] = append(bufs[i][:TUN_OFFSET_BYTES], msg...)
bufPool.Put(msg) // nolint:staticcheck
}
if !tun.isEnabled {
continue // Nothing to do, the tun isn't enabled
}
if _, err := tun.iface.Write(bufs[:n], TUN_OFFSET_BYTES); err != nil {
tun.Act(nil, func() {
if !tun.isOpen {
tun.log.Errorln("TUN iface write error:", err)
}
})
}
}
}

View file

@ -1,24 +0,0 @@
package tun
func (m *TunAdapter) _applyOption(opt SetupOption) {
switch v := opt.(type) {
case InterfaceName:
m.config.name = v
case InterfaceMTU:
m.config.mtu = v
case FileDescriptor:
m.config.fd = int32(v)
}
}
type SetupOption interface {
isSetupOption()
}
type InterfaceName string
type InterfaceMTU uint64
type FileDescriptor int32
func (a InterfaceName) isSetupOption() {}
func (a InterfaceMTU) isSetupOption() {}
func (a FileDescriptor) isSetupOption() {}

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