Compare commits

..

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

88 changed files with 2292 additions and 4521 deletions

View file

@ -15,12 +15,12 @@ jobs:
name: Lint
runs-on: ubuntu-latest
steps:
- uses: actions/setup-go@v5
- uses: actions/setup-go@v3
with:
go-version: stable
- uses: actions/checkout@v4
go-version: 1.19
- uses: actions/checkout@v3
- name: golangci-lint
uses: golangci/golangci-lint-action@v6
uses: golangci/golangci-lint-action@v3
with:
args: --issues-exit-code=1
@ -34,7 +34,7 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@v4
uses: actions/checkout@v3
- name: Initialize CodeQL
uses: github/codeql-action/init@v2
@ -51,17 +51,17 @@ jobs:
strategy:
fail-fast: false
matrix:
goversion: ["1.22", "1.23", "1.24"]
goversion: ["1.17", "1.18", "1.19"]
name: Build & Test (Linux, Go ${{ matrix.goversion }})
needs: [lint]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v3
- name: Set up Go
uses: actions/setup-go@v5
uses: actions/setup-go@v3
with:
go-version: ${{ matrix.goversion }}
@ -75,17 +75,17 @@ jobs:
strategy:
fail-fast: false
matrix:
goversion: ["1.22", "1.23", "1.24"]
goversion: ["1.17", "1.18", "1.19"]
name: Build & Test (Windows, Go ${{ matrix.goversion }})
needs: [lint]
runs-on: windows-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v3
- name: Set up Go
uses: actions/setup-go@v5
uses: actions/setup-go@v3
with:
go-version: ${{ matrix.goversion }}
@ -99,17 +99,17 @@ jobs:
strategy:
fail-fast: false
matrix:
goversion: ["1.22", "1.23", "1.24"]
goversion: ["1.17", "1.18", "1.19"]
name: Build & Test (macOS, Go ${{ matrix.goversion }})
needs: [lint]
runs-on: macos-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v3
- name: Set up Go
uses: actions/setup-go@v5
uses: actions/setup-go@v3
with:
go-version: ${{ matrix.goversion }}
@ -119,32 +119,6 @@ jobs:
- 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]
@ -154,4 +128,4 @@ jobs:
- name: Check all tests passed
uses: re-actors/alls-green@release/v1
with:
jobs: ${{ toJSON(needs) }}
jobs: ${{ toJSON(needs) }}

View file

@ -16,16 +16,16 @@ jobs:
name: Package (Debian, ${{ matrix.pkgarch }})
runs-on: ubuntu-20.04
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v3
with:
fetch-depth: 0
- name: Set up Go
uses: actions/setup-go@v5
uses: actions/setup-go@v3
with:
go-version: "stable"
go-version: 1.19
- name: Build package
env:
@ -33,7 +33,7 @@ jobs:
run: sh contrib/deb/generate.sh
- name: Upload artifacts
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v3
with:
name: Debian package (${{ matrix.pkgarch }})
path: "*.deb"
@ -49,14 +49,14 @@ jobs:
runs-on: macos-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v3
with:
fetch-depth: 0
- name: Set up Go
uses: actions/setup-go@v5
uses: actions/setup-go@v3
with:
go-version: "stable"
go-version: 1.19
- name: Build package
env:
@ -64,7 +64,7 @@ jobs:
run: sh contrib/macos/create-pkg.sh
- name: Upload artifacts
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v3
with:
name: macOS package (${{ matrix.pkgarch }})
path: "*.pkg"
@ -80,23 +80,20 @@ jobs:
runs-on: windows-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v3
with:
fetch-depth: 0
- name: Set up Go
uses: actions/setup-go@v5
uses: actions/setup-go@v3
with:
go-version: "stable"
- name: Setup .NET Core SDK
uses: actions/setup-dotnet@v4
go-version: 1.19
- name: Build package
run: sh contrib/msi/build-msi.sh ${{ matrix.pkgarch }}
- name: Upload artifacts
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v3
with:
name: Windows package (${{ matrix.pkgarch }})
path: "*.msi"
@ -110,22 +107,22 @@ jobs:
name: Package (Router, ${{ matrix.pkgarch }})
runs-on: ubuntu-20.04
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v3
with:
fetch-depth: 0
path: yggdrasil
- uses: actions/checkout@v4
- uses: actions/checkout@v3
with:
repository: neilalexander/vyatta-yggdrasil
path: vyatta-yggdrasil
- name: Set up Go
uses: actions/setup-go@v5
uses: actions/setup-go@v3
with:
go-version: "stable"
go-version: 1.19
- name: Build package
env:
@ -133,7 +130,7 @@ jobs:
run: cd /home/runner/work/yggdrasil-go/yggdrasil-go/vyatta-yggdrasil && ./build-${{ matrix.pkgarch }}
- name: Upload artifacts
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v3
with:
name: Router package (${{ matrix.pkgarch }})
path: "/home/runner/work/yggdrasil-go/yggdrasil-go/vyatta-yggdrasil/*.deb"

View file

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

File diff suppressed because it is too large Load diff

View file

@ -24,7 +24,7 @@ or tools in the `contrib` folder.
If you want to build from source, as opposed to installing one of the pre-built
packages:
1. Install [Go](https://golang.org) (requires Go 1.22 or later)
1. Install [Go](https://golang.org) (requires Go 1.17 or later)
2. Clone this repository
2. Run `./build`

View file

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

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,29 +1,34 @@
package main
import (
"bytes"
"context"
"crypto/ed25519"
"encoding/hex"
"encoding/json"
"flag"
"fmt"
"io"
"net"
"os"
"os/signal"
"regexp"
"strings"
"sync"
"syscall"
"suah.dev/protect"
"golang.org/x/text/encoding/unicode"
"github.com/gologme/log"
gsyslog "github.com/hashicorp/go-syslog"
"github.com/hjson/hjson-go/v4"
"github.com/hjson/hjson-go"
"github.com/kardianos/minwinsvc"
"github.com/mitchellh/mapstructure"
"github.com/yggdrasil-network/yggdrasil-go/src/address"
"github.com/yggdrasil-network/yggdrasil-go/src/admin"
"github.com/yggdrasil-network/yggdrasil-go/src/config"
"github.com/yggdrasil-network/yggdrasil-go/src/defaults"
"github.com/yggdrasil-network/yggdrasil-go/src/ipv6rwc"
"github.com/yggdrasil-network/yggdrasil-go/src/core"
@ -39,302 +44,75 @@ type node struct {
admin *admin.AdminSocket
}
// The main function is responsible for configuring and starting Yggdrasil.
func main() {
// Not all operations are coverable with pledge(2), so immediately
// limit file system access with unveil(2), effectively preventing
// "proc exec" promises right from the start:
//
// - read arbitrary config file
// - create/write arbitrary log file
// - read/write/chmod/remove admin socket, if at all
if err := protect.Unveil("/", "rwc"); err != nil {
panic(fmt.Sprintf("unveil: / rwc: %v", err))
}
if err := protect.UnveilBlock(); err != nil {
panic(fmt.Sprintf("unveil: %v", err))
}
genconf := flag.Bool("genconf", false, "print a new config to stdout")
useconf := flag.Bool("useconf", false, "read HJSON/JSON config from stdin")
useconffile := flag.String("useconffile", "", "read HJSON/JSON config from specified file path")
normaliseconf := flag.Bool("normaliseconf", false, "use in combination with either -useconf or -useconffile, outputs your configuration normalised")
exportkey := flag.Bool("exportkey", false, "use in combination with either -useconf or -useconffile, outputs your private key in PEM format")
confjson := flag.Bool("json", false, "print configuration from -genconf or -normaliseconf as JSON instead of HJSON")
autoconf := flag.Bool("autoconf", false, "automatic mode (dynamic IP, peer with IPv6 neighbors)")
ver := flag.Bool("version", false, "prints the version of this build")
logto := flag.String("logto", "stdout", "file path to log to, \"syslog\" or \"stdout\"")
getaddr := flag.Bool("address", false, "use in combination with either -useconf or -useconffile, outputs your IPv6 address")
getsnet := flag.Bool("subnet", false, "use in combination with either -useconf or -useconffile, outputs your IPv6 subnet")
getpkey := flag.Bool("publickey", false, "use in combination with either -useconf or -useconffile, outputs your public key")
loglevel := flag.String("loglevel", "info", "loglevel to enable")
chuserto := flag.String("user", "", "user (and, optionally, group) to set UID/GID to")
flag.Parse()
done := make(chan struct{})
defer close(done)
// Catch interrupts from the operating system to exit gracefully.
ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
// Create a new logger that logs output to stdout.
var logger *log.Logger
switch *logto {
case "stdout":
logger = log.New(os.Stdout, "", log.Flags())
case "syslog":
if syslogger, err := gsyslog.NewLogger(gsyslog.LOG_NOTICE, "DAEMON", version.BuildName()); err == nil {
logger = log.New(syslogger, "", log.Flags()&^(log.Ldate|log.Ltime))
}
default:
if logfd, err := os.OpenFile(*logto, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644); err == nil {
logger = log.New(logfd, "", log.Flags())
}
}
if logger == nil {
logger = log.New(os.Stdout, "", log.Flags())
logger.Warnln("Logging defaulting to stdout")
}
if *normaliseconf {
setLogLevel("error", logger)
} else {
setLogLevel(*loglevel, logger)
}
cfg := config.GenerateConfig()
func readConfig(log *log.Logger, useconf bool, useconffile string, normaliseconf bool) *config.NodeConfig {
// Use a configuration file. If -useconf, the configuration will be read
// from stdin. If -useconffile, the configuration will be read from the
// filesystem.
var conf []byte
var err error
switch {
case *ver:
fmt.Println("Build name:", version.BuildName())
fmt.Println("Build version:", version.BuildVersion())
return
case *autoconf:
// Use an autoconf-generated config, this will give us random keys and
// port numbers, and will use an automatically selected TUN interface.
case *useconf:
if _, err := cfg.ReadFrom(os.Stdin); err != nil {
panic(err)
}
case *useconffile != "":
f, err := os.Open(*useconffile)
if err != nil {
panic(err)
}
if _, err := cfg.ReadFrom(f); err != nil {
panic(err)
}
_ = f.Close()
case *genconf:
cfg.AdminListen = ""
var bs []byte
if *confjson {
bs, err = json.MarshalIndent(cfg, "", " ")
} else {
bs, err = hjson.Marshal(cfg)
}
if err != nil {
panic(err)
}
fmt.Println(string(bs))
return
default:
fmt.Println("Usage:")
flag.PrintDefaults()
if *getaddr || *getsnet {
fmt.Println("\nError: You need to specify some config data using -useconf or -useconffile.")
}
return
if useconffile != "" {
// Read the file from the filesystem
conf, err = os.ReadFile(useconffile)
} else {
// Read the file from stdin.
conf, err = io.ReadAll(os.Stdin)
}
privateKey := ed25519.PrivateKey(cfg.PrivateKey)
publicKey := privateKey.Public().(ed25519.PublicKey)
switch {
case *getaddr:
addr := address.AddrForKey(publicKey)
ip := net.IP(addr[:])
fmt.Println(ip.String())
return
case *getsnet:
snet := address.SubnetForKey(publicKey)
ipnet := net.IPNet{
IP: append(snet[:], 0, 0, 0, 0, 0, 0, 0, 0),
Mask: net.CIDRMask(len(snet)*8, 128),
}
fmt.Println(ipnet.String())
return
case *getpkey:
fmt.Println(hex.EncodeToString(publicKey))
return
case *normaliseconf:
cfg.AdminListen = ""
if cfg.PrivateKeyPath != "" {
cfg.PrivateKey = nil
}
var bs []byte
if *confjson {
bs, err = json.MarshalIndent(cfg, "", " ")
} else {
bs, err = hjson.Marshal(cfg)
}
if err != nil {
panic(err)
}
fmt.Println(string(bs))
return
case *exportkey:
pem, err := cfg.MarshalPEMPrivateKey()
if err != nil {
panic(err)
}
fmt.Println(string(pem))
return
if err != nil {
panic(err)
}
n := &node{}
// Set up the Yggdrasil node itself.
{
iprange := net.IPNet{
IP: net.ParseIP("200::"),
Mask: net.CIDRMask(7, 128),
}
options := []core.SetupOption{
core.NodeInfo(cfg.NodeInfo),
core.NodeInfoPrivacy(cfg.NodeInfoPrivacy),
core.PeerFilter(func(ip net.IP) bool {
return !iprange.Contains(ip)
}),
}
for _, addr := range cfg.Listen {
options = append(options, core.ListenAddress(addr))
}
for _, peer := range cfg.Peers {
options = append(options, core.Peer{URI: peer})
}
for intf, peers := range cfg.InterfacePeers {
for _, peer := range peers {
options = append(options, core.Peer{URI: peer, SourceInterface: intf})
}
}
for _, allowed := range cfg.AllowedPublicKeys {
k, err := hex.DecodeString(allowed)
if err != nil {
panic(err)
}
options = append(options, core.AllowedPublicKey(k[:]))
}
if n.core, err = core.New(cfg.Certificate, logger, options...); err != nil {
panic(err)
}
address, subnet := n.core.Address(), n.core.Subnet()
logger.Printf("Your public key is %s", hex.EncodeToString(n.core.PublicKey()))
logger.Printf("Your IPv6 address is %s", address.String())
logger.Printf("Your IPv6 subnet is %s", subnet.String())
}
// Set up the admin socket.
{
options := []admin.SetupOption{
admin.ListenAddress(cfg.AdminListen),
}
if cfg.LogLookups {
options = append(options, admin.LogLookups{})
}
if n.admin, err = admin.New(n.core, logger, options...); err != nil {
panic(err)
}
if n.admin != nil {
n.admin.SetupAdminHandlers()
}
}
// Set up the multicast module.
{
options := []multicast.SetupOption{}
for _, intf := range cfg.MulticastInterfaces {
options = append(options, multicast.MulticastInterface{
Regex: regexp.MustCompile(intf.Regex),
Beacon: intf.Beacon,
Listen: intf.Listen,
Port: intf.Port,
Priority: uint8(intf.Priority),
Password: intf.Password,
})
}
if n.multicast, err = multicast.New(n.core, logger, options...); err != nil {
panic(err)
}
if n.admin != nil && n.multicast != nil {
n.multicast.SetupAdminHandlers(n.admin)
}
}
// Set up the TUN module.
{
options := []tun.SetupOption{
tun.InterfaceName(cfg.IfName),
tun.InterfaceMTU(cfg.IfMTU),
}
if n.tun, err = tun.New(ipv6rwc.NewReadWriteCloser(n.core), logger, options...); err != nil {
panic(err)
}
if n.admin != nil && n.tun != nil {
n.tun.SetupAdminHandlers(n.admin)
}
}
//Windows service shutdown
minwinsvc.SetOnExit(func() {
logger.Infof("Shutting down service ...")
cancel()
// Wait for all parts to shutdown properly
<-done
})
// Change user if requested
if *chuserto != "" {
err = chuser(*chuserto)
// If there's a byte order mark - which Windows 10 is now incredibly fond of
// throwing everywhere when it's converting things into UTF-16 for the hell
// of it - remove it and decode back down into UTF-8. This is necessary
// because hjson doesn't know what to do with UTF-16 and will panic
if bytes.Equal(conf[0:2], []byte{0xFF, 0xFE}) ||
bytes.Equal(conf[0:2], []byte{0xFE, 0xFF}) {
utf := unicode.UTF16(unicode.BigEndian, unicode.UseBOM)
decoder := utf.NewDecoder()
conf, err = decoder.Bytes(conf)
if err != nil {
panic(err)
}
}
// Promise final modes of operation. At this point, if at all:
// - raw socket is created/open
// - admin socket is created/open
// - privileges are dropped to non-root user
//
// Peers, InterfacePeers, Listen can be UNIX sockets;
// Go's net.Listen.Close() deletes files on shutdown.
promises := []string{"stdio", "cpath", "inet", "unix", "dns"}
if len(cfg.MulticastInterfaces) > 0 {
promises = append(promises, "mcast")
// Generate a new configuration - this gives us a set of sane defaults -
// then parse the configuration we loaded above on top of it. The effect
// of this is that any configuration item that is missing from the provided
// configuration will use a sane default.
cfg := defaults.GenerateConfig()
var dat map[string]interface{}
if err := hjson.Unmarshal(conf, &dat); err != nil {
panic(err)
}
if err := protect.Pledge(strings.Join(promises, " ")); err != nil {
panic(fmt.Sprintf("pledge: %v: %v", promises, err))
// Sanitise the config
confJson, err := json.Marshal(dat)
if err != nil {
panic(err)
}
if err := json.Unmarshal(confJson, &cfg); err != nil {
panic(err)
}
// Overlay our newly mapped configuration onto the autoconf node config that
// we generated above.
if err = mapstructure.Decode(dat, &cfg); err != nil {
panic(err)
}
return cfg
}
// Block until we are told to shut down.
<-ctx.Done()
// Shut down the node.
_ = n.admin.Stop()
_ = n.multicast.Stop()
_ = n.tun.Stop()
n.core.Stop()
// Generates a new configuration and returns it in HJSON format. This is used
// with -genconf.
func doGenconf(isjson bool) string {
cfg := defaults.GenerateConfig()
var bs []byte
var err error
if isjson {
bs, err = json.MarshalIndent(cfg, "", " ")
} else {
bs, err = hjson.Marshal(cfg)
}
if err != nil {
panic(err)
}
return string(bs)
}
func setLogLevel(loglevel string, logger *log.Logger) {
@ -362,3 +140,264 @@ func setLogLevel(loglevel string, logger *log.Logger) {
}
}
}
type yggArgs struct {
genconf bool
useconf bool
normaliseconf bool
confjson bool
autoconf bool
ver bool
getaddr bool
getsnet bool
useconffile string
logto string
loglevel string
}
func getArgs() yggArgs {
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()
return yggArgs{
genconf: *genconf,
useconf: *useconf,
useconffile: *useconffile,
normaliseconf: *normaliseconf,
confjson: *confjson,
autoconf: *autoconf,
ver: *ver,
logto: *logto,
getaddr: *getaddr,
getsnet: *getsnet,
loglevel: *loglevel,
}
}
// The main function is responsible for configuring and starting Yggdrasil.
func run(args yggArgs, ctx context.Context) {
// Create a new logger that logs output to stdout.
var logger *log.Logger
switch args.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(args.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 args.normaliseconf {
setLogLevel("error", logger)
} else {
setLogLevel(args.loglevel, logger)
}
var cfg *config.NodeConfig
var err error
switch {
case args.ver:
fmt.Println("Build name:", version.BuildName())
fmt.Println("Build version:", version.BuildVersion())
return
case args.autoconf:
// Use an autoconf-generated config, this will give us random keys and
// port numbers, and will use an automatically selected TUN interface.
cfg = defaults.GenerateConfig()
case args.useconffile != "" || args.useconf:
// Read the configuration from either stdin or from the filesystem
cfg = readConfig(logger, args.useconf, args.useconffile, args.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 args.normaliseconf {
var bs []byte
if args.confjson {
bs, err = json.MarshalIndent(cfg, "", " ")
} else {
bs, err = hjson.Marshal(cfg)
}
if err != nil {
panic(err)
}
fmt.Println(string(bs))
return
}
case args.genconf:
// Generate a new configuration and print it to stdout.
fmt.Println(doGenconf(args.confjson))
return
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 args.getaddr:
if key := getNodeKey(); key != nil {
addr := address.AddrForKey(key)
ip := net.IP(addr[:])
fmt.Println(ip.String())
}
return
case args.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
}
n := &node{}
// Setup the Yggdrasil node itself.
{
sk, err := hex.DecodeString(cfg.PrivateKey)
if err != nil {
panic(err)
}
options := []core.SetupOption{
core.NodeInfo(cfg.NodeInfo),
core.NodeInfoPrivacy(cfg.NodeInfoPrivacy),
}
for _, addr := range cfg.Listen {
options = append(options, core.ListenAddress(addr))
}
for _, peer := range cfg.Peers {
options = append(options, core.Peer{URI: peer})
}
for intf, peers := range cfg.InterfacePeers {
for _, peer := range peers {
options = append(options, core.Peer{URI: peer, SourceInterface: intf})
}
}
for _, allowed := range cfg.AllowedPublicKeys {
k, err := hex.DecodeString(allowed)
if err != nil {
panic(err)
}
options = append(options, core.AllowedPublicKey(k[:]))
}
if n.core, err = core.New(sk[:], logger, options...); err != nil {
panic(err)
}
}
// Setup the admin socket.
{
options := []admin.SetupOption{
admin.ListenAddress(cfg.AdminListen),
}
if n.admin, err = admin.New(n.core, logger, options...); err != nil {
panic(err)
}
if n.admin != nil {
n.admin.SetupAdminHandlers()
}
}
// Setup the multicast module.
{
options := []multicast.SetupOption{}
for _, intf := range cfg.MulticastInterfaces {
options = append(options, multicast.MulticastInterface{
Regex: regexp.MustCompile(intf.Regex),
Beacon: intf.Beacon,
Listen: intf.Listen,
Port: intf.Port,
Priority: intf.Priority,
})
}
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)
}
}
// Setup the TUN module.
{
options := []tun.SetupOption{
tun.InterfaceName(cfg.IfName),
tun.InterfaceMTU(cfg.IfMTU),
}
if n.tun, err = tun.New(ipv6rwc.NewReadWriteCloser(n.core), logger, options...); err != nil {
panic(err)
}
if n.admin != nil && n.tun != nil {
n.tun.SetupAdminHandlers(n.admin)
}
}
// 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())
// 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 main() {
args := getArgs()
// Catch interrupts from the operating system to exit gracefully.
ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
// Capture the service being stopped on Windows.
minwinsvc.SetOnExit(cancel)
// Start the node, block and then wait for it to shut down.
var wg sync.WaitGroup
wg.Add(1)
go func() {
defer wg.Done()
run(args, ctx)
}()
wg.Wait()
}

View file

@ -7,10 +7,10 @@ import (
"log"
"os"
"github.com/hjson/hjson-go/v4"
"github.com/hjson/hjson-go"
"golang.org/x/text/encoding/unicode"
"github.com/yggdrasil-network/yggdrasil-go/src/config"
"github.com/yggdrasil-network/yggdrasil-go/src/defaults"
)
type CmdLineEnv struct {
@ -21,7 +21,7 @@ type CmdLineEnv struct {
func newCmdLineEnv() CmdLineEnv {
var cmdLineEnv CmdLineEnv
cmdLineEnv.endpoint = config.GetDefaults().DefaultAdminListen
cmdLineEnv.endpoint = defaults.GetDefaults().DefaultAdminListen
return cmdLineEnv
}
@ -38,8 +38,10 @@ func (cmdLineEnv *CmdLineEnv) parseFlagsAndArgs() {
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")
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", cmdLineEnv.endpoint, "Admin socket endpoint")
@ -56,31 +58,31 @@ func (cmdLineEnv *CmdLineEnv) parseFlagsAndArgs() {
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}) {
if config, err := os.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()
cfg, err = decoder.Bytes(cfg)
config, err = decoder.Bytes(config)
if err != nil {
panic(err)
}
}
var dat map[string]interface{}
if err := hjson.Unmarshal(cfg, &dat); err != nil {
if err := hjson.Unmarshal(config, &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("Found platform default config file", defaults.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)
logger.Println("Falling back to platform default", defaults.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)
logger.Println("Can't open config file from default location", defaults.GetDefaults().DefaultConfigFile)
logger.Println("Falling back to platform default", defaults.GetDefaults().DefaultAdminListen)
}
} else {
cmdLineEnv.endpoint = cmdLineEnv.server

View file

@ -13,8 +13,6 @@ import (
"strings"
"time"
"suah.dev/protect"
"github.com/olekukonko/tablewriter"
"github.com/yggdrasil-network/yggdrasil-go/src/admin"
"github.com/yggdrasil-network/yggdrasil-go/src/core"
@ -24,11 +22,6 @@ import (
)
func main() {
// read config, speak DNS/TCP and/or over a UNIX socket
if err := protect.Pledge("stdio rpath inet unix dns"); err != nil {
panic(err)
}
// makes sure we can use defer and still return an error code to the OS
os.Exit(run())
}
@ -85,11 +78,6 @@ func run() int {
panic(err)
}
// config and socket are done, work without unprivileges
if err := protect.Pledge("stdio"); err != nil {
panic(err)
}
logger.Println("Connected")
defer conn.Close()
@ -177,7 +165,7 @@ func run() int {
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{"Coordinates:", fmt.Sprintf("%v", resp.Coords)})
table.Append([]string{"Public key:", resp.PublicKey})
table.Render()
@ -186,61 +174,33 @@ func run() int {
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"})
table.SetHeader([]string{"Port", "Public Key", "IP Address", "Uptime", "RX", "TX", "Pr", "URI"})
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,
fmt.Sprintf("%d", peer.Port),
peer.PublicKey,
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,
peer.Remote,
})
}
table.Render()
case "gettree":
var resp admin.GetTreeResponse
case "getdht":
var resp admin.GetDHTResponse
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.SetHeader([]string{"Public Key", "IP Address", "Port", "Rest"})
for _, dht := range resp.DHT {
table.Append([]string{
tree.PublicKey,
tree.IPAddress,
tree.Parent,
fmt.Sprintf("%d", tree.Sequence),
//fmt.Sprintf("%d", dht.Port),
//fmt.Sprintf("%d", dht.Rest),
dht.PublicKey,
dht.IPAddress,
fmt.Sprintf("%d", dht.Port),
fmt.Sprintf("%d", dht.Rest),
})
}
table.Render()
@ -250,13 +210,12 @@ func run() int {
if err := json.Unmarshal(recv.Response, &resp); err != nil {
panic(err)
}
table.SetHeader([]string{"Public Key", "IP Address", "Path", "Seq"})
table.SetHeader([]string{"Public Key", "IP Address", "Path"})
for _, p := range resp.Paths {
table.Append([]string{
p.PublicKey,
p.IPAddress,
fmt.Sprintf("%v", p.Path),
fmt.Sprintf("%d", p.Sequence),
})
}
table.Render()
@ -293,21 +252,9 @@ func run() int {
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"})
table.SetHeader([]string{"Interface"})
for _, p := range resp.Interfaces {
table.Append([]string{
p.Name,
p.Address,
fmtBool(p.Beacon),
fmtBool(p.Listen),
fmtBool(p.Password),
})
table.Append([]string{p})
}
table.Render()

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

View file

@ -7,7 +7,6 @@ set -ef
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"
@ -34,20 +33,11 @@ if [ ! $IOS ] && [ ! $ANDROID ]; then
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 \
-target ios -tags mobile -o Yggdrasil.xcframework \
-ldflags="$LDFLAGS $STRIP" -gcflags="$GCFLAGS" \
./contrib/mobile ./src/config;
fi

View file

@ -1,9 +1,9 @@
package mobile
import (
"crypto/ed25519"
"encoding/hex"
"encoding/json"
"fmt"
"net"
"regexp"
@ -12,10 +12,12 @@ import (
"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/defaults"
"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"
_ "golang.org/x/mobile/bind"
)
// Yggdrasil mobile package is meant to "plug the gap" for mobile support, as
@ -28,9 +30,7 @@ type Yggdrasil struct {
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
@ -41,28 +41,21 @@ func (m *Yggdrasil) StartAutoconfigure() error {
// 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 {
m.config = defaults.GenerateConfig()
if err := json.Unmarshal(configjson, &m.config); err != nil {
return err
}
// Set up the Yggdrasil node itself.
// Setup 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)
}),
sk, err := hex.DecodeString(m.config.PrivateKey)
if err != nil {
panic(err)
}
options := []core.SetupOption{}
for _, peer := range m.config.Peers {
options = append(options, core.Peer{URI: peer})
}
@ -78,24 +71,15 @@ func (m *Yggdrasil) StartJSON(configjson []byte) error {
}
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...)
m.core, err = core.New(sk[:], 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.
// Setup 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{
@ -103,12 +87,10 @@ func (m *Yggdrasil) StartJSON(configjson []byte) error {
Beacon: intf.Beacon,
Listen: intf.Listen,
Port: intf.Port,
Priority: uint8(intf.Priority),
Password: intf.Password,
Priority: intf.Priority,
})
}
logger.Infof("Starting multicast %s", "")
m.multicast, err = multicast.New(m.core, m.logger, options...)
m.multicast, err = multicast.New(m.core, logger, options...)
if err != nil {
logger.Errorln("An error occurred starting multicast:", err)
}
@ -133,18 +115,6 @@ func (m *Yggdrasil) Send(p []byte) error {
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) {
@ -156,45 +126,21 @@ func (m *Yggdrasil) Recv() ([]byte, error) {
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("Stop the mobile Yggdrasil instance %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 := defaults.GenerateConfig()
nc.IfName = "none"
if json, err := json.Marshal(nc); err == nil {
return json
@ -219,9 +165,9 @@ 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)
// GetCoordsString gets the node's coordinates
func (m *Yggdrasil) GetCoordsString() string {
return fmt.Sprintf("%v", m.core.GetSelf().Coords)
}
func (m *Yggdrasil) GetPeersJSON() (result string) {
@ -230,11 +176,8 @@ func (m *Yggdrasil) GetPeersJSON() (result string) {
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()
}
a := address.AddrForKey(v.Key)
ip := net.IP(a[:]).String()
peers = append(peers, struct {
core.PeerInfo
IP string
@ -250,16 +193,8 @@ func (m *Yggdrasil) GetPeersJSON() (result string) {
}
}
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 {
func (m *Yggdrasil) GetDHTJSON() (result string) {
if res, err := json.Marshal(m.core.GetDHT()); err == nil {
return string(res)
} else {
return "{}"
@ -274,28 +209,3 @@ func (m *Yggdrasil) GetMTU() int {
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,5 +1,5 @@
//go:build ios || darwin
// +build ios darwin
//go:build ios
// +build ios
package mobile
@ -15,8 +15,6 @@ void Log(const char *text) {
import "C"
import (
"unsafe"
"github.com/yggdrasil-network/yggdrasil-go/src/tun"
)
type MobileLogger struct {
@ -28,13 +26,3 @@ func (nsl MobileLogger) Write(p []byte) (n int, err error) {
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,5 +1,5 @@
//go:build !android && !ios && !darwin
// +build !android,!ios,!darwin
//go:build !android && !ios
// +build !android,!ios
package mobile

View file

@ -1,27 +1,15 @@
package mobile
import (
"os"
"testing"
"github.com/gologme/log"
)
import "testing"
func TestStartYggdrasil(t *testing.T) {
logger := log.New(os.Stdout, "", 0)
logger.EnableLevel("error")
logger.EnableLevel("warn")
logger.EnableLevel("info")
ygg := &Yggdrasil{
logger: logger,
}
ygg := &Yggdrasil{}
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())
t.Log("Coords:", ygg.GetCoordsString())
if err := ygg.Stop(); err != nil {
t.Fatalf("Failed to stop Yggdrasil: %s", err)
}

View file

@ -1,7 +1,7 @@
#!/bin/sh
# 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.17 or later must be installed on
# the system and within the PATH. This is ran currently by GitHub Actions (see
# the workflows in the repository).
#
@ -16,7 +16,20 @@ then
fi
# Download the wix tools!
dotnet tool install --global wix --version 5.0.0
if [ ! -d wixbin ];
then
curl -LO https://wixtoolset.org/downloads/v3.14.0.3910/wix314-binaries.zip
if [ `md5sum wix314-binaries.zip | cut -f 1 -d " "` != "34f655cf108086838dd5a76d4318063b" ];
then
echo "wix package didn't match expected checksum"
exit 1
fi
mkdir -p wixbin
unzip -o wix314-binaries.zip -d wixbin || (
echo "failed to unzip WiX"
exit 1
)
fi
# Build Yggdrasil!
[ "${PKGARCH}" == "x64" ] && GOOS=windows GOARCH=amd64 CGO_ENABLED=0 ./build
@ -48,11 +61,6 @@ PKGVERSIONMS=$(echo $PKGVERSION | tr - .)
if [ ! -d wintun ];
then
curl -o wintun.zip https://www.wintun.net/builds/wintun-0.14.1.zip
if [ `sha256sum wintun.zip | cut -f 1 -d " "` != "07c256185d6ee3652e09fa55c0b673e2624b565e02c4b9091c79ca7d2f24ef51" ];
then
echo "wintun package didn't match expected checksum"
exit 1
fi
unzip wintun.zip
fi
if [ $PKGARCH = "x64" ]; then
@ -93,7 +101,7 @@ cat > wix.xml << EOF
Description="Yggdrasil Network Installer"
Comments="Yggdrasil Network standalone router for Windows."
Manufacturer="github.com/yggdrasil-network"
InstallerVersion="500"
InstallerVersion="200"
InstallScope="perMachine"
Languages="1033"
Compressed="yes"
@ -197,5 +205,5 @@ EOF
# Generate the MSI
CANDLEFLAGS="-nologo"
LIGHTFLAGS="-nologo -spdb -sice:ICE71 -sice:ICE61"
candle $CANDLEFLAGS -out ${PKGNAME}-${PKGVERSION}-${PKGARCH}.wixobj -arch ${PKGARCH} wix.xml && \
light $LIGHTFLAGS -ext WixUtilExtension.dll -out ${PKGNAME}-${PKGVERSION}-${PKGARCH}.msi ${PKGNAME}-${PKGVERSION}-${PKGARCH}.wixobj
wixbin/candle $CANDLEFLAGS -out ${PKGNAME}-${PKGVERSION}-${PKGARCH}.wixobj -arch ${PKGARCH} wix.xml && \
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"
command="/usr/bin/yggdrasil"
extra_started_commands="reload"
depend() {
use net dns logger
@ -41,6 +42,12 @@ start() {
eend $?
}
reload() {
ebegin "Reloading ${RC_SVCNAME}"
start-stop-daemon --signal HUP --pidfile "${pidfile}"
eend $?
}
stop() {
ebegin "Stopping ${RC_SVCNAME}"
start-stop-daemon --stop --pidfile "${pidfile}" --exec "${command}"

View file

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

55
go.mod
View file

@ -1,49 +1,38 @@
module github.com/yggdrasil-network/yggdrasil-go
go 1.22
go 1.17
require (
github.com/Arceliar/ironwood v0.0.0-20241213013129-743fe2fccbd3
github.com/Arceliar/phony v0.0.0-20220903101357-530938a4b13d
github.com/cheggaaa/pb/v3 v3.1.5
github.com/coder/websocket v1.8.12
github.com/gologme/log v1.3.0
github.com/Arceliar/ironwood v0.0.0-20221025225125-45b4281814c2
github.com/Arceliar/phony v0.0.0-20210209235338-dde1a8dca979
github.com/cheggaaa/pb/v3 v3.0.8
github.com/gologme/log v1.2.0
github.com/hashicorp/go-syslog v1.0.0
github.com/hjson/hjson-go/v4 v4.4.0
github.com/hjson/hjson-go v3.1.0+incompatible
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
github.com/mitchellh/mapstructure v1.4.1
github.com/vishvananda/netlink v1.1.0
golang.org/x/mobile v0.0.0-20221012134814-c746ac228303
golang.org/x/net v0.0.0-20221014081412-f15817d10f9b
golang.org/x/sys v0.0.0-20221013171732-95e765b1cc43
golang.org/x/text v0.3.8
golang.zx2c4.com/wireguard v0.0.0-20211017052713-f87e87af0d9a
golang.zx2c4.com/wireguard/windows v0.4.12
)
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/mattn/go-colorable v0.1.8 // 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
golang.org/x/crypto v0.0.0-20221012134737-56aed061732a // indirect
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4 // indirect
golang.org/x/tools v0.1.12 // indirect
)
require (
github.com/VividCortex/ewma v1.2.0 // indirect
github.com/fatih/color v1.18.0 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-runewidth v0.0.16 // indirect
github.com/fatih/color v1.12.0 // indirect
github.com/mattn/go-isatty v0.0.13 // indirect
github.com/mattn/go-runewidth v0.0.13 // indirect
github.com/olekukonko/tablewriter v0.0.5
github.com/vishvananda/netns v0.0.5 // indirect
suah.dev/protect v1.2.4
github.com/vishvananda/netns v0.0.0-20210104183010-2eb08e3e575f // indirect
)

218
go.sum
View file

@ -1,119 +1,121 @@
github.com/Arceliar/ironwood v0.0.0-20241213013129-743fe2fccbd3 h1:d8N0z+udAnbU5PdjpLSNPTWlqeU/nnYsQ42B6+879aw=
github.com/Arceliar/ironwood v0.0.0-20241213013129-743fe2fccbd3/go.mod h1:SrrElc3FFMpYCODSr11jWbLFeOM8WsY+DbDY/l2AXF0=
github.com/Arceliar/phony v0.0.0-20220903101357-530938a4b13d h1:UK9fsWbWqwIQkMCz1CP+v5pGbsGoWAw6g4AyvMpm1EM=
github.com/Arceliar/phony v0.0.0-20220903101357-530938a4b13d/go.mod h1:BCnxhRf47C/dy/e/D2pmB8NkB3dQVIrkD98b220rx5Q=
github.com/Arceliar/ironwood v0.0.0-20221025225125-45b4281814c2 h1:Usab30pNT2i/vZvpXcN9uOr5IO1RZPcUqoGH0DIAPnU=
github.com/Arceliar/ironwood v0.0.0-20221025225125-45b4281814c2/go.mod h1:RP72rucOFm5udrnEzTmIWLRVGQiV/fSUAQXJ0RST/nk=
github.com/Arceliar/phony v0.0.0-20210209235338-dde1a8dca979 h1:WndgpSW13S32VLQ3ugUxx2EnnWmgba1kCqPkd4Gk1yQ=
github.com/Arceliar/phony v0.0.0-20210209235338-dde1a8dca979/go.mod h1:6Lkn+/zJilRMsKmbmG1RPoamiArC6HS73xbwRyp3UyI=
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
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/go.mod h1:nz4BbCtbLyFDeC9SUHbtcT5644juEuWfUAUnGx7j5l4=
github.com/bits-and-blooms/bitset v1.10.0/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8=
github.com/bits-and-blooms/bitset v1.13.0 h1:bAQ9OPNFYbGHV6Nez0tmNI0RiEu7/hxlYJRUA0wFAVE=
github.com/bits-and-blooms/bitset v1.13.0/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8=
github.com/bits-and-blooms/bloom/v3 v3.7.0 h1:VfknkqV4xI+PsaDIsoHueyxVDZrfvMn56jeWUzvzdls=
github.com/bits-and-blooms/bloom/v3 v3.7.0/go.mod h1:VKlUSvp0lFIYqxJjzdnSsZEw4iHb1kOL2tfHTgyJBHg=
github.com/cheggaaa/pb/v3 v3.1.5 h1:QuuUzeM2WsAqG2gMqtzaWithDJv0i+i6UlnwSCI4QLk=
github.com/cheggaaa/pb/v3 v3.1.5/go.mod h1:CrxkeghYTXi1lQBEI7jSn+3svI3cuc19haAj6jM60XI=
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
github.com/coder/websocket v1.8.12 h1:5bUXkEPPIbewrnkU8LTCLVaxi4N4J8ahufH2vlo4NAo=
github.com/coder/websocket v1.8.12/go.mod h1:LNVeNrXQZfe5qhS9ALED3uA+l5pPqvwXg3CKoDBB2gs=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU=
github.com/go-logr/logr v1.2.4 h1:g01GSCwiDw2xSZfjJ2/T9M+S6pFdcNtFYsp+Y43HYDQ=
github.com/go-logr/logr v1.2.4/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI=
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls=
github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
github.com/gologme/log v1.3.0 h1:l781G4dE+pbigClDSDzSaaYKtiueHCILUa/qSDsmHAo=
github.com/gologme/log v1.3.0/go.mod h1:yKT+DvIPdDdDoPtqFrFxheooyVmoqi0BAsw+erN3wA4=
github.com/google/btree v1.0.1 h1:gK4Kx5IaGY9CD5sPJ36FHiBJ6ZXl0kilRiiCj+jdYp4=
github.com/google/btree v1.0.1/go.mod h1:xXMiIv4Fb/0kKde4SpL7qlzvu5cMJDRkFDxJfI9uaxA=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38 h1:yAJXTCF9TqKcTiHJAE8dj7HMvPfh66eeA2JYW7eFpSE=
github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/cheggaaa/pb/v3 v3.0.8 h1:bC8oemdChbke2FHIIGy9mn4DPJ2caZYQnfbRqwmdCoA=
github.com/cheggaaa/pb/v3 v3.0.8/go.mod h1:UICbiLec/XO6Hw6k+BHEtHeQFzzBH4i2/qk/ow1EJTA=
github.com/fatih/color v1.10.0/go.mod h1:ELkj/draVOlAH/xkhN6mQ50Qd0MPOk5AAr3maGEBuJM=
github.com/fatih/color v1.12.0 h1:mRhaKNwANqRgUBGKmnI5ZxEk7QXmjQeCcuYFMX2bfcc=
github.com/fatih/color v1.12.0/go.mod h1:ELkj/draVOlAH/xkhN6mQ50Qd0MPOk5AAr3maGEBuJM=
github.com/gologme/log v1.2.0 h1:Ya5Ip/KD6FX7uH0S31QO87nCCSucKtF44TLbTtO7V4c=
github.com/gologme/log v1.2.0/go.mod h1:gq31gQ8wEHkR+WekdWsqDuf8pXTUZA9BnnzTuPz1Y9U=
github.com/hashicorp/go-syslog v1.0.0 h1:KaodqZuhUoZereWVIYmpUgZysurB1kBLX2j0MwMrUAE=
github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4=
github.com/hjson/hjson-go/v4 v4.4.0 h1:D/NPvqOCH6/eisTb5/ztuIS8GUvmpHaLOcNk1Bjr298=
github.com/hjson/hjson-go/v4 v4.4.0/go.mod h1:KaYt3bTw3zhBjYqnXkYywcYctk0A2nxeEFTse3rH13E=
github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
github.com/hjson/hjson-go v3.1.0+incompatible h1:DY/9yE8ey8Zv22bY+mHV1uk2yRy0h8tKhZ77hEdi0Aw=
github.com/hjson/hjson-go v3.1.0+incompatible/go.mod h1:qsetwF8NlsTsOTwZTApNlTCerV+b2GjYRRcIk4JMFio=
github.com/kardianos/minwinsvc v1.0.2 h1:JmZKFJQrmTGa/WiW+vkJXKmfzdjabuEW4Tirj5lLdR0=
github.com/kardianos/minwinsvc v1.0.2/go.mod h1:LUZNYhNmxujx2tR7FbdxqYJ9XDDoCd3MQcl1o//FWl4=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/lxn/walk v0.0.0-20210112085537-c389da54e794/go.mod h1:E23UucZGqpuUANJooIbHWCufXvOcT6E7Stq81gU+CSQ=
github.com/lxn/win v0.0.0-20210218163916-a377121e959e/go.mod h1:KxxjdtRkfNoYDCUP5ryK7XJJNTnpC8atvtmTheChOtk=
github.com/mattn/go-colorable v0.1.8 h1:c1ghPdyEDarC70ftn0y+A/Ee++9zz8ljHG1b13eJ0s8=
github.com/mattn/go-colorable v0.1.8/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
github.com/mattn/go-isatty v0.0.13 h1:qdl+GuBjcsKKDco5BsxPJlId98mSWNKqYA+Co0SC1yA=
github.com/mattn/go-isatty v0.0.13/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
github.com/mattn/go-runewidth v0.0.13 h1:lTGmDsbAYt5DmK6OnoV7EuIF1wEIFAcxld6ypU4OSgU=
github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/mitchellh/mapstructure v1.4.1 h1:CpVNEelQCZBooIPDn+AR3NpivK/TIKU8bDxdASFVQag=
github.com/mitchellh/mapstructure v1.4.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec=
github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY=
github.com/onsi/ginkgo/v2 v2.9.5 h1:+6Hr4uxzP4XIUyAkg61dWBw8lb/gc4/X5luuxN/EC+Q=
github.com/onsi/ginkgo/v2 v2.9.5/go.mod h1:tvAoo1QUJwNEU2ITftXTpR7R1RbCzoZUOs3RonqW57k=
github.com/onsi/gomega v1.27.6 h1:ENqfyGeS5AX/rlXDd/ETokDz93u0YufY1Pgxuy/PvWE=
github.com/onsi/gomega v1.27.6/go.mod h1:PIQNjfQwkP3aQAH7lf7j87O/5FiNr+ZR8+ipb+qQlhg=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/quic-go/quic-go v0.48.2 h1:wsKXZPeGWpMpCGSWqOcqpW2wZYic/8T3aqiOID0/KWE=
github.com/quic-go/quic-go v0.48.2/go.mod h1:yBgs3rWBOADpga7F+jJsb6Ybg1LSYiQvwWlLX+/6HMs=
github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/twmb/murmur3 v1.1.6 h1:mqrRot1BRxm+Yct+vavLMou2/iJt0tNVTTC0QoIjaZg=
github.com/twmb/murmur3 v1.1.6/go.mod h1:Qq/R7NUyOfr65zD+6Q5IHKsJLwP7exErjN6lyyq3OSQ=
github.com/vishvananda/netlink v1.3.0 h1:X7l42GfcV4S6E4vHTsw48qbrV+9PVojNfIhZcwQdrZk=
github.com/vishvananda/netlink v1.3.0/go.mod h1:i6NetklAujEcC6fK0JPjT8qSwWyO0HLn4UKG+hGqeJs=
github.com/vishvananda/netns v0.0.4/go.mod h1:SpkAiCQRtJ6TvvxPnOSyH3BMl6unz3xZlaprSwhNNJM=
github.com/vishvananda/netns v0.0.5 h1:DfiHV+j8bA32MFM7bfEunvT8IAqQ/NzSJHtcmW5zdEY=
github.com/vishvananda/netns v0.0.5/go.mod h1:SpkAiCQRtJ6TvvxPnOSyH3BMl6unz3xZlaprSwhNNJM=
github.com/wlynxg/anet v0.0.5 h1:J3VJGi1gvo0JwZ/P1/Yc/8p63SoW98B5dHkYDmpgvvU=
github.com/wlynxg/anet v0.0.5/go.mod h1:eay5PRQr7fIVAMbTbchTnO9gG65Hg/uYGdc7mguHxoA=
go.uber.org/mock v0.5.0 h1:KAMbZvZPyBPWgD14IrIQ38QCyjwpvVVV6K/bHl1IwQU=
go.uber.org/mock v0.5.0/go.mod h1:ge71pBPLYDk7QIi1LupWxdAykm7KIEFchiOqd6z7qMM=
golang.org/x/crypto v0.33.0 h1:IOBPskki6Lysi0lo9qQvbxiQ+FvsCC/YWOecCHAixus=
golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M=
golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 h1:2dVuKD2vS7b0QIHQbpyTISPd0LeHDbnYEryqj5Q1ug8=
golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY=
golang.org/x/mod v0.19.0 h1:fEdghXQSo20giMthA7cd28ZC+jts4amQ3YMXiP5oMQ8=
golang.org/x/mod v0.19.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8=
golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk=
golang.org/x/sync v0.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w=
golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM=
golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=
golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk=
golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
golang.org/x/tools v0.23.0 h1:SGsXPZ+2l4JsgaCKkx+FQ9YZ5XEtA1GZYuoDjenLjvg=
golang.org/x/tools v0.23.0/go.mod h1:pnu6ufv6vQkll6szChhK3C3L/ruaIv5eBeztNG8wtsI=
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 h1:B82qJJgjvYKsXS9jeunTOisW56dUokqW/FOteYJJ/yg=
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2/go.mod h1:deeaetjYA+DHMHg+sMSMI58GrEteJUUzzw7en6TJQcI=
golang.zx2c4.com/wireguard v0.0.0-20231211153847-12269c276173 h1:/jFs0duh4rdb8uIfPMv78iAJGcPKDeqAFnaLBropIC4=
golang.zx2c4.com/wireguard v0.0.0-20231211153847-12269c276173/go.mod h1:tkCQ4FQXmpAgYVh++1cq16/dH4QJtmvpRv19DWGAHSA=
golang.zx2c4.com/wireguard/windows v0.5.3 h1:On6j2Rpn3OEMXqBq00QEDC7bWSZrPIHKIus8eIuExIE=
golang.zx2c4.com/wireguard/windows v0.5.3/go.mod h1:9TEe8TJmtwyQebdFwAkEWOPr3prrtqm+REGFifP60hI=
google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI=
google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gvisor.dev/gvisor v0.0.0-20230927004350-cbd86285d259 h1:TbRPT0HtzFP3Cno1zZo7yPzEEnfu8EjLfl6IU9VfqkQ=
gvisor.dev/gvisor v0.0.0-20230927004350-cbd86285d259/go.mod h1:AVgIgHMwK63XvmAzWG9vLQ41YnVHN0du0tEC46fI7yY=
suah.dev/protect v1.2.4 h1:iVZG/zQB63FKNpITDYM/cXoAeCTIjCiXHuFVByJFDzg=
suah.dev/protect v1.2.4/go.mod h1:vVrquYO3u1Ep9Ez2z8x+6N6/czm+TBmWKZfiXU2tb54=
github.com/vishvananda/netlink v1.1.0 h1:1iyaYNBLmP6L0220aDnYQpo1QEV4t4hJ+xEEhhJH8j0=
github.com/vishvananda/netlink v1.1.0/go.mod h1:cTgwzPIzzgDAYoQrMm0EdrjRUBkTqKYppBueQtXaqoE=
github.com/vishvananda/netns v0.0.0-20191106174202-0a2b9b5464df/go.mod h1:JP3t17pCcGlemwknint6hfoeCVQrEMVwxRLRjXpq+BU=
github.com/vishvananda/netns v0.0.0-20210104183010-2eb08e3e575f h1:p4VB7kIXpOQvVn1ZaTIVp+3vuYAXFe3OJEvjbUYJLaA=
github.com/vishvananda/netns v0.0.0-20210104183010-2eb08e3e575f/go.mod h1:DD4vA1DwXk04H54A1oHXtwZmA0grkVMdPxx/VGLCah0=
github.com/yuin/goldmark v1.4.0/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20221012134737-56aed061732a h1:NmSIgad6KjE6VvHciPZuNRTKxGhlPfD6OA87W/PLkqg=
golang.org/x/crypto v0.0.0-20221012134737-56aed061732a/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/exp v0.0.0-20190731235908-ec7cb31e5a56/go.mod h1:JhuoJpWY28nO4Vef9tZUw9qufEGTyX1+7lmHxV5q5G4=
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE=
golang.org/x/mobile v0.0.0-20221012134814-c746ac228303 h1:K4fp1rDuJBz0FCPAWzIJwnzwNEM7S6yobdZzMrZ/Zws=
golang.org/x/mobile v0.0.0-20221012134814-c746ac228303/go.mod h1:M32cGdzp91A8Ex9qQtyZinr19EYxzkFqDjW2oyHzTDQ=
golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4 h1:6zppjxzCulZykYSLyVDYbneBfbaBIQPYMevg0bEwv2s=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20210927181540-4e4d966f7476/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20211011170408-caeb26a5c8c0/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.0.0-20221014081412-f15817d10f9b h1:tvrvnPFcdzp294diPnrdZZZ8XUt2Tyj7svb7X52iDuU=
golang.org/x/net v0.0.0-20221014081412-f15817d10f9b/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220819030929-7fc1605a5dde/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190606203320-7fc4e5ec1444/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200217220822-9197077df867/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201018230417-eeed37f84f13/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210403161142-5e06dd20ab57/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20221013171732-95e765b1cc43 h1:OK7RB6t2WQX54srQQYSXMW8dF5C6/8+oA/s5QBmmto4=
golang.org/x/sys v0.0.0-20221013171732-95e765b1cc43/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.3.8-0.20211004125949-5bd84dd9b33b/go.mod h1:EFNZuWvGYxIRUEX+K8UmCFwYmZjqcrnq15ZuVldZkZ0=
golang.org/x/text v0.3.8 h1:nAL+RVCQ9uMn3vJZbV+MRnydTJFPf8qqY42YiA6MrqY=
golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.7/go.mod h1:LGqMHiF4EqQNHR1JncWGqT5BVaXmza+X+BDGol+dOxo=
golang.org/x/tools v0.1.8-0.20211022200916-316ba0b74098/go.mod h1:LGqMHiF4EqQNHR1JncWGqT5BVaXmza+X+BDGol+dOxo=
golang.org/x/tools v0.1.12 h1:VveCTK38A2rkS8ZqFY25HIDFscX5X9OoEhJd3quQmXU=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.zx2c4.com/wireguard v0.0.0-20211012062646-82d2aa87aa62/go.mod h1:id8Oh3eCCmpj9uVGWVjsUAl6UPX5ysMLzu6QxJU2UOU=
golang.zx2c4.com/wireguard v0.0.0-20211017052713-f87e87af0d9a h1:tTbyylK9/D3u/wEP26Vx7L700UpY48nhioJWZM1vhZw=
golang.zx2c4.com/wireguard v0.0.0-20211017052713-f87e87af0d9a/go.mod h1:id8Oh3eCCmpj9uVGWVjsUAl6UPX5ysMLzu6QxJU2UOU=
golang.zx2c4.com/wireguard/windows v0.4.12 h1:CUmbdWKVNzTSsVb4yUAiEwL3KsabdJkEPdDjCHxBlhA=
golang.zx2c4.com/wireguard/windows v0.4.12/go.mod h1:PW4y+d9oY83XU9rRwRwrJDwEMuhVjMxu2gfD1cfzS7w=

View file

@ -113,7 +113,7 @@ func SubnetForKey(publicKey ed25519.PublicKey) *Subnet {
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.
func (a *Address) GetKey() ed25519.PublicKey {
var key [ed25519.PublicKeySize]byte
@ -141,7 +141,7 @@ func (a *Address) GetKey() ed25519.PublicKey {
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.
func (s *Subnet) GetKey() ed25519.PublicKey {
var addr Address

View file

@ -3,13 +3,13 @@ package address
import (
"bytes"
"crypto/ed25519"
"crypto/rand"
"math/rand"
"testing"
)
func TestAddress_Address_IsValid(t *testing.T) {
var address Address
_, _ = rand.Read(address[:])
rand.Read(address[:])
address[0] = 0
@ -32,7 +32,7 @@ func TestAddress_Address_IsValid(t *testing.T) {
func TestAddress_Subnet_IsValid(t *testing.T) {
var subnet Subnet
_, _ = rand.Read(subnet[:])
rand.Read(subnet[:])
subnet[0] = 0

View file

@ -1,10 +1,5 @@
package admin
import (
"fmt"
"net/url"
)
type AddPeerRequest struct {
Uri string `json:"uri"`
Sintf string `json:"interface,omitempty"`
@ -12,10 +7,6 @@ type AddPeerRequest struct {
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)
func (a *AdminSocket) addPeerHandler(req *AddPeerRequest, res *AddPeerResponse) error {
return a.core.AddPeer(req.Uri, req.Sintf)
}

View file

@ -35,10 +35,10 @@ type AdminSocketRequest struct {
}
type AdminSocketResponse struct {
Status string `json:"status"`
Error string `json:"error,omitempty"`
Request AdminSocketRequest `json:"request"`
Response json.RawMessage `json:"response"`
Status string `json:"status"`
Error string `json:"error,omitempty"`
Request json.RawMessage `json:"request"`
Response json.RawMessage `json:"response"`
}
type handler struct {
@ -83,52 +83,6 @@ func New(c *core.Core, log core.Logger, opts ...SetupOption) (*AdminSocket, erro
if a.config.listenaddr == "none" || a.config.listenaddr == "" {
return nil, nil
}
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 {
@ -178,14 +132,14 @@ func (a *AdminSocket) SetupAdminHandlers() {
},
)
_ = a.AddHandler(
"getTree", "Show known Tree entries", []string{},
"getDHT", "Show known DHT entries", []string{},
func(in json.RawMessage) (interface{}, error) {
req := &GetTreeRequest{}
res := &GetTreeResponse{}
req := &GetDHTRequest{}
res := &GetDHTResponse{}
if err := json.Unmarshal(in, &req); err != nil {
return nil, err
}
if err := a.getTreeHandler(req, res); err != nil {
if err := a.getDHTHandler(req, res); err != nil {
return nil, err
}
return res, nil
@ -247,6 +201,10 @@ func (a *AdminSocket) SetupAdminHandlers() {
return res, nil
},
)
//_ = a.AddHandler("getNodeInfo", []string{"key"}, t.proto.nodeinfo.nodeInfoAdminHandler)
//_ = a.AddHandler("debug_remoteGetSelf", []string{"key"}, t.proto.getSelfHandler)
//_ = a.AddHandler("debug_remoteGetPeers", []string{"key"}, t.proto.getPeersHandler)
//_ = a.AddHandler("debug_remoteGetDHT", []string{"key"}, t.proto.getDHTHandler)
}
// IsStarted returns true if the module has been started.
@ -279,6 +237,51 @@ func (a *AdminSocket) Stop() error {
// listen is run by start and manages API connections.
func (a *AdminSocket) listen() {
listenaddr := string(a.config.listenaddr)
u, err := url.Parse(listenaddr)
if err == nil {
switch strings.ToLower(u.Scheme) {
case "unix":
if _, err := os.Stat(listenaddr[7:]); err == nil {
a.log.Debugln("Admin socket", listenaddr[7:], "already exists, trying to clean up")
if _, err := net.DialTimeout("unix", listenaddr[7:], time.Second*2); err == nil || err.(net.Error).Timeout() {
a.log.Errorln("Admin socket", listenaddr[7:], "already exists and is in use by another process")
os.Exit(1)
} else {
if err := os.Remove(listenaddr[7:]); err == nil {
a.log.Debugln(listenaddr[7:], "was cleaned up")
} else {
a.log.Errorln(listenaddr[7:], "already exists and was not cleaned up:", err)
os.Exit(1)
}
}
}
a.listener, err = net.Listen("unix", listenaddr[7:])
if err == nil {
switch listenaddr[7:8] {
case "@": // maybe abstract namespace
default:
if err := os.Chmod(listenaddr[7:], 0660); err != nil {
a.log.Warnln("WARNING:", 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", 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())
defer a.listener.Close()
for {
conn, err := a.listener.Accept()
@ -306,12 +309,24 @@ func (a *AdminSocket) handleRequest(conn net.Conn) {
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 {
var err error
var buf json.RawMessage
var req AdminSocketRequest
var resp AdminSocketResponse
req.Arguments = []byte("{}")
if err := func() error {
if err = decoder.Decode(&buf); err != nil {
return fmt.Errorf("Failed to find request")
@ -319,7 +334,6 @@ func (a *AdminSocket) handleRequest(conn net.Conn) {
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")
}
@ -356,15 +370,13 @@ 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)
case d > 1024*1024*1024*1024:
return fmt.Sprintf("%2.ftb", float64(d)/1024/1024/1024/1024)
case d > 1024*1024*1024:
return fmt.Sprintf("%2.fgb", float64(d)/1024/1024/1024)
case d > 1024*1024:
return fmt.Sprintf("%2.fmb", float64(d)/1024/1024)
default:
return fmt.Sprintf("%dB", d)
return fmt.Sprintf("%2.fkb", float64(d)/1024)
}
}

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

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

View file

@ -3,7 +3,7 @@ package admin
import (
"encoding/hex"
"net"
"slices"
"sort"
"strings"
"github.com/yggdrasil-network/yggdrasil-go/src/address"
@ -20,10 +20,9 @@ type PathEntry struct {
IPAddress string `json:"address"`
PublicKey string `json:"key"`
Path []uint64 `json:"path"`
Sequence uint64 `json:"sequence"`
}
func (a *AdminSocket) getPathsHandler(_ *GetPathsRequest, res *GetPathsResponse) error {
func (a *AdminSocket) getPathsHandler(req *GetPathsRequest, res *GetPathsResponse) error {
paths := a.core.GetPaths()
res.Paths = make([]PathEntry, 0, len(paths))
for _, p := range paths {
@ -32,11 +31,10 @@ func (a *AdminSocket) getPathsHandler(_ *GetPathsRequest, res *GetPathsResponse)
IPAddress: net.IP(addr[:]).String(),
PublicKey: hex.EncodeToString(p.Key),
Path: p.Path,
Sequence: p.Sequence,
})
}
slices.SortStableFunc(res.Paths, func(a, b PathEntry) int {
return strings.Compare(a.PublicKey, b.PublicKey)
sort.SliceStable(res.Paths, func(i, j int) bool {
return strings.Compare(res.Paths[i].PublicKey, res.Paths[j].PublicKey) < 0
})
return nil
}

View file

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

View file

@ -9,15 +9,15 @@ import (
type GetSelfRequest struct{}
type GetSelfResponse struct {
BuildName string `json:"build_name"`
BuildVersion string `json:"build_version"`
PublicKey string `json:"key"`
IPAddress string `json:"address"`
RoutingEntries uint64 `json:"routing_entries"`
Subnet string `json:"subnet"`
BuildName string `json:"build_name"`
BuildVersion string `json:"build_version"`
PublicKey string `json:"key"`
IPAddress string `json:"address"`
Coords []uint64 `json:"coords"`
Subnet string `json:"subnet"`
}
func (a *AdminSocket) getSelfHandler(_ *GetSelfRequest, res *GetSelfResponse) error {
func (a *AdminSocket) getSelfHandler(req *GetSelfRequest, res *GetSelfResponse) error {
self := a.core.GetSelf()
snet := a.core.Subnet()
res.BuildName = version.BuildName()
@ -25,6 +25,6 @@ func (a *AdminSocket) getSelfHandler(_ *GetSelfRequest, res *GetSelfResponse) er
res.PublicKey = hex.EncodeToString(self.Key[:])
res.IPAddress = a.core.Address().String()
res.Subnet = snet.String()
res.RoutingEntries = self.RoutingEntries
res.Coords = self.Coords
return nil
}

View file

@ -3,7 +3,7 @@ package admin
import (
"encoding/hex"
"net"
"slices"
"sort"
"strings"
"github.com/yggdrasil-network/yggdrasil-go/src/address"
@ -23,7 +23,7 @@ type SessionEntry struct {
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 = make([]SessionEntry, 0, len(sessions))
for _, s := range sessions {
@ -36,8 +36,8 @@ func (a *AdminSocket) getSessionsHandler(_ *GetSessionsRequest, res *GetSessions
Uptime: s.Uptime.Seconds(),
})
}
slices.SortStableFunc(res.Sessions, func(a, b SessionEntry) int {
return strings.Compare(a.PublicKey, b.PublicKey)
sort.SliceStable(res.Sessions, func(i, j int) bool {
return strings.Compare(res.Sessions[i].PublicKey, res.Sessions[j].PublicKey) < 0
})
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,24 +1,9 @@
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()
}
}
@ -29,51 +14,3 @@ type SetupOption interface {
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,10 +1,5 @@
package admin
import (
"fmt"
"net/url"
)
type RemovePeerRequest struct {
Uri string `json:"uri"`
Sintf string `json:"interface,omitempty"`
@ -12,10 +7,6 @@ type RemovePeerRequest struct {
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)
func (a *AdminSocket) removePeerHandler(req *RemovePeerRequest, res *RemovePeerResponse) error {
return a.core.RemovePeer(req.Uri, req.Sintf)
}

View file

@ -17,244 +17,44 @@ configuration option that is not provided.
package config
import (
"bytes"
"crypto/ed25519"
"crypto/rand"
"crypto/tls"
"crypto/x509"
"crypto/x509/pkix"
"encoding/hex"
"encoding/json"
"encoding/pem"
"fmt"
"io"
"math/big"
"os"
"time"
"github.com/hjson/hjson-go/v4"
"golang.org/x/text/encoding/unicode"
)
// NodeConfig is the main configuration structure, containing configuration
// options that are necessary for an Yggdrasil node to run. You will need to
// supply one of these structs to the Yggdrasil core when starting a node.
type NodeConfig struct {
PrivateKey KeyBytes `json:",omitempty" comment:"Your private key. DO NOT share this with anyone!"`
PrivateKeyPath string `json:",omitempty" comment:"The path to your private key file in PEM format."`
Certificate *tls.Certificate `json:"-"`
Peers []string `comment:"List of outbound peer connection strings (e.g. tls://a.b.c.d:e or\nsocks://a.b.c.d:e/f.g.h.i:j). Connection strings can contain options,\nsee https://yggdrasil-network.github.io/configurationref.html#peers.\nYggdrasil has no concept of bootstrap nodes - all network traffic\nwill transit peer connections. Therefore make sure to only peer with\nnearby nodes that have good connectivity and low latency. Avoid adding\npeers to this list from distant countries as this will worsen your\nnode's connectivity and performance considerably."`
InterfacePeers map[string][]string `comment:"List of connection strings for outbound peer connections in URI format,\narranged by source interface, e.g. { \"eth0\": [ \"tls://a.b.c.d:e\" ] }.\nYou should only use this option if your machine is multi-homed and you\nwant to establish outbound peer connections on different interfaces.\nOtherwise you should use \"Peers\"."`
Listen []string `comment:"Listen addresses for incoming connections. You will need to add\nlisteners in order to accept incoming peerings from non-local nodes.\nThis is not required if you wish to establish outbound peerings only.\nMulticast peer discovery will work regardless of any listeners set\nhere. Each listener should be specified in URI format as above, e.g.\ntls://0.0.0.0:0 or tls://[::]:0 to listen on all interfaces."`
AdminListen string `json:",omitempty" comment:"Listen address for admin connections. Default is to listen for local\nconnections either on TCP/9001 or a UNIX socket depending on your\nplatform. Use this value for yggdrasilctl -endpoint=X. To disable\nthe admin socket, use the value \"none\" instead."`
MulticastInterfaces []MulticastInterfaceConfig `comment:"Configuration for which interfaces multicast peer discovery should be\nenabled on. Regex is a regular expression which is matched against an\ninterface name, and interfaces use the first configuration that they\nmatch against. Beacon controls whether or not your node advertises its\npresence to others, whereas Listen controls whether or not your node\nlistens out for and tries to connect to other advertising nodes. See\nhttps://yggdrasil-network.github.io/configurationref.html#multicastinterfaces\nfor more supported options."`
AllowedPublicKeys []string `comment:"List of peer public keys to allow incoming peering connections\nfrom. If left empty/undefined then all connections will be allowed\nby default. This does not affect outgoing peerings, nor does it\naffect link-local peers discovered via multicast.\nWARNING: THIS IS NOT A FIREWALL and DOES NOT limit who can reach\nopen ports or services running on your machine!"`
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."`
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."`
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."`
AdminListen string `comment:"Listen address for admin connections. Default is to listen for local\nconnections either on TCP/9001 or a UNIX socket depending on your\nplatform. Use this value for yggdrasilctl -endpoint=X. To disable\nthe admin socket, use the value \"none\" instead."`
MulticastInterfaces []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."`
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."`
PublicKey string `comment:"Your public key. Your peers may ask you for this to put\ninto their AllowedPublicKeys configuration."`
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."`
IfMTU uint64 `comment:"Maximum Transmission Unit (MTU) size for your local TUN interface.\nDefault is the largest supported size for your platform. The lowest\npossible value is 1280."`
LogLookups bool `json:",omitempty"`
NodeInfoPrivacy bool `comment:"By default, nodeinfo contains some defaults including the platform,\narchitecture and Yggdrasil version. These can help when surveying\nthe network and diagnosing network routing problems. Enabling\nnodeinfo privacy prevents this, so that only items specified in\n\"NodeInfo\" are sent back if specified."`
NodeInfo map[string]interface{} `comment:"Optional nodeinfo. This must be a { \"key\": \"value\", ... } map\nor set as null. This is entirely optional but, if set, is visible\nto the whole network on request."`
NodeInfo map[string]interface{} `comment:"Optional node info. This must be a { \"key\": \"value\", ... } map\nor set as null. This is entirely optional but, if set, is visible\nto the whole network on request."`
}
type MulticastInterfaceConfig struct {
Regex string
Beacon bool
Listen bool
Port uint16 `json:",omitempty"`
Priority uint64 `json:",omitempty"` // really uint8, but gobind won't export it
Password string
Port uint16
Priority uint8
}
// Generates default configuration and returns a pointer to the resulting
// NodeConfig. This is used when outputting the -genconf parameter and also when
// using -autoconf.
func GenerateConfig() *NodeConfig {
// Get the defaults for the platform.
defaults := GetDefaults()
// 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)
// NewSigningKeys replaces the signing keypair in the NodeConfig with a new
// signing keypair. The signing keys are used by the switch to derive the
// structure of the spanning tree.
func (cfg *NodeConfig) NewKeys() {
spub, spriv, err := ed25519.GenerateKey(nil)
if err != nil {
panic(err)
}
cfg.PrivateKey = KeyBytes(spriv)
}
func (cfg *NodeConfig) MarshalPEMPrivateKey() ([]byte, error) {
b, err := x509.MarshalPKCS8PrivateKey(ed25519.PrivateKey(cfg.PrivateKey))
if err != nil {
return nil, fmt.Errorf("failed to marshal PKCS8 key: %w", err)
}
block := &pem.Block{
Type: "PRIVATE KEY",
Bytes: b,
}
return pem.EncodeToMemory(block), nil
}
func (cfg *NodeConfig) UnmarshalPEMPrivateKey(b []byte) error {
p, _ := pem.Decode(b)
if p == nil {
return fmt.Errorf("failed to parse PEM file")
}
if p.Type != "PRIVATE KEY" {
return fmt.Errorf("unexpected PEM type %q", p.Type)
}
k, err := x509.ParsePKCS8PrivateKey(p.Bytes)
if err != nil {
return fmt.Errorf("failed to unmarshal PKCS8 key: %w", err)
}
key, ok := k.(ed25519.PrivateKey)
if !ok {
return fmt.Errorf("private key must be ed25519 key")
}
if len(key) != ed25519.PrivateKeySize {
return fmt.Errorf("unexpected ed25519 private key length")
}
cfg.PrivateKey = KeyBytes(key)
return nil
}
type KeyBytes []byte
func (k KeyBytes) MarshalJSON() ([]byte, error) {
return json.Marshal(hex.EncodeToString(k))
}
func (k *KeyBytes) UnmarshalJSON(b []byte) error {
var s string
var err error
if err = json.Unmarshal(b, &s); err != nil {
return err
}
*k, err = hex.DecodeString(s)
return err
cfg.PublicKey = hex.EncodeToString(spub[:])
cfg.PrivateKey = hex.EncodeToString(spriv[:])
}

View file

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

@ -3,54 +3,43 @@ package core
import (
"crypto/ed25519"
"encoding/json"
"fmt"
"net"
"net/url"
"sync/atomic"
"time"
"github.com/Arceliar/phony"
"github.com/Arceliar/ironwood/network"
"github.com/yggdrasil-network/yggdrasil-go/src/address"
)
type SelfInfo struct {
Key ed25519.PublicKey
RoutingEntries uint64
Key ed25519.PublicKey
Root ed25519.PublicKey
Coords []uint64
}
type PeerInfo struct {
URI string
Up bool
Inbound bool
LastError error
LastErrorTime time.Time
Key ed25519.PublicKey
Root ed25519.PublicKey
Coords []uint64
Port uint64
Priority uint8
Cost uint64
RXBytes uint64
TXBytes uint64
RXRate uint64
TXRate uint64
Uptime time.Duration
Latency time.Duration
Key ed25519.PublicKey
Root ed25519.PublicKey
Coords []uint64
Port uint64
Priority uint8
Remote string
RXBytes uint64
TXBytes uint64
Uptime time.Duration
}
type TreeEntryInfo struct {
Key ed25519.PublicKey
Parent ed25519.PublicKey
Sequence uint64
//Port uint64
//Rest uint64
type DHTEntryInfo struct {
Key ed25519.PublicKey
Port uint64
Rest uint64
}
type PathEntryInfo struct {
Key ed25519.PublicKey
Path []uint64
Sequence uint64
Key ed25519.PublicKey
Path []uint64
}
type SessionInfo struct {
@ -64,63 +53,55 @@ func (c *Core) GetSelf() SelfInfo {
var self SelfInfo
s := c.PacketConn.PacketConn.Debug.GetSelf()
self.Key = s.Key
self.RoutingEntries = s.RoutingEntries
self.Root = s.Root
self.Coords = s.Coords
return self
}
func (c *Core) GetPeers() []PeerInfo {
peers := []PeerInfo{}
conns := map[net.Conn]network.DebugPeerInfo{}
iwpeers := c.PacketConn.PacketConn.Debug.GetPeers()
for _, p := range iwpeers {
conns[p.Conn] = p
}
var peers []PeerInfo
names := make(map[net.Conn]string)
phony.Block(&c.links, func() {
for info, state := range c.links._links {
var peerinfo PeerInfo
var conn net.Conn
peerinfo.URI = info.uri
peerinfo.LastError = state._err
peerinfo.LastErrorTime = state._errtime
if c := state._conn; c != nil {
conn = c
peerinfo.Up = true
peerinfo.Inbound = state.linkType == linkTypeIncoming
peerinfo.RXBytes = atomic.LoadUint64(&c.rx)
peerinfo.TXBytes = atomic.LoadUint64(&c.tx)
peerinfo.RXRate = atomic.LoadUint64(&c.rxrate)
peerinfo.TXRate = atomic.LoadUint64(&c.txrate)
peerinfo.Uptime = time.Since(c.up)
for _, info := range c.links._links {
if info == nil {
continue
}
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)
names[info.conn] = info.lname
}
})
ps := c.PacketConn.PacketConn.Debug.GetPeers()
for _, p := range ps {
var info PeerInfo
info.Key = p.Key
info.Root = p.Root
info.Coords = p.Coords
info.Port = p.Port
info.Priority = p.Priority
info.Remote = p.Conn.RemoteAddr().String()
if name := names[p.Conn]; name != "" {
info.Remote = name
}
if linkconn, ok := p.Conn.(*linkConn); ok {
info.RXBytes = atomic.LoadUint64(&linkconn.rx)
info.TXBytes = atomic.LoadUint64(&linkconn.tx)
info.Uptime = time.Since(linkconn.up)
}
peers = append(peers, info)
}
return peers
}
func (c *Core) GetTree() []TreeEntryInfo {
var trees []TreeEntryInfo
ts := c.PacketConn.PacketConn.Debug.GetTree()
for _, t := range ts {
var info TreeEntryInfo
info.Key = t.Key
info.Parent = t.Parent
info.Sequence = t.Sequence
//info.Port = d.Port
//info.Rest = d.Rest
trees = append(trees, info)
func (c *Core) GetDHT() []DHTEntryInfo {
var dhts []DHTEntryInfo
ds := c.PacketConn.PacketConn.Debug.GetDHT()
for _, d := range ds {
var info DHTEntryInfo
info.Key = d.Key
info.Port = d.Port
info.Rest = d.Rest
dhts = append(dhts, info)
}
return trees
return dhts
}
func (c *Core) GetPaths() []PathEntryInfo {
@ -129,7 +110,6 @@ func (c *Core) GetPaths() []PathEntryInfo {
for _, p := range ps {
var info PathEntryInfo
info.Key = p.Key
info.Sequence = p.Sequence
info.Path = p.Path
paths = append(paths, info)
}
@ -154,14 +134,16 @@ func (c *Core) GetSessions() []SessionInfo {
// parsed from a string of the form e.g. "tcp://a.b.c.d:e". In the case of a
// link-local address, the interface should be provided as the second argument.
func (c *Core) Listen(u *url.URL, sintf string) (*Listener, error) {
return c.links.listen(u, sintf, false)
}
// ListenLocal starts a listener, like the Listen function, but is used for
// more trustworthy situations where you want to ignore AllowedPublicKeys, i.e.
// with multicast listeners.
func (c *Core) ListenLocal(u *url.URL, sintf string) (*Listener, error) {
return c.links.listen(u, sintf, true)
switch u.Scheme {
case "tcp":
return c.links.tcp.listen(u, sintf)
case "tls":
return c.links.tls.listen(u, sintf)
case "unix":
return c.links.unix.listen(u, sintf)
default:
return nil, fmt.Errorf("unrecognised scheme %q", u.Scheme)
}
}
// Address gets the IPv6 address of the Yggdrasil node. This is always a /128
@ -200,14 +182,49 @@ func (c *Core) SetLogger(log Logger) {
//
// This adds the peer to the peer list, so that they will be called again if the
// connection drops.
func (c *Core) AddPeer(u *url.URL, sintf string) error {
return c.links.add(u, sintf, linkTypePersistent)
func (c *Core) AddPeer(uri string, sourceInterface string) error {
var known bool
phony.Block(c, func() {
_, known = c.config._peers[Peer{uri, sourceInterface}]
})
if known {
return fmt.Errorf("peer already configured")
}
u, err := url.Parse(uri)
if err != nil {
return err
}
info, err := c.links.call(u, sourceInterface)
if err != nil {
return err
}
phony.Block(c, func() {
c.config._peers[Peer{uri, sourceInterface}] = &info
})
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(u *url.URL, sintf string) error {
return c.links.remove(u, sintf, linkTypePersistent)
func (c *Core) RemovePeer(uri string, sourceInterface string) error {
var err error
phony.Block(c, func() {
peer := Peer{uri, sourceInterface}
linkInfo, ok := c.config._peers[peer]
if !ok {
err = fmt.Errorf("peer not configured")
return
}
if ok && linkInfo != nil {
c.links.Act(nil, func() {
if link := c.links._links[*linkInfo]; link != nil {
_ = link.close()
}
})
}
delete(c.config._peers, peer)
})
return err
}
// CallPeer calls a peer once. This should be specified in the peer URI format,
@ -219,7 +236,8 @@ func (c *Core) RemovePeer(u *url.URL, sintf string) error {
// This does not add the peer to the peer list, so if the connection drops, the
// peer will not be called again automatically.
func (c *Core) CallPeer(u *url.URL, sintf string) error {
return c.links.add(u, sintf, linkTypeEphemeral)
_, err := c.links.call(u, sintf)
return err
}
func (c *Core) PublicKey() ed25519.PublicKey {
@ -256,8 +274,8 @@ func (c *Core) SetAdmin(a AddHandler) error {
return err
}
if err := a.AddHandler(
"debug_remoteGetTree", "Debug use only", []string{"key"},
c.proto.getTreeHandler,
"debug_remoteGetDHT", "Debug use only", []string{"key"},
c.proto.getDHTHandler,
); err != nil {
return err
}

View file

@ -3,7 +3,6 @@ package core
import (
"context"
"crypto/ed25519"
"crypto/tls"
"fmt"
"io"
"net"
@ -11,12 +10,10 @@ import (
"time"
iwe "github.com/Arceliar/ironwood/encrypted"
iwn "github.com/Arceliar/ironwood/network"
iwt "github.com/Arceliar/ironwood/types"
"github.com/Arceliar/phony"
"github.com/gologme/log"
"github.com/yggdrasil-network/yggdrasil-go/src/address"
"github.com/yggdrasil-network/yggdrasil-go/src/version"
)
@ -37,88 +34,49 @@ type Core struct {
log Logger
addPeerTimer *time.Timer
config struct {
tls *tls.Config // immutable after startup
//_peers map[Peer]*linkInfo // configurable after startup
_peers map[Peer]*linkInfo // configurable after startup
_listeners map[ListenAddress]struct{} // configurable after startup
peerFilter func(ip net.IP) bool // immutable after startup
nodeinfo NodeInfo // immutable after startup
nodeinfoPrivacy NodeInfoPrivacy // immutable after startup
_allowedPublicKeys map[[32]byte]struct{} // configurable after startup
}
pathNotify func(ed25519.PublicKey)
}
func New(cert *tls.Certificate, logger Logger, opts ...SetupOption) (*Core, error) {
func New(secret ed25519.PrivateKey, logger Logger, opts ...SetupOption) (*Core, error) {
c := &Core{
log: logger,
}
c.ctx, c.cancel = context.WithCancel(context.Background())
if c.log == nil {
c.log = log.New(io.Discard, "", 0)
}
if name := version.BuildName(); name != "unknown" {
c.log.Infoln("Build name:", name)
}
if version := version.BuildVersion(); version != "unknown" {
c.log.Infoln("Build version:", version)
}
c.ctx, c.cancel = context.WithCancel(context.Background())
// Take a copy of the private key so that it is in our own memory space.
if len(secret) != ed25519.PrivateKeySize {
return nil, fmt.Errorf("private key is incorrect length")
}
c.secret = make(ed25519.PrivateKey, ed25519.PrivateKeySize)
copy(c.secret, secret)
c.public = secret.Public().(ed25519.PublicKey)
var err error
if c.PacketConn, err = iwe.NewPacketConn(c.secret); err != nil {
return nil, fmt.Errorf("error creating encryption: %w", err)
}
c.config._peers = map[Peer]*linkInfo{}
c.config._listeners = map[ListenAddress]struct{}{}
c.config._allowedPublicKeys = map[[32]byte]struct{}{}
for _, opt := range opts {
switch opt.(type) {
case Peer, ListenAddress:
// We can't do peers yet as the links aren't set up.
continue
default:
if err = c._applyOption(opt); err != nil {
return nil, fmt.Errorf("failed to apply configuration option %T: %w", opt, err)
}
}
c._applyOption(opt)
}
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)
if c.log == nil {
c.log = log.New(io.Discard, "", 0)
}
c.proto.init(c)
if err := c.links.init(c); err != nil {
return nil, fmt.Errorf("error initialising links: %w", err)
}
for _, opt := range opts {
switch opt.(type) {
case Peer, ListenAddress:
// Now do the peers and listeners.
if err = c._applyOption(opt); err != nil {
return nil, fmt.Errorf("failed to apply configuration option %T: %w", opt, err)
}
default:
continue
}
}
if err := c.proto.nodeinfo.setNodeInfo(c.config.nodeinfo, bool(c.config.nodeinfoPrivacy)); err != nil {
return nil, fmt.Errorf("error setting node info: %w", err)
}
@ -128,21 +86,38 @@ func New(cert *tls.Certificate, logger Logger, opts ...SetupOption) (*Core, erro
c.log.Errorf("Invalid listener URI %q specified, ignoring\n", listenaddr)
continue
}
if _, err = c.links.listen(u, "", false); err != nil {
if _, err = c.links.listen(u, ""); err != nil {
c.log.Errorf("Failed to start listener %q: %s\n", listenaddr, err)
}
}
c.Act(nil, c._addPeerLoop)
return c, nil
}
func (c *Core) RetryPeersNow() {
phony.Block(&c.links, func() {
for _, l := range c.links._links {
select {
case l.kick <- struct{}{}:
default:
// 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() {
select {
case <-c.ctx.Done():
return
default:
}
// 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.URI, peer.SourceInterface) // TODO: this should be acted and not in a goroutine?
}
c.addPeerTimer = time.AfterFunc(time.Minute, func() {
c.Act(nil, c._addPeerLoop)
})
}
@ -169,16 +144,11 @@ func (c *Core) _close() error {
func (c *Core) MTU() uint64 {
const sessionTypeOverhead = 1
MTU := c.PacketConn.MTU() - sessionTypeOverhead
if MTU > 65535 {
MTU = 65535
}
return MTU
return c.PacketConn.MTU() - sessionTypeOverhead
}
func (c *Core) ReadFrom(p []byte) (n int, from net.Addr, err error) {
buf := allocBytes(int(c.PacketConn.MTU()))
defer freeBytes(buf)
buf := make([]byte, c.PacketConn.MTU(), 65535)
for {
bs := buf
n, from, err = c.PacketConn.ReadFrom(bs)
@ -212,8 +182,7 @@ func (c *Core) ReadFrom(p []byte) (n int, from net.Addr, err error) {
}
func (c *Core) WriteTo(p []byte, addr net.Addr) (n int, err error) {
buf := allocBytes(0)
defer func() { freeBytes(buf) }()
buf := make([]byte, 0, 65535)
buf = append(buf, typeSessionTraffic)
buf = append(buf, p...)
n, err = c.PacketConn.WriteTo(buf, addr)
@ -223,20 +192,6 @@ func (c *Core) WriteTo(p []byte, addr net.Addr) (n int, err error) {
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{})
@ -248,5 +203,4 @@ type Logger interface {
Errorln(...interface{})
Debugf(string, ...interface{})
Debugln(...interface{})
Traceln(...interface{})
}

View file

@ -2,14 +2,14 @@ package core
import (
"bytes"
"crypto/rand"
"crypto/ed25519"
"math/rand"
"net/url"
"os"
"testing"
"time"
"github.com/gologme/log"
"github.com/yggdrasil-network/yggdrasil-go/src/config"
)
// GetLoggerWithPrefix creates a new logger instance with prefix.
@ -25,65 +25,33 @@ func GetLoggerWithPrefix(prefix string, verbose bool) *log.Logger {
return l
}
func require_NoError(t *testing.T, err error) {
t.Helper()
if err != nil {
t.Fatal(err)
}
}
func require_Equal[T comparable](t *testing.T, a, b T) {
t.Helper()
if a != b {
t.Fatalf("%v != %v", a, b)
}
}
func require_True(t *testing.T, a bool) {
t.Helper()
if !a {
t.Fatal("expected true")
}
}
// CreateAndConnectTwo creates two nodes. nodeB connects to nodeA.
// Verbosity flag is passed to logger.
func CreateAndConnectTwo(t testing.TB, verbose bool) (nodeA *Core, nodeB *Core) {
var err error
cfgA, cfgB := config.GenerateConfig(), config.GenerateConfig()
if err = cfgA.GenerateSelfSignedCertificate(); err != nil {
var skA, skB ed25519.PrivateKey
if _, skA, err = ed25519.GenerateKey(nil); err != nil {
t.Fatal(err)
}
if err = cfgB.GenerateSelfSignedCertificate(); err != nil {
if _, skB, err = ed25519.GenerateKey(nil); err != nil {
t.Fatal(err)
}
logger := GetLoggerWithPrefix("", false)
logger.EnableLevel("debug")
if nodeA, err = New(cfgA.Certificate, logger); err != nil {
if nodeA, err = New(skA, logger, ListenAddress("tcp://127.0.0.1:0")); err != nil {
t.Fatal(err)
}
if nodeB, err = New(cfgB.Certificate, logger); err != nil {
if nodeB, err = New(skB, logger, ListenAddress("tcp://127.0.0.1:0")); err != nil {
t.Fatal(err)
}
nodeAListenURL, err := url.Parse("tcp://localhost:0")
u, err := url.Parse("tcp://" + nodeA.links.tcp.getAddr().String())
if err != nil {
t.Fatal(err)
}
nodeAListener, err := nodeA.Listen(nodeAListenURL, "")
err = nodeB.CallPeer(u, "")
if err != nil {
t.Fatal(err)
}
nodeAURL, err := url.Parse("tcp://" + nodeAListener.Addr().String())
if err != nil {
t.Fatal(err)
}
if err = nodeB.CallPeer(nodeAURL, ""); err != nil {
t.Fatal(err)
}
time.Sleep(100 * time.Millisecond)
@ -102,13 +70,7 @@ func WaitConnected(nodeA, nodeB *Core) bool {
// It may take up to 3 seconds, but let's wait 5.
for i := 0; i < 50; i++ {
time.Sleep(100 * time.Millisecond)
/*
if len(nodeA.GetPeers()) > 0 && len(nodeB.GetPeers()) > 0 {
return true
}
*/
if len(nodeA.GetTree()) > 1 && len(nodeB.GetTree()) > 1 {
time.Sleep(3 * time.Second) // FIXME hack, there's still stuff happening internally
if len(nodeA.GetPeers()) > 0 && len(nodeB.GetPeers()) > 0 {
return true
}
}
@ -167,7 +129,7 @@ func TestCore_Start_Transfer(t *testing.T) {
// Send
msg := make([]byte, msgLen)
_, _ = rand.Read(msg[40:])
rand.Read(msg[40:])
msg[0] = 0x60
copy(msg[8:24], nodeB.Address())
copy(msg[24:40], nodeA.Address())
@ -199,7 +161,7 @@ func BenchmarkCore_Start_Transfer(b *testing.B) {
// Send
msg := make([]byte, msgLen)
_, _ = rand.Read(msg[40:])
rand.Read(msg[40:])
msg[0] = 0x60
copy(msg[8:24], nodeB.Address())
copy(msg[24:40], nodeA.Address())
@ -222,69 +184,3 @@ func BenchmarkCore_Start_Transfer(b *testing.B) {
}
<-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,3 +1,6 @@
//go:build debug
// +build debug
package core
import (
@ -5,15 +8,28 @@ import (
"net/http"
_ "net/http/pprof"
"os"
"runtime"
"github.com/gologme/log"
)
// Start the profiler if the required environment variable is set.
// Start the profiler in debug builds, if the required environment variable is set.
func init() {
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)
go func() {
fmt.Fprintf(os.Stderr, "DEBUG: %s", http.ListenAndServe(hostPort, nil))
}()
go func() { fmt.Println(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

@ -2,7 +2,6 @@ package core
import (
"bytes"
"context"
"encoding/hex"
"errors"
"fmt"
@ -16,75 +15,49 @@ import (
"github.com/Arceliar/phony"
"github.com/yggdrasil-network/yggdrasil-go/src/address"
"golang.org/x/crypto/blake2b"
)
type linkType int
const (
linkTypePersistent linkType = iota // Statically configured
linkTypeEphemeral // Multicast discovered
linkTypeIncoming // Incoming connection
)
const defaultBackoffLimit = time.Second << 12 // 1h8m16s
const minimumBackoffLimit = time.Second * 30
type links struct {
phony.Inbox
core *Core
tcp *linkTCP // TCP interface support
tls *linkTLS // TLS interface support
unix *linkUNIX // UNIX interface support
socks *linkSOCKS // SOCKS interface support
quic *linkQUIC // QUIC interface support
ws *linkWS // WS interface support
wss *linkWSS // WSS interface support
// _links can only be modified safely from within the links actor
_links map[linkInfo]*link // *link is nil if connection in progress
_listeners map[*Listener]context.CancelFunc
}
type linkProtocol interface {
dial(ctx context.Context, url *url.URL, info linkInfo, options linkOptions) (net.Conn, error)
listen(ctx context.Context, url *url.URL, sintf string) (net.Listener, error)
core *Core
tcp *linkTCP // TCP interface support
tls *linkTLS // TLS interface support
unix *linkUNIX // UNIX interface support
socks *linkSOCKS // SOCKS interface support
_links map[linkInfo]*link // *link is nil if connection in progress
}
// linkInfo is used as a map key
type linkInfo struct {
uri string // Peering URI in complete form
sintf string // Peering source interface (i.e. from InterfacePeers)
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 {
ctx context.Context // Connection context
cancel context.CancelFunc // Stop future redial attempts (when peer removed)
kick chan struct{} // Attempt to reconnect now, if backing off
linkType linkType // Type of link, i.e. outbound/inbound, persistent/ephemeral
linkProto string // Protocol carrier of link, e.g. TCP, AWDL
// The remaining fields can only be modified safely from within the links actor
_conn *linkConn // Connected link, if any, nil if not connected
_err error // Last error on the connection, if any
_errtime time.Time // Last time an error occurred
lname string
links *links
conn *linkConn
options linkOptions
info linkInfo
incoming bool
force bool
}
type linkOptions struct {
pinnedEd25519Keys map[keyArray]struct{}
priority uint8
tlsSNI string
password []byte
maxBackoff time.Duration
}
type Listener struct {
listener net.Listener
ctx context.Context
Cancel context.CancelFunc
net.Listener
closed chan struct{}
}
func (l *Listener) Addr() net.Addr {
return l.listener.Addr()
func (l *Listener) Close() error {
err := l.Listener.Close()
<-l.closed
return err
}
func (l *links) init(c *Core) error {
@ -93,664 +66,282 @@ func (l *links) init(c *Core) error {
l.tls = l.newLinkTLS(l.tcp)
l.unix = l.newLinkUNIX()
l.socks = l.newLinkSOCKS()
l.quic = l.newLinkQUIC()
l.ws = l.newLinkWS()
l.wss = l.newLinkWSS()
l._links = make(map[linkInfo]*link)
l._listeners = make(map[*Listener]context.CancelFunc)
l.Act(nil, l._updateAverages)
var listeners []ListenAddress
phony.Block(c, func() {
listeners = make([]ListenAddress, 0, len(c.config._listeners))
for listener := range c.config._listeners {
listeners = append(listeners, listener)
}
})
return nil
}
func (l *links) _updateAverages() {
select {
case <-l.core.ctx.Done():
return
default:
}
for _, l := range l._links {
if l._conn == nil {
continue
}
rx := atomic.LoadUint64(&l._conn.rx)
tx := atomic.LoadUint64(&l._conn.tx)
lastrx := atomic.LoadUint64(&l._conn.lastrx)
lasttx := atomic.LoadUint64(&l._conn.lasttx)
atomic.StoreUint64(&l._conn.rxrate, rx-lastrx)
atomic.StoreUint64(&l._conn.txrate, tx-lasttx)
atomic.StoreUint64(&l._conn.lastrx, rx)
atomic.StoreUint64(&l._conn.lasttx, tx)
}
time.AfterFunc(time.Second, func() {
l.Act(nil, l._updateAverages)
})
}
func (l *links) shutdown() {
phony.Block(l, func() {
for _, cancel := range l._listeners {
cancel()
phony.Block(l.tcp, func() {
for l := range l.tcp._listeners {
_ = l.Close()
}
for _, link := range l._links {
if link._conn != nil {
_ = link._conn.Close()
}
})
phony.Block(l.tls, func() {
for l := range l.tls._listeners {
_ = l.Close()
}
})
phony.Block(l.unix, func() {
for l := range l.unix._listeners {
_ = l.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
func (l *links) isConnectedTo(info linkInfo) bool {
var isConnected bool
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,
}
_, isConnected = l._links[info]
})
return isConnected
}
// Collect together the link options, these are global options
// that are not specific to any given protocol.
options := linkOptions{
maxBackoff: defaultBackoffLimit,
func (l *links) call(u *url.URL, sintf string) (linkInfo, error) {
info := linkInfoFor(u.Scheme, sintf, u.Host)
if l.isConnectedTo(info) {
return info, nil
}
options := linkOptions{
pinnedEd25519Keys: map[keyArray]struct{}{},
}
for _, pubkey := range u.Query()["key"] {
sigPub, err := hex.DecodeString(pubkey)
if err != nil {
return info, fmt.Errorf("pinned key contains invalid hex characters")
}
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{}{}
var sigPubKey keyArray
copy(sigPubKey[:], sigPub)
options.pinnedEd25519Keys[sigPubKey] = struct{}{}
}
if p := u.Query().Get("priority"); p != "" {
pi, err := strconv.ParseUint(p, 10, 8)
if err != nil {
return info, fmt.Errorf("priority invalid: %w", err)
}
if p := u.Query().Get("priority"); p != "" {
pi, err := strconv.ParseUint(p, 10, 8)
if err != nil {
retErr = ErrLinkPriorityInvalid
return
options.priority = uint8(pi)
}
switch info.linkType {
case "tcp":
go func() {
if err := l.tcp.dial(u, options, sintf); err != nil && err != io.EOF {
l.core.log.Warnf("Failed to dial TCP %s: %s\n", u.Host, err)
}
options.priority = uint8(pi)
}
if p := u.Query().Get("password"); p != "" {
if len(p) > blake2b.Size {
retErr = ErrLinkPasswordInvalid
return
}()
case "socks":
go func() {
if err := l.socks.dial(u, options); err != nil && err != io.EOF {
l.core.log.Warnf("Failed to dial SOCKS %s: %s\n", u.Host, err)
}
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
}
}()
case "tls":
// 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.
var tlsSNI string
if sni := u.Query().Get("sni"); sni != "" {
if net.ParseIP(sni) == nil {
options.tlsSNI = sni
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 tlsSNI == "" {
if host, _, err := net.SplitHostPort(u.Host); err == nil && net.ParseIP(host) == nil {
options.tlsSNI = host
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
if err := l.tls.dial(u, options, sintf, tlsSNI); err != nil && err != io.EOF {
l.core.log.Warnf("Failed to dial TLS %s: %s\n", u.Host, err)
}
}()
})
return retErr
}
func (l *links) remove(u *url.URL, sintf string, _ linkType) error {
var retErr error
phony.Block(l, func() {
// Generate the link info and see whether we think we already
// have an open peering to this peer.
lu := urlForLinkInfo(*u)
info := linkInfo{
uri: lu.String(),
sintf: sintf,
}
// If this peer is already configured then we will close the
// connection and stop it from retrying.
state, ok := l._links[info]
if ok && state != nil {
state.cancel()
if conn := state._conn; conn != nil {
retErr = conn.Close()
}
return
}
retErr = ErrLinkNotConfigured
})
return retErr
}
func (l *links) listen(u *url.URL, sintf string, local bool) (*Listener, error) {
ctx, ctxcancel := context.WithCancel(l.core.ctx)
var protocol linkProtocol
switch strings.ToLower(u.Scheme) {
case "tcp":
protocol = l.tcp
case "tls":
protocol = l.tls
case "unix":
protocol = l.unix
case "quic":
protocol = l.quic
case "ws":
protocol = l.ws
case "wss":
protocol = l.wss
default:
ctxcancel()
return nil, ErrLinkUnrecognisedSchema
}
listener, err := protocol.listen(ctx, u, sintf)
if err != nil {
ctxcancel()
return nil, err
}
addr := listener.Addr()
cancel := func() {
ctxcancel()
if err := listener.Close(); err != nil && !errors.Is(err, net.ErrClosed) {
l.core.log.Warnf("Error closing %s listener %s: %s", strings.ToUpper(u.Scheme), addr, err)
}
}
li := &Listener{
listener: listener,
ctx: ctx,
Cancel: cancel,
}
var options linkOptions
if p := u.Query().Get("priority"); p != "" {
pi, err := strconv.ParseUint(p, 10, 8)
if err != nil {
return nil, ErrLinkPriorityInvalid
}
options.priority = uint8(pi)
}
if p := u.Query().Get("password"); p != "" {
if len(p) > blake2b.Size {
return nil, ErrLinkPasswordInvalid
}
options.password = []byte(p)
}
phony.Block(l, func() {
l._listeners[li] = cancel
})
go func() {
l.core.log.Infof("%s listener started on %s", strings.ToUpper(u.Scheme), addr)
defer phony.Block(l, func() {
cancel()
delete(l._listeners, li)
l.core.log.Infof("%s listener stopped on %s", strings.ToUpper(u.Scheme), addr)
})
for {
conn, err := li.listener.Accept()
if err != nil {
return
go func() {
if err := l.unix.dial(u, options, sintf); err != nil && err != io.EOF {
l.core.log.Warnf("Failed to dial UNIX %s: %s\n", u.Host, err)
}
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,
}
default:
return info, errors.New("unknown call scheme: " + u.Scheme)
}
return info, nil
}
// 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
}
func (l *links) listen(u *url.URL, sintf string) (*Listener, error) {
var listener *Listener
var err error
switch u.Scheme {
case "tcp":
listener, err = l.tcp.listen(u, sintf)
case "tls":
listener, err = l.tls.listen(u, sintf)
case "unix":
listener, err = l.unix.listen(u, sintf)
default:
return nil, fmt.Errorf("unrecognised scheme %q", u.Scheme)
}
return listener, err
}
// 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)
func (l *links) create(conn net.Conn, name string, info linkInfo, incoming, force bool, options linkOptions) error {
intf := link{
conn: &linkConn{
Conn: conn,
up: time.Now(),
},
lname: name,
links: l,
options: options,
info: info,
incoming: incoming,
force: force,
}
go func() {
if err := intf.handler(); err != nil {
l.core.log.Errorf("Link handler %s error (%s): %s", name, conn.RemoteAddr(), err)
}
}()
return li, nil
return 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
func (intf *link) handler() error {
defer intf.conn.Close() // nolint:errcheck
// Don't connect to this link more than once.
if intf.links.isConnectedTo(intf.info) {
return nil
}
return dialer.dial(ctx, u, info, options)
}
func (l *links) handler(linkType linkType, options linkOptions, conn net.Conn, success func(), local bool) error {
// Mark the connection as in progress.
phony.Block(intf.links, func() {
intf.links._links[intf.info] = nil
})
// When we're done, clean up the connection entry.
defer phony.Block(intf.links, func() {
delete(intf.links._links, intf.info)
})
meta := version_getBaseMetadata()
meta.publicKey = l.core.public
meta.priority = options.priority
metaBytes, err := meta.encode(l.core.secret, options.password)
if err != nil {
return fmt.Errorf("failed to generate handshake: %w", err)
}
if err := conn.SetDeadline(time.Now().Add(time.Second * 6)); err != nil {
meta.key = intf.links.core.public
metaBytes := meta.encode()
if err := intf.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)
n, err := intf.conn.Write(metaBytes)
switch {
case err != nil:
return fmt.Errorf("write handshake: %w", err)
case n != len(metaBytes):
case err == nil && n != len(metaBytes):
return fmt.Errorf("incomplete handshake send")
}
if _, err = io.ReadFull(intf.conn, metaBytes); err != nil {
return fmt.Errorf("read handshake: %w", err)
}
if err = intf.conn.SetDeadline(time.Time{}); err != nil {
return fmt.Errorf("failed to clear handshake deadline: %w", err)
}
meta = version_metadata{}
base := version_getBaseMetadata()
if err := meta.decode(conn, options.password); err != nil {
_ = conn.Close()
return err
if !meta.decode(metaBytes) {
return errors.New("failed to decode metadata")
}
if !meta.check() {
return fmt.Errorf("remote node incompatible version (local %s, remote %s)",
fmt.Sprintf("%d.%d", base.majorVer, base.minorVer),
fmt.Sprintf("%d.%d", meta.majorVer, meta.minorVer),
var connectError string
if intf.incoming {
connectError = "Rejected incoming connection"
} else {
connectError = "Failed to connect"
}
intf.links.core.log.Debugf("%s: %s is incompatible version (local %s, remote %s)",
connectError,
intf.lname,
fmt.Sprintf("%d.%d", base.ver, base.minorVer),
fmt.Sprintf("%d.%d", meta.ver, meta.minorVer),
)
}
if err = conn.SetDeadline(time.Time{}); err != nil {
return fmt.Errorf("failed to clear handshake deadline: %w", err)
return errors.New("remote node is incompatible version")
}
// Check if the remote side matches the keys we expected. This is a bit of a weak
// check - in future versions we really should check a signature or something like that.
if pinned := options.pinnedEd25519Keys; len(pinned) > 0 {
if pinned := intf.options.pinnedEd25519Keys; len(pinned) > 0 {
var key keyArray
copy(key[:], meta.publicKey)
copy(key[:], meta.key)
if _, allowed := pinned[key]; !allowed {
return fmt.Errorf("node public key that does not match pinned keys")
}
}
// Check if we're authorized to connect to this key / IP
if !local {
var allowed map[[32]byte]struct{}
phony.Block(l.core, func() {
allowed = l.core.config._allowedPublicKeys
})
isallowed := len(allowed) == 0
for k := range allowed {
if bytes.Equal(k[:], meta.publicKey) {
isallowed = true
break
}
}
if linkType == linkTypeIncoming && !isallowed {
return fmt.Errorf("node public key %q is not in AllowedPublicKeys", hex.EncodeToString(meta.publicKey))
allowed := intf.links.core.config._allowedPublicKeys
isallowed := len(allowed) == 0
for k := range allowed {
if bytes.Equal(k[:], meta.key) {
isallowed = true
break
}
}
if intf.incoming && !intf.force && !isallowed {
_ = intf.close()
return fmt.Errorf("node public key %q is not in AllowedPublicKeys", hex.EncodeToString(meta.key))
}
phony.Block(intf.links, func() {
intf.links._links[intf.info] = intf
})
dir := "outbound"
if linkType == linkTypeIncoming {
if intf.incoming {
dir = "inbound"
}
remoteAddr := net.IP(address.AddrForKey(meta.publicKey)[:]).String()
remoteStr := fmt.Sprintf("%s@%s", remoteAddr, conn.RemoteAddr())
localStr := conn.LocalAddr()
priority := options.priority
if meta.priority > priority {
priority = meta.priority
}
l.core.log.Infof("Connected %s: %s, source %s",
dir, remoteStr, localStr)
if success != nil {
success()
}
remoteAddr := net.IP(address.AddrForKey(meta.key)[:]).String()
remoteStr := fmt.Sprintf("%s@%s", remoteAddr, intf.info.remote)
localStr := intf.conn.LocalAddr()
intf.links.core.log.Infof("Connected %s %s: %s, source %s",
dir, strings.ToUpper(intf.info.linkType), remoteStr, localStr)
err = l.core.HandleConn(meta.publicKey, conn, priority)
err = intf.links.core.HandleConn(meta.key, intf.conn, intf.options.priority)
switch err {
case io.EOF, net.ErrClosed, nil:
l.core.log.Infof("Disconnected %s: %s, source %s",
dir, remoteStr, localStr)
intf.links.core.log.Infof("Disconnected %s %s: %s, source %s",
dir, strings.ToUpper(intf.info.linkType), remoteStr, localStr)
default:
l.core.log.Infof("Disconnected %s: %s, source %s; error: %s",
dir, remoteStr, localStr, err)
intf.links.core.log.Infof("Disconnected %s %s: %s, source %s; error: %s",
dir, strings.ToUpper(intf.info.linkType), remoteStr, localStr, err)
}
return err
return nil
}
func (l *links) findSuitableIP(url *url.URL, fn func(hostname string, ip net.IP, port int) (net.Conn, error)) (net.Conn, error) {
host, p, err := net.SplitHostPort(url.Host)
if err != nil {
return nil, err
}
port, err := strconv.Atoi(p)
if err != nil {
return nil, err
}
resp, err := net.LookupIP(host)
if err != nil {
return nil, err
}
var _ips [64]net.IP
ips := _ips[:0]
for _, ip := range resp {
switch {
case ip.IsUnspecified():
continue
case ip.IsMulticast():
continue
case ip.IsLinkLocalMulticast():
continue
case ip.IsInterfaceLocalMulticast():
continue
case l.core.config.peerFilter != nil && !l.core.config.peerFilter(ip):
continue
}
ips = append(ips, ip)
}
if len(ips) == 0 {
return nil, ErrLinkNoSuitableIPs
}
for _, ip := range ips {
var conn net.Conn
if conn, err = fn(host, ip, port); err != nil {
url := *url
url.RawQuery = ""
l.core.log.Debugln("Dialling", url.Redacted(), "reported error:", err)
continue
}
return conn, nil
}
return nil, err
func (intf *link) close() error {
return intf.conn.Close()
}
func urlForLinkInfo(u url.URL) url.URL {
u.RawQuery = ""
return u
func linkInfoFor(linkType, sintf, remote string) linkInfo {
return linkInfo{
linkType: linkType,
local: sintf,
remote: remote,
}
}
type linkConn struct {
// tx and rx are at the beginning of the struct to ensure 64-bit alignment
// on 32-bit platforms, see https://pkg.go.dev/sync/atomic#pkg-note-BUG
rx uint64
tx uint64
rxrate uint64
txrate uint64
lastrx uint64
lasttx uint64
up time.Time
rx uint64
tx uint64
up time.Time
net.Conn
}
@ -765,3 +356,12 @@ func (c *linkConn) Write(p []byte) (n int, err error) {
atomic.AddUint64(&c.tx, uint64(n))
return
}
func linkOptionsForListener(u *url.URL) (l linkOptions) {
if p := u.Query().Get("priority"); p != "" {
if pi, err := strconv.ParseUint(p, 10, 8); err == nil {
l.priority = uint8(pi)
}
}
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,8 +1,6 @@
package core
import (
"context"
"crypto/tls"
"fmt"
"net"
"net/url"
@ -22,46 +20,33 @@ func (l *links) newLinkSOCKS() *linkSOCKS {
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()
func (l *linkSOCKS) dial(url *url.URL, options linkOptions) error {
info := linkInfoFor("socks", "", url.Path)
if l.links.isConnectedTo(info) {
return nil
}
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
})
proxyAuth := &proxy.Auth{}
proxyAuth.User = url.User.Username()
proxyAuth.Password, _ = url.User.Password()
dialer, err := proxy.SOCKS5("tcp", url.Host, proxyAuth, proxy.Direct)
if err != nil {
return fmt.Errorf("failed to configure proxy")
}
pathtokens := strings.Split(strings.Trim(url.Path, "/"), "/")
conn, err := dialer.Dial("tcp", pathtokens[0])
if err != nil {
return err
}
return l.handler(url.String(), info, conn, options, false)
}
func (l *linkSOCKS) listen(ctx context.Context, url *url.URL, _ string) (net.Listener, error) {
return nil, fmt.Errorf("SOCKS listener not supported")
func (l *linkSOCKS) handler(name string, info linkInfo, conn net.Conn, options linkOptions, incoming bool) error {
return l.links.create(
conn, // connection
name, // connection name
info, // connection info
incoming, // not incoming
false, // not forced
options, // connection options
)
}

View file

@ -5,6 +5,7 @@ import (
"fmt"
"net"
"net/url"
"strings"
"time"
"github.com/Arceliar/phony"
@ -13,42 +14,112 @@ import (
type linkTCP struct {
phony.Inbox
*links
listenconfig *net.ListenConfig
listener *net.ListenConfig
_listeners map[*Listener]context.CancelFunc
}
func (l *links) newLinkTCP() *linkTCP {
lt := &linkTCP{
links: l,
listenconfig: &net.ListenConfig{
listener: &net.ListenConfig{
KeepAlive: -1,
},
_listeners: map[*Listener]context.CancelFunc{},
}
lt.listenconfig.Control = lt.tcpContext
lt.listener.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) dial(url *url.URL, options linkOptions, sintf string) error {
addr, err := net.ResolveTCPAddr("tcp", url.Host)
if err != nil {
return err
}
dialer, err := l.dialerFor(addr, sintf)
if err != nil {
return err
}
info := linkInfoFor("tcp", sintf, tcpIDFor(dialer.LocalAddr, addr))
if l.links.isConnectedTo(info) {
return nil
}
conn, err := dialer.DialContext(l.core.ctx, "tcp", addr.String())
if err != nil {
return err
}
uri := strings.TrimRight(strings.SplitN(url.String(), "?", 2)[0], "/")
return l.handler(uri, info, conn, options, false, false)
}
func (l *linkTCP) listen(ctx context.Context, url *url.URL, sintf string) (net.Listener, error) {
func (l *linkTCP) listen(url *url.URL, sintf string) (*Listener, error) {
ctx, cancel := context.WithCancel(l.core.ctx)
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)
listener, err := l.listener.Listen(ctx, "tcp", hostport)
if err != nil {
cancel()
return nil, err
}
entry := &Listener{
Listener: listener,
closed: make(chan struct{}),
}
phony.Block(l, func() {
l._listeners[entry] = cancel
})
l.core.log.Printf("TCP listener started on %s", listener.Addr())
go func() {
defer phony.Block(l, func() {
delete(l._listeners, entry)
})
for {
conn, err := listener.Accept()
if err != nil {
cancel()
break
}
laddr := conn.LocalAddr().(*net.TCPAddr)
raddr := conn.RemoteAddr().(*net.TCPAddr)
name := fmt.Sprintf("tcp://%s", raddr)
info := linkInfoFor("tcp", sintf, tcpIDFor(laddr, raddr))
if err = l.handler(name, info, conn, linkOptionsForListener(url), true, raddr.IP.IsLinkLocalUnicast()); err != nil {
l.core.log.Errorln("Failed to create inbound link:", err)
}
}
_ = listener.Close()
close(entry.closed)
l.core.log.Printf("TCP listener stopped on %s", listener.Addr())
}()
return entry, nil
}
func (l *linkTCP) handler(name string, info linkInfo, conn net.Conn, options linkOptions, incoming, force bool) error {
return l.links.create(
conn, // connection
name, // connection name
info, // connection info
incoming, // not incoming
force, // not forced
options, // connection options
)
}
// Returns the address of the listener.
func (l *linkTCP) 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
var addr *net.TCPAddr
phony.Block(l, func() {
for listener := range l._listeners {
addr = listener.Addr().(*net.TCPAddr)
}
})
return addr
}
func (l *linkTCP) dialerFor(dst *net.TCPAddr, sintf string) (*net.Dialer, error) {
@ -109,3 +180,16 @@ func (l *linkTCP) dialerFor(dst *net.TCPAddr, sintf string) (*net.Dialer, error)
}
return dialer, nil
}
func tcpIDFor(local net.Addr, remoteAddr *net.TCPAddr) string {
if localAddr, ok := local.(*net.TCPAddr); ok && localAddr.IP.Equal(remoteAddr.IP) {
// Nodes running on the same host — include both the IP and port.
return remoteAddr.String()
}
if remoteAddr.IP.IsLinkLocalUnicast() {
// Nodes discovered via multicast — include the IP only.
return remoteAddr.IP.String()
}
// Nodes connected remotely — include both the IP and port.
return remoteAddr.String()
}

View file

@ -28,6 +28,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 *linkTCP) getControl(sintf string) func(string, string, syscall.RawConn) error {
return t.tcpContext
}

View file

@ -12,6 +12,22 @@ import (
// 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 {
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
}

View file

@ -1,11 +1,20 @@
package core
import (
"bytes"
"context"
"crypto/rand"
"crypto/tls"
"crypto/x509"
"crypto/x509/pkix"
"encoding/hex"
"encoding/pem"
"fmt"
"math/big"
"net"
"net/url"
"strings"
"time"
"github.com/Arceliar/phony"
)
@ -13,9 +22,10 @@ import (
type linkTLS struct {
phony.Inbox
*links
tcp *linkTCP
listener *net.ListenConfig
config *tls.Config
tcp *linkTCP
listener *net.ListenConfig
config *tls.Config
_listeners map[*Listener]context.CancelFunc
}
func (l *links) newLinkTLS(tcp *linkTCP) *linkTLS {
@ -26,37 +36,45 @@ func (l *links) newLinkTLS(tcp *linkTCP) *linkTLS {
Control: tcp.tcpContext,
KeepAlive: -1,
},
config: l.core.config.tls.Clone(),
_listeners: map[*Listener]context.CancelFunc{},
}
var err error
lt.config, err = lt.generateConfig()
if err != nil {
panic(err)
}
return lt
}
func (l *linkTLS) dial(ctx context.Context, url *url.URL, info linkInfo, options linkOptions) (net.Conn, error) {
func (l *linkTLS) dial(url *url.URL, options linkOptions, sintf, sni string) error {
addr, err := net.ResolveTCPAddr("tcp", url.Host)
if err != nil {
return err
}
dialer, err := l.tcp.dialerFor(addr, sintf)
if err != nil {
return err
}
info := linkInfoFor("tls", sintf, tcpIDFor(dialer.LocalAddr, addr))
if l.links.isConnectedTo(info) {
return nil
}
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())
})
tlsconfig.ServerName = sni
tlsdialer := &tls.Dialer{
NetDialer: dialer,
Config: tlsconfig,
}
conn, err := tlsdialer.DialContext(l.core.ctx, "tcp", addr.String())
if err != nil {
return err
}
uri := strings.TrimRight(strings.SplitN(url.String(), "?", 2)[0], "/")
return l.handler(uri, info, conn, options, false, false)
}
func (l *linkTLS) listen(ctx context.Context, url *url.URL, sintf string) (net.Listener, error) {
func (l *linkTLS) listen(url *url.URL, sintf string) (*Listener, error) {
ctx, cancel := context.WithCancel(l.core.ctx)
hostport := url.Host
if sintf != "" {
if host, port, err := net.SplitHostPort(hostport); err == nil {
@ -65,8 +83,90 @@ func (l *linkTLS) listen(ctx context.Context, url *url.URL, sintf string) (net.L
}
listener, err := l.listener.Listen(ctx, "tcp", hostport)
if err != nil {
cancel()
return nil, err
}
tlslistener := tls.NewListener(listener, l.config)
return tlslistener, nil
entry := &Listener{
Listener: tlslistener,
closed: make(chan struct{}),
}
phony.Block(l, func() {
l._listeners[entry] = cancel
})
l.core.log.Printf("TLS listener started on %s", listener.Addr())
go func() {
defer phony.Block(l, func() {
delete(l._listeners, entry)
})
for {
conn, err := tlslistener.Accept()
if err != nil {
cancel()
break
}
laddr := conn.LocalAddr().(*net.TCPAddr)
raddr := conn.RemoteAddr().(*net.TCPAddr)
name := fmt.Sprintf("tls://%s", raddr)
info := linkInfoFor("tls", sintf, tcpIDFor(laddr, raddr))
if err = l.handler(name, info, conn, linkOptionsForListener(url), true, raddr.IP.IsLinkLocalUnicast()); err != nil {
l.core.log.Errorln("Failed to create inbound link:", err)
}
}
_ = tlslistener.Close()
close(entry.closed)
l.core.log.Printf("TLS listener stopped on %s", listener.Addr())
}()
return entry, nil
}
func (l *linkTLS) generateConfig() (*tls.Config, error) {
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.
cert := x509.Certificate{
SerialNumber: big.NewInt(1),
Subject: pkix.Name{
CommonName: hex.EncodeToString(l.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,
}
certbytes, err := x509.CreateCertificate(rand.Reader, &cert, &cert, l.links.core.public, l.links.core.secret)
if err != nil {
return nil, err
}
if err := pem.Encode(certBuf, &pem.Block{
Type: "CERTIFICATE",
Bytes: certbytes,
}); err != nil {
return nil, err
}
rootCAs := x509.NewCertPool()
rootCAs.AppendCertsFromPEM(certbytes)
return &tls.Config{
RootCAs: rootCAs,
Certificates: []tls.Certificate{
{
Certificate: [][]byte{certbytes},
PrivateKey: l.links.core.secret,
},
},
InsecureSkipVerify: true,
MinVersion: tls.VersionTLS13,
}, nil
}
func (l *linkTLS) handler(name string, info linkInfo, conn net.Conn, options linkOptions, incoming, force bool) error {
return l.tcp.handler(name, info, conn, options, incoming, force)
}

View file

@ -12,8 +12,9 @@ import (
type linkUNIX struct {
phony.Inbox
*links
dialer *net.Dialer
listener *net.ListenConfig
dialer *net.Dialer
listener *net.ListenConfig
_listeners map[*Listener]context.CancelFunc
}
func (l *links) newLinkUNIX() *linkUNIX {
@ -26,18 +27,71 @@ func (l *links) newLinkUNIX() *linkUNIX {
listener: &net.ListenConfig{
KeepAlive: -1,
},
_listeners: map[*Listener]context.CancelFunc{},
}
return lt
}
func (l *linkUNIX) dial(ctx context.Context, url *url.URL, info linkInfo, options linkOptions) (net.Conn, error) {
func (l *linkUNIX) dial(url *url.URL, options linkOptions, _ string) error {
info := linkInfoFor("unix", "", url.Path)
if l.links.isConnectedTo(info) {
return nil
}
addr, err := net.ResolveUnixAddr("unix", url.Path)
if err != nil {
return nil, err
return err
}
return l.dialer.DialContext(ctx, "unix", addr.String())
conn, err := l.dialer.DialContext(l.core.ctx, "unix", addr.String())
if err != nil {
return err
}
return l.handler(url.String(), info, conn, options, false)
}
func (l *linkUNIX) listen(ctx context.Context, url *url.URL, _ string) (net.Listener, error) {
return l.listener.Listen(ctx, "unix", url.Path)
func (l *linkUNIX) listen(url *url.URL, _ string) (*Listener, error) {
ctx, cancel := context.WithCancel(l.core.ctx)
listener, err := l.listener.Listen(ctx, "unix", url.Path)
if err != nil {
cancel()
return nil, err
}
entry := &Listener{
Listener: listener,
closed: make(chan struct{}),
}
phony.Block(l, func() {
l._listeners[entry] = cancel
})
l.core.log.Printf("UNIX listener started on %s", listener.Addr())
go func() {
defer phony.Block(l, func() {
delete(l._listeners, entry)
})
for {
conn, err := listener.Accept()
if err != nil {
cancel()
break
}
info := linkInfoFor("unix", "", url.String())
if err = l.handler(url.String(), info, conn, linkOptionsForListener(url), true); err != nil {
l.core.log.Errorln("Failed to create inbound link:", err)
}
}
_ = listener.Close()
close(entry.closed)
l.core.log.Printf("UNIX listener stopped on %s", listener.Addr())
}()
return entry, nil
}
func (l *linkUNIX) handler(name string, info linkInfo, conn net.Conn, options linkOptions, incoming bool) error {
return l.links.create(
conn, // connection
name, // connection name
info, // connection info
incoming, // not incoming
false, // not forced
options, // connection options
)
}

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

@ -2,31 +2,14 @@ package core
import (
"crypto/ed25519"
"fmt"
"net"
"net/url"
)
func (c *Core) _applyOption(opt SetupOption) (err error) {
func (c *Core) _applyOption(opt SetupOption) {
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
}
c.config._peers[v] = nil
case ListenAddress:
c.config._listeners[v] = struct{}{}
case PeerFilter:
c.config.peerFilter = v
case NodeInfo:
c.config.nodeinfo = v
case NodeInfoPrivacy:
@ -36,7 +19,6 @@ func (c *Core) _applyOption(opt SetupOption) (err error) {
copy(pk[:], v)
c.config._allowedPublicKeys[pk] = struct{}{}
}
return
}
type SetupOption interface {
@ -51,11 +33,9 @@ type Peer struct {
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

@ -21,8 +21,8 @@ const (
typeDebugGetSelfResponse
typeDebugGetPeersRequest
typeDebugGetPeersResponse
typeDebugGetTreeRequest
typeDebugGetTreeResponse
typeDebugGetDHTRequest
typeDebugGetDHTResponse
)
type reqInfo struct {
@ -40,7 +40,7 @@ type protoHandler struct {
selfRequests map[keyArray]*reqInfo
peersRequests map[keyArray]*reqInfo
treeRequests map[keyArray]*reqInfo
dhtRequests map[keyArray]*reqInfo
}
func (p *protoHandler) init(core *Core) {
@ -49,7 +49,7 @@ func (p *protoHandler) init(core *Core) {
p.selfRequests = make(map[keyArray]*reqInfo)
p.peersRequests = make(map[keyArray]*reqInfo)
p.treeRequests = make(map[keyArray]*reqInfo)
p.dhtRequests = make(map[keyArray]*reqInfo)
}
// Common functions
@ -89,10 +89,10 @@ func (p *protoHandler) _handleDebug(key keyArray, bs []byte) {
p._handleGetPeersRequest(key)
case typeDebugGetPeersResponse:
p._handleGetPeersResponse(key, bs[1:])
case typeDebugGetTreeRequest:
p._handleGetTreeRequest(key)
case typeDebugGetTreeResponse:
p._handleGetTreeResponse(key, bs[1:])
case typeDebugGetDHTRequest:
p._handleGetDHTRequest(key)
case typeDebugGetDHTResponse:
p._handleGetDHTResponse(key, bs[1:])
}
}
@ -126,8 +126,8 @@ func (p *protoHandler) sendGetSelfRequest(key keyArray, callback func([]byte)) {
func (p *protoHandler) _handleGetSelfRequest(key keyArray) {
self := p.core.GetSelf()
res := map[string]string{
"key": hex.EncodeToString(self.Key[:]),
"routing_entries": fmt.Sprintf("%v", self.RoutingEntries),
"key": hex.EncodeToString(self.Key[:]),
"coords": fmt.Sprintf("%v", self.Coords),
}
bs, err := json.Marshal(res) // FIXME this puts keys in base64, not hex
if err != nil {
@ -188,47 +188,47 @@ func (p *protoHandler) _handleGetPeersResponse(key keyArray, bs []byte) {
}
}
// Get Tree
// Get DHT
func (p *protoHandler) sendGetTreeRequest(key keyArray, callback func([]byte)) {
func (p *protoHandler) sendGetDHTRequest(key keyArray, callback func([]byte)) {
p.Act(nil, func() {
if info := p.treeRequests[key]; info != nil {
if info := p.dhtRequests[key]; info != nil {
info.timer.Stop()
delete(p.treeRequests, key)
delete(p.dhtRequests, key)
}
info := new(reqInfo)
info.callback = callback
info.timer = time.AfterFunc(time.Minute, func() {
p.Act(nil, func() {
if p.treeRequests[key] == info {
delete(p.treeRequests, key)
if p.dhtRequests[key] == info {
delete(p.dhtRequests, key)
}
})
})
p.treeRequests[key] = info
p._sendDebug(key, typeDebugGetTreeRequest, nil)
p.dhtRequests[key] = info
p._sendDebug(key, typeDebugGetDHTRequest, nil)
})
}
func (p *protoHandler) _handleGetTreeRequest(key keyArray) {
dinfos := p.core.GetTree()
func (p *protoHandler) _handleGetDHTRequest(key keyArray) {
dinfos := p.core.GetDHT()
var bs []byte
for _, dinfo := range dinfos {
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() {
break
}
bs = tmp
}
p._sendDebug(key, typeDebugGetTreeResponse, bs)
p._sendDebug(key, typeDebugGetDHTResponse, bs)
}
func (p *protoHandler) _handleGetTreeResponse(key keyArray, bs []byte) {
if info := p.treeRequests[key]; info != nil {
func (p *protoHandler) _handleGetDHTResponse(key keyArray, bs []byte) {
if info := p.dhtRequests[key]; info != nil {
info.timer.Stop()
info.callback(bs)
delete(p.treeRequests, key)
delete(p.dhtRequests, key)
}
}
@ -251,16 +251,15 @@ func (p *protoHandler) getSelfHandler(in json.RawMessage) (interface{}, error) {
if kbs, err = hex.DecodeString(req.Key); err != nil {
return nil, err
}
if len(kbs) != ed25519.PublicKeySize {
return nil, fmt.Errorf("invalid public key length")
}
copy(key[:], kbs)
ch := make(chan []byte, 1)
p.sendGetSelfRequest(key, func(info []byte) {
ch <- info
})
timer := time.NewTimer(6 * time.Second)
defer timer.Stop()
select {
case <-time.After(6 * time.Second):
case <-timer.C:
return nil, errors.New("timeout")
case info := <-ch:
var msg json.RawMessage
@ -292,16 +291,15 @@ func (p *protoHandler) getPeersHandler(in json.RawMessage) (interface{}, error)
if kbs, err = hex.DecodeString(req.Key); err != nil {
return nil, err
}
if len(kbs) != ed25519.PublicKeySize {
return nil, fmt.Errorf("invalid public key length")
}
copy(key[:], kbs)
ch := make(chan []byte, 1)
p.sendGetPeersRequest(key, func(info []byte) {
ch <- info
})
timer := time.NewTimer(6 * time.Second)
defer timer.Stop()
select {
case <-time.After(6 * time.Second):
case <-timer.C:
return nil, errors.New("timeout")
case info := <-ch:
ks := make(map[string][]string)
@ -324,16 +322,16 @@ func (p *protoHandler) getPeersHandler(in json.RawMessage) (interface{}, error)
}
}
// Admin socket stuff for "Get Tree"
// Admin socket stuff for "Get DHT"
type DebugGetTreeRequest struct {
type DebugGetDHTRequest struct {
Key string `json:"key"`
}
type DebugGetTreeResponse map[string]interface{}
type DebugGetDHTResponse map[string]interface{}
func (p *protoHandler) getTreeHandler(in json.RawMessage) (interface{}, error) {
var req DebugGetTreeRequest
func (p *protoHandler) getDHTHandler(in json.RawMessage) (interface{}, error) {
var req DebugGetDHTRequest
if err := json.Unmarshal(in, &req); err != nil {
return nil, err
}
@ -343,16 +341,15 @@ func (p *protoHandler) getTreeHandler(in json.RawMessage) (interface{}, error) {
if kbs, err = hex.DecodeString(req.Key); err != nil {
return nil, err
}
if len(kbs) != ed25519.PublicKeySize {
return nil, fmt.Errorf("invalid public key length")
}
copy(key[:], kbs)
ch := make(chan []byte, 1)
p.sendGetTreeRequest(key, func(info []byte) {
p.sendGetDHTRequest(key, func(info []byte) {
ch <- info
})
timer := time.NewTimer(6 * time.Second)
defer timer.Stop()
select {
case <-time.After(6 * time.Second):
case <-timer.C:
return nil, errors.New("timeout")
case info := <-ch:
ks := make(map[string][]string)
@ -370,7 +367,7 @@ func (p *protoHandler) getTreeHandler(in json.RawMessage) (interface{}, error) {
return nil, err
}
ip := net.IP(address.AddrForKey(kbs)[:])
res := DebugGetTreeResponse{ip.String(): msg}
res := DebugGetDHTResponse{ip.String(): msg}
return res, nil
}
}

View file

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

View file

@ -4,166 +4,65 @@ package core
// Used in the initial connection setup and key exchange
// Some of this could arguably go in wire.go instead
import (
"bytes"
"crypto/ed25519"
"encoding/binary"
"io"
"golang.org/x/crypto/blake2b"
)
import "crypto/ed25519"
// This is the version-specific metadata exchanged at the start of a connection.
// It must always begin with the 4 bytes "meta" and a wire formatted uint64 major version number.
// The current version also includes a minor version number, and the box/sig/link keys that need to be exchanged to open a connection.
type version_metadata struct {
majorVer uint16
minorVer uint16
publicKey ed25519.PublicKey
priority uint8
meta [4]byte
ver uint8 // 1 byte in this version
// Everything after this point potentially depends on the version number, and is subject to change in future versions
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.
func version_getBaseMetadata() version_metadata {
return version_metadata{
majorVer: ProtocolVersionMajor,
minorVer: ProtocolVersionMinor,
meta: [4]byte{'m', 'e', 't', 'a'},
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.
func (m *version_metadata) encode(privateKey ed25519.PrivateKey, password []byte) ([]byte, error) {
bs := make([]byte, 0, 64)
bs = append(bs, 'm', 'e', 't', 'a')
bs = append(bs, 0, 0) // Remaining message length
bs = binary.BigEndian.AppendUint16(bs, metaVersionMajor)
bs = binary.BigEndian.AppendUint16(bs, 2)
bs = binary.BigEndian.AppendUint16(bs, m.majorVer)
bs = binary.BigEndian.AppendUint16(bs, metaVersionMinor)
bs = binary.BigEndian.AppendUint16(bs, 2)
bs = binary.BigEndian.AppendUint16(bs, m.minorVer)
bs = binary.BigEndian.AppendUint16(bs, metaPublicKey)
bs = binary.BigEndian.AppendUint16(bs, ed25519.PublicKeySize)
bs = append(bs, m.publicKey[:]...)
bs = binary.BigEndian.AppendUint16(bs, metaPriority)
bs = binary.BigEndian.AppendUint16(bs, 1)
bs = append(bs, m.priority)
hasher, err := blake2b.New512(password)
if err != nil {
return nil, err
func (m *version_metadata) encode() []byte {
bs := make([]byte, 0, version_getMetaLength())
bs = append(bs, m.meta[:]...)
bs = append(bs, m.ver)
bs = append(bs, m.minorVer)
bs = append(bs, m.key[:]...)
if len(bs) != version_getMetaLength() {
panic("Inconsistent metadata length")
}
n, err := hasher.Write(m.publicKey)
if err != nil {
return nil, err
}
if n != ed25519.PublicKeySize {
return nil, ErrHandshakeHashFailure
}
hash := hasher.Sum(nil)
bs = append(bs, ed25519.Sign(privateKey, hash)...)
binary.BigEndian.PutUint16(bs[4:6], uint16(len(bs)-6))
return bs, nil
return bs
}
// Decodes version metadata from its wire format into the struct.
func (m *version_metadata) decode(r io.Reader, password []byte) error {
bh := [6]byte{}
if _, err := io.ReadFull(r, bh[:]); err != nil {
return err
func (m *version_metadata) decode(bs []byte) bool {
if len(bs) != version_getMetaLength() {
return false
}
meta := [4]byte{'m', 'e', 't', 'a'}
if !bytes.Equal(bh[:4], meta[:]) {
return ErrHandshakeInvalidPreamble
}
hl := binary.BigEndian.Uint16(bh[4:6])
if hl < ed25519.SignatureSize {
return ErrHandshakeInvalidLength
}
bs := make([]byte, hl)
if _, err := io.ReadFull(r, bs); err != nil {
return err
}
sig := bs[len(bs)-ed25519.SignatureSize:]
bs = bs[:len(bs)-ed25519.SignatureSize]
for len(bs) >= 4 {
op := binary.BigEndian.Uint16(bs[:2])
oplen := binary.BigEndian.Uint16(bs[2:4])
if bs = bs[4:]; len(bs) < int(oplen) {
break
}
switch op {
case metaVersionMajor:
m.majorVer = binary.BigEndian.Uint16(bs[:2])
case metaVersionMinor:
m.minorVer = binary.BigEndian.Uint16(bs[:2])
case metaPublicKey:
m.publicKey = make(ed25519.PublicKey, ed25519.PublicKeySize)
copy(m.publicKey, bs[:ed25519.PublicKeySize])
case metaPriority:
m.priority = bs[0]
}
bs = bs[oplen:]
}
hasher, err := blake2b.New512(password)
if err != nil {
return ErrHandshakeInvalidPassword
}
n, err := hasher.Write(m.publicKey)
if err != nil || n != ed25519.PublicKeySize {
return ErrHandshakeHashFailure
}
hash := hasher.Sum(nil)
if !ed25519.Verify(m.publicKey, hash, sig) {
return ErrHandshakeIncorrectPassword
}
return nil
offset := 0
offset += copy(m.meta[:], bs[offset:])
m.ver, offset = bs[offset], offset+1
m.minorVer, offset = bs[offset], offset+1
m.key = append([]byte(nil), bs[offset:]...)
return true
}
// Checks that the "meta" bytes and the version numbers are the expected values.
func (m *version_metadata) check() bool {
switch {
case m.majorVer != ProtocolVersionMajor:
return false
case m.minorVer != ProtocolVersionMinor:
return false
case len(m.publicKey) != ed25519.PublicKeySize:
return false
default:
return true
}
base := version_getBaseMetadata()
return base.meta == m.meta && base.ver == m.ver && base.minorVer == m.minorVer
}

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

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

@ -0,0 +1,60 @@
package defaults
import "github.com/yggdrasil-network/yggdrasil-go/src/config"
type MulticastInterfaceConfig = config.MulticastInterfaceConfig
var defaultConfig = "" // LDFLAGS='-X github.com/yggdrasil-network/yggdrasil-go/src/defaults.defaultConfig=/path/to/config
var defaultAdminListen = "" // LDFLAGS='-X github.com/yggdrasil-network/yggdrasil-go/src/defaults.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
}
// 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 {
// Get the defaults for the platform.
defaults := GetDefaults()
// Create a node configuration and populate it.
cfg := new(config.NodeConfig)
cfg.NewKeys()
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
return cfg
}

View file

@ -1,7 +1,7 @@
//go:build darwin
// +build darwin
package config
package defaults
// Sane defaults for the macOS/Darwin platform. The "default" options may be
// may be replaced by the running configuration.
@ -17,7 +17,6 @@ func getDefaults() platformDefaultParameters {
DefaultMulticastInterfaces: []MulticastInterfaceConfig{
{Regex: "en.*", Beacon: true, Listen: true},
{Regex: "bridge.*", Beacon: true, Listen: true},
{Regex: "awdl0", Beacon: false, Listen: false},
},
// TUN

View file

@ -1,7 +1,7 @@
//go:build freebsd
// +build freebsd
package config
package defaults
// Sane defaults for the BSD platforms. The "default" options may be
// may be replaced by the running configuration.

View file

@ -1,7 +1,7 @@
//go:build linux
// +build linux
package config
package defaults
// Sane defaults for the Linux platform. The "default" options may be
// may be replaced by the running configuration.

View file

@ -1,7 +1,7 @@
//go:build openbsd
// +build openbsd
package config
package defaults
// Sane defaults for the BSD platforms. The "default" options may be
// may be replaced by the running configuration.

View file

@ -1,7 +1,7 @@
//go:build !linux && !darwin && !windows && !openbsd && !freebsd
// +build !linux,!darwin,!windows,!openbsd,!freebsd
package config
package defaults
// Sane defaults for the other platforms. The "default" options may be
// may be replaced by the running configuration.

View file

@ -1,7 +1,7 @@
//go:build windows
// +build windows
package config
package defaults
// Sane defaults for the Windows platform. The "default" options may be
// may be replaced by the running configuration.

View file

@ -19,14 +19,12 @@ import (
const keyStoreTimeout = 2 * time.Minute
/*
// Out-of-band packet types
const (
typeKeyDummy = iota // nolint:deadcode,varcheck
typeKeyLookup
typeKeyResponse
)
*/
type keyArray [ed25519.PublicKeySize]byte
@ -59,13 +57,10 @@ func (k *keyStore) init(c *core.Core) {
k.core = c
k.address = *address.AddrForKey(k.core.PublicKey())
k.subnet = *address.SubnetForKey(k.core.PublicKey())
/*if err := k.core.SetOutOfBandHandler(k.oobHandler); err != nil {
if err := k.core.SetOutOfBandHandler(k.oobHandler); err != nil {
err = fmt.Errorf("tun.core.SetOutOfBandHander: %w", err)
panic(err)
}*/
k.core.SetPathNotify(func(key ed25519.PublicKey) {
k.update(key)
})
}
k.keyToInfo = make(map[keyArray]*keyInfo)
k.addrToInfo = make(map[address.Address]*keyInfo)
k.addrBuffer = make(map[address.Address]*buffer)
@ -182,8 +177,7 @@ func (k *keyStore) resetTimeout(info *keyInfo) {
})
}
/*
func (k *keyStore) oobHandler(fromKey, toKey ed25519.PublicKey, data []byte) { // nolint:unused
func (k *keyStore) oobHandler(fromKey, toKey ed25519.PublicKey, data []byte) {
if len(data) != 1+ed25519.SignatureSize {
return
}
@ -204,26 +198,18 @@ func (k *keyStore) oobHandler(fromKey, toKey ed25519.PublicKey, data []byte) { /
}
}
}
*/
func (k *keyStore) sendKeyLookup(partial ed25519.PublicKey) {
/*
sig := ed25519.Sign(k.core.PrivateKey(), partial[:])
bs := append([]byte{typeKeyLookup}, sig...)
//_ = k.core.SendOutOfBand(partial, bs)
_ = bs
*/
k.core.SendLookup(partial)
sig := ed25519.Sign(k.core.PrivateKey(), partial[:])
bs := append([]byte{typeKeyLookup}, sig...)
_ = k.core.SendOutOfBand(partial, bs)
}
/*
func (k *keyStore) sendKeyResponse(dest ed25519.PublicKey) { // nolint:unused
func (k *keyStore) sendKeyResponse(dest ed25519.PublicKey) {
sig := ed25519.Sign(k.core.PrivateKey(), dest[:])
bs := append([]byte{typeKeyResponse}, sig...)
//_ = k.core.SendOutOfBand(dest, bs)
_ = bs
_ = k.core.SendOutOfBand(dest, bs)
}
*/
func (k *keyStore) readPC(p []byte) (int, error) {
buf := make([]byte, k.core.MTU(), 65535)

View file

@ -2,47 +2,20 @@ package multicast
import (
"encoding/json"
"slices"
"strings"
"github.com/Arceliar/phony"
"github.com/yggdrasil-network/yggdrasil-go/src/admin"
)
type GetMulticastInterfacesRequest struct{}
type GetMulticastInterfacesResponse struct {
Interfaces []MulticastInterfaceState `json:"multicast_interfaces"`
Interfaces []string `json:"multicast_interfaces"`
}
type MulticastInterfaceState struct {
Name string `json:"name"`
Address string `json:"address"`
Beacon bool `json:"beacon"`
Listen bool `json:"listen"`
Password bool `json:"password"`
}
func (m *Multicast) getMulticastInterfacesHandler(_ *GetMulticastInterfacesRequest, res *GetMulticastInterfacesResponse) error {
res.Interfaces = []MulticastInterfaceState{}
phony.Block(m, func() {
for name, intf := range m._interfaces {
is := MulticastInterfaceState{
Name: intf.iface.Name,
Beacon: intf.beacon,
Listen: intf.listen,
Password: len(intf.password) > 0,
}
if li := m._listeners[name]; li != nil && li.listener != nil {
is.Address = li.listener.Addr().String()
} else {
is.Address = "-"
}
res.Interfaces = append(res.Interfaces, is)
}
})
slices.SortStableFunc(res.Interfaces, func(a, b MulticastInterfaceState) int {
return strings.Compare(a.Name, b.Name)
})
func (m *Multicast) getMulticastInterfacesHandler(req *GetMulticastInterfacesRequest, res *GetMulticastInterfacesResponse) error {
res.Interfaces = []string{}
for _, v := range m.Interfaces() {
res.Interfaces = append(res.Interfaces, v.Name)
}
return 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,17 @@ import (
"bytes"
"context"
"crypto/ed25519"
"encoding/binary"
"encoding/hex"
"fmt"
"math/rand"
"net"
"net/url"
"sync/atomic"
"time"
"github.com/Arceliar/phony"
"github.com/gologme/log"
"github.com/wlynxg/anet"
"github.com/yggdrasil-network/yggdrasil-go/src/core"
"golang.org/x/crypto/blake2b"
"golang.org/x/net/ipv6"
)
@ -30,10 +27,9 @@ type Multicast struct {
core *core.Core
log *log.Logger
sock *ipv6.PacketConn
running atomic.Bool
_isOpen bool
_listeners map[string]*listenerInfo
_interfaces map[string]*interfaceInfo
_timer *time.Timer
config struct {
_groupAddr GroupAddress
_interfaces map[MulticastInterface]struct{}
@ -47,8 +43,6 @@ type interfaceInfo struct {
listen bool
port uint16
priority uint8
password []byte
hash []byte
}
type listenerInfo struct {
@ -81,7 +75,7 @@ func New(core *core.Core, log *log.Logger, opts ...SetupOption) (*Multicast, err
}
func (m *Multicast) _start() error {
if !m.running.CompareAndSwap(false, true) {
if m._isOpen {
return fmt.Errorf("multicast module is already started")
}
var anyEnabled bool
@ -89,14 +83,12 @@ func (m *Multicast) _start() error {
anyEnabled = anyEnabled || intf.Beacon || intf.Listen
}
if !anyEnabled {
m.running.Store(false)
return nil
}
m.log.Debugln("Starting multicast module")
defer m.log.Debugln("Started multicast module")
addr, err := net.ResolveUDPAddr("udp", string(m.config._groupAddr))
if err != nil {
m.running.Store(false)
return err
}
listenString := fmt.Sprintf("[::]:%v", addr.Port)
@ -105,7 +97,6 @@ func (m *Multicast) _start() error {
}
conn, err := lc.ListenPacket(context.Background(), "udp6", listenString)
if err != nil {
m.running.Store(false)
return err
}
m.sock = ipv6.NewPacketConn(conn)
@ -113,6 +104,7 @@ func (m *Multicast) _start() error {
// Windows can't set this flag, so we need to handle it in other ways
}
m._isOpen = true
go m.listen()
m.Act(nil, m._multicastStarted)
m.Act(nil, m._announce)
@ -122,7 +114,11 @@ func (m *Multicast) _start() error {
// IsStarted returns true if the module has been started.
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.
@ -136,10 +132,8 @@ 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._isOpen = false
if m.sock != nil {
m.sock.Close()
}
@ -149,22 +143,14 @@ func (m *Multicast) _stop() error {
func (m *Multicast) _updateInterfaces() {
interfaces := m._getAllowedInterfaces()
for name, info := range interfaces {
// 'anet' package is used here to avoid https://github.com/golang/go/issues/40569
addrs, err := anet.InterfaceAddrsByInterface(&info.iface)
addrs, err := info.iface.Addrs()
if err != nil {
m.log.Warnf("Failed up get addresses for interface %s: %s", name, err)
delete(interfaces, name)
continue
}
for _, addr := range addrs {
addrIP, _, err := net.ParseCIDR(addr.String())
if err != nil || addrIP.To4() != nil || !addrIP.IsLinkLocalUnicast() {
continue
}
info.addrs = append(info.addrs, addr)
}
info.addrs = addrs
interfaces[name] = info
m.log.Debugf("Discovered addresses for interface %s: %s", name, addrs)
}
m._interfaces = interfaces
}
@ -183,21 +169,17 @@ func (m *Multicast) Interfaces() map[string]net.Interface {
func (m *Multicast) _getAllowedInterfaces() map[string]*interfaceInfo {
interfaces := make(map[string]*interfaceInfo)
// Ask the system for network interfaces
// 'anet' package is used here to avoid https://github.com/golang/go/issues/40569
allifaces, err := anet.Interfaces()
allifaces, err := net.Interfaces()
if err != nil {
// 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
}
// Work out which interfaces to announce on
pk := m.core.PublicKey()
for _, iface := range allifaces {
switch {
case iface.Flags&net.FlagUp == 0:
continue // Ignore interfaces that are down
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:
@ -212,23 +194,12 @@ func (m *Multicast) _getAllowedInterfaces() map[string]*interfaceInfo {
if !ifcfg.Regex.MatchString(iface.Name) {
continue
}
hasher, err := blake2b.New512([]byte(ifcfg.Password))
if err != nil {
continue
}
if n, err := hasher.Write(pk); err != nil {
continue
} else if n != ed25519.PublicKeySize {
continue
}
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
}
@ -236,17 +207,8 @@ func (m *Multicast) _getAllowedInterfaces() map[string]*interfaceInfo {
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() {
if !m.running.Load() {
if !m._isOpen {
return
}
m._updateInterfaces()
@ -263,7 +225,7 @@ func (m *Multicast) _announce() {
for name, info := range m._listeners {
// Prepare our stop function!
stop := func() {
info.listener.Cancel()
info.listener.Close()
delete(m._listeners, name)
m.log.Debugln("No longer multicasting on", name)
}
@ -276,7 +238,7 @@ func (m *Multicast) _announce() {
// 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
found := false
listenaddr, err := net.ResolveTCPAddr("tcp6", info.listener.Addr().String())
listenaddr, err := net.ResolveTCPAddr("tcp6", info.listener.Listener.Addr().String())
if err != nil {
stop()
continue
@ -305,9 +267,13 @@ func (m *Multicast) _announce() {
for _, info := range m._interfaces {
iface := info.iface
for _, addr := range info.addrs {
addrIP, _, err := net.ParseCIDR(addr.String())
// Ignore IPv4 addresses or non-link-local addresses
if err != nil || addrIP.To4() != nil || !addrIP.IsLinkLocalUnicast() {
addrIP, _, _ := net.ParseCIDR(addr.String())
// Ignore IPv4 addresses
if addrIP.To4() != nil {
continue
}
// Ignore non-link-local addresses
if !addrIP.IsLinkLocalUnicast() {
continue
}
if info.listen {
@ -319,17 +285,14 @@ func (m *Multicast) _announce() {
}
// Try and see if we already have a TCP listener for this interface
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
v := &url.Values{}
v.Add("priority", fmt.Sprintf("%d", info.priority))
v.Add("password", string(info.password))
u := &url.URL{
Scheme: "tls",
Host: net.JoinHostPort(addrIP.String(), fmt.Sprintf("%d", info.port)),
RawQuery: v.Encode(),
urlString := fmt.Sprintf("tls://[%s]:%d", addrIP, info.port)
u, err := url.Parse(urlString)
if err != nil {
panic(err)
}
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)
// Store the listener so that we can stop it later if needed
linfo = &listenerInfo{listener: li, time: time.Now(), port: info.port}
@ -348,31 +311,25 @@ func (m *Multicast) _announce() {
if time.Since(linfo.time) < linfo.interval {
continue
}
addr := linfo.listener.Addr().(*net.TCPAddr)
adv := multicastAdvertisement{
MajorVersion: core.ProtocolVersionMajor,
MinorVersion: core.ProtocolVersionMinor,
PublicKey: m.core.PublicKey(),
Port: uint16(addr.Port),
Hash: info.hash,
}
msg, err := adv.MarshalBinary()
if err != nil {
continue
}
destAddr.Zone = iface.Name
if _, err = m.sock.WriteTo(msg, nil, destAddr); err != nil {
m.log.Warn("Failed to send multicast beacon:", err)
// Get the listener details and construct the multicast beacon
lladdr := linfo.listener.Listener.Addr().String()
if a, err := net.ResolveTCPAddr("tcp6", lladdr); err == nil {
a.Zone = ""
destAddr.Zone = iface.Name
msg := append([]byte(nil), m.core.GetSelf().Key...)
msg = append(msg, a.IP...)
pbs := make([]byte, 2)
binary.BigEndian.PutUint16(pbs, uint16(a.Port))
msg = append(msg, pbs...)
_, _ = m.sock.WriteTo(msg, nil, destAddr)
}
if linfo.interval.Seconds() < 15 {
linfo.interval += time.Second
}
linfo.time = time.Now()
break
}
}
annInterval := time.Second + time.Microsecond*(time.Duration(rand.Intn(1048576))) // Randomize delay
m._timer = time.AfterFunc(annInterval, func() {
time.AfterFunc(time.Second, func() {
m.Act(nil, m._announce)
})
}
@ -383,12 +340,8 @@ func (m *Multicast) listen() {
panic(err)
}
bs := make([]byte, 2048)
hb := make([]byte, 0, blake2b.Size) // Reused to reduce hash allocations
for {
if !m.running.Load() {
return
}
n, rcm, fromAddr, err := m.sock.ReadFrom(bs)
nBytes, rcm, fromAddr, err := m.sock.ReadFrom(bs)
if err != nil {
if !m.IsStarted() {
return
@ -406,45 +359,40 @@ func (m *Multicast) listen() {
continue
}
}
var adv multicastAdvertisement
if err := adv.UnmarshalBinary(bs[:n]); err != nil {
if nBytes < ed25519.PublicKeySize {
continue
}
switch {
case adv.MajorVersion != core.ProtocolVersionMajor:
continue
case adv.MinorVersion != core.ProtocolVersionMinor:
continue
case adv.PublicKey.Equal(m.core.PublicKey()):
var key ed25519.PublicKey
key = append(key, bs[:ed25519.PublicKeySize]...)
if bytes.Equal(key, m.core.GetSelf().Key) {
continue // don't bother trying to peer with self
}
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
}
from := fromAddr.(*net.UDPAddr)
from.Port = int(adv.Port)
if !from.IP.Equal(addr.IP) {
continue
}
var interfaces map[string]*interfaceInfo
phony.Block(m, func() {
interfaces = m._interfaces
})
if info, ok := interfaces[from.Zone]; ok && info.listen {
hasher, err := blake2b.New512(info.password)
addr.Zone = ""
pin := fmt.Sprintf("/?key=%s&priority=%d", hex.EncodeToString(key), info.priority)
u, err := url.Parse("tls://" + addr.String() + pin)
if err != nil {
continue
}
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(),
m.log.Debugln("Call from multicast failed, parse error:", addr.String(), err)
}
if err := m.core.CallPeer(u, from.Zone); err != nil {
m.log.Debugln("Call from multicast failed:", err)

View file

@ -31,7 +31,7 @@ import (
)
func (m *Multicast) _multicastStarted() {
if !m.running.Load() {
if !m._isOpen {
return
}
C.StopAWDLBrowsing()

View file

@ -21,7 +21,6 @@ type MulticastInterface struct {
Listen bool
Port uint16
Priority uint8
Password string
}
type GroupAddress string

View file

@ -1,70 +1,42 @@
package tun
import (
"errors"
wgtun "golang.zx2c4.com/wireguard/tun"
)
const TUN_OFFSET_BYTES = 80 // sizeof(virtio_net_hdr)
const TUN_OFFSET_BYTES = 4
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)
}
var buf [TUN_OFFSET_BYTES + 65535]byte
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
}
n, err := tun.iface.Read(buf[:], TUN_OFFSET_BYTES)
if n <= TUN_OFFSET_BYTES || err != nil {
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)
ferr := tun.iface.Flush()
if ferr != nil {
tun.log.Errorln("Unable to flush packets:", ferr)
}
}
}
}
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]
begin := TUN_OFFSET_BYTES
end := begin + n
bs := buf[begin:end]
if _, err := tun.rwc.Write(bs); err != nil {
tun.log.Debugln("Unable to send packet:", err)
}
}
}
func (tun *TunAdapter) write() {
vs := cap(tun.ch)
bufs := make([][]byte, vs)
for i := range bufs {
bufs[i] = make([]byte, TUN_OFFSET_BYTES+65535)
}
var buf [TUN_OFFSET_BYTES + 65535]byte
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
bs := buf[TUN_OFFSET_BYTES:]
n, err := tun.rwc.Read(bs)
if err != nil {
tun.log.Errorln("Exiting tun writer due to core read error:", err)
return
}
if !tun.isEnabled {
continue // Nothing to do, the tun isn't enabled
}
if _, err := tun.iface.Write(bufs[:n], TUN_OFFSET_BYTES); err != nil {
bs = buf[:TUN_OFFSET_BYTES+n]
if _, err = tun.iface.Write(bs, TUN_OFFSET_BYTES); err != nil {
tun.Act(nil, func() {
if !tun.isOpen {
tun.log.Errorln("TUN iface write error:", err)

View file

@ -6,8 +6,6 @@ func (m *TunAdapter) _applyOption(opt SetupOption) {
m.config.name = v
case InterfaceMTU:
m.config.mtu = v
case FileDescriptor:
m.config.fd = int32(v)
}
}
@ -17,8 +15,6 @@ type SetupOption interface {
type InterfaceName string
type InterfaceMTU uint64
type FileDescriptor int32
func (a InterfaceName) isSetupOption() {}
func (a InterfaceMTU) isSetupOption() {}
func (a FileDescriptor) isSetupOption() {}
func (a InterfaceName) isSetupOption() {}
func (a InterfaceMTU) isSetupOption() {}

View file

@ -8,52 +8,42 @@ package tun
import (
"errors"
"fmt"
"io"
"net"
"sync"
"github.com/Arceliar/phony"
wgtun "golang.zx2c4.com/wireguard/tun"
"golang.zx2c4.com/wireguard/tun"
"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/defaults"
"github.com/yggdrasil-network/yggdrasil-go/src/ipv6rwc"
)
type MTU uint16
type ReadWriteCloser interface {
io.ReadWriteCloser
Address() address.Address
Subnet() address.Subnet
MaxMTU() uint64
SetMTU(uint64)
}
// TunAdapter represents a running TUN interface and extends the
// yggdrasil.Adapter type. In order to use the TUN adapter with Yggdrasil, you
// should pass this object to the yggdrasil.SetRouterAdapter() function before
// calling yggdrasil.Start().
type TunAdapter struct {
rwc ReadWriteCloser
rwc *ipv6rwc.ReadWriteCloser
log core.Logger
addr address.Address
subnet address.Subnet
mtu uint64
iface wgtun.Device
iface tun.Device
phony.Inbox // Currently only used for _handlePacket from the reader, TODO: all the stuff that currently needs a mutex below
isOpen bool
isEnabled bool // Used by the writer to drop sessionTraffic if not enabled
config struct {
fd int32
//mutex sync.RWMutex // Protects the below
isOpen bool
isEnabled bool // Used by the writer to drop sessionTraffic if not enabled
config struct {
name InterfaceName
mtu InterfaceMTU
}
ch chan []byte
}
// Gets the maximum supported MTU for the platform based on the defaults in
// config.GetDefaults().
// defaults.GetDefaults().
func getSupportedMTU(mtu uint64) uint64 {
if mtu < 1280 {
return 1280
@ -82,25 +72,25 @@ func (tun *TunAdapter) MTU() uint64 {
// DefaultName gets the default TUN interface name for your platform.
func DefaultName() string {
return config.GetDefaults().DefaultIfName
return defaults.GetDefaults().DefaultIfName
}
// DefaultMTU gets the default TUN interface MTU for your platform. This can
// be as high as MaximumMTU(), depending on platform, but is never lower than 1280.
func DefaultMTU() uint64 {
return config.GetDefaults().DefaultIfMTU
return defaults.GetDefaults().DefaultIfMTU
}
// MaximumMTU returns the maximum supported TUN interface MTU for your
// platform. This can be as high as 65535, depending on platform, but is never
// lower than 1280.
func MaximumMTU() uint64 {
return config.GetDefaults().MaximumIfMTU
return defaults.GetDefaults().MaximumIfMTU
}
// Init initialises the TUN module. You must have acquired a Listener from
// the Yggdrasil core before this point and it must not be in use elsewhere.
func New(rwc ReadWriteCloser, log core.Logger, opts ...SetupOption) (*TunAdapter, error) {
func New(rwc *ipv6rwc.ReadWriteCloser, log core.Logger, opts ...SetupOption) (*TunAdapter, error) {
tun := &TunAdapter{
rwc: rwc,
log: log,
@ -117,15 +107,10 @@ func (tun *TunAdapter) _start() error {
}
tun.addr = tun.rwc.Address()
tun.subnet = tun.rwc.Subnet()
prefix := address.GetPrefix()
var addr string
if tun.addr.IsValid() {
addr = fmt.Sprintf("%s/%d", net.IP(tun.addr[:]).String(), 8*len(prefix[:])-1)
}
addr := fmt.Sprintf("%s/%d", net.IP(tun.addr[:]).String(), 8*len(address.GetPrefix())-1)
if tun.config.name == "none" || tun.config.name == "dummy" {
tun.log.Debugln("Not starting TUN as ifname is none or dummy")
tun.isEnabled = false
go tun.queue()
go tun.write()
return nil
}
@ -133,13 +118,7 @@ func (tun *TunAdapter) _start() error {
if tun.rwc.MaxMTU() < mtu {
mtu = tun.rwc.MaxMTU()
}
var err error
if tun.config.fd > 0 {
err = tun.setupFD(tun.config.fd, addr, mtu)
} else {
err = tun.setup(string(tun.config.name), addr, mtu)
}
if err != nil {
if err := tun.setup(string(tun.config.name), addr, mtu); err != nil {
return err
}
if tun.MTU() != mtu {
@ -148,8 +127,6 @@ func (tun *TunAdapter) _start() error {
tun.rwc.SetMTU(tun.MTU())
tun.isOpen = true
tun.isEnabled = true
tun.ch = make(chan []byte, tun.iface.BatchSize())
go tun.queue()
go tun.read()
go tun.write()
return nil
@ -183,12 +160,3 @@ func (tun *TunAdapter) _stop() error {
}
return nil
}
const bufPoolSize = TUN_OFFSET_BYTES + 65535
var bufPool = sync.Pool{
New: func() any {
b := [bufPoolSize]byte{}
return b[:]
},
}

View file

@ -1,11 +1,10 @@
//go:build freebsd
// +build freebsd
//go:build openbsd || freebsd
// +build openbsd freebsd
package tun
import (
"encoding/binary"
"fmt"
"os/exec"
"strconv"
"strings"
@ -54,6 +53,11 @@ struct in6_ifreq {
290 };
*/
type in6_ifreq_mtu struct {
ifr_name [syscall.IFNAMSIZ]byte
ifru_mtu int
}
type in6_ifreq_addr struct {
ifr_name [syscall.IFNAMSIZ]byte
ifru_addr sockaddr_in6
@ -73,7 +77,7 @@ type in6_ifreq_lifetime struct {
func (tun *TunAdapter) setup(ifname string, addr string, mtu uint64) error {
iface, err := wgtun.CreateTUN(ifname, int(mtu))
if err != nil {
return fmt.Errorf("failed to create TUN: %w", err)
panic(err)
}
tun.iface = iface
if mtu, err := iface.MTU(); err == nil {
@ -81,15 +85,7 @@ func (tun *TunAdapter) setup(ifname string, addr string, mtu uint64) error {
} else {
tun.mtu = 0
}
if addr != "" {
return tun.setupAddress(addr)
}
return nil
}
// Configures the "utun" adapter from an existing file descriptor.
func (tun *TunAdapter) setupFD(fd int32, addr string, mtu uint64) error {
return fmt.Errorf("setup via FD not supported on this platform")
return tun.setupAddress(addr)
}
func (tun *TunAdapter) setupAddress(addr string) error {
@ -107,6 +103,26 @@ func (tun *TunAdapter) setupAddress(addr string) error {
tun.log.Infof("Interface IPv6: %s", addr)
tun.log.Infof("Interface MTU: %d", tun.mtu)
// Create the MTU request
var ir in6_ifreq_mtu
copy(ir.ifr_name[:], tun.Name())
ir.ifru_mtu = int(tun.mtu)
// Set the MTU
if _, _, errno := unix.Syscall(unix.SYS_IOCTL, uintptr(sfd), uintptr(syscall.SIOCSIFMTU), uintptr(unsafe.Pointer(&ir))); errno != 0 {
err = errno
tun.log.Errorf("Error in SIOCSIFMTU: %v", errno)
// Fall back to ifconfig to set the MTU
cmd := exec.Command("ifconfig", tun.Name(), "mtu", string(tun.mtu))
tun.log.Warnf("Using ifconfig as fallback: %v", strings.Join(cmd.Args, " "))
output, err := cmd.CombinedOutput()
if err != nil {
tun.log.Errorf("SIOCSIFMTU fallback failed: %v.", err)
tun.log.Traceln(string(output))
}
}
// Create the address request
// FIXME: I don't work!
var ar in6_ifreq_addr

View file

@ -1,5 +1,5 @@
//go:build darwin || ios
// +build darwin ios
//go:build !mobile
// +build !mobile
package tun
@ -7,8 +7,6 @@ package tun
import (
"encoding/binary"
"fmt"
"os"
"strconv"
"strings"
"unsafe"
@ -25,7 +23,7 @@ func (tun *TunAdapter) setup(ifname string, addr string, mtu uint64) error {
}
iface, err := wgtun.CreateTUN(ifname, int(mtu))
if err != nil {
return fmt.Errorf("failed to create TUN: %w", err)
panic(err)
}
tun.iface = iface
if m, err := iface.MTU(); err == nil {
@ -33,35 +31,7 @@ func (tun *TunAdapter) setup(ifname string, addr string, mtu uint64) error {
} else {
tun.mtu = 0
}
if addr != "" {
return tun.setupAddress(addr)
}
return nil
}
// Configures the "utun" adapter from an existing file descriptor.
func (tun *TunAdapter) setupFD(fd int32, addr string, mtu uint64) error {
dfd, err := unix.Dup(int(fd))
if err != nil {
return fmt.Errorf("failed to duplicate FD: %w", err)
}
err = unix.SetNonblock(dfd, true)
if err != nil {
unix.Close(dfd)
return fmt.Errorf("failed to set FD as non-blocking: %w", err)
}
iface, err := wgtun.CreateTUNFromFile(os.NewFile(uintptr(dfd), "/dev/tun"), 0)
if err != nil {
unix.Close(dfd)
return fmt.Errorf("failed to create TUN from FD: %w", err)
}
tun.iface = iface
if m, err := iface.MTU(); err == nil {
tun.mtu = getSupportedMTU(uint64(m))
} else {
tun.mtu = 0
}
return nil // tun.setupAddress(addr)
return tun.setupAddress(addr)
}
const (
@ -111,8 +81,8 @@ func (tun *TunAdapter) setupAddress(addr string) error {
var err error
if fd, err = unix.Socket(unix.AF_INET6, unix.SOCK_DGRAM, 0); err != nil {
tun.log.Errorf("Create AF_SYSTEM socket failed: %v.", err)
return fmt.Errorf("failed to open AF_SYSTEM: %w", err)
tun.log.Printf("Create AF_SYSTEM socket failed: %v.", err)
return err
}
var ar in6_aliasreq
@ -147,16 +117,16 @@ func (tun *TunAdapter) setupAddress(addr string) error {
tun.log.Infof("Interface IPv6: %s", addr)
tun.log.Infof("Interface MTU: %d", ir.ifru_mtu)
if _, _, errno := unix.Syscall(unix.SYS_IOCTL, uintptr(fd), uintptr(darwin_SIOCAIFADDR_IN6), uintptr(unsafe.Pointer(&ar))); errno != 0 { // nolint:staticcheck
if _, _, errno := unix.Syscall(unix.SYS_IOCTL, uintptr(fd), uintptr(darwin_SIOCAIFADDR_IN6), uintptr(unsafe.Pointer(&ar))); errno != 0 {
err = errno
tun.log.Errorf("Error in darwin_SIOCAIFADDR_IN6: %v", errno)
return fmt.Errorf("failed to call SIOCAIFADDR_IN6: %w", err)
return err
}
if _, _, errno := unix.Syscall(unix.SYS_IOCTL, uintptr(fd), uintptr(unix.SIOCSIFMTU), uintptr(unsafe.Pointer(&ir))); errno != 0 { // nolint:staticcheck
if _, _, errno := unix.Syscall(unix.SYS_IOCTL, uintptr(fd), uintptr(unix.SIOCSIFMTU), uintptr(unsafe.Pointer(&ir))); errno != 0 {
err = errno
tun.log.Errorf("Error in SIOCSIFMTU: %v", errno)
return fmt.Errorf("failed to call SIOCSIFMTU: %w", err)
return err
}
return err

View file

@ -1,13 +1,11 @@
//go:build linux || android
// +build linux android
//go:build !mobile
// +build !mobile
package tun
// The linux platform specific tun parts
import (
"fmt"
"github.com/vishvananda/netlink"
wgtun "golang.zx2c4.com/wireguard/tun"
)
@ -19,7 +17,7 @@ func (tun *TunAdapter) setup(ifname string, addr string, mtu uint64) error {
}
iface, err := wgtun.CreateTUN(ifname, int(mtu))
if err != nil {
return fmt.Errorf("failed to create TUN: %w", err)
panic(err)
}
tun.iface = iface
if mtu, err := iface.MTU(); err == nil {
@ -27,15 +25,7 @@ func (tun *TunAdapter) setup(ifname string, addr string, mtu uint64) error {
} else {
tun.mtu = 0
}
if addr != "" {
return tun.setupAddress(addr)
}
return nil
}
// Configures the "utun" adapter from an existing file descriptor.
func (tun *TunAdapter) setupFD(fd int32, addr string, mtu uint64) error {
return fmt.Errorf("setup via FD not supported on this platform")
return tun.setupAddress(addr)
}
// Configures the TUN adapter with the correct IPv6 address and MTU. Netlink
@ -45,20 +35,20 @@ func (tun *TunAdapter) setupFD(fd int32, addr string, mtu uint64) error {
func (tun *TunAdapter) setupAddress(addr string) error {
nladdr, err := netlink.ParseAddr(addr)
if err != nil {
return fmt.Errorf("couldn't parse address %q: %w", addr, err)
return err
}
nlintf, err := netlink.LinkByName(tun.Name())
if err != nil {
return fmt.Errorf("failed to find link by name: %w", err)
return err
}
if err := netlink.AddrAdd(nlintf, nladdr); err != nil {
return fmt.Errorf("failed to add address to link: %w", err)
return err
}
if err := netlink.LinkSetMTU(nlintf, int(tun.mtu)); err != nil {
return fmt.Errorf("failed to set link MTU: %w", err)
return err
}
if err := netlink.LinkSetUp(nlintf); err != nil {
return fmt.Errorf("failed to bring link up: %w", err)
return err
}
// Friendly output
tun.log.Infof("Interface name: %s", tun.Name())

View file

@ -1,122 +0,0 @@
//go:build openbsd
// +build openbsd
package tun
import (
"fmt"
"net"
"syscall"
"unsafe"
"golang.org/x/sys/unix"
wgtun "golang.zx2c4.com/wireguard/tun"
)
const (
SIOCAIFADDR_IN6 = 0x8080691a
ND6_INFINITE_LIFETIME = 0xffffffff
)
type in6_addrlifetime struct {
ia6t_expire int64
ia6t_preferred int64
ia6t_vltime uint32
ia6t_pltime uint32
}
// Match types from the net package, effectively being [16]byte for IPv6 addresses.
type in6_addr [16]uint8
type sockaddr_in6 struct {
sin6_len uint8
sin6_family uint8
sin6_port uint16
sin6_flowinfo uint32
sin6_addr in6_addr
sin6_scope_id uint32
}
func (sa6 *sockaddr_in6) setSockaddr(addr [/*16*/]byte /* net.IP or net.IPMask */) {
sa6.sin6_len = uint8(unsafe.Sizeof(*sa6))
sa6.sin6_family = unix.AF_INET6
for i := range sa6.sin6_addr {
sa6.sin6_addr[i] = addr[i]
}
}
type in6_aliasreq struct {
ifra_name [syscall.IFNAMSIZ]byte
ifra_addr sockaddr_in6
ifra_dstaddr sockaddr_in6
ifra_prefixmask sockaddr_in6
ifra_flags int32
ifra_lifetime in6_addrlifetime
}
// Configures the TUN adapter with the correct IPv6 address and MTU.
func (tun *TunAdapter) setup(ifname string, addr string, mtu uint64) error {
iface, err := wgtun.CreateTUN(ifname, int(mtu))
if err != nil {
return fmt.Errorf("failed to create TUN: %w", err)
}
tun.iface = iface
if mtu, err := iface.MTU(); err == nil {
tun.mtu = getSupportedMTU(uint64(mtu))
} else {
tun.mtu = 0
}
if addr != "" {
return tun.setupAddress(addr)
}
return nil
}
// Configures the "utun" adapter from an existing file descriptor.
func (tun *TunAdapter) setupFD(fd int32, addr string, mtu uint64) error {
return fmt.Errorf("setup via FD not supported on this platform")
}
func (tun *TunAdapter) setupAddress(addr string) error {
var sfd int
var err error
ip, prefix, err := net.ParseCIDR(addr)
if err != nil {
tun.log.Errorf("Error in ParseCIDR: %v", err)
return err
}
// Create system socket
if sfd, err = unix.Socket(unix.AF_INET6, unix.SOCK_DGRAM, 0); err != nil {
tun.log.Printf("Create AF_INET6 socket failed: %v", err)
return err
}
// Friendly output
tun.log.Infof("Interface name: %s", tun.Name())
tun.log.Infof("Interface IPv6: %s", addr)
tun.log.Infof("Interface MTU: %d", tun.mtu)
// Create the address request
var ar in6_aliasreq
copy(ar.ifra_name[:], tun.Name())
ar.ifra_addr.setSockaddr(ip)
prefixmask := net.CIDRMask(prefix.Mask.Size())
ar.ifra_prefixmask.setSockaddr(prefixmask)
ar.ifra_lifetime.ia6t_vltime = ND6_INFINITE_LIFETIME
ar.ifra_lifetime.ia6t_pltime = ND6_INFINITE_LIFETIME
// Set the interface address
if err = unix.IoctlSetInt(sfd, SIOCAIFADDR_IN6, int(uintptr(unsafe.Pointer(&ar)))); err != nil {
tun.log.Errorf("Error in SIOCAIFADDR_IN6: %v", err)
return err
}
return nil
}

View file

@ -1,5 +1,5 @@
//go:build !linux && !darwin && !ios && !android && !windows && !openbsd && !freebsd && !mobile
// +build !linux,!darwin,!ios,!android,!windows,!openbsd,!freebsd,!mobile
//go:build !linux && !darwin && !windows && !openbsd && !freebsd && !mobile
// +build !linux,!darwin,!windows,!openbsd,!freebsd,!mobile
package tun
@ -7,8 +7,6 @@ package tun
// If your platform supports tun devices, you could try configuring it manually
import (
"fmt"
wgtun "golang.zx2c4.com/wireguard/tun"
)
@ -16,7 +14,7 @@ import (
func (tun *TunAdapter) setup(ifname string, addr string, mtu uint64) error {
iface, err := wgtun.CreateTUN(ifname, mtu)
if err != nil {
return fmt.Errorf("failed to create TUN: %w", err)
panic(err)
}
tun.iface = iface
if mtu, err := iface.MTU(); err == nil {
@ -24,15 +22,7 @@ func (tun *TunAdapter) setup(ifname string, addr string, mtu uint64) error {
} else {
tun.mtu = 0
}
if addr != "" {
return tun.setupAddress(addr)
}
return nil
}
// Configures the "utun" adapter from an existing file descriptor.
func (tun *TunAdapter) setupFD(fd int32, addr string, mtu uint64) error {
return fmt.Errorf("setup via FD not supported on this platform")
return tun.setupAddress(addr)
}
// We don't know how to set the IPv6 address on an unknown platform, therefore

View file

@ -4,16 +4,14 @@
package tun
import (
"bytes"
"errors"
"fmt"
"log"
"net/netip"
"time"
"net"
"github.com/yggdrasil-network/yggdrasil-go/src/config"
"github.com/yggdrasil-network/yggdrasil-go/src/defaults"
"golang.org/x/sys/windows"
"golang.zx2c4.com/wintun"
wgtun "golang.zx2c4.com/wireguard/tun"
"golang.zx2c4.com/wireguard/windows/elevate"
"golang.zx2c4.com/wireguard/windows/tunnel/winipcfg"
@ -24,7 +22,7 @@ import (
// Configures the TUN adapter with the correct IPv6 address and MTU.
func (tun *TunAdapter) setup(ifname string, addr string, mtu uint64) error {
if ifname == "auto" {
ifname = config.GetDefaults().DefaultIfName
ifname = defaults.GetDefaults().DefaultIfName
}
return elevate.DoAsSystem(func() error {
var err error
@ -33,27 +31,13 @@ func (tun *TunAdapter) setup(ifname string, addr string, mtu uint64) error {
if guid, err = windows.GUIDFromString("{8f59971a-7872-4aa6-b2eb-061fc4e9d0a7}"); err != nil {
return err
}
iface, err = wgtun.CreateTUNWithRequestedGUID(ifname, &guid, int(mtu))
if err != nil {
// Very rare condition, it will purge the old device and create new
tun.log.Printf("Error creating TUN: '%s'", err)
wintun.Uninstall()
time.Sleep(3 * time.Second)
tun.log.Printf("Trying again")
iface, err = wgtun.CreateTUNWithRequestedGUID(ifname, &guid, int(mtu))
if err != nil {
return err
}
if iface, err = wgtun.CreateTUNWithRequestedGUID(ifname, &guid, int(mtu)); err != nil {
return err
}
tun.log.Printf("Waiting for TUN to come up")
time.Sleep(1 * time.Second)
tun.iface = iface
if addr != "" {
tun.log.Printf("Setting up address")
if err = tun.setupAddress(addr); err != nil {
tun.log.Errorln("Failed to set up TUN address:", err)
return err
}
if err = tun.setupAddress(addr); err != nil {
tun.log.Errorln("Failed to set up TUN address:", err)
return err
}
if err = tun.setupMTU(getSupportedMTU(mtu)); err != nil {
tun.log.Errorln("Failed to set up TUN MTU:", err)
@ -62,16 +46,10 @@ func (tun *TunAdapter) setup(ifname string, addr string, mtu uint64) error {
if mtu, err := iface.MTU(); err == nil {
tun.mtu = uint64(mtu)
}
tun.log.Printf("TUN is set up successfully")
return nil
})
}
// Configures the "utun" adapter from an existing file descriptor.
func (tun *TunAdapter) setupFD(fd int32, addr string, mtu uint64) error {
return fmt.Errorf("setup via FD not supported on this platform")
}
// Sets the MTU of the TUN adapter.
func (tun *TunAdapter) setupMTU(mtu uint64) error {
if tun.iface == nil || tun.Name() == "" {
@ -105,9 +83,13 @@ func (tun *TunAdapter) setupAddress(addr string) error {
return errors.New("Can't configure IPv6 address as TUN adapter is not present")
}
if intf, ok := tun.iface.(*wgtun.NativeTun); ok {
if ipnet, err := netip.ParsePrefix(addr); err == nil {
if ipaddr, ipnet, err := net.ParseCIDR(addr); err == nil {
luid := winipcfg.LUID(intf.LUID())
addresses := []netip.Prefix{ipnet}
addresses := append([]net.IPNet{}, net.IPNet{
IP: ipaddr,
Mask: ipnet.Mask,
})
err := luid.SetIPAddressesForFamily(windows.AF_INET6, addresses)
if err == windows.ERROR_OBJECT_ALREADY_EXISTS {
cleanupAddressesOnDisconnectedInterfaces(windows.AF_INET6, addresses)
@ -130,13 +112,24 @@ func (tun *TunAdapter) setupAddress(addr string) error {
* SPDX-License-Identifier: MIT
* Copyright (C) 2019 WireGuard LLC. All Rights Reserved.
*/
func cleanupAddressesOnDisconnectedInterfaces(family winipcfg.AddressFamily, addresses []netip.Prefix) {
func cleanupAddressesOnDisconnectedInterfaces(family winipcfg.AddressFamily, addresses []net.IPNet) {
if len(addresses) == 0 {
return
}
addrHash := make(map[netip.Addr]bool, len(addresses))
for i := range addresses {
addrHash[addresses[i].Addr()] = true
includedInAddresses := func(a net.IPNet) bool {
// TODO: this makes the whole algorithm O(n^2). But we can't stick net.IPNet in a Go hashmap. Bummer!
for _, addr := range addresses {
ip := addr.IP
if ip4 := ip.To4(); ip4 != nil {
ip = ip4
}
mA, _ := addr.Mask.Size()
mB, _ := a.Mask.Size()
if bytes.Equal(ip, a.IP) && mA == mB {
return true
}
}
return false
}
interfaces, err := winipcfg.GetAdaptersAddresses(family, winipcfg.GAAFlagDefault)
if err != nil {
@ -147,10 +140,11 @@ func cleanupAddressesOnDisconnectedInterfaces(family winipcfg.AddressFamily, add
continue
}
for address := iface.FirstUnicastAddress; address != nil; address = address.Next {
if ip, _ := netip.AddrFromSlice(address.Address.IP()); addrHash[ip] {
prefix := netip.PrefixFrom(ip, int(address.OnLinkPrefixLength))
log.Printf("Cleaning up stale address %s from interface %s", prefix.String(), iface.FriendlyName())
iface.LUID.DeleteIPAddress(prefix)
ip := address.Address.IP()
ipnet := net.IPNet{IP: ip, Mask: net.CIDRMask(int(address.OnLinkPrefixLength), 8*len(ip))}
if includedInAddresses(ipnet) {
log.Printf("Cleaning up stale address %s from interface %s", ipnet.String(), iface.FriendlyName())
iface.LUID.DeleteIPAddress(ipnet)
}
}
}