Release: Yggdrasil 0.5.0

This commit is contained in:
Neil Alexander 2023-10-28 15:11:34 +01:00
commit b332664acb
No known key found for this signature in database
GPG Key ID: A02A2019A2BB0944
70 changed files with 2954 additions and 2054 deletions

View File

@ -17,7 +17,7 @@ jobs:
steps: steps:
- uses: actions/setup-go@v3 - uses: actions/setup-go@v3
with: with:
go-version: 1.19 go-version: 1.21
- uses: actions/checkout@v3 - uses: actions/checkout@v3
- name: golangci-lint - name: golangci-lint
uses: golangci/golangci-lint-action@v3 uses: golangci/golangci-lint-action@v3
@ -51,7 +51,7 @@ jobs:
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
goversion: ["1.17", "1.18", "1.19"] goversion: ["1.20", "1.21"]
name: Build & Test (Linux, Go ${{ matrix.goversion }}) name: Build & Test (Linux, Go ${{ matrix.goversion }})
needs: [lint] needs: [lint]
@ -75,7 +75,7 @@ jobs:
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
goversion: ["1.17", "1.18", "1.19"] goversion: ["1.20", "1.21"]
name: Build & Test (Windows, Go ${{ matrix.goversion }}) name: Build & Test (Windows, Go ${{ matrix.goversion }})
needs: [lint] needs: [lint]
@ -99,7 +99,7 @@ jobs:
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
goversion: ["1.17", "1.18", "1.19"] goversion: ["1.20", "1.21"]
name: Build & Test (macOS, Go ${{ matrix.goversion }}) name: Build & Test (macOS, Go ${{ matrix.goversion }})
needs: [lint] needs: [lint]
@ -119,6 +119,32 @@ jobs:
- name: Unit tests - name: Unit tests
run: go test -v ./... run: go test -v ./...
build-freebsd:
strategy:
fail-fast: false
matrix:
goversion: ["1.20", "1.21"]
goos:
- freebsd
- openbsd
name: Build (Cross ${{ matrix.goos }}, Go ${{ matrix.goversion }})
needs: [lint]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Set up Go
uses: actions/setup-go@v3
with:
go-version: ${{ matrix.goversion }}
- name: Build Yggdrasil
run: go build -v ./...
env:
GOOS: ${{ matrix.goos }}
tests-ok: tests-ok:
name: All tests passed name: All tests passed
needs: [lint, codeql, build-linux, build-windows, build-macos] needs: [lint, codeql, build-linux, build-windows, build-macos]

View File

@ -16,7 +16,7 @@ jobs:
name: Package (Debian, ${{ matrix.pkgarch }}) name: Package (Debian, ${{ matrix.pkgarch }})
runs-on: ubuntu-latest runs-on: ubuntu-20.04
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v3
with: with:
@ -25,7 +25,7 @@ jobs:
- name: Set up Go - name: Set up Go
uses: actions/setup-go@v3 uses: actions/setup-go@v3
with: with:
go-version: 1.19 go-version: "1.20"
- name: Build package - name: Build package
env: env:
@ -56,7 +56,7 @@ jobs:
- name: Set up Go - name: Set up Go
uses: actions/setup-go@v3 uses: actions/setup-go@v3
with: with:
go-version: 1.19 go-version: "1.20"
- name: Build package - name: Build package
env: env:
@ -87,7 +87,7 @@ jobs:
- name: Set up Go - name: Set up Go
uses: actions/setup-go@v3 uses: actions/setup-go@v3
with: with:
go-version: 1.19 go-version: "1.20"
- name: Build package - name: Build package
run: sh contrib/msi/build-msi.sh ${{ matrix.pkgarch }} run: sh contrib/msi/build-msi.sh ${{ matrix.pkgarch }}
@ -107,7 +107,7 @@ jobs:
name: Package (Router, ${{ matrix.pkgarch }}) name: Package (Router, ${{ matrix.pkgarch }})
runs-on: ubuntu-latest runs-on: ubuntu-20.04
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v3
with: with:
@ -122,7 +122,7 @@ jobs:
- name: Set up Go - name: Set up Go
uses: actions/setup-go@v3 uses: actions/setup-go@v3
with: with:
go-version: 1.19 go-version: "1.20"
- name: Build package - name: Build package
env: env:

File diff suppressed because it is too large Load Diff

2
build
View File

@ -6,7 +6,7 @@ PKGSRC=${PKGSRC:-github.com/yggdrasil-network/yggdrasil-go/src/version}
PKGNAME=${PKGNAME:-$(sh contrib/semver/name.sh)} PKGNAME=${PKGNAME:-$(sh contrib/semver/name.sh)}
PKGVER=${PKGVER:-$(sh contrib/semver/version.sh --bare)} PKGVER=${PKGVER:-$(sh contrib/semver/version.sh --bare)}
LDFLAGS="-X $PKGSRC.buildName=$PKGNAME -X $PKGSRC.buildVersion=$PKGVER" LDFLAGS="${LDFLAGS} -X $PKGSRC.buildName=$PKGNAME -X $PKGSRC.buildVersion=$PKGVER"
ARGS="-v" ARGS="-v"
while getopts "utc:l:dro:p" option while getopts "utc:l:dro:p" option

View File

@ -1,5 +1,4 @@
/* /*
This file generates crypto keys. This file generates crypto keys.
It prints out a new set of keys each time if finds a "better" one. It prints out a new set of keys each time if finds a "better" one.
By default, "better" means a higher NodeID (-> higher IP address). By default, "better" means a higher NodeID (-> higher IP address).
@ -8,7 +7,6 @@ This is because the IP address format can compress leading 1s in the address, to
If run with the "-sig" flag, it generates signing keys instead. If run with the "-sig" flag, it generates signing keys instead.
A "better" signing key means one with a higher TreeID. A "better" signing key means one with a higher TreeID.
This only matters if it's high enough to make you the root of the tree. This only matters if it's high enough to make you the root of the tree.
*/ */
package main package main
@ -18,6 +16,7 @@ import (
"fmt" "fmt"
"net" "net"
"runtime" "runtime"
"time"
"github.com/yggdrasil-network/yggdrasil-go/src/address" "github.com/yggdrasil-network/yggdrasil-go/src/address"
) )
@ -29,6 +28,8 @@ type keySet struct {
func main() { func main() {
threads := runtime.GOMAXPROCS(0) threads := runtime.GOMAXPROCS(0)
fmt.Println("Threads:", threads)
start := time.Now()
var currentBest ed25519.PublicKey var currentBest ed25519.PublicKey
newKeys := make(chan keySet, threads) newKeys := make(chan keySet, threads)
for i := 0; i < threads; i++ { for i := 0; i < threads; i++ {
@ -38,7 +39,7 @@ func main() {
newKey := <-newKeys newKey := <-newKeys
if isBetter(currentBest, newKey.pub) || len(currentBest) == 0 { if isBetter(currentBest, newKey.pub) || len(currentBest) == 0 {
currentBest = newKey.pub currentBest = newKey.pub
fmt.Println("-----") fmt.Println("-----", time.Since(start))
fmt.Println("Priv:", hex.EncodeToString(newKey.priv)) fmt.Println("Priv:", hex.EncodeToString(newKey.priv))
fmt.Println("Pub:", hex.EncodeToString(newKey.pub)) fmt.Println("Pub:", hex.EncodeToString(newKey.pub))
addr := address.AddrForKey(newKey.pub) addr := address.AddrForKey(newKey.pub)

View File

@ -1,34 +1,27 @@
package main package main
import ( import (
"bytes"
"context" "context"
"crypto/ed25519" "crypto/ed25519"
"encoding/hex" "encoding/hex"
"encoding/json" "encoding/json"
"flag" "flag"
"fmt" "fmt"
"io"
"net" "net"
"os" "os"
"os/signal" "os/signal"
"regexp" "regexp"
"strings" "strings"
"sync"
"syscall" "syscall"
"golang.org/x/text/encoding/unicode"
"github.com/gologme/log" "github.com/gologme/log"
gsyslog "github.com/hashicorp/go-syslog" gsyslog "github.com/hashicorp/go-syslog"
"github.com/hjson/hjson-go" "github.com/hjson/hjson-go/v4"
"github.com/kardianos/minwinsvc" "github.com/kardianos/minwinsvc"
"github.com/mitchellh/mapstructure"
"github.com/yggdrasil-network/yggdrasil-go/src/address" "github.com/yggdrasil-network/yggdrasil-go/src/address"
"github.com/yggdrasil-network/yggdrasil-go/src/admin" "github.com/yggdrasil-network/yggdrasil-go/src/admin"
"github.com/yggdrasil-network/yggdrasil-go/src/config" "github.com/yggdrasil-network/yggdrasil-go/src/config"
"github.com/yggdrasil-network/yggdrasil-go/src/defaults"
"github.com/yggdrasil-network/yggdrasil-go/src/ipv6rwc" "github.com/yggdrasil-network/yggdrasil-go/src/ipv6rwc"
"github.com/yggdrasil-network/yggdrasil-go/src/core" "github.com/yggdrasil-network/yggdrasil-go/src/core"
@ -44,67 +37,85 @@ type node struct {
admin *admin.AdminSocket admin *admin.AdminSocket
} }
func readConfig(log *log.Logger, useconf bool, useconffile string, normaliseconf bool) *config.NodeConfig { // The main function is responsible for configuring and starting Yggdrasil.
// Use a configuration file. If -useconf, the configuration will be read func main() {
// from stdin. If -useconffile, the configuration will be read from the genconf := flag.Bool("genconf", false, "print a new config to stdout")
// filesystem. useconf := flag.Bool("useconf", false, "read HJSON/JSON config from stdin")
var conf []byte useconffile := flag.String("useconffile", "", "read HJSON/JSON config from specified file path")
var err error normaliseconf := flag.Bool("normaliseconf", false, "use in combination with either -useconf or -useconffile, outputs your configuration normalised")
if useconffile != "" { exportkey := flag.Bool("exportkey", false, "use in combination with either -useconf or -useconffile, outputs your private key in PEM format")
// Read the file from the filesystem confjson := flag.Bool("json", false, "print configuration from -genconf or -normaliseconf as JSON instead of HJSON")
conf, err = os.ReadFile(useconffile) autoconf := flag.Bool("autoconf", false, "automatic mode (dynamic IP, peer with IPv6 neighbors)")
} else { ver := flag.Bool("version", false, "prints the version of this build")
// Read the file from stdin. logto := flag.String("logto", "stdout", "file path to log to, \"syslog\" or \"stdout\"")
conf, err = io.ReadAll(os.Stdin) 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")
if err != nil { loglevel := flag.String("loglevel", "info", "loglevel to enable")
panic(err) flag.Parse()
}
// 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)
}
}
// 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)
}
// 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
}
// Generates a new configuration and returns it in HJSON format. This is used // Catch interrupts from the operating system to exit gracefully.
// with -genconf. ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
func doGenconf(isjson bool) string {
cfg := defaults.GenerateConfig() // Capture the service being stopped on Windows.
var bs []byte minwinsvc.SetOnExit(cancel)
// Create a new logger that logs output to stdout.
var logger *log.Logger
switch *logto {
case "stdout":
logger = log.New(os.Stdout, "", log.Flags())
case "syslog":
if syslogger, err := gsyslog.NewLogger(gsyslog.LOG_NOTICE, "DAEMON", version.BuildName()); err == nil {
logger = log.New(syslogger, "", log.Flags()&^(log.Ldate|log.Ltime))
}
default:
if logfd, err := os.OpenFile(*logto, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644); err == nil {
logger = log.New(logfd, "", log.Flags())
}
}
if logger == nil {
logger = log.New(os.Stdout, "", log.Flags())
logger.Warnln("Logging defaulting to stdout")
}
if *normaliseconf {
setLogLevel("error", logger)
} else {
setLogLevel(*loglevel, logger)
}
cfg := config.GenerateConfig()
var err error var err error
if isjson { 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, "", " ") bs, err = json.MarshalIndent(cfg, "", " ")
} else { } else {
bs, err = hjson.Marshal(cfg) bs, err = hjson.Marshal(cfg)
@ -112,7 +123,154 @@ func doGenconf(isjson bool) string {
if err != nil { if err != nil {
panic(err) panic(err)
} }
return string(bs) 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
}
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 *normaliseconf:
var bs []byte
if *confjson {
bs, err = json.MarshalIndent(cfg, "", " ")
} else {
bs, err = hjson.Marshal(cfg)
}
if err != nil {
panic(err)
}
fmt.Println(string(bs))
return
case *exportkey:
pem, err := cfg.MarshalPEMPrivateKey()
if err != nil {
panic(err)
}
fmt.Println(string(pem))
return
}
n := &node{}
// Setup the Yggdrasil node itself.
{
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(cfg.Certificate, logger, options...); err != nil {
panic(err)
}
address, subnet := n.core.Address(), n.core.Subnet()
logger.Infof("Your public key is %s", hex.EncodeToString(n.core.PublicKey()))
logger.Infof("Your IPv6 address is %s", address.String())
logger.Infof("Your IPv6 subnet is %s", subnet.String())
}
// Setup 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()
}
}
// 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: 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)
}
}
// 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)
}
}
// Block until we are told to shut down.
<-ctx.Done()
// Shut down the node.
_ = n.admin.Stop()
_ = n.multicast.Stop()
_ = n.tun.Stop()
n.core.Stop()
} }
func setLogLevel(loglevel string, logger *log.Logger) { func setLogLevel(loglevel string, logger *log.Logger) {
@ -140,269 +298,3 @@ 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.
fmt.Println("Usage:")
flag.PrintDefaults()
if args.getaddr || args.getsnet {
fmt.Println("\nError: You need to specify some config data using -useconf or -useconffile.")
}
}
// 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: uint8(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" "log"
"os" "os"
"github.com/hjson/hjson-go" "github.com/hjson/hjson-go/v4"
"golang.org/x/text/encoding/unicode" "golang.org/x/text/encoding/unicode"
"github.com/yggdrasil-network/yggdrasil-go/src/defaults" "github.com/yggdrasil-network/yggdrasil-go/src/config"
) )
type CmdLineEnv struct { type CmdLineEnv struct {
@ -21,7 +21,7 @@ type CmdLineEnv struct {
func newCmdLineEnv() CmdLineEnv { func newCmdLineEnv() CmdLineEnv {
var cmdLineEnv CmdLineEnv var cmdLineEnv CmdLineEnv
cmdLineEnv.endpoint = defaults.GetDefaults().DefaultAdminListen cmdLineEnv.endpoint = config.GetDefaults().DefaultAdminListen
return cmdLineEnv return cmdLineEnv
} }
@ -38,10 +38,9 @@ func (cmdLineEnv *CmdLineEnv) parseFlagsAndArgs() {
fmt.Println("Examples:") fmt.Println("Examples:")
fmt.Println(" - ", os.Args[0], "list") fmt.Println(" - ", os.Args[0], "list")
fmt.Println(" - ", os.Args[0], "getPeers") fmt.Println(" - ", os.Args[0], "getPeers")
fmt.Println(" - ", os.Args[0], "-v getSelf")
fmt.Println(" - ", os.Args[0], "setTunTap name=auto mtu=1500 tap_mode=false") fmt.Println(" - ", os.Args[0], "setTunTap name=auto mtu=1500 tap_mode=false")
fmt.Println(" - ", os.Args[0], "-endpoint=tcp://localhost:9001 getDHT") fmt.Println(" - ", os.Args[0], "-endpoint=tcp://localhost:9001 getPeers")
fmt.Println(" - ", os.Args[0], "-endpoint=unix:///var/run/ygg.sock getDHT") fmt.Println(" - ", os.Args[0], "-endpoint=unix:///var/run/ygg.sock getPeers")
} }
server := flag.String("endpoint", cmdLineEnv.endpoint, "Admin socket endpoint") server := flag.String("endpoint", cmdLineEnv.endpoint, "Admin socket endpoint")
@ -58,31 +57,31 @@ func (cmdLineEnv *CmdLineEnv) parseFlagsAndArgs() {
func (cmdLineEnv *CmdLineEnv) setEndpoint(logger *log.Logger) { func (cmdLineEnv *CmdLineEnv) setEndpoint(logger *log.Logger) {
if cmdLineEnv.server == cmdLineEnv.endpoint { if cmdLineEnv.server == cmdLineEnv.endpoint {
if config, err := os.ReadFile(defaults.GetDefaults().DefaultConfigFile); err == nil { if cfg, err := os.ReadFile(config.GetDefaults().DefaultConfigFile); err == nil {
if bytes.Equal(config[0:2], []byte{0xFF, 0xFE}) || if bytes.Equal(cfg[0:2], []byte{0xFF, 0xFE}) ||
bytes.Equal(config[0:2], []byte{0xFE, 0xFF}) { bytes.Equal(cfg[0:2], []byte{0xFE, 0xFF}) {
utf := unicode.UTF16(unicode.BigEndian, unicode.UseBOM) utf := unicode.UTF16(unicode.BigEndian, unicode.UseBOM)
decoder := utf.NewDecoder() decoder := utf.NewDecoder()
config, err = decoder.Bytes(config) cfg, err = decoder.Bytes(cfg)
if err != nil { if err != nil {
panic(err) panic(err)
} }
} }
var dat map[string]interface{} var dat map[string]interface{}
if err := hjson.Unmarshal(config, &dat); err != nil { if err := hjson.Unmarshal(cfg, &dat); err != nil {
panic(err) panic(err)
} }
if ep, ok := dat["AdminListen"].(string); ok && (ep != "none" && ep != "") { if ep, ok := dat["AdminListen"].(string); ok && (ep != "none" && ep != "") {
cmdLineEnv.endpoint = ep cmdLineEnv.endpoint = ep
logger.Println("Found platform default config file", defaults.GetDefaults().DefaultConfigFile) logger.Println("Found platform default config file", config.GetDefaults().DefaultConfigFile)
logger.Println("Using endpoint", cmdLineEnv.endpoint, "from AdminListen") logger.Println("Using endpoint", cmdLineEnv.endpoint, "from AdminListen")
} else { } else {
logger.Println("Configuration file doesn't contain appropriate AdminListen option") logger.Println("Configuration file doesn't contain appropriate AdminListen option")
logger.Println("Falling back to platform default", defaults.GetDefaults().DefaultAdminListen) logger.Println("Falling back to platform default", config.GetDefaults().DefaultAdminListen)
} }
} else { } else {
logger.Println("Can't open config file from default location", defaults.GetDefaults().DefaultConfigFile) logger.Println("Can't open config file from default location", config.GetDefaults().DefaultConfigFile)
logger.Println("Falling back to platform default", defaults.GetDefaults().DefaultAdminListen) logger.Println("Falling back to platform default", config.GetDefaults().DefaultAdminListen)
} }
} else { } else {
cmdLineEnv.endpoint = cmdLineEnv.server cmdLineEnv.endpoint = cmdLineEnv.server

View File

@ -165,7 +165,7 @@ func run() int {
table.Append([]string{"Build version:", resp.BuildVersion}) table.Append([]string{"Build version:", resp.BuildVersion})
table.Append([]string{"IPv6 address:", resp.IPAddress}) table.Append([]string{"IPv6 address:", resp.IPAddress})
table.Append([]string{"IPv6 subnet:", resp.Subnet}) table.Append([]string{"IPv6 subnet:", resp.Subnet})
table.Append([]string{"Coordinates:", fmt.Sprintf("%v", resp.Coords)}) table.Append([]string{"Routing table size:", fmt.Sprintf("%d", resp.RoutingEntries)})
table.Append([]string{"Public key:", resp.PublicKey}) table.Append([]string{"Public key:", resp.PublicKey})
table.Render() table.Render()
@ -174,33 +174,49 @@ func run() int {
if err := json.Unmarshal(recv.Response, &resp); err != nil { if err := json.Unmarshal(recv.Response, &resp); err != nil {
panic(err) panic(err)
} }
table.SetHeader([]string{"Port", "Public Key", "IP Address", "Uptime", "RX", "TX", "Pr", "URI"}) table.SetHeader([]string{"URI", "State", "Dir", "IP Address", "Uptime", "RX", "TX", "Pr", "Last Error"})
for _, peer := range resp.Peers { for _, peer := range resp.Peers {
state, lasterr, dir := "Up", "-", "Out"
if !peer.Up {
state, lasterr = "Down", fmt.Sprintf("%s ago: %s", peer.LastErrorTime.Round(time.Second), peer.LastError)
}
if peer.Inbound {
dir = "In"
}
uri, err := url.Parse(peer.URI)
if err != nil {
panic(err)
}
uri.RawQuery = ""
table.Append([]string{ table.Append([]string{
fmt.Sprintf("%d", peer.Port), uri.String(),
peer.PublicKey, state,
dir,
peer.IPAddress, peer.IPAddress,
(time.Duration(peer.Uptime) * time.Second).String(), (time.Duration(peer.Uptime) * time.Second).String(),
peer.RXBytes.String(), peer.RXBytes.String(),
peer.TXBytes.String(), peer.TXBytes.String(),
fmt.Sprintf("%d", peer.Priority), fmt.Sprintf("%d", peer.Priority),
peer.Remote, lasterr,
}) })
} }
table.Render() table.Render()
case "getdht": case "gettree":
var resp admin.GetDHTResponse var resp admin.GetTreeResponse
if err := json.Unmarshal(recv.Response, &resp); err != nil { if err := json.Unmarshal(recv.Response, &resp); err != nil {
panic(err) panic(err)
} }
table.SetHeader([]string{"Public Key", "IP Address", "Port", "Rest"}) //table.SetHeader([]string{"Public Key", "IP Address", "Port", "Rest"})
for _, dht := range resp.DHT { table.SetHeader([]string{"Public Key", "IP Address", "Parent", "Sequence"})
for _, tree := range resp.Tree {
table.Append([]string{ table.Append([]string{
dht.PublicKey, tree.PublicKey,
dht.IPAddress, tree.IPAddress,
fmt.Sprintf("%d", dht.Port), tree.Parent,
fmt.Sprintf("%d", dht.Rest), fmt.Sprintf("%d", tree.Sequence),
//fmt.Sprintf("%d", dht.Port),
//fmt.Sprintf("%d", dht.Rest),
}) })
} }
table.Render() table.Render()
@ -210,12 +226,13 @@ func run() int {
if err := json.Unmarshal(recv.Response, &resp); err != nil { if err := json.Unmarshal(recv.Response, &resp); err != nil {
panic(err) panic(err)
} }
table.SetHeader([]string{"Public Key", "IP Address", "Path"}) table.SetHeader([]string{"Public Key", "IP Address", "Path", "Seq"})
for _, p := range resp.Paths { for _, p := range resp.Paths {
table.Append([]string{ table.Append([]string{
p.PublicKey, p.PublicKey,
p.IPAddress, p.IPAddress,
fmt.Sprintf("%v", p.Path), fmt.Sprintf("%v", p.Path),
fmt.Sprintf("%d", p.Sequence),
}) })
} }
table.Render() table.Render()

BIN
contrib/.DS_Store vendored Normal file

Binary file not shown.

View File

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

View File

@ -21,6 +21,9 @@ if [ $PKGBRANCH = "master" ]; then
PKGREPLACES=yggdrasil-develop PKGREPLACES=yggdrasil-develop
fi fi
export LDFLAGS="-X github.com/yggdrasil-network/yggdrasil-go/src/config.defaultConfig=/etc/yggdrasil/yggdrasil.conf"
export LDFLAGS="${LDFLAGS} -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 if [ $PKGARCH = "amd64" ]; then GOARCH=amd64 GOOS=linux ./build
elif [ $PKGARCH = "i386" ]; then GOARCH=386 GOOS=linux ./build elif [ $PKGARCH = "i386" ]; then GOARCH=386 GOOS=linux ./build
elif [ $PKGARCH = "mipsel" ]; then GOARCH=mipsle GOOS=linux ./build elif [ $PKGARCH = "mipsel" ]; then GOARCH=mipsle GOOS=linux ./build
@ -38,7 +41,7 @@ echo "Building $PKGFILE"
mkdir -p /tmp/$PKGNAME/ mkdir -p /tmp/$PKGNAME/
mkdir -p /tmp/$PKGNAME/debian/ mkdir -p /tmp/$PKGNAME/debian/
mkdir -p /tmp/$PKGNAME/usr/bin/ mkdir -p /tmp/$PKGNAME/usr/bin/
mkdir -p /tmp/$PKGNAME/etc/systemd/system/ mkdir -p /tmp/$PKGNAME/usr/lib/systemd/system/
cat > /tmp/$PKGNAME/debian/changelog << EOF cat > /tmp/$PKGNAME/debian/changelog << EOF
Please see https://github.com/yggdrasil-network/yggdrasil-go/ Please see https://github.com/yggdrasil-network/yggdrasil-go/
@ -68,35 +71,52 @@ EOF
cat > /tmp/$PKGNAME/debian/install << EOF cat > /tmp/$PKGNAME/debian/install << EOF
usr/bin/yggdrasil usr/bin usr/bin/yggdrasil usr/bin
usr/bin/yggdrasilctl usr/bin usr/bin/yggdrasilctl usr/bin
etc/systemd/system/*.service etc/systemd/system usr/lib/systemd/system/*.service usr/lib/systemd/system
EOF EOF
cat > /tmp/$PKGNAME/debian/postinst << EOF cat > /tmp/$PKGNAME/debian/postinst << EOF
#!/bin/sh #!/bin/sh
systemctl daemon-reload
if ! getent group yggdrasil 2>&1 > /dev/null; then if ! getent group yggdrasil 2>&1 > /dev/null; then
groupadd --system --force yggdrasil || echo "Failed to create group 'yggdrasil' - please create it manually and reinstall" groupadd --system --force yggdrasil
fi fi
if [ -f /etc/yggdrasil.conf ]; 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 ];
then then
mkdir -p /var/backups mkdir -p /var/backups
echo "Backing up configuration file to /var/backups/yggdrasil.conf.`date +%Y%m%d`" echo "Backing up configuration file to /var/backups/yggdrasil.conf.`date +%Y%m%d`"
cp /etc/yggdrasil.conf /var/backups/yggdrasil.conf.`date +%Y%m%d` cp /etc/yggdrasil/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
if command -v systemctl >/dev/null; then echo "Normalising and updating /etc/yggdrasil/yggdrasil.conf"
systemctl daemon-reload >/dev/null || true /usr/bin/yggdrasil -useconf -normaliseconf < /var/backups/yggdrasil.conf.`date +%Y%m%d` > /etc/yggdrasil/yggdrasil.conf
systemctl enable yggdrasil || true
systemctl start yggdrasil || true chown root:yggdrasil /etc/yggdrasil/yggdrasil.conf
fi chmod 640 /etc/yggdrasil/yggdrasil.conf
else else
echo "Generating initial configuration file /etc/yggdrasil.conf" echo "Generating initial configuration file /etc/yggdrasil/yggdrasil.conf"
echo "Please familiarise yourself with this file before starting Yggdrasil" /usr/bin/yggdrasil -genconf > /etc/yggdrasil/yggdrasil.conf
sh -c 'umask 0027 && /usr/bin/yggdrasil -genconf > /etc/yggdrasil.conf'
chgrp yggdrasil /etc/yggdrasil.conf chown root:yggdrasil /etc/yggdrasil/yggdrasil.conf
chmod 640 /etc/yggdrasil/yggdrasil.conf
fi fi
systemctl enable yggdrasil
systemctl restart yggdrasil
exit 0
EOF EOF
cat > /tmp/$PKGNAME/debian/prerm << EOF cat > /tmp/$PKGNAME/debian/prerm << EOF
#!/bin/sh #!/bin/sh
@ -110,13 +130,14 @@ EOF
cp yggdrasil /tmp/$PKGNAME/usr/bin/ cp yggdrasil /tmp/$PKGNAME/usr/bin/
cp yggdrasilctl /tmp/$PKGNAME/usr/bin/ cp yggdrasilctl /tmp/$PKGNAME/usr/bin/
cp contrib/systemd/*.service /tmp/$PKGNAME/etc/systemd/system/ cp contrib/systemd/yggdrasil-default-config.service.debian /tmp/$PKGNAME/usr/lib/systemd/system/yggdrasil-default-config.service
cp contrib/systemd/yggdrasil.service.debian /tmp/$PKGNAME/usr/lib/systemd/system/yggdrasil.service
tar -czvf /tmp/$PKGNAME/data.tar.gz -C /tmp/$PKGNAME/ \ tar --no-xattrs -czvf /tmp/$PKGNAME/data.tar.gz -C /tmp/$PKGNAME/ \
usr/bin/yggdrasil usr/bin/yggdrasilctl \ usr/bin/yggdrasil usr/bin/yggdrasilctl \
etc/systemd/system/yggdrasil.service \ usr/lib/systemd/system/yggdrasil.service \
etc/systemd/system/yggdrasil-default-config.service usr/lib/systemd/system/yggdrasil-default-config.service
tar -czvf /tmp/$PKGNAME/control.tar.gz -C /tmp/$PKGNAME/debian . tar --no-xattrs -czvf /tmp/$PKGNAME/control.tar.gz -C /tmp/$PKGNAME/debian .
echo 2.0 > /tmp/$PKGNAME/debian-binary echo 2.0 > /tmp/$PKGNAME/debian-binary
ar -r $PKGFILE \ ar -r $PKGFILE \

View File

@ -37,7 +37,7 @@ if [ $IOS ]; then
echo "Building framework for iOS" echo "Building framework for iOS"
go get golang.org/x/mobile/bind go get golang.org/x/mobile/bind
gomobile bind \ gomobile bind \
-target ios -tags mobile -o Yggdrasil.xcframework \ -target ios,macos -tags mobile -o Yggdrasil.xcframework \
-ldflags="$LDFLAGS $STRIP" -gcflags="$GCFLAGS" \ -ldflags="$LDFLAGS $STRIP" -gcflags="$GCFLAGS" \
./contrib/mobile ./src/config; ./contrib/mobile ./src/config;
fi fi

View File

@ -3,7 +3,6 @@ package mobile
import ( import (
"encoding/hex" "encoding/hex"
"encoding/json" "encoding/json"
"fmt"
"net" "net"
"regexp" "regexp"
@ -12,9 +11,9 @@ import (
"github.com/yggdrasil-network/yggdrasil-go/src/address" "github.com/yggdrasil-network/yggdrasil-go/src/address"
"github.com/yggdrasil-network/yggdrasil-go/src/config" "github.com/yggdrasil-network/yggdrasil-go/src/config"
"github.com/yggdrasil-network/yggdrasil-go/src/core" "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/ipv6rwc"
"github.com/yggdrasil-network/yggdrasil-go/src/multicast" "github.com/yggdrasil-network/yggdrasil-go/src/multicast"
"github.com/yggdrasil-network/yggdrasil-go/src/tun"
"github.com/yggdrasil-network/yggdrasil-go/src/version" "github.com/yggdrasil-network/yggdrasil-go/src/version"
_ "golang.org/x/mobile/bind" _ "golang.org/x/mobile/bind"
@ -30,7 +29,9 @@ type Yggdrasil struct {
iprwc *ipv6rwc.ReadWriteCloser iprwc *ipv6rwc.ReadWriteCloser
config *config.NodeConfig config *config.NodeConfig
multicast *multicast.Multicast multicast *multicast.Multicast
tun *tun.TunAdapter // optional
log MobileLogger log MobileLogger
logger *log.Logger
} }
// StartAutoconfigure starts a node with a randomly generated config // StartAutoconfigure starts a node with a randomly generated config
@ -41,20 +42,19 @@ func (m *Yggdrasil) StartAutoconfigure() error {
// StartJSON starts a node with the given JSON config. You can get JSON config // StartJSON starts a node with the given JSON config. You can get JSON config
// (rather than HJSON) by using the GenerateConfigJSON() function // (rather than HJSON) by using the GenerateConfigJSON() function
func (m *Yggdrasil) StartJSON(configjson []byte) error { func (m *Yggdrasil) StartJSON(configjson []byte) error {
setMemLimitIfPossible()
logger := log.New(m.log, "", 0) logger := log.New(m.log, "", 0)
logger.EnableLevel("error") logger.EnableLevel("error")
logger.EnableLevel("warn") logger.EnableLevel("warn")
logger.EnableLevel("info") logger.EnableLevel("info")
m.config = defaults.GenerateConfig() m.logger = logger
if err := json.Unmarshal(configjson, &m.config); err != nil { m.config = config.GenerateConfig()
if err := m.config.UnmarshalHJSON(configjson); err != nil {
return err return err
} }
// Setup the Yggdrasil node itself. // Setup the Yggdrasil node itself.
{ {
sk, err := hex.DecodeString(m.config.PrivateKey)
if err != nil {
panic(err)
}
options := []core.SetupOption{} options := []core.SetupOption{}
for _, peer := range m.config.Peers { for _, peer := range m.config.Peers {
options = append(options, core.Peer{URI: peer}) options = append(options, core.Peer{URI: peer})
@ -71,15 +71,24 @@ func (m *Yggdrasil) StartJSON(configjson []byte) error {
} }
options = append(options, core.AllowedPublicKey(k[:])) options = append(options, core.AllowedPublicKey(k[:]))
} }
m.core, err = core.New(sk[:], logger, options...) for _, lAddr := range m.config.Listen {
options = append(options, core.ListenAddress(lAddr))
}
var err error
m.core, err = core.New(m.config.Certificate, logger, options...)
if err != nil { if err != nil {
panic(err) 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())
} }
// Setup the multicast module. // Setup the multicast module.
if len(m.config.MulticastInterfaces) > 0 { if len(m.config.MulticastInterfaces) > 0 {
var err error var err error
logger.Infof("Initializing multicast %s", "")
options := []multicast.SetupOption{} options := []multicast.SetupOption{}
for _, intf := range m.config.MulticastInterfaces { for _, intf := range m.config.MulticastInterfaces {
options = append(options, multicast.MulticastInterface{ options = append(options, multicast.MulticastInterface{
@ -88,9 +97,11 @@ func (m *Yggdrasil) StartJSON(configjson []byte) error {
Listen: intf.Listen, Listen: intf.Listen,
Port: intf.Port, Port: intf.Port,
Priority: uint8(intf.Priority), Priority: uint8(intf.Priority),
Password: intf.Password,
}) })
} }
m.multicast, err = multicast.New(m.core, logger, options...) logger.Infof("Starting multicast %s", "")
m.multicast, err = multicast.New(m.core, m.logger, options...)
if err != nil { if err != nil {
logger.Errorln("An error occurred starting multicast:", err) logger.Errorln("An error occurred starting multicast:", err)
} }
@ -151,10 +162,20 @@ func (m *Yggdrasil) RecvBuffer(buf []byte) (int, error) {
func (m *Yggdrasil) Stop() error { func (m *Yggdrasil) Stop() error {
logger := log.New(m.log, "", 0) logger := log.New(m.log, "", 0)
logger.EnableLevel("info") logger.EnableLevel("info")
logger.Infof("Stop the mobile Yggdrasil instance %s", "") logger.Infof("Stopping the mobile Yggdrasil instance %s", "")
if m.multicast != nil {
logger.Infof("Stopping multicast %s", "")
if err := m.multicast.Stop(); err != nil { if err := m.multicast.Stop(); err != nil {
return err 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() m.core.Stop()
return nil return nil
} }
@ -166,7 +187,7 @@ func (m *Yggdrasil) RetryPeersNow() {
// GenerateConfigJSON generates mobile-friendly configuration in JSON format // GenerateConfigJSON generates mobile-friendly configuration in JSON format
func GenerateConfigJSON() []byte { func GenerateConfigJSON() []byte {
nc := defaults.GenerateConfig() nc := config.GenerateConfig()
nc.IfName = "none" nc.IfName = "none"
if json, err := json.Marshal(nc); err == nil { if json, err := json.Marshal(nc); err == nil {
return json return json
@ -191,9 +212,9 @@ func (m *Yggdrasil) GetPublicKeyString() string {
return hex.EncodeToString(m.core.GetSelf().Key) return hex.EncodeToString(m.core.GetSelf().Key)
} }
// GetCoordsString gets the node's coordinates // GetRoutingEntries gets the number of entries in the routing table
func (m *Yggdrasil) GetCoordsString() string { func (m *Yggdrasil) GetRoutingEntries() int {
return fmt.Sprintf("%v", m.core.GetSelf().Coords) return int(m.core.GetSelf().RoutingEntries)
} }
func (m *Yggdrasil) GetPeersJSON() (result string) { func (m *Yggdrasil) GetPeersJSON() (result string) {
@ -202,8 +223,11 @@ func (m *Yggdrasil) GetPeersJSON() (result string) {
IP string IP string
}{} }{}
for _, v := range m.core.GetPeers() { for _, v := range m.core.GetPeers() {
var ip string
if v.Key != nil {
a := address.AddrForKey(v.Key) a := address.AddrForKey(v.Key)
ip := net.IP(a[:]).String() ip = net.IP(a[:]).String()
}
peers = append(peers, struct { peers = append(peers, struct {
core.PeerInfo core.PeerInfo
IP string IP string
@ -219,8 +243,16 @@ func (m *Yggdrasil) GetPeersJSON() (result string) {
} }
} }
func (m *Yggdrasil) GetDHTJSON() (result string) { func (m *Yggdrasil) GetPathsJSON() (result string) {
if res, err := json.Marshal(m.core.GetDHT()); err == nil { if res, err := json.Marshal(m.core.GetPaths()); err == nil {
return string(res)
} else {
return "{}"
}
}
func (m *Yggdrasil) GetTreeJSON() (result string) {
if res, err := json.Marshal(m.core.GetTree()); err == nil {
return string(res) return string(res)
} else { } else {
return "{}" return "{}"

View File

@ -15,6 +15,8 @@ void Log(const char *text) {
import "C" import "C"
import ( import (
"unsafe" "unsafe"
"github.com/yggdrasil-network/yggdrasil-go/src/tun"
) )
type MobileLogger struct { type MobileLogger struct {
@ -26,3 +28,13 @@ func (nsl MobileLogger) Write(p []byte) (n int, err error) {
C.Log(cstr) C.Log(cstr)
return len(p), nil 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

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

View File

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

View File

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

View File

@ -0,0 +1,13 @@
[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

@ -0,0 +1,25 @@
[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

49
go.mod
View File

@ -1,38 +1,47 @@
module github.com/yggdrasil-network/yggdrasil-go module github.com/yggdrasil-network/yggdrasil-go
go 1.17 go 1.20
require ( require (
github.com/Arceliar/ironwood v0.0.0-20221115123222-ec61cea2f439 github.com/Arceliar/ironwood v0.0.0-20231028101932-ceac99571f43
github.com/Arceliar/phony v0.0.0-20210209235338-dde1a8dca979 github.com/Arceliar/phony v0.0.0-20220903101357-530938a4b13d
github.com/cheggaaa/pb/v3 v3.0.8 github.com/cheggaaa/pb/v3 v3.1.4
github.com/gologme/log v1.2.0 github.com/gologme/log v1.3.0
github.com/hashicorp/go-syslog v1.0.0 github.com/hashicorp/go-syslog v1.0.0
github.com/hjson/hjson-go v3.1.0+incompatible github.com/hjson/hjson-go/v4 v4.3.0
github.com/kardianos/minwinsvc v1.0.2 github.com/kardianos/minwinsvc v1.0.2
github.com/mitchellh/mapstructure v1.4.1 github.com/quic-go/quic-go v0.39.3
github.com/vishvananda/netlink v1.1.0 github.com/vishvananda/netlink v1.1.0
golang.org/x/mobile v0.0.0-20221110043201-43a038452099 golang.org/x/crypto v0.14.0
golang.org/x/net v0.0.0-20221014081412-f15817d10f9b golang.org/x/mobile v0.0.0-20231006135142-2b44d11868fe
golang.org/x/sys v0.0.0-20221013171732-95e765b1cc43 golang.org/x/net v0.17.0
golang.org/x/text v0.3.8 golang.org/x/sys v0.13.0
golang.zx2c4.com/wireguard v0.0.0-20211017052713-f87e87af0d9a golang.org/x/text v0.13.0
golang.zx2c4.com/wireguard/windows v0.4.12 golang.zx2c4.com/wireguard v0.0.0-20230223181233-21636207a675
golang.zx2c4.com/wireguard/windows v0.5.3
) )
require ( require (
github.com/mattn/go-colorable v0.1.8 // indirect github.com/bits-and-blooms/bitset v1.5.0 // indirect
github.com/bits-and-blooms/bloom/v3 v3.3.1 // 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/quic-go/qtls-go1-20 v0.3.4 // indirect
github.com/rivo/uniseg v0.2.0 // indirect github.com/rivo/uniseg v0.2.0 // indirect
golang.org/x/crypto v0.0.0-20221012134737-56aed061732a // indirect go.uber.org/mock v0.3.0 // indirect
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4 // indirect golang.org/x/exp v0.0.0-20221205204356-47842c84f3db // indirect
golang.org/x/tools v0.1.12 // indirect golang.org/x/mod v0.13.0 // indirect
golang.org/x/tools v0.14.0 // indirect
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 // indirect
) )
require ( require (
github.com/VividCortex/ewma v1.2.0 // indirect github.com/VividCortex/ewma v1.2.0 // indirect
github.com/fatih/color v1.12.0 // indirect github.com/fatih/color v1.15.0 // indirect
github.com/mattn/go-isatty v0.0.13 // indirect github.com/mattn/go-isatty v0.0.19 // indirect
github.com/mattn/go-runewidth v0.0.13 // indirect github.com/mattn/go-runewidth v0.0.14 // indirect
github.com/olekukonko/tablewriter v0.0.5 github.com/olekukonko/tablewriter v0.0.5
github.com/vishvananda/netns v0.0.0-20210104183010-2eb08e3e575f // indirect github.com/vishvananda/netns v0.0.0-20210104183010-2eb08e3e575f // indirect
) )

175
go.sum
View File

@ -1,120 +1,137 @@
github.com/Arceliar/ironwood v0.0.0-20221115123222-ec61cea2f439 h1:eOW6/XIs06TnUn9GPCnfv71CQZw8edP3u3mH3lZt6iM= github.com/Arceliar/ironwood v0.0.0-20231028101932-ceac99571f43 h1:M4NczBPk7Fy0Uy2YvNoXwSkk3dGoGTOYtUjyqpOC5ko=
github.com/Arceliar/ironwood v0.0.0-20221115123222-ec61cea2f439/go.mod h1:RP72rucOFm5udrnEzTmIWLRVGQiV/fSUAQXJ0RST/nk= github.com/Arceliar/ironwood v0.0.0-20231028101932-ceac99571f43/go.mod h1:5x7fWW0mshe9WQ1lvSMmmHBYC3BeHH9gpwW5tz7cbfw=
github.com/Arceliar/phony v0.0.0-20210209235338-dde1a8dca979 h1:WndgpSW13S32VLQ3ugUxx2EnnWmgba1kCqPkd4Gk1yQ= github.com/Arceliar/phony v0.0.0-20220903101357-530938a4b13d h1:UK9fsWbWqwIQkMCz1CP+v5pGbsGoWAw6g4AyvMpm1EM=
github.com/Arceliar/phony v0.0.0-20210209235338-dde1a8dca979/go.mod h1:6Lkn+/zJilRMsKmbmG1RPoamiArC6HS73xbwRyp3UyI= github.com/Arceliar/phony v0.0.0-20220903101357-530938a4b13d/go.mod h1:BCnxhRf47C/dy/e/D2pmB8NkB3dQVIrkD98b220rx5Q=
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 h1:f58SaIzcDXrSy3kWaHNvuJgJ3Nmz59Zji6XoJR/q1ow=
github.com/VividCortex/ewma v1.2.0/go.mod h1:nz4BbCtbLyFDeC9SUHbtcT5644juEuWfUAUnGx7j5l4= github.com/VividCortex/ewma v1.2.0/go.mod h1:nz4BbCtbLyFDeC9SUHbtcT5644juEuWfUAUnGx7j5l4=
github.com/cheggaaa/pb/v3 v3.0.8 h1:bC8oemdChbke2FHIIGy9mn4DPJ2caZYQnfbRqwmdCoA= github.com/bits-and-blooms/bitset v1.3.1/go.mod h1:gIdJ4wp64HaoK2YrL1Q5/N7Y16edYb8uY+O0FJTyyDA=
github.com/cheggaaa/pb/v3 v3.0.8/go.mod h1:UICbiLec/XO6Hw6k+BHEtHeQFzzBH4i2/qk/ow1EJTA= github.com/bits-and-blooms/bitset v1.5.0 h1:NpE8frKRLGHIcEzkR+gZhiioW1+WbYV6fKwD6ZIpQT8=
github.com/fatih/color v1.10.0/go.mod h1:ELkj/draVOlAH/xkhN6mQ50Qd0MPOk5AAr3maGEBuJM= github.com/bits-and-blooms/bitset v1.5.0/go.mod h1:gIdJ4wp64HaoK2YrL1Q5/N7Y16edYb8uY+O0FJTyyDA=
github.com/fatih/color v1.12.0 h1:mRhaKNwANqRgUBGKmnI5ZxEk7QXmjQeCcuYFMX2bfcc= github.com/bits-and-blooms/bloom/v3 v3.3.1 h1:K2+A19bXT8gJR5mU7y+1yW6hsKfNCjcP2uNfLFKncjQ=
github.com/fatih/color v1.12.0/go.mod h1:ELkj/draVOlAH/xkhN6mQ50Qd0MPOk5AAr3maGEBuJM= github.com/bits-and-blooms/bloom/v3 v3.3.1/go.mod h1:bhUUknWd5khVbTe4UgMCSiOOVJzr3tMoijSK3WwvW90=
github.com/gologme/log v1.2.0 h1:Ya5Ip/KD6FX7uH0S31QO87nCCSucKtF44TLbTtO7V4c= github.com/cheggaaa/pb/v3 v3.1.4 h1:DN8j4TVVdKu3WxVwcRKu0sG00IIU6FewoABZzXbRQeo=
github.com/gologme/log v1.2.0/go.mod h1:gq31gQ8wEHkR+WekdWsqDuf8pXTUZA9BnnzTuPz1Y9U= github.com/cheggaaa/pb/v3 v3.1.4/go.mod h1:6wVjILNBaXMs8c21qRiaUM8BR82erfgau1DQ4iUXmSA=
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/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.15.0 h1:kOqh6YHBtK8aywxGerMG2Eq3H6Qgoqeo13Bk2Mv/nBs=
github.com/fatih/color v1.15.0/go.mod h1:0h5ZqXfHYED7Bhv2ZJamyIOUej9KtShiJESRwBDUSsw=
github.com/go-logr/logr v1.2.4 h1:g01GSCwiDw2xSZfjJ2/T9M+S6pFdcNtFYsp+Y43HYDQ=
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/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/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38 h1:yAJXTCF9TqKcTiHJAE8dj7HMvPfh66eeA2JYW7eFpSE=
github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/hashicorp/go-syslog v1.0.0 h1:KaodqZuhUoZereWVIYmpUgZysurB1kBLX2j0MwMrUAE= github.com/hashicorp/go-syslog v1.0.0 h1:KaodqZuhUoZereWVIYmpUgZysurB1kBLX2j0MwMrUAE=
github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4= github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4=
github.com/hjson/hjson-go v3.1.0+incompatible h1:DY/9yE8ey8Zv22bY+mHV1uk2yRy0h8tKhZ77hEdi0Aw= github.com/hjson/hjson-go/v4 v4.3.0 h1:dyrzJdqqFGhHt+FSrs5n9s6b0fPM8oSJdWo+oS3YnJw=
github.com/hjson/hjson-go v3.1.0+incompatible/go.mod h1:qsetwF8NlsTsOTwZTApNlTCerV+b2GjYRRcIk4JMFio= github.com/hjson/hjson-go/v4 v4.3.0/go.mod h1:KaYt3bTw3zhBjYqnXkYywcYctk0A2nxeEFTse3rH13E=
github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
github.com/kardianos/minwinsvc v1.0.2 h1:JmZKFJQrmTGa/WiW+vkJXKmfzdjabuEW4Tirj5lLdR0= github.com/kardianos/minwinsvc v1.0.2 h1:JmZKFJQrmTGa/WiW+vkJXKmfzdjabuEW4Tirj5lLdR0=
github.com/kardianos/minwinsvc v1.0.2/go.mod h1:LUZNYhNmxujx2tR7FbdxqYJ9XDDoCd3MQcl1o//FWl4= github.com/kardianos/minwinsvc v1.0.2/go.mod h1:LUZNYhNmxujx2tR7FbdxqYJ9XDDoCd3MQcl1o//FWl4=
github.com/lxn/walk v0.0.0-20210112085537-c389da54e794/go.mod h1:E23UucZGqpuUANJooIbHWCufXvOcT6E7Stq81gU+CSQ= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/lxn/win v0.0.0-20210218163916-a377121e959e/go.mod h1:KxxjdtRkfNoYDCUP5ryK7XJJNTnpC8atvtmTheChOtk= github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-colorable v0.1.8 h1:c1ghPdyEDarC70ftn0y+A/Ee++9zz8ljHG1b13eJ0s8= github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-colorable v0.1.8/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA=
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
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.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk= github.com/mattn/go-runewidth v0.0.14 h1:+xnbZSEeDbOIg5/mE6JF0w6n9duR1l3/WmbinWVwUuU=
github.com/mattn/go-runewidth v0.0.13 h1:lTGmDsbAYt5DmK6OnoV7EuIF1wEIFAcxld6ypU4OSgU= github.com/mattn/go-runewidth v0.0.14/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
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 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec=
github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY= github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY=
github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= 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/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/qtls-go1-20 v0.3.4 h1:MfFAPULvst4yoMgY9QmtpYmfij/em7O8UUi+bNVm7Cg=
github.com/quic-go/qtls-go1-20 v0.3.4/go.mod h1:X9Nh97ZL80Z+bX/gUXMbipO6OxdiDi58b/fMC9mAL+k=
github.com/quic-go/quic-go v0.39.3 h1:o3YB6t2SR+HU/pgwF29kJ6g4jJIJEwEZ8CKia1h1TKg=
github.com/quic-go/quic-go v0.39.3/go.mod h1:T09QsDQWjLiQ74ZmacDfqZmhY/NLnw5BC40MANNNZ1Q=
github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
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.1.0 h1:1iyaYNBLmP6L0220aDnYQpo1QEV4t4hJ+xEEhhJH8j0= github.com/vishvananda/netlink v1.1.0 h1:1iyaYNBLmP6L0220aDnYQpo1QEV4t4hJ+xEEhhJH8j0=
github.com/vishvananda/netlink v1.1.0/go.mod h1:cTgwzPIzzgDAYoQrMm0EdrjRUBkTqKYppBueQtXaqoE= 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-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 h1:p4VB7kIXpOQvVn1ZaTIVp+3vuYAXFe3OJEvjbUYJLaA=
github.com/vishvananda/netns v0.0.0-20210104183010-2eb08e3e575f/go.mod h1:DD4vA1DwXk04H54A1oHXtwZmA0grkVMdPxx/VGLCah0= 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= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
go.uber.org/mock v0.3.0 h1:3mUxI1No2/60yUYax92Pt8eNOEecx2D3lcXZh2NEZJo=
go.uber.org/mock v0.3.0/go.mod h1:a6FSlNadKUHUa9IP5Vyt1zh4fC7uAwxMutEAscFbkZc=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 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-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.8.0/go.mod h1:mRqEX+O9/h5TFCrQhkgjo2yKi0yYA+9ecGkdQoHrywE=
golang.org/x/crypto v0.0.0-20221012134737-56aed061732a/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.14.0 h1:wBqGXzWJW6m1XrIKlAH0Hs1JJ7+9KBwnIO8v66Q9cHc=
golang.org/x/exp v0.0.0-20190731235908-ec7cb31e5a56/go.mod h1:JhuoJpWY28nO4Vef9tZUw9qufEGTyX1+7lmHxV5q5G4= golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4=
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= golang.org/x/exp v0.0.0-20221205204356-47842c84f3db h1:D/cFflL63o2KSLJIwjlcIt8PR064j/xsmdEJL/YvY/o=
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/exp v0.0.0-20221205204356-47842c84f3db/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc=
golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= golang.org/x/mobile v0.0.0-20231006135142-2b44d11868fe h1:lrXv4yHeD9FA8PSJATWowP1QvexpyAPWmPia+Kbzql8=
golang.org/x/mobile v0.0.0-20221110043201-43a038452099 h1:aIu0lKmfdgtn2uTj7JI2oN4TUrQvgB+wzTPO23bCKt8= golang.org/x/mobile v0.0.0-20231006135142-2b44d11868fe/go.mod h1:BrnXpEObnFxpaT75Jo9hsCazwOWcp7nVIa8NNuH5cuA=
golang.org/x/mobile v0.0.0-20221110043201-43a038452099/go.mod h1:aAjjkJNdrh3PMckS4B10TGS2nag27cbKR1y2BpUxsiY=
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/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/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/mod v0.13.0 h1:I/DsJXRlw/8l/0c24sM9yb0T4z9liZTduXvdAWYiysY=
golang.org/x/mod v0.13.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 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-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-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.0.0-20221014081412-f15817d10f9b h1:tvrvnPFcdzp294diPnrdZZZ8XUt2Tyj7svb7X52iDuU= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.0.0-20221014081412-f15817d10f9b/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk= golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns=
golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM=
golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 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-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/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.4.0 h1:zxkM55ReGkDlKSM+Fu41A+zmbZuaPVbGMzvvdUPznYQ=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 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-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-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200217220822-9197077df867/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-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-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-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-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-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20221013171732-95e765b1cc43 h1:OK7RB6t2WQX54srQQYSXMW8dF5C6/8+oA/s5QBmmto4= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20221013171732-95e765b1cc43/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE=
golang.org/x/sys v0.13.0/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-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 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.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.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.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.3.8 h1:nAL+RVCQ9uMn3vJZbV+MRnydTJFPf8qqY42YiA6MrqY= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k=
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 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.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.12 h1:VveCTK38A2rkS8ZqFY25HIDFscX5X9OoEhJd3quQmXU=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/tools v0.14.0 h1:jvNa2pY0M4r62jkRQ6RwEZZyPcymeL9XZMLBbV7U2nc=
golang.org/x/tools v0.14.0/go.mod h1:uYBEerGOWcJyEORxN+Ek8+TT266gXkNlHdJBwexUsBg=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 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.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 h1:B82qJJgjvYKsXS9jeunTOisW56dUokqW/FOteYJJ/yg=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2/go.mod h1:deeaetjYA+DHMHg+sMSMI58GrEteJUUzzw7en6TJQcI=
golang.zx2c4.com/wireguard v0.0.0-20211012062646-82d2aa87aa62/go.mod h1:id8Oh3eCCmpj9uVGWVjsUAl6UPX5ysMLzu6QxJU2UOU= golang.zx2c4.com/wireguard v0.0.0-20230223181233-21636207a675 h1:/J/RVnr7ng4fWPRH3xa4WtBJ1Jp+Auu4YNLmGiPv5QU=
golang.zx2c4.com/wireguard v0.0.0-20211017052713-f87e87af0d9a h1:tTbyylK9/D3u/wEP26Vx7L700UpY48nhioJWZM1vhZw= golang.zx2c4.com/wireguard v0.0.0-20230223181233-21636207a675/go.mod h1:whfbyDBt09xhCYQWtO2+3UVjlaq6/9hDZrjg2ZE6SyA=
golang.zx2c4.com/wireguard v0.0.0-20211017052713-f87e87af0d9a/go.mod h1:id8Oh3eCCmpj9uVGWVjsUAl6UPX5ysMLzu6QxJU2UOU= golang.zx2c4.com/wireguard/windows v0.5.3 h1:On6j2Rpn3OEMXqBq00QEDC7bWSZrPIHKIus8eIuExIE=
golang.zx2c4.com/wireguard/windows v0.4.12 h1:CUmbdWKVNzTSsVb4yUAiEwL3KsabdJkEPdDjCHxBlhA= golang.zx2c4.com/wireguard/windows v0.5.3/go.mod h1:9TEe8TJmtwyQebdFwAkEWOPr3prrtqm+REGFifP60hI=
golang.zx2c4.com/wireguard/windows v0.4.12/go.mod h1:PW4y+d9oY83XU9rRwRwrJDwEMuhVjMxu2gfD1cfzS7w= google.golang.org/protobuf v1.28.0 h1:w43yiav+6bVFTBQFZX0r7ipe9JQ1QsbMgHwbBziscLw=
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=

View File

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

View File

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

View File

@ -37,7 +37,7 @@ type AdminSocketRequest struct {
type AdminSocketResponse struct { type AdminSocketResponse struct {
Status string `json:"status"` Status string `json:"status"`
Error string `json:"error,omitempty"` Error string `json:"error,omitempty"`
Request json.RawMessage `json:"request"` Request AdminSocketRequest `json:"request"`
Response json.RawMessage `json:"response"` Response json.RawMessage `json:"response"`
} }
@ -132,14 +132,14 @@ func (a *AdminSocket) SetupAdminHandlers() {
}, },
) )
_ = a.AddHandler( _ = a.AddHandler(
"getDHT", "Show known DHT entries", []string{}, "getTree", "Show known Tree entries", []string{},
func(in json.RawMessage) (interface{}, error) { func(in json.RawMessage) (interface{}, error) {
req := &GetDHTRequest{} req := &GetTreeRequest{}
res := &GetDHTResponse{} res := &GetTreeResponse{}
if err := json.Unmarshal(in, &req); err != nil { if err := json.Unmarshal(in, &req); err != nil {
return nil, err return nil, err
} }
if err := a.getDHTHandler(req, res); err != nil { if err := a.getTreeHandler(req, res); err != nil {
return nil, err return nil, err
} }
return res, nil return res, nil
@ -309,18 +309,22 @@ func (a *AdminSocket) handleRequest(conn net.Conn) {
defer conn.Close() defer conn.Close()
/*
defer func() { defer func() {
r := recover() r := recover()
if r != nil { if r != nil {
fmt.Println("ERROR:", r)
a.log.Debugln("Admin socket error:", r) a.log.Debugln("Admin socket error:", r)
if err := encoder.Encode(&ErrorResponse{ if err := encoder.Encode(&ErrorResponse{
Error: "Check your syntax and input types", Error: "Check your syntax and input types",
}); err != nil { }); err != nil {
fmt.Println("ERROR 2:", err)
a.log.Debugln("Admin socket JSON encode error:", err) a.log.Debugln("Admin socket JSON encode error:", err)
} }
conn.Close() conn.Close()
} }
}() }()
*/
for { for {
var err error var err error
@ -335,6 +339,7 @@ func (a *AdminSocket) handleRequest(conn net.Conn) {
if err = json.Unmarshal(buf, &req); err != nil { if err = json.Unmarshal(buf, &req); err != nil {
return fmt.Errorf("Failed to unmarshal request") return fmt.Errorf("Failed to unmarshal request")
} }
resp.Request = req
if req.Name == "" { if req.Name == "" {
return fmt.Errorf("No request specified") return fmt.Errorf("No request specified")
} }

View File

@ -1,41 +0,0 @@
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

@ -20,6 +20,7 @@ type PathEntry struct {
IPAddress string `json:"address"` IPAddress string `json:"address"`
PublicKey string `json:"key"` PublicKey string `json:"key"`
Path []uint64 `json:"path"` Path []uint64 `json:"path"`
Sequence uint64 `json:"sequence"`
} }
func (a *AdminSocket) getPathsHandler(req *GetPathsRequest, res *GetPathsResponse) error { func (a *AdminSocket) getPathsHandler(req *GetPathsRequest, res *GetPathsResponse) error {
@ -31,6 +32,7 @@ func (a *AdminSocket) getPathsHandler(req *GetPathsRequest, res *GetPathsRespons
IPAddress: net.IP(addr[:]).String(), IPAddress: net.IP(addr[:]).String(),
PublicKey: hex.EncodeToString(p.Key), PublicKey: hex.EncodeToString(p.Key),
Path: p.Path, Path: p.Path,
Sequence: p.Sequence,
}) })
} }
sort.SliceStable(res.Paths, func(i, j int) bool { sort.SliceStable(res.Paths, func(i, j int) bool {

View File

@ -4,6 +4,7 @@ import (
"encoding/hex" "encoding/hex"
"net" "net"
"sort" "sort"
"time"
"github.com/yggdrasil-network/yggdrasil-go/src/address" "github.com/yggdrasil-network/yggdrasil-go/src/address"
) )
@ -16,33 +17,43 @@ type GetPeersResponse struct {
} }
type PeerEntry struct { type PeerEntry struct {
IPAddress string `json:"address"` URI string `json:"remote,omitempty"`
Up bool `json:"up"`
Inbound bool `json:"inbound"`
IPAddress string `json:"address,omitempty"`
PublicKey string `json:"key"` PublicKey string `json:"key"`
Port uint64 `json:"port"` Port uint64 `json:"port"`
Priority uint64 `json:"priority"` Priority uint64 `json:"priority"`
Coords []uint64 `json:"coords"` RXBytes DataUnit `json:"bytes_recvd,omitempty"`
Remote string `json:"remote"` TXBytes DataUnit `json:"bytes_sent,omitempty"`
RXBytes DataUnit `json:"bytes_recvd"` Uptime float64 `json:"uptime,omitempty"`
TXBytes DataUnit `json:"bytes_sent"` LastError string `json:"last_error,omitempty"`
Uptime float64 `json:"uptime"` LastErrorTime time.Duration `json:"last_error_time,omitempty"`
} }
func (a *AdminSocket) getPeersHandler(req *GetPeersRequest, res *GetPeersResponse) error { func (a *AdminSocket) getPeersHandler(req *GetPeersRequest, res *GetPeersResponse) error {
peers := a.core.GetPeers() peers := a.core.GetPeers()
res.Peers = make([]PeerEntry, 0, len(peers)) res.Peers = make([]PeerEntry, 0, len(peers))
for _, p := range peers { for _, p := range peers {
addr := address.AddrForKey(p.Key) peer := PeerEntry{
res.Peers = append(res.Peers, PeerEntry{
IPAddress: net.IP(addr[:]).String(),
PublicKey: hex.EncodeToString(p.Key),
Port: p.Port, Port: p.Port,
Up: p.Up,
Inbound: p.Inbound,
Priority: uint64(p.Priority), // can't be uint8 thanks to gobind Priority: uint64(p.Priority), // can't be uint8 thanks to gobind
Coords: p.Coords, URI: p.URI,
Remote: p.Remote,
RXBytes: DataUnit(p.RXBytes), RXBytes: DataUnit(p.RXBytes),
TXBytes: DataUnit(p.TXBytes), TXBytes: DataUnit(p.TXBytes),
Uptime: p.Uptime.Seconds(), Uptime: p.Uptime.Seconds(),
}) }
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)
} }
sort.Slice(res.Peers, func(i, j int) bool { sort.Slice(res.Peers, func(i, j int) bool {
if res.Peers[i].Port == res.Peers[j].Port { if res.Peers[i].Port == res.Peers[j].Port {

View File

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

45
src/admin/gettree.go Normal file
View File

@ -0,0 +1,45 @@
package admin
import (
"encoding/hex"
"net"
"sort"
"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"`
//Port uint64 `json:"port"`
//Rest uint64 `json:"rest"`
}
func (a *AdminSocket) getTreeHandler(req *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,
//Port: d.Port,
//Rest: d.Rest,
})
}
sort.SliceStable(res.Tree, func(i, j int) bool {
return strings.Compare(res.Tree[i].PublicKey, res.Tree[j].PublicKey) < 0
})
return nil
}

View File

@ -1,9 +1,24 @@
package admin 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) { func (c *AdminSocket) _applyOption(opt SetupOption) {
switch v := opt.(type) { switch v := opt.(type) {
case ListenAddress: case ListenAddress:
c.config.listenaddr = v c.config.listenaddr = v
case LogLookups:
c.logLookups()
} }
} }
@ -14,3 +29,51 @@ type SetupOption interface {
type ListenAddress string type ListenAddress string
func (a ListenAddress) isSetupOption() {} 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,5 +1,10 @@
package admin package admin
import (
"fmt"
"net/url"
)
type RemovePeerRequest struct { type RemovePeerRequest struct {
Uri string `json:"uri"` Uri string `json:"uri"`
Sintf string `json:"interface,omitempty"` Sintf string `json:"interface,omitempty"`
@ -8,5 +13,9 @@ type RemovePeerRequest struct {
type RemovePeerResponse struct{} type RemovePeerResponse struct{}
func (a *AdminSocket) removePeerHandler(req *RemovePeerRequest, res *RemovePeerResponse) error { func (a *AdminSocket) removePeerHandler(req *RemovePeerRequest, res *RemovePeerResponse) error {
return a.core.RemovePeer(req.Uri, req.Sintf) u, err := url.Parse(req.Uri)
if err != nil {
return fmt.Errorf("unable to parse peering URI: %w", err)
}
return a.core.RemovePeer(u, req.Sintf)
} }

View File

@ -17,26 +17,43 @@ configuration option that is not provided.
package config package config
import ( import (
"bytes"
"crypto/ed25519" "crypto/ed25519"
"crypto/rand"
"crypto/tls"
"crypto/x509"
"crypto/x509/pkix"
"encoding/hex" "encoding/hex"
"encoding/json"
"encoding/pem"
"fmt"
"io"
"math/big"
"os"
"time"
"github.com/hjson/hjson-go/v4"
"golang.org/x/text/encoding/unicode"
) )
// NodeConfig is the main configuration structure, containing configuration // NodeConfig is the main configuration structure, containing configuration
// options that are necessary for an Yggdrasil node to run. You will need to // options that are necessary for an Yggdrasil node to run. You will need to
// supply one of these structs to the Yggdrasil core when starting a node. // supply one of these structs to the Yggdrasil core when starting a node.
type NodeConfig struct { type NodeConfig struct {
PrivateKey KeyBytes `comment:"Your private key. DO NOT share this with anyone!"`
PrivateKeyPath string `json:",omitempty"`
Certificate *tls.Certificate `json:"-"`
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."` 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."` 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."` 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."` 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. 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."` 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."` 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."` IfName string `comment:"Local network interface name for TUN adapter, or \"auto\" to select\nan interface automatically, or \"none\" to run without TUN."`
IfMTU uint64 `comment:"Maximum Transmission Unit (MTU) size for your local TUN interface.\nDefault is the largest supported size for your platform. The lowest\npossible value is 1280."` IfMTU uint64 `comment:"Maximum Transmission Unit (MTU) size for your local TUN interface.\nDefault is the largest supported size for your platform. The lowest\npossible value is 1280."`
NodeInfoPrivacy bool `comment:"By default, nodeinfo contains some defaults including the platform,\narchitecture and Yggdrasil version. These can help when surveying\nthe network and diagnosing network routing problems. Enabling\nnodeinfo privacy prevents this, so that only items specified in\n\"NodeInfo\" are sent back if specified."` NodeInfoPrivacy bool `comment:"By default, nodeinfo contains some defaults including the platform,\narchitecture and Yggdrasil version. These can help when surveying\nthe network and diagnosing network routing problems. Enabling\nnodeinfo privacy prevents this, so that only items specified in\n\"NodeInfo\" are sent back if specified."`
NodeInfo map[string]interface{} `comment:"Optional 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."` 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."`
LogLookups bool `json:",omitempty"`
} }
type MulticastInterfaceConfig struct { type MulticastInterfaceConfig struct {
@ -45,16 +62,199 @@ type MulticastInterfaceConfig struct {
Listen bool Listen bool
Port uint16 Port uint16
Priority uint64 // really uint8, but gobind won't export it Priority uint64 // really uint8, but gobind won't export it
Password string
} }
// NewSigningKeys replaces the signing keypair in the NodeConfig with a new // Generates default configuration and returns a pointer to the resulting
// signing keypair. The signing keys are used by the switch to derive the // NodeConfig. This is used when outputting the -genconf parameter and also when
// structure of the spanning tree. // using -autoconf.
func (cfg *NodeConfig) NewKeys() { func GenerateConfig() *NodeConfig {
spub, spriv, err := ed25519.GenerateKey(nil) // 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)
if err != nil { if err != nil {
panic(err) panic(err)
} }
cfg.PublicKey = hex.EncodeToString(spub[:]) cfg.PrivateKey = KeyBytes(spriv)
cfg.PrivateKey = hex.EncodeToString(spriv[:]) }
func (cfg *NodeConfig) MarshalPEMPrivateKey() ([]byte, error) {
b, err := x509.MarshalPKCS8PrivateKey(ed25519.PrivateKey(cfg.PrivateKey))
if err != nil {
return nil, fmt.Errorf("failed to marshal PKCS8 key: %w", err)
}
block := &pem.Block{
Type: "PRIVATE KEY",
Bytes: b,
}
return pem.EncodeToMemory(block), nil
}
func (cfg *NodeConfig) UnmarshalPEMPrivateKey(b []byte) error {
p, _ := pem.Decode(b)
if p == nil {
return fmt.Errorf("failed to parse PEM file")
}
if p.Type != "PRIVATE KEY" {
return fmt.Errorf("unexpected PEM type %q", p.Type)
}
k, err := x509.ParsePKCS8PrivateKey(p.Bytes)
if err != nil {
return fmt.Errorf("failed to unmarshal PKCS8 key: %w", err)
}
key, ok := k.(ed25519.PrivateKey)
if !ok {
return fmt.Errorf("private key must be ed25519 key")
}
if len(key) != ed25519.PrivateKeySize {
return fmt.Errorf("unexpected ed25519 private key length")
}
cfg.PrivateKey = KeyBytes(key)
return nil
}
type KeyBytes []byte
func (k KeyBytes) MarshalJSON() ([]byte, error) {
return json.Marshal(hex.EncodeToString(k))
}
func (k *KeyBytes) UnmarshalJSON(b []byte) error {
var s string
var err error
if err = json.Unmarshal(b, &s); err != nil {
return err
}
*k, err = hex.DecodeString(s)
return err
} }

View File

@ -1,12 +1,11 @@
package config package config
import ( import (
"bytes"
"encoding/hex"
"testing" "testing"
) )
func TestConfig_Keys(t *testing.T) { func TestConfig_Keys(t *testing.T) {
/*
var nodeConfig NodeConfig var nodeConfig NodeConfig
nodeConfig.NewKeys() nodeConfig.NewKeys()
@ -51,4 +50,5 @@ func TestConfig_Keys(t *testing.T) {
if bytes.Equal(privateKey2, privateKey1) { if bytes.Equal(privateKey2, privateKey1) {
t.Fatal("same private key generated") t.Fatal("same private key generated")
} }
*/
} }

34
src/config/defaults.go Normal file
View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -3,43 +3,50 @@ package core
import ( import (
"crypto/ed25519" "crypto/ed25519"
"encoding/json" "encoding/json"
"fmt"
"net" "net"
"net/url" "net/url"
"sync/atomic" "sync/atomic"
"time" "time"
"github.com/Arceliar/phony" "github.com/Arceliar/phony"
"github.com/Arceliar/ironwood/network"
"github.com/yggdrasil-network/yggdrasil-go/src/address" "github.com/yggdrasil-network/yggdrasil-go/src/address"
) )
type SelfInfo struct { type SelfInfo struct {
Key ed25519.PublicKey Key ed25519.PublicKey
Root ed25519.PublicKey RoutingEntries uint64
Coords []uint64
} }
type PeerInfo struct { type PeerInfo struct {
URI string
Up bool
Inbound bool
LastError error
LastErrorTime time.Time
Key ed25519.PublicKey Key ed25519.PublicKey
Root ed25519.PublicKey Root ed25519.PublicKey
Coords []uint64 Coords []uint64
Port uint64 Port uint64
Priority uint8 Priority uint8
Remote string
RXBytes uint64 RXBytes uint64
TXBytes uint64 TXBytes uint64
Uptime time.Duration Uptime time.Duration
} }
type DHTEntryInfo struct { type TreeEntryInfo struct {
Key ed25519.PublicKey Key ed25519.PublicKey
Port uint64 Parent ed25519.PublicKey
Rest uint64 Sequence uint64
//Port uint64
//Rest uint64
} }
type PathEntryInfo struct { type PathEntryInfo struct {
Key ed25519.PublicKey Key ed25519.PublicKey
Path []uint64 Path []uint64
Sequence uint64
} }
type SessionInfo struct { type SessionInfo struct {
@ -53,55 +60,59 @@ func (c *Core) GetSelf() SelfInfo {
var self SelfInfo var self SelfInfo
s := c.PacketConn.PacketConn.Debug.GetSelf() s := c.PacketConn.PacketConn.Debug.GetSelf()
self.Key = s.Key self.Key = s.Key
self.Root = s.Root self.RoutingEntries = s.RoutingEntries
self.Coords = s.Coords
return self return self
} }
func (c *Core) GetPeers() []PeerInfo { func (c *Core) GetPeers() []PeerInfo {
var peers []PeerInfo peers := []PeerInfo{}
names := make(map[net.Conn]string) conns := map[net.Conn]network.DebugPeerInfo{}
phony.Block(&c.links, func() { iwpeers := c.PacketConn.PacketConn.Debug.GetPeers()
for _, info := range c.links._links { for _, p := range iwpeers {
if info == nil { conns[p.Conn] = p
continue
} }
names[info.conn] = info.lname
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.Uptime = time.Since(c.up)
}
if p, ok := conns[conn]; ok {
peerinfo.Key = p.Key
peerinfo.Root = p.Root
peerinfo.Port = p.Port
peerinfo.Priority = p.Priority
}
peers = append(peers, peerinfo)
} }
}) })
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 return peers
} }
func (c *Core) GetDHT() []DHTEntryInfo { func (c *Core) GetTree() []TreeEntryInfo {
var dhts []DHTEntryInfo var trees []TreeEntryInfo
ds := c.PacketConn.PacketConn.Debug.GetDHT() ts := c.PacketConn.PacketConn.Debug.GetTree()
for _, d := range ds { for _, t := range ts {
var info DHTEntryInfo var info TreeEntryInfo
info.Key = d.Key info.Key = t.Key
info.Port = d.Port info.Parent = t.Parent
info.Rest = d.Rest info.Sequence = t.Sequence
dhts = append(dhts, info) //info.Port = d.Port
//info.Rest = d.Rest
trees = append(trees, info)
} }
return dhts return trees
} }
func (c *Core) GetPaths() []PathEntryInfo { func (c *Core) GetPaths() []PathEntryInfo {
@ -110,6 +121,7 @@ func (c *Core) GetPaths() []PathEntryInfo {
for _, p := range ps { for _, p := range ps {
var info PathEntryInfo var info PathEntryInfo
info.Key = p.Key info.Key = p.Key
info.Sequence = p.Sequence
info.Path = p.Path info.Path = p.Path
paths = append(paths, info) paths = append(paths, info)
} }
@ -134,16 +146,7 @@ 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 // parsed from a string of the form e.g. "tcp://a.b.c.d:e". In the case of a
// link-local address, the interface should be provided as the second argument. // link-local address, the interface should be provided as the second argument.
func (c *Core) Listen(u *url.URL, sintf string) (*Listener, error) { func (c *Core) Listen(u *url.URL, sintf string) (*Listener, error) {
switch u.Scheme { return c.links.listen(u, sintf)
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 // Address gets the IPv6 address of the Yggdrasil node. This is always a /128
@ -182,49 +185,14 @@ func (c *Core) SetLogger(log Logger) {
// //
// This adds the peer to the peer list, so that they will be called again if the // This adds the peer to the peer list, so that they will be called again if the
// connection drops. // connection drops.
func (c *Core) AddPeer(uri string, sourceInterface string) error { func (c *Core) AddPeer(u *url.URL, sintf string) error {
var known bool return c.links.add(u, sintf, linkTypePersistent)
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, nil)
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. // RemovePeer removes a peer. The peer should be specified in URI format, see AddPeer.
// The peer is not disconnected immediately. // The peer is not disconnected immediately.
func (c *Core) RemovePeer(uri string, sourceInterface string) error { func (c *Core) RemovePeer(u *url.URL, sintf string) error {
var err error return c.links.remove(u, sintf, linkTypePersistent)
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, // CallPeer calls a peer once. This should be specified in the peer URI format,
@ -236,8 +204,7 @@ func (c *Core) RemovePeer(uri string, sourceInterface string) error {
// This does not add the peer to the peer list, so if the connection drops, the // This does not add the peer to the peer list, so if the connection drops, the
// peer will not be called again automatically. // peer will not be called again automatically.
func (c *Core) CallPeer(u *url.URL, sintf string) error { func (c *Core) CallPeer(u *url.URL, sintf string) error {
_, err := c.links.call(u, sintf, nil) return c.links.add(u, sintf, linkTypeEphemeral)
return err
} }
func (c *Core) PublicKey() ed25519.PublicKey { func (c *Core) PublicKey() ed25519.PublicKey {
@ -274,8 +241,8 @@ func (c *Core) SetAdmin(a AddHandler) error {
return err return err
} }
if err := a.AddHandler( if err := a.AddHandler(
"debug_remoteGetDHT", "Debug use only", []string{"key"}, "debug_remoteGetTree", "Debug use only", []string{"key"},
c.proto.getDHTHandler, c.proto.getTreeHandler,
); err != nil { ); err != nil {
return err return err
} }

View File

@ -3,6 +3,7 @@ package core
import ( import (
"context" "context"
"crypto/ed25519" "crypto/ed25519"
"crypto/tls"
"fmt" "fmt"
"io" "io"
"net" "net"
@ -10,10 +11,12 @@ import (
"time" "time"
iwe "github.com/Arceliar/ironwood/encrypted" iwe "github.com/Arceliar/ironwood/encrypted"
iwn "github.com/Arceliar/ironwood/network"
iwt "github.com/Arceliar/ironwood/types" iwt "github.com/Arceliar/ironwood/types"
"github.com/Arceliar/phony" "github.com/Arceliar/phony"
"github.com/gologme/log" "github.com/gologme/log"
"github.com/yggdrasil-network/yggdrasil-go/src/address"
"github.com/yggdrasil-network/yggdrasil-go/src/version" "github.com/yggdrasil-network/yggdrasil-go/src/version"
) )
@ -34,49 +37,87 @@ type Core struct {
log Logger log Logger
addPeerTimer *time.Timer addPeerTimer *time.Timer
config struct { config struct {
_peers map[Peer]*linkInfo // configurable after startup tls *tls.Config // immutable after startup
//_peers map[Peer]*linkInfo // configurable after startup
_listeners map[ListenAddress]struct{} // configurable after startup _listeners map[ListenAddress]struct{} // configurable after startup
nodeinfo NodeInfo // immutable after startup nodeinfo NodeInfo // immutable after startup
nodeinfoPrivacy NodeInfoPrivacy // immutable after startup nodeinfoPrivacy NodeInfoPrivacy // immutable after startup
_allowedPublicKeys map[[32]byte]struct{} // configurable after startup _allowedPublicKeys map[[32]byte]struct{} // configurable after startup
} }
pathNotify func(ed25519.PublicKey)
} }
func New(secret ed25519.PrivateKey, logger Logger, opts ...SetupOption) (*Core, error) { func New(cert *tls.Certificate, logger Logger, opts ...SetupOption) (*Core, error) {
c := &Core{ c := &Core{
log: logger, 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" { if name := version.BuildName(); name != "unknown" {
c.log.Infoln("Build name:", name) c.log.Infoln("Build name:", name)
} }
if version := version.BuildVersion(); version != "unknown" { if version := version.BuildVersion(); version != "unknown" {
c.log.Infoln("Build version:", version) 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 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._listeners = map[ListenAddress]struct{}{}
c.config._allowedPublicKeys = map[[32]byte]struct{}{} c.config._allowedPublicKeys = map[[32]byte]struct{}{}
for _, opt := range opts { for _, opt := range opts {
c._applyOption(opt) switch opt.(type) {
case Peer, ListenAddress:
// We can't do peers yet as the links aren't set up.
continue
default:
if err = c._applyOption(opt); err != nil {
return nil, fmt.Errorf("failed to apply configuration option %T: %w", opt, err)
} }
if c.log == nil { }
c.log = log.New(io.Discard, "", 0) }
if cert == nil || cert.PrivateKey == nil {
return nil, fmt.Errorf("no private key supplied")
}
var ok bool
if c.secret, ok = cert.PrivateKey.(ed25519.PrivateKey); !ok {
return nil, fmt.Errorf("private key must be ed25519")
}
if len(c.secret) != ed25519.PrivateKeySize {
return nil, fmt.Errorf("private key is incorrect length")
}
c.public = c.secret.Public().(ed25519.PublicKey)
if c.config.tls, err = c.generateTLSConfig(cert); err != nil {
return nil, fmt.Errorf("error generating TLS config: %w", err)
}
keyXform := func(key ed25519.PublicKey) ed25519.PublicKey {
return address.SubnetForKey(key).GetKey()
}
if c.PacketConn, err = iwe.NewPacketConn(
c.secret,
iwn.WithBloomTransform(keyXform),
iwn.WithPeerMaxMessageSize(65535*2),
iwn.WithPathNotify(c.doPathNotify),
); err != nil {
return nil, fmt.Errorf("error creating encryption: %w", err)
} }
c.proto.init(c) c.proto.init(c)
if err := c.links.init(c); err != nil { if err := c.links.init(c); err != nil {
return nil, fmt.Errorf("error initialising links: %w", err) 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 { 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) return nil, fmt.Errorf("error setting node info: %w", err)
} }
@ -90,44 +131,20 @@ func New(secret ed25519.PrivateKey, logger Logger, opts ...SetupOption) (*Core,
c.log.Errorf("Failed to start listener %q: %s\n", listenaddr, err) c.log.Errorf("Failed to start listener %q: %s\n", listenaddr, err)
} }
} }
c.Act(nil, c._addPeerLoop)
return c, nil return c, nil
} }
// If any static peers were provided in the configuration above then we should func (c *Core) RetryPeersNow() {
// configure them. The loop ensures that disconnected peers will eventually phony.Block(&c.links, func() {
// be reconnected with. for _, l := range c.links._links {
func (c *Core) _addPeerLoop() {
select { select {
case <-c.ctx.Done(): case l.kick <- struct{}{}:
return
default: 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)
}) })
} }
func (c *Core) RetryPeersNow() {
if c.addPeerTimer != nil && !c.addPeerTimer.Stop() {
<-c.addPeerTimer.C
}
c.Act(nil, c._addPeerLoop)
}
// Stop shuts down the Yggdrasil node. // Stop shuts down the Yggdrasil node.
func (c *Core) Stop() { func (c *Core) Stop() {
phony.Block(c, func() { phony.Block(c, func() {
@ -151,11 +168,16 @@ func (c *Core) _close() error {
func (c *Core) MTU() uint64 { func (c *Core) MTU() uint64 {
const sessionTypeOverhead = 1 const sessionTypeOverhead = 1
return c.PacketConn.MTU() - sessionTypeOverhead MTU := c.PacketConn.MTU() - sessionTypeOverhead
if MTU > 65535 {
MTU = 65535
}
return MTU
} }
func (c *Core) ReadFrom(p []byte) (n int, from net.Addr, err error) { func (c *Core) ReadFrom(p []byte) (n int, from net.Addr, err error) {
buf := make([]byte, c.PacketConn.MTU(), 65535) buf := allocBytes(int(c.PacketConn.MTU()))
defer freeBytes(buf)
for { for {
bs := buf bs := buf
n, from, err = c.PacketConn.ReadFrom(bs) n, from, err = c.PacketConn.ReadFrom(bs)
@ -189,7 +211,8 @@ 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) { func (c *Core) WriteTo(p []byte, addr net.Addr) (n int, err error) {
buf := make([]byte, 0, 65535) buf := allocBytes(0)
defer freeBytes(buf)
buf = append(buf, typeSessionTraffic) buf = append(buf, typeSessionTraffic)
buf = append(buf, p...) buf = append(buf, p...)
n, err = c.PacketConn.WriteTo(buf, addr) n, err = c.PacketConn.WriteTo(buf, addr)
@ -199,6 +222,20 @@ func (c *Core) WriteTo(p []byte, addr net.Addr) (n int, err error) {
return 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 { type Logger interface {
Printf(string, ...interface{}) Printf(string, ...interface{})
Println(...interface{}) Println(...interface{})
@ -210,4 +247,5 @@ type Logger interface {
Errorln(...interface{}) Errorln(...interface{})
Debugf(string, ...interface{}) Debugf(string, ...interface{})
Debugln(...interface{}) Debugln(...interface{})
Traceln(...interface{})
} }

View File

@ -2,14 +2,14 @@ package core
import ( import (
"bytes" "bytes"
"crypto/ed25519" "crypto/rand"
"math/rand"
"net/url" "net/url"
"os" "os"
"testing" "testing"
"time" "time"
"github.com/gologme/log" "github.com/gologme/log"
"github.com/yggdrasil-network/yggdrasil-go/src/config"
) )
// GetLoggerWithPrefix creates a new logger instance with prefix. // GetLoggerWithPrefix creates a new logger instance with prefix.
@ -29,29 +29,40 @@ func GetLoggerWithPrefix(prefix string, verbose bool) *log.Logger {
// Verbosity flag is passed to logger. // Verbosity flag is passed to logger.
func CreateAndConnectTwo(t testing.TB, verbose bool) (nodeA *Core, nodeB *Core) { func CreateAndConnectTwo(t testing.TB, verbose bool) (nodeA *Core, nodeB *Core) {
var err error var err error
var skA, skB ed25519.PrivateKey
if _, skA, err = ed25519.GenerateKey(nil); err != nil { cfgA, cfgB := config.GenerateConfig(), config.GenerateConfig()
if err = cfgA.GenerateSelfSignedCertificate(); err != nil {
t.Fatal(err) t.Fatal(err)
} }
if _, skB, err = ed25519.GenerateKey(nil); err != nil { if err = cfgB.GenerateSelfSignedCertificate(); err != nil {
t.Fatal(err)
}
logger := GetLoggerWithPrefix("", false)
if nodeA, err = New(skA, logger, ListenAddress("tcp://127.0.0.1:0")); err != nil {
t.Fatal(err)
}
if nodeB, err = New(skB, logger, ListenAddress("tcp://127.0.0.1:0")); err != nil {
t.Fatal(err) t.Fatal(err)
} }
u, err := url.Parse("tcp://" + nodeA.links.tcp.getAddr().String()) logger := GetLoggerWithPrefix("", false)
logger.EnableLevel("debug")
if nodeA, err = New(cfgA.Certificate, logger); err != nil {
t.Fatal(err)
}
if nodeB, err = New(cfgB.Certificate, logger); err != nil {
t.Fatal(err)
}
nodeAListenURL, err := url.Parse("tcp://localhost:0")
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
err = nodeB.CallPeer(u, "") nodeAListener, err := nodeA.Listen(nodeAListenURL, "")
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
nodeAURL, err := url.Parse("tcp://" + nodeAListener.Addr().String())
if err != nil {
t.Fatal(err)
}
if err = nodeB.CallPeer(nodeAURL, ""); err != nil {
t.Fatal(err)
}
time.Sleep(100 * time.Millisecond) time.Sleep(100 * time.Millisecond)
@ -70,9 +81,15 @@ func WaitConnected(nodeA, nodeB *Core) bool {
// It may take up to 3 seconds, but let's wait 5. // It may take up to 3 seconds, but let's wait 5.
for i := 0; i < 50; i++ { for i := 0; i < 50; i++ {
time.Sleep(100 * time.Millisecond) time.Sleep(100 * time.Millisecond)
/*
if len(nodeA.GetPeers()) > 0 && len(nodeB.GetPeers()) > 0 { if len(nodeA.GetPeers()) > 0 && len(nodeB.GetPeers()) > 0 {
return true return true
} }
*/
if len(nodeA.GetTree()) > 1 && len(nodeB.GetTree()) > 1 {
time.Sleep(3 * time.Second) // FIXME hack, there's still stuff happening internally
return true
}
} }
return false return false
} }
@ -129,7 +146,7 @@ func TestCore_Start_Transfer(t *testing.T) {
// Send // Send
msg := make([]byte, msgLen) msg := make([]byte, msgLen)
rand.Read(msg[40:]) _, _ = rand.Read(msg[40:])
msg[0] = 0x60 msg[0] = 0x60
copy(msg[8:24], nodeB.Address()) copy(msg[8:24], nodeB.Address())
copy(msg[24:40], nodeA.Address()) copy(msg[24:40], nodeA.Address())
@ -161,7 +178,7 @@ func BenchmarkCore_Start_Transfer(b *testing.B) {
// Send // Send
msg := make([]byte, msgLen) msg := make([]byte, msgLen)
rand.Read(msg[40:]) _, _ = rand.Read(msg[40:])
msg[0] = 0x60 msg[0] = 0x60
copy(msg[8:24], nodeB.Address()) copy(msg[8:24], nodeB.Address())
copy(msg[24:40], nodeA.Address()) copy(msg[24:40], nodeA.Address())

View File

@ -1,6 +1,3 @@
//go:build debug
// +build debug
package core package core
import ( import (
@ -8,28 +5,13 @@ import (
"net/http" "net/http"
_ "net/http/pprof" _ "net/http/pprof"
"os" "os"
"runtime"
"github.com/gologme/log"
) )
// Start the profiler in debug builds, if the required environment variable is set. // Start the profiler if the required environment variable is set.
func init() { func init() {
envVarName := "PPROFLISTEN" envVarName := "PPROFLISTEN"
hostPort := os.Getenv(envVarName) if hostPort := os.Getenv(envVarName); hostPort != "" {
switch {
case hostPort == "":
fmt.Fprintf(os.Stderr, "DEBUG: %s not set, profiler not started.\n", envVarName)
default:
fmt.Fprintf(os.Stderr, "DEBUG: Starting pprof on %s\n", hostPort) fmt.Fprintf(os.Stderr, "DEBUG: Starting pprof on %s\n", hostPort)
go func() { fmt.Println(http.ListenAndServe(hostPort, nil)) }() go 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,11 +2,13 @@ package core
import ( import (
"bytes" "bytes"
"context"
"encoding/hex" "encoding/hex"
"errors"
"fmt" "fmt"
"io" "io"
"math"
"net" "net"
"net/netip"
"net/url" "net/url"
"strconv" "strconv"
"strings" "strings"
@ -15,6 +17,15 @@ import (
"github.com/Arceliar/phony" "github.com/Arceliar/phony"
"github.com/yggdrasil-network/yggdrasil-go/src/address" "github.com/yggdrasil-network/yggdrasil-go/src/address"
"golang.org/x/crypto/blake2b"
)
type linkType int
const (
linkTypePersistent linkType = iota // Statically configured
linkTypeEphemeral // Multicast discovered
linkTypeIncoming // Incoming connection
) )
type links struct { type links struct {
@ -24,44 +35,56 @@ type links struct {
tls *linkTLS // TLS interface support tls *linkTLS // TLS interface support
unix *linkUNIX // UNIX interface support unix *linkUNIX // UNIX interface support
socks *linkSOCKS // SOCKS interface support socks *linkSOCKS // SOCKS interface support
quic *linkQUIC // QUIC interface support
// _links can only be modified safely from within the links actor
_links map[linkInfo]*link // *link is nil if connection in progress _links map[linkInfo]*link // *link is nil if connection in progress
} }
type linkProtocol interface {
dial(ctx context.Context, url *url.URL, info linkInfo, options linkOptions) (net.Conn, error)
listen(ctx context.Context, url *url.URL, sintf string) (net.Listener, error)
}
// linkInfo is used as a map key // linkInfo is used as a map key
type linkInfo struct { type linkInfo struct {
linkType string // Type of link, e.g. TCP, AWDL uri string // Peering URI in complete form
local string // Local name or address sintf string // Peering source interface (i.e. from InterfacePeers)
remote string // Remote name or address
}
type linkDial struct {
url *url.URL
sintf string
} }
// link tracks the state of a connection, either persistent or non-persistent
type link struct { type link struct {
lname string ctx context.Context // Connection context
links *links cancel context.CancelFunc // Stop future redial attempts (when peer removed)
conn *linkConn kick chan struct{} // Attempt to reconnect now, if backing off
options linkOptions linkType linkType // Type of link, i.e. outbound/inbound, persistent/ephemeral
info linkInfo linkProto string // Protocol carrier of link, e.g. TCP, AWDL
incoming bool // The remaining fields can only be modified safely from within the links actor
force bool _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 occured
} }
type linkOptions struct { type linkOptions struct {
pinnedEd25519Keys map[keyArray]struct{} pinnedEd25519Keys map[keyArray]struct{}
priority uint8 priority uint8
tlsSNI string
password []byte
} }
type Listener struct { type Listener struct {
net.Listener listener net.Listener
closed chan struct{} ctx context.Context
Cancel context.CancelFunc
}
func (l *Listener) Addr() net.Addr {
return l.listener.Addr()
} }
func (l *Listener) Close() error { func (l *Listener) Close() error {
err := l.Listener.Close() l.Cancel()
<-l.closed err := l.listener.Close()
<-l.ctx.Done()
return err return err
} }
@ -71,6 +94,7 @@ func (l *links) init(c *Core) error {
l.tls = l.newLinkTLS(l.tcp) l.tls = l.newLinkTLS(l.tcp)
l.unix = l.newLinkUNIX() l.unix = l.newLinkUNIX()
l.socks = l.newLinkSOCKS() l.socks = l.newLinkSOCKS()
l.quic = l.newLinkQUIC()
l._links = make(map[linkInfo]*link) l._links = make(map[linkInfo]*link)
var listeners []ListenAddress var listeners []ListenAddress
@ -102,303 +126,502 @@ func (l *links) shutdown() {
}) })
} }
func (l *links) isConnectedTo(info linkInfo) bool { type linkError string
var isConnected bool
phony.Block(l, func() {
_, isConnected = l._links[info]
})
return isConnected
}
func (l *links) call(u *url.URL, sintf string, errch chan<- error) (info linkInfo, err error) { func (e linkError) Error() string { return string(e) }
info = linkInfoFor(u.Scheme, sintf, u.Host)
if l.isConnectedTo(info) { const ErrLinkAlreadyConfigured = linkError("peer is already configured")
if errch != nil { const ErrLinkNotConfigured = linkError("peer is not configured")
close(errch) // already connected, no error const ErrLinkPriorityInvalid = linkError("priority value is invalid")
} const ErrLinkPinnedKeyInvalid = linkError("pinned public key is invalid")
return info, nil const ErrLinkPasswordInvalid = linkError("password is invalid")
} const ErrLinkUnrecognisedSchema = linkError("link schema unknown")
options := linkOptions{
pinnedEd25519Keys: map[keyArray]struct{}{}, func (l *links) add(u *url.URL, sintf string, linkType linkType) error {
var retErr error
phony.Block(l, func() {
// Generate the link info and see whether we think we already
// have an open peering to this peer.
lu := urlForLinkInfo(*u)
info := linkInfo{
uri: lu.String(),
sintf: sintf,
} }
// Collect together the link options, these are global options
// that are not specific to any given protocol.
var options linkOptions
for _, pubkey := range u.Query()["key"] { for _, pubkey := range u.Query()["key"] {
sigPub, err := hex.DecodeString(pubkey) sigPub, err := hex.DecodeString(pubkey)
if err != nil { if err != nil {
if errch != nil { retErr = ErrLinkPinnedKeyInvalid
close(errch) return
}
return info, fmt.Errorf("pinned key contains invalid hex characters")
} }
var sigPubKey keyArray var sigPubKey keyArray
copy(sigPubKey[:], sigPub) copy(sigPubKey[:], sigPub)
if options.pinnedEd25519Keys == nil {
options.pinnedEd25519Keys = map[keyArray]struct{}{}
}
options.pinnedEd25519Keys[sigPubKey] = struct{}{} options.pinnedEd25519Keys[sigPubKey] = struct{}{}
} }
if p := u.Query().Get("priority"); p != "" { if p := u.Query().Get("priority"); p != "" {
pi, err := strconv.ParseUint(p, 10, 8) pi, err := strconv.ParseUint(p, 10, 8)
if err != nil { if err != nil {
if errch != nil { retErr = ErrLinkPriorityInvalid
close(errch) return
}
return info, fmt.Errorf("priority invalid: %w", err)
} }
options.priority = uint8(pi) options.priority = uint8(pi)
} }
switch info.linkType { if p := u.Query().Get("password"); p != "" {
if len(p) > blake2b.Size {
retErr = ErrLinkPasswordInvalid
return
}
options.password = []byte(p)
}
// 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 {
backoff++
duration := time.Second * time.Duration(math.Exp2(float64(backoff)))
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
}
}
// 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 {
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.
if err = l.handler(linkType, options, lc); err != nil && err != io.EOF {
l.core.log.Debugf("Link %s error: %s\n", info.uri, err)
} else {
backoff = 0
}
// 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 state._err = err; state._err != nil {
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
}
return
}
}
}()
})
return retErr
}
func (l *links) remove(u *url.URL, sintf string, linkType linkType) error {
var retErr error
phony.Block(l, func() {
// Generate the link info and see whether we think we already
// have an open peering to this peer.
lu := urlForLinkInfo(*u)
info := linkInfo{
uri: lu.String(),
sintf: sintf,
}
// 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) (*Listener, error) {
ctx, cancel := context.WithCancel(l.core.ctx)
var protocol linkProtocol
switch strings.ToLower(u.Scheme) {
case "tcp": case "tcp":
protocol = l.tcp
case "tls":
protocol = l.tls
case "unix":
protocol = l.unix
case "quic":
protocol = l.quic
default:
cancel()
return nil, ErrLinkUnrecognisedSchema
}
listener, err := protocol.listen(ctx, u, sintf)
if err != nil {
cancel()
return nil, 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)
}
go func() { go func() {
if errch != nil { l.core.log.Printf("%s listener started on %s", strings.ToUpper(u.Scheme), listener.Addr())
defer close(errch) defer l.core.log.Printf("%s listener stopped on %s", strings.ToUpper(u.Scheme), listener.Addr())
for {
conn, err := listener.Accept()
if err != nil {
return
} }
if err := l.tcp.dial(u, options, sintf); err != nil && err != io.EOF { go func(conn net.Conn) {
l.core.log.Warnf("Failed to dial TCP %s: %s\n", u.Host, err) defer conn.Close()
if errch != nil {
errch <- err // 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()
case "socks": lu := urlForLinkInfo(pu)
go func() { info := linkInfo{
if errch != nil { uri: lu.String(),
defer close(errch) sintf: sintf,
}
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)
if errch != nil {
errch <- err
} }
// If there's an existing link state for this link, get it.
// If this node is already connected to us, just drop the
// connection. This prevents duplicate peerings.
var lc *linkConn
var state *link
phony.Block(l, func() {
var ok bool
state, ok = l._links[info]
if !ok || state == nil {
state = &link{
linkType: linkTypeIncoming,
linkProto: strings.ToUpper(u.Scheme),
kick: make(chan struct{}),
}
}
if state._conn != nil {
// If a connection has come up in this time, abort
// this one.
return
}
// The linkConn wrapper allows us to track the number of
// bytes written to and read from this connection without
// the help of ironwood.
lc = &linkConn{
Conn: conn,
up: time.Now(),
}
// Update the link state with our newly wrapped connection.
// Clear the error state.
state._conn = lc
state._err = nil
state._errtime = time.Time{}
// Store the state of the link so that it can be queried later.
l._links[info] = state
})
if lc == nil {
return
}
// Give the connection to the handler. The handler will block
// for the lifetime of the connection.
if err = l.handler(linkTypeIncoming, options, lc); err != nil && err != io.EOF {
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()
phony.Block(l, func() {
if l._links[info] == state {
delete(l._links, info)
}
})
}(conn)
} }
}() }()
return li, nil
}
func (l *links) connect(ctx context.Context, u *url.URL, info linkInfo, options linkOptions) (net.Conn, error) {
var dialer linkProtocol
switch strings.ToLower(u.Scheme) {
case "tcp":
dialer = l.tcp
case "tls": case "tls":
// SNI headers must contain hostnames and not IP addresses, so we must make sure // 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 // 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 // the host-port combo from the query option and then seeing if it parses to an
// IP address successfully or not. // IP address successfully or not.
var tlsSNI string
if sni := u.Query().Get("sni"); sni != "" { if sni := u.Query().Get("sni"); sni != "" {
if net.ParseIP(sni) == nil { if net.ParseIP(sni) == nil {
tlsSNI = sni options.tlsSNI = sni
} }
} }
// If the SNI is not configured still because the above failed then we'll try // 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. // again but this time we'll use the host part of the peering URI instead.
if tlsSNI == "" { if options.tlsSNI == "" {
if host, _, err := net.SplitHostPort(u.Host); err == nil && net.ParseIP(host) == nil { if host, _, err := net.SplitHostPort(u.Host); err == nil && net.ParseIP(host) == nil {
tlsSNI = host options.tlsSNI = host
} }
} }
go func() { dialer = l.tls
if errch != nil { case "socks":
defer close(errch) dialer = l.socks
}
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)
if errch != nil {
errch <- err
}
}
}()
case "unix": case "unix":
go func() { dialer = l.unix
if errch != nil { case "quic":
defer close(errch) dialer = l.quic
}
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)
if errch != nil {
errch <- err
}
}
}()
default: default:
if errch != nil { return nil, ErrLinkUnrecognisedSchema
close(errch)
} }
return info, errors.New("unknown call scheme: " + u.Scheme) return dialer.dial(ctx, u, info, options)
}
return info, nil
} }
func (l *links) listen(u *url.URL, sintf string) (*Listener, error) { func (l *links) handler(linkType linkType, options linkOptions, conn net.Conn) 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
}
func (l *links) create(conn net.Conn, dial *linkDial, 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(dial); err != nil {
l.core.log.Errorf("Link handler %s error (%s): %s", name, conn.RemoteAddr(), err)
}
}()
return nil
}
func (intf *link) handler(dial *linkDial) error {
defer intf.conn.Close() // nolint:errcheck
// Don't connect to this link more than once.
if intf.links.isConnectedTo(intf.info) {
return nil
}
// 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 := version_getBaseMetadata()
meta.key = intf.links.core.public meta.publicKey = l.core.public
metaBytes := meta.encode() meta.priority = options.priority
if err := intf.conn.SetDeadline(time.Now().Add(time.Second * 6)); err != nil { metaBytes, err := meta.encode(l.core.secret, options.password)
if err != nil {
return fmt.Errorf("failed to generate handshake: %w", err)
}
if err := conn.SetDeadline(time.Now().Add(time.Second * 6)); err != nil {
return fmt.Errorf("failed to set handshake deadline: %w", err) return fmt.Errorf("failed to set handshake deadline: %w", err)
} }
n, err := intf.conn.Write(metaBytes) n, err := conn.Write(metaBytes)
switch { switch {
case err != nil: case err != nil:
return fmt.Errorf("write handshake: %w", err) return fmt.Errorf("write handshake: %w", err)
case err == nil && n != len(metaBytes): case err == nil && n != len(metaBytes):
return fmt.Errorf("incomplete handshake send") 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{} meta = version_metadata{}
base := version_getBaseMetadata() base := version_getBaseMetadata()
if !meta.decode(metaBytes) { if !meta.decode(conn, options.password) {
return errors.New("failed to decode metadata") return conn.Close()
} }
if !meta.check() { if !meta.check() {
var connectError string return fmt.Errorf("remote node incompatible version (local %s, remote %s)",
if intf.incoming { fmt.Sprintf("%d.%d", base.majorVer, base.minorVer),
connectError = "Rejected incoming connection" fmt.Sprintf("%d.%d", meta.majorVer, meta.minorVer),
} 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),
) )
return errors.New("remote node is incompatible version") }
if err = conn.SetDeadline(time.Time{}); err != nil {
return fmt.Errorf("failed to clear handshake deadline: %w", err)
} }
// Check if the remote side matches the keys we expected. This is a bit of a weak // Check if the remote side matches the keys we expected. This is a bit of a weak
// check - in future versions we really should check a signature or something like that. // check - in future versions we really should check a signature or something like that.
if pinned := intf.options.pinnedEd25519Keys; len(pinned) > 0 { if pinned := options.pinnedEd25519Keys; len(pinned) > 0 {
var key keyArray var key keyArray
copy(key[:], meta.key) copy(key[:], meta.publicKey)
if _, allowed := pinned[key]; !allowed { if _, allowed := pinned[key]; !allowed {
return fmt.Errorf("node public key that does not match pinned keys") return fmt.Errorf("node public key that does not match pinned keys")
} }
} }
// Check if we're authorized to connect to this key / IP // Check if we're authorized to connect to this key / IP
allowed := intf.links.core.config._allowedPublicKeys var allowed map[[32]byte]struct{}
phony.Block(l.core, func() {
allowed = l.core.config._allowedPublicKeys
})
isallowed := len(allowed) == 0 isallowed := len(allowed) == 0
for k := range allowed { for k := range allowed {
if bytes.Equal(k[:], meta.key) { if bytes.Equal(k[:], meta.publicKey) {
isallowed = true isallowed = true
break break
} }
} }
if intf.incoming && !intf.force && !isallowed { if linkType == linkTypeIncoming && !isallowed {
_ = intf.close() return fmt.Errorf("node public key %q is not in AllowedPublicKeys", hex.EncodeToString(meta.publicKey))
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" dir := "outbound"
if intf.incoming { if linkType == linkTypeIncoming {
dir = "inbound" dir = "inbound"
} }
remoteAddr := net.IP(address.AddrForKey(meta.key)[:]).String() remoteAddr := net.IP(address.AddrForKey(meta.publicKey)[:]).String()
remoteStr := fmt.Sprintf("%s@%s", remoteAddr, intf.info.remote) remoteStr := fmt.Sprintf("%s@%s", remoteAddr, conn.RemoteAddr())
localStr := intf.conn.LocalAddr() localStr := conn.LocalAddr()
intf.links.core.log.Infof("Connected %s %s: %s, source %s", priority := options.priority
dir, strings.ToUpper(intf.info.linkType), remoteStr, localStr) if meta.priority > priority {
priority = meta.priority
}
l.core.log.Infof("Connected %s: %s, source %s",
dir, remoteStr, localStr)
err = intf.links.core.HandleConn(meta.key, intf.conn, intf.options.priority) err = l.core.HandleConn(meta.publicKey, conn, priority)
switch err { switch err {
case io.EOF, net.ErrClosed, nil: case io.EOF, net.ErrClosed, nil:
intf.links.core.log.Infof("Disconnected %s %s: %s, source %s", l.core.log.Infof("Disconnected %s: %s, source %s",
dir, strings.ToUpper(intf.info.linkType), remoteStr, localStr) dir, remoteStr, localStr)
default: default:
intf.links.core.log.Infof("Disconnected %s %s: %s, source %s; error: %s", l.core.log.Infof("Disconnected %s: %s, source %s; error: %s",
dir, strings.ToUpper(intf.info.linkType), remoteStr, localStr, err) dir, remoteStr, localStr, err)
} }
if !intf.incoming && dial != nil {
// The connection was one that we dialled, so wait a second and try to
// dial it again.
var retry func(attempt int)
retry = func(attempt int) {
// intf.links.core.log.Infof("Retrying %s (attempt %d of 5)...", dial.url.String(), attempt)
errch := make(chan error, 1)
if _, err := intf.links.call(dial.url, dial.sintf, errch); err != nil {
return
}
if err := <-errch; err != nil {
if attempt < 3 {
time.AfterFunc(time.Second, func() {
retry(attempt + 1)
})
}
}
}
time.AfterFunc(time.Second, func() {
retry(1)
})
}
return nil return nil
} }
func (intf *link) close() error { func urlForLinkInfo(u url.URL) url.URL {
return intf.conn.Close() u.RawQuery = ""
} if host, _, err := net.SplitHostPort(u.Host); err == nil {
if addr, err := netip.ParseAddr(host); err == nil {
func linkInfoFor(linkType, sintf, remote string) linkInfo { // For peers that look like multicast peers (i.e.
return linkInfo{ // link-local addresses), we will ignore the port number,
linkType: linkType, // otherwise we might open multiple connections to them.
local: sintf, if addr.IsLinkLocalUnicast() {
remote: remote, u.Host = fmt.Sprintf("[%s]", addr.String())
} }
}
}
return u
} }
type linkConn struct { type linkConn struct {
@ -421,12 +644,3 @@ func (c *linkConn) Write(p []byte) (n int, err error) {
atomic.AddUint64(&c.tx, uint64(n)) atomic.AddUint64(&c.tx, uint64(n))
return 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
}

96
src/core/link_quic.go Normal file
View File

@ -0,0 +1,96 @@
package core
import (
"context"
"crypto/tls"
"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.EarlyListener
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) {
qc, err := quic.DialAddrEarly(ctx, url.Host, l.tlsconfig, l.quicconfig)
if err != nil {
return nil, err
}
qs, err := qc.OpenStream()
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.ListenAddrEarly(url.Host, l.tlsconfig, l.quicconfig)
if err != nil {
return nil, err
}
ch := make(chan *linkQUICStream)
lql := &linkQUICListener{
EarlyListener: ql,
ch: ch,
}
go func() {
for {
qc, err := ql.Accept(ctx)
if err != nil {
ql.Close()
return
}
qs, err := qc.AcceptStream(ctx)
if err != nil {
ql.Close()
return
}
ch <- &linkQUICStream{
Connection: qc,
Stream: qs,
}
}
}()
return lql, nil
}

View File

@ -1,6 +1,7 @@
package core package core
import ( import (
"context"
"fmt" "fmt"
"net" "net"
"net/url" "net/url"
@ -20,37 +21,22 @@ func (l *links) newLinkSOCKS() *linkSOCKS {
return lt return lt
} }
func (l *linkSOCKS) dial(url *url.URL, options linkOptions) error { func (l *linkSOCKS) dial(_ context.Context, url *url.URL, info linkInfo, options linkOptions) (net.Conn, error) {
info := linkInfoFor("socks", "", url.Path) var proxyAuth *proxy.Auth
if l.links.isConnectedTo(info) { if url.User != nil && url.User.Username() != "" {
return nil proxyAuth = &proxy.Auth{
User: url.User.Username(),
} }
proxyAuth := &proxy.Auth{}
proxyAuth.User = url.User.Username()
proxyAuth.Password, _ = url.User.Password() proxyAuth.Password, _ = url.User.Password()
}
dialer, err := proxy.SOCKS5("tcp", url.Host, proxyAuth, proxy.Direct) dialer, err := proxy.SOCKS5("tcp", url.Host, proxyAuth, proxy.Direct)
if err != nil { if err != nil {
return fmt.Errorf("failed to configure proxy") return nil, fmt.Errorf("failed to configure proxy")
} }
pathtokens := strings.Split(strings.Trim(url.Path, "/"), "/") pathtokens := strings.Split(strings.Trim(url.Path, "/"), "/")
conn, err := dialer.Dial("tcp", pathtokens[0]) return dialer.Dial("tcp", pathtokens[0])
if err != nil {
return err
}
dial := &linkDial{
url: url,
}
return l.handler(dial, info, conn, options, false)
} }
func (l *linkSOCKS) handler(dial *linkDial, info linkInfo, conn net.Conn, options linkOptions, incoming bool) error { func (l *linkSOCKS) listen(ctx context.Context, url *url.URL, _ string) (net.Listener, error) {
return l.links.create( return nil, fmt.Errorf("SOCKS listener not supported")
conn, // connection
dial, // connection URL
dial.url.String(), // connection name
info, // connection info
incoming, // not incoming
false, // not forced
options, // connection options
)
} }

View File

@ -5,7 +5,7 @@ import (
"fmt" "fmt"
"net" "net"
"net/url" "net/url"
"strings" "strconv"
"time" "time"
"github.com/Arceliar/phony" "github.com/Arceliar/phony"
@ -14,117 +14,88 @@ import (
type linkTCP struct { type linkTCP struct {
phony.Inbox phony.Inbox
*links *links
listener *net.ListenConfig listenconfig *net.ListenConfig
_listeners map[*Listener]context.CancelFunc _listeners map[*Listener]context.CancelFunc
} }
func (l *links) newLinkTCP() *linkTCP { func (l *links) newLinkTCP() *linkTCP {
lt := &linkTCP{ lt := &linkTCP{
links: l, links: l,
listener: &net.ListenConfig{ listenconfig: &net.ListenConfig{
KeepAlive: -1, KeepAlive: -1,
}, },
_listeners: map[*Listener]context.CancelFunc{}, _listeners: map[*Listener]context.CancelFunc{},
} }
lt.listener.Control = lt.tcpContext lt.listenconfig.Control = lt.tcpContext
return lt return lt
} }
func (l *linkTCP) dial(url *url.URL, options linkOptions, sintf string) error { type tcpDialer struct {
addr, err := net.ResolveTCPAddr("tcp", url.Host) info linkInfo
if err != nil { dialer *net.Dialer
return err addr *net.TCPAddr
}
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
}
name := strings.TrimRight(strings.SplitN(url.String(), "?", 2)[0], "/")
dial := &linkDial{
url: url,
sintf: sintf,
}
return l.handler(dial, name, info, conn, options, false, false)
} }
func (l *linkTCP) listen(url *url.URL, sintf string) (*Listener, error) { func (l *linkTCP) dialersFor(url *url.URL, info linkInfo) ([]*tcpDialer, error) {
ctx, cancel := context.WithCancel(l.core.ctx) host, p, err := net.SplitHostPort(url.Host)
if err != nil {
return nil, err
}
port, err := strconv.Atoi(p)
if err != nil {
return nil, err
}
ips, err := net.LookupIP(host)
if err != nil {
return nil, err
}
dialers := make([]*tcpDialer, 0, len(ips))
for _, ip := range ips {
addr := &net.TCPAddr{
IP: ip,
Port: port,
}
dialer, err := l.dialerFor(addr, info.sintf)
if err != nil {
continue
}
dialers = append(dialers, &tcpDialer{
info: info,
dialer: dialer,
addr: addr,
})
}
return dialers, nil
}
func (l *linkTCP) dial(ctx context.Context, url *url.URL, info linkInfo, options linkOptions) (net.Conn, error) {
dialers, err := l.dialersFor(url, info)
if err != nil {
return nil, err
}
if len(dialers) == 0 {
return nil, nil
}
for _, d := range dialers {
var conn net.Conn
conn, err = d.dialer.DialContext(ctx, "tcp", d.addr.String())
if err != nil {
l.core.log.Warnf("Failed to connect to %s: %s", d.addr, err)
continue
}
return conn, nil
}
return nil, err
}
func (l *linkTCP) listen(ctx context.Context, url *url.URL, sintf string) (net.Listener, error) {
hostport := url.Host hostport := url.Host
if sintf != "" { if sintf != "" {
if host, port, err := net.SplitHostPort(hostport); err == nil { if host, port, err := net.SplitHostPort(hostport); err == nil {
hostport = fmt.Sprintf("[%s%%%s]:%s", host, sintf, port) hostport = fmt.Sprintf("[%s%%%s]:%s", host, sintf, port)
} }
} }
listener, err := l.listener.Listen(ctx, "tcp", hostport) return l.listenconfig.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(nil, 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(dial *linkDial, name string, info linkInfo, conn net.Conn, options linkOptions, incoming, force bool) error {
return l.links.create(
conn, // connection
dial, // connection URL
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) { func (l *linkTCP) dialerFor(dst *net.TCPAddr, sintf string) (*net.Dialer, error) {
@ -185,16 +156,3 @@ func (l *linkTCP) dialerFor(dst *net.TCPAddr, sintf string) (*net.Dialer, error)
} }
return dialer, nil 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

@ -12,22 +12,6 @@ import (
// WARNING: This context is used both by net.Dialer and net.Listen in tcp.go // WARNING: This context is used both by net.Dialer and net.Listen in tcp.go
func (t *linkTCP) tcpContext(network, address string, c syscall.RawConn) error { func (t *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 return nil
} }

View File

@ -1,20 +1,11 @@
package core package core
import ( import (
"bytes"
"context" "context"
"crypto/rand"
"crypto/tls" "crypto/tls"
"crypto/x509"
"crypto/x509/pkix"
"encoding/hex"
"encoding/pem"
"fmt" "fmt"
"math/big"
"net" "net"
"net/url" "net/url"
"strings"
"time"
"github.com/Arceliar/phony" "github.com/Arceliar/phony"
) )
@ -36,49 +27,38 @@ func (l *links) newLinkTLS(tcp *linkTCP) *linkTLS {
Control: tcp.tcpContext, Control: tcp.tcpContext,
KeepAlive: -1, KeepAlive: -1,
}, },
config: l.core.config.tls.Clone(),
_listeners: map[*Listener]context.CancelFunc{}, _listeners: map[*Listener]context.CancelFunc{},
} }
var err error
lt.config, err = lt.generateConfig()
if err != nil {
panic(err)
}
return lt return lt
} }
func (l *linkTLS) dial(url *url.URL, options linkOptions, sintf, sni string) error { func (l *linkTLS) dial(ctx context.Context, url *url.URL, info linkInfo, options linkOptions) (net.Conn, error) {
addr, err := net.ResolveTCPAddr("tcp", url.Host) dialers, err := l.tcp.dialersFor(url, info)
if err != nil { if err != nil {
return err return nil, err
} }
dialer, err := l.tcp.dialerFor(addr, sintf) if len(dialers) == 0 {
if err != nil { return nil, nil
return err
}
info := linkInfoFor("tls", sintf, tcpIDFor(dialer.LocalAddr, addr))
if l.links.isConnectedTo(info) {
return nil
} }
for _, d := range dialers {
tlsconfig := l.config.Clone() tlsconfig := l.config.Clone()
tlsconfig.ServerName = sni tlsconfig.ServerName = options.tlsSNI
tlsdialer := &tls.Dialer{ tlsdialer := &tls.Dialer{
NetDialer: dialer, NetDialer: d.dialer,
Config: tlsconfig, Config: tlsconfig,
} }
conn, err := tlsdialer.DialContext(l.core.ctx, "tcp", addr.String()) var conn net.Conn
conn, err = tlsdialer.DialContext(ctx, "tcp", d.addr.String())
if err != nil { if err != nil {
return err continue
} }
name := strings.TrimRight(strings.SplitN(url.String(), "?", 2)[0], "/") return conn, nil
dial := &linkDial{
url: url,
sintf: sintf,
} }
return l.handler(dial, name, info, conn, options, false, false) return nil, err
} }
func (l *linkTLS) listen(url *url.URL, sintf string) (*Listener, error) { func (l *linkTLS) listen(ctx context.Context, url *url.URL, sintf string) (net.Listener, error) {
ctx, cancel := context.WithCancel(l.core.ctx)
hostport := url.Host hostport := url.Host
if sintf != "" { if sintf != "" {
if host, port, err := net.SplitHostPort(hostport); err == nil { if host, port, err := net.SplitHostPort(hostport); err == nil {
@ -87,88 +67,8 @@ func (l *linkTLS) listen(url *url.URL, sintf string) (*Listener, error) {
} }
listener, err := l.listener.Listen(ctx, "tcp", hostport) listener, err := l.listener.Listen(ctx, "tcp", hostport)
if err != nil { if err != nil {
cancel()
return nil, err return nil, err
} }
tlslistener := tls.NewListener(listener, l.config) tlslistener := tls.NewListener(listener, l.config)
entry := &Listener{ return tlslistener, nil
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(nil, 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
}
// RFC5280 section 4.1.2.5
var notAfterNeverExpires = time.Date(9999, time.December, 31, 23, 59, 59, 0, time.UTC)
func (l *linkTLS) generateConfig() (*tls.Config, error) {
certBuf := &bytes.Buffer{}
cert := x509.Certificate{
SerialNumber: big.NewInt(1),
Subject: pkix.Name{
CommonName: hex.EncodeToString(l.links.core.public[:]),
},
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, 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(dial *linkDial, name string, info linkInfo, conn net.Conn, options linkOptions, incoming, force bool) error {
return l.tcp.handler(dial, name, info, conn, options, incoming, force)
} }

View File

@ -32,70 +32,14 @@ func (l *links) newLinkUNIX() *linkUNIX {
return lt return lt
} }
func (l *linkUNIX) dial(url *url.URL, options linkOptions, _ string) error { func (l *linkUNIX) dial(ctx context.Context, url *url.URL, info linkInfo, options linkOptions) (net.Conn, error) {
info := linkInfoFor("unix", "", url.Path)
if l.links.isConnectedTo(info) {
return nil
}
addr, err := net.ResolveUnixAddr("unix", url.Path) addr, err := net.ResolveUnixAddr("unix", url.Path)
if err != nil { if err != nil {
return err
}
conn, err := l.dialer.DialContext(l.core.ctx, "unix", addr.String())
if err != nil {
return err
}
dial := &linkDial{
url: url,
}
return l.handler(dial, url.String(), info, conn, options, false)
}
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 return nil, err
} }
entry := &Listener{ return l.dialer.DialContext(ctx, "unix", addr.String())
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(nil, 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(dial *linkDial, name string, info linkInfo, conn net.Conn, options linkOptions, incoming bool) error { func (l *linkUNIX) listen(ctx context.Context, url *url.URL, _ string) (net.Listener, error) {
return l.links.create( return l.listener.Listen(ctx, "unix", url.Path)
conn, // connection
dial, // connection URL
name, // connection name
info, // connection info
incoming, // not incoming
false, // not forced
options, // connection options
)
} }

View File

@ -2,12 +2,18 @@ package core
import ( import (
"crypto/ed25519" "crypto/ed25519"
"fmt"
"net/url"
) )
func (c *Core) _applyOption(opt SetupOption) { func (c *Core) _applyOption(opt SetupOption) (err error) {
switch v := opt.(type) { switch v := opt.(type) {
case Peer: case Peer:
c.config._peers[v] = nil u, err := url.Parse(v.URI)
if err != nil {
return fmt.Errorf("unable to parse peering URI: %w", err)
}
return c.links.add(u, v.SourceInterface, linkTypePersistent)
case ListenAddress: case ListenAddress:
c.config._listeners[v] = struct{}{} c.config._listeners[v] = struct{}{}
case NodeInfo: case NodeInfo:
@ -19,6 +25,7 @@ func (c *Core) _applyOption(opt SetupOption) {
copy(pk[:], v) copy(pk[:], v)
c.config._allowedPublicKeys[pk] = struct{}{} c.config._allowedPublicKeys[pk] = struct{}{}
} }
return
} }
type SetupOption interface { type SetupOption interface {

17
src/core/pool.go Normal file
View File

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

29
src/core/tls.go Normal file
View File

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

78
src/core/version_test.go Normal file
View File

@ -0,0 +1,78 @@
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); 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 !decoded.decode(encoded, password) {
t.Fatalf("failed to decode")
}
if !reflect.DeepEqual(test, decoded) {
t.Fatalf("round-trip failed\nwant: %+v\n got: %+v", test, decoded)
}
}
}
}

View File

@ -1,60 +0,0 @@
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

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

View File

@ -0,0 +1,39 @@
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

@ -0,0 +1,38 @@
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,9 +4,9 @@ import (
"bytes" "bytes"
"context" "context"
"crypto/ed25519" "crypto/ed25519"
"encoding/binary"
"encoding/hex" "encoding/hex"
"fmt" "fmt"
"math/rand"
"net" "net"
"net/url" "net/url"
"time" "time"
@ -15,6 +15,7 @@ import (
"github.com/gologme/log" "github.com/gologme/log"
"github.com/yggdrasil-network/yggdrasil-go/src/core" "github.com/yggdrasil-network/yggdrasil-go/src/core"
"golang.org/x/crypto/blake2b"
"golang.org/x/net/ipv6" "golang.org/x/net/ipv6"
) )
@ -44,6 +45,8 @@ type interfaceInfo struct {
listen bool listen bool
port uint16 port uint16
priority uint8 priority uint8
password []byte
hash []byte
} }
type listenerInfo struct { type listenerInfo struct {
@ -177,6 +180,7 @@ func (m *Multicast) _getAllowedInterfaces() map[string]*interfaceInfo {
return nil return nil
} }
// Work out which interfaces to announce on // Work out which interfaces to announce on
pk := m.core.PublicKey()
for _, iface := range allifaces { for _, iface := range allifaces {
switch { switch {
case iface.Flags&net.FlagUp == 0: case iface.Flags&net.FlagUp == 0:
@ -195,12 +199,23 @@ func (m *Multicast) _getAllowedInterfaces() map[string]*interfaceInfo {
if !ifcfg.Regex.MatchString(iface.Name) { if !ifcfg.Regex.MatchString(iface.Name) {
continue 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{ interfaces[iface.Name] = &interfaceInfo{
iface: iface, iface: iface,
beacon: ifcfg.Beacon, beacon: ifcfg.Beacon,
listen: ifcfg.Listen, listen: ifcfg.Listen,
port: ifcfg.Port, port: ifcfg.Port,
priority: ifcfg.Priority, priority: ifcfg.Priority,
password: []byte(ifcfg.Password),
hash: hasher.Sum(nil),
} }
break break
} }
@ -248,7 +263,7 @@ func (m *Multicast) _announce() {
// It's possible that the link-local listener address has changed so if // It's possible that the link-local listener address has changed so if
// that is the case then we should clean up the interface listener // that is the case then we should clean up the interface listener
found := false found := false
listenaddr, err := net.ResolveTCPAddr("tcp6", info.listener.Listener.Addr().String()) listenaddr, err := net.ResolveTCPAddr("tcp6", info.listener.Addr().String())
if err != nil { if err != nil {
stop() stop()
continue continue
@ -295,12 +310,15 @@ func (m *Multicast) _announce() {
} }
// Try and see if we already have a TCP listener for this interface // Try and see if we already have a TCP listener for this interface
var linfo *listenerInfo var linfo *listenerInfo
if nfo, ok := m._listeners[iface.Name]; !ok || nfo.listener.Listener == nil { if _, ok := m._listeners[iface.Name]; !ok {
// No listener was found - let's create one // No listener was found - let's create one
urlString := fmt.Sprintf("tls://[%s]:%d", addrIP, info.port) v := &url.Values{}
u, err := url.Parse(urlString) v.Add("priority", fmt.Sprintf("%d", info.priority))
if err != nil { v.Add("password", string(info.password))
panic(err) u := &url.URL{
Scheme: "tls",
Host: net.JoinHostPort(addrIP.String(), fmt.Sprintf("%d", info.port)),
RawQuery: v.Encode(),
} }
if li, err := m.core.Listen(u, iface.Name); err == nil { if li, err := m.core.Listen(u, iface.Name); err == nil {
m.log.Debugln("Started multicasting on", iface.Name) m.log.Debugln("Started multicasting on", iface.Name)
@ -321,17 +339,21 @@ func (m *Multicast) _announce() {
if time.Since(linfo.time) < linfo.interval { if time.Since(linfo.time) < linfo.interval {
continue continue
} }
// Get the listener details and construct the multicast beacon addr := linfo.listener.Addr().(*net.TCPAddr)
lladdr := linfo.listener.Listener.Addr().String() adv := multicastAdvertisement{
if a, err := net.ResolveTCPAddr("tcp6", lladdr); err == nil { MajorVersion: core.ProtocolVersionMajor,
a.Zone = "" 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 destAddr.Zone = iface.Name
msg := append([]byte(nil), m.core.GetSelf().Key...) if _, err = m.sock.WriteTo(msg, nil, destAddr); err != nil {
msg = append(msg, a.IP...) m.log.Warn("Failed to send multicast beacon:", err)
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 { if linfo.interval.Seconds() < 15 {
linfo.interval += time.Second linfo.interval += time.Second
@ -339,7 +361,8 @@ func (m *Multicast) _announce() {
break break
} }
} }
m._timer = time.AfterFunc(time.Second, func() { annInterval := time.Second + time.Microsecond*(time.Duration(rand.Intn(1048576))) // Randomize delay
m._timer = time.AfterFunc(annInterval, func() {
m.Act(nil, m._announce) m.Act(nil, m._announce)
}) })
} }
@ -350,8 +373,9 @@ func (m *Multicast) listen() {
panic(err) panic(err)
} }
bs := make([]byte, 2048) bs := make([]byte, 2048)
hb := make([]byte, 0, blake2b.Size) // Reused to reduce hash allocations
for { for {
nBytes, rcm, fromAddr, err := m.sock.ReadFrom(bs) n, rcm, fromAddr, err := m.sock.ReadFrom(bs)
if err != nil { if err != nil {
if !m.IsStarted() { if !m.IsStarted() {
return return
@ -369,40 +393,45 @@ func (m *Multicast) listen() {
continue continue
} }
} }
if nBytes < ed25519.PublicKeySize { var adv multicastAdvertisement
if err := adv.UnmarshalBinary(bs[:n]); err != nil {
continue continue
} }
var key ed25519.PublicKey switch {
key = append(key, bs[:ed25519.PublicKeySize]...) case adv.MajorVersion != core.ProtocolVersionMajor:
if bytes.Equal(key, m.core.GetSelf().Key) { continue
continue // don't bother trying to peer with self case adv.MinorVersion != core.ProtocolVersionMinor:
} continue
begin := ed25519.PublicKeySize case adv.PublicKey.Equal(m.core.PublicKey()):
end := nBytes - 2
if end <= begin {
continue // malformed address
}
ip := bs[begin:end]
port := binary.BigEndian.Uint16(bs[end:nBytes])
anAddr := net.TCPAddr{IP: ip, Port: int(port)}
addr, err := net.ResolveTCPAddr("tcp6", anAddr.String())
if err != nil {
continue continue
} }
from := fromAddr.(*net.UDPAddr) from := fromAddr.(*net.UDPAddr)
if !from.IP.Equal(addr.IP) { from.Port = int(adv.Port)
continue
}
var interfaces map[string]*interfaceInfo var interfaces map[string]*interfaceInfo
phony.Block(m, func() { phony.Block(m, func() {
interfaces = m._interfaces interfaces = m._interfaces
}) })
if info, ok := interfaces[from.Zone]; ok && info.listen { if info, ok := interfaces[from.Zone]; ok && info.listen {
addr.Zone = "" hasher, err := blake2b.New512(info.password)
pin := fmt.Sprintf("/?key=%s&priority=%d", hex.EncodeToString(key), info.priority)
u, err := url.Parse("tls://" + addr.String() + pin)
if err != nil { if err != nil {
m.log.Debugln("Call from multicast failed, parse error:", addr.String(), err) 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(),
} }
if err := m.core.CallPeer(u, from.Zone); err != nil { if err := m.core.CallPeer(u, from.Zone); err != nil {
m.log.Debugln("Call from multicast failed:", err) m.log.Debugln("Call from multicast failed:", err)

View File

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

View File

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

View File

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

View File

@ -5,6 +5,7 @@ package tun
import ( import (
"encoding/binary" "encoding/binary"
"fmt"
"os/exec" "os/exec"
"strconv" "strconv"
"strings" "strings"
@ -85,7 +86,15 @@ func (tun *TunAdapter) setup(ifname string, addr string, mtu uint64) error {
} else { } else {
tun.mtu = 0 tun.mtu = 0
} }
if addr != "" {
return tun.setupAddress(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 { func (tun *TunAdapter) setupAddress(addr string) error {

View File

@ -1,5 +1,5 @@
//go:build !mobile //go:build darwin || ios
// +build !mobile // +build darwin ios
package tun package tun
@ -7,6 +7,7 @@ package tun
import ( import (
"encoding/binary" "encoding/binary"
"os"
"strconv" "strconv"
"strings" "strings"
"unsafe" "unsafe"
@ -31,7 +32,35 @@ func (tun *TunAdapter) setup(ifname string, addr string, mtu uint64) error {
} else { } else {
tun.mtu = 0 tun.mtu = 0
} }
if addr != "" {
return tun.setupAddress(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 err
}
err = unix.SetNonblock(dfd, true)
if err != nil {
unix.Close(dfd)
return err
}
iface, err := wgtun.CreateTUNFromFile(os.NewFile(uintptr(dfd), "/dev/tun"), 0)
if err != nil {
unix.Close(dfd)
return 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)
} }
const ( const (
@ -117,13 +146,13 @@ func (tun *TunAdapter) setupAddress(addr string) error {
tun.log.Infof("Interface IPv6: %s", addr) tun.log.Infof("Interface IPv6: %s", addr)
tun.log.Infof("Interface MTU: %d", ir.ifru_mtu) 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 { if _, _, errno := unix.Syscall(unix.SYS_IOCTL, uintptr(fd), uintptr(darwin_SIOCAIFADDR_IN6), uintptr(unsafe.Pointer(&ar))); errno != 0 { // nolint:staticcheck
err = errno err = errno
tun.log.Errorf("Error in darwin_SIOCAIFADDR_IN6: %v", errno) tun.log.Errorf("Error in darwin_SIOCAIFADDR_IN6: %v", errno)
return err return err
} }
if _, _, errno := unix.Syscall(unix.SYS_IOCTL, uintptr(fd), uintptr(unix.SIOCSIFMTU), uintptr(unsafe.Pointer(&ir))); errno != 0 { if _, _, errno := unix.Syscall(unix.SYS_IOCTL, uintptr(fd), uintptr(unix.SIOCSIFMTU), uintptr(unsafe.Pointer(&ir))); errno != 0 { // nolint:staticcheck
err = errno err = errno
tun.log.Errorf("Error in SIOCSIFMTU: %v", errno) tun.log.Errorf("Error in SIOCSIFMTU: %v", errno)
return err return err

View File

@ -1,11 +1,13 @@
//go:build !mobile //go:build linux || android
// +build !mobile // +build linux android
package tun package tun
// The linux platform specific tun parts // The linux platform specific tun parts
import ( import (
"fmt"
"github.com/vishvananda/netlink" "github.com/vishvananda/netlink"
wgtun "golang.zx2c4.com/wireguard/tun" wgtun "golang.zx2c4.com/wireguard/tun"
) )
@ -25,7 +27,15 @@ func (tun *TunAdapter) setup(ifname string, addr string, mtu uint64) error {
} else { } else {
tun.mtu = 0 tun.mtu = 0
} }
if addr != "" {
return tun.setupAddress(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")
} }
// Configures the TUN adapter with the correct IPv6 address and MTU. Netlink // Configures the TUN adapter with the correct IPv6 address and MTU. Netlink

View File

@ -1,5 +1,5 @@
//go:build !linux && !darwin && !windows && !openbsd && !freebsd && !mobile //go:build !linux && !darwin && !ios && !android && !windows && !openbsd && !freebsd && !mobile
// +build !linux,!darwin,!windows,!openbsd,!freebsd,!mobile // +build !linux,!darwin,!ios,!android,!windows,!openbsd,!freebsd,!mobile
package tun package tun
@ -7,6 +7,8 @@ package tun
// If your platform supports tun devices, you could try configuring it manually // If your platform supports tun devices, you could try configuring it manually
import ( import (
"fmt"
wgtun "golang.zx2c4.com/wireguard/tun" wgtun "golang.zx2c4.com/wireguard/tun"
) )
@ -22,7 +24,15 @@ func (tun *TunAdapter) setup(ifname string, addr string, mtu uint64) error {
} else { } else {
tun.mtu = 0 tun.mtu = 0
} }
if addr != "" {
return tun.setupAddress(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")
} }
// We don't know how to set the IPv6 address on an unknown platform, therefore // We don't know how to set the IPv6 address on an unknown platform, therefore

View File

@ -4,12 +4,12 @@
package tun package tun
import ( import (
"bytes"
"errors" "errors"
"fmt"
"log" "log"
"net" "net/netip"
"github.com/yggdrasil-network/yggdrasil-go/src/defaults" "github.com/yggdrasil-network/yggdrasil-go/src/config"
"golang.org/x/sys/windows" "golang.org/x/sys/windows"
wgtun "golang.zx2c4.com/wireguard/tun" wgtun "golang.zx2c4.com/wireguard/tun"
@ -22,7 +22,7 @@ import (
// Configures the TUN adapter with the correct IPv6 address and MTU. // Configures the TUN adapter with the correct IPv6 address and MTU.
func (tun *TunAdapter) setup(ifname string, addr string, mtu uint64) error { func (tun *TunAdapter) setup(ifname string, addr string, mtu uint64) error {
if ifname == "auto" { if ifname == "auto" {
ifname = defaults.GetDefaults().DefaultIfName ifname = config.GetDefaults().DefaultIfName
} }
return elevate.DoAsSystem(func() error { return elevate.DoAsSystem(func() error {
var err error var err error
@ -35,10 +35,12 @@ func (tun *TunAdapter) setup(ifname string, addr string, mtu uint64) error {
return err return err
} }
tun.iface = iface tun.iface = iface
if addr != "" {
if err = tun.setupAddress(addr); err != nil { if err = tun.setupAddress(addr); err != nil {
tun.log.Errorln("Failed to set up TUN address:", err) tun.log.Errorln("Failed to set up TUN address:", err)
return err return err
} }
}
if err = tun.setupMTU(getSupportedMTU(mtu)); err != nil { if err = tun.setupMTU(getSupportedMTU(mtu)); err != nil {
tun.log.Errorln("Failed to set up TUN MTU:", err) tun.log.Errorln("Failed to set up TUN MTU:", err)
return err return err
@ -50,6 +52,11 @@ func (tun *TunAdapter) setup(ifname string, addr string, mtu uint64) error {
}) })
} }
// 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. // Sets the MTU of the TUN adapter.
func (tun *TunAdapter) setupMTU(mtu uint64) error { func (tun *TunAdapter) setupMTU(mtu uint64) error {
if tun.iface == nil || tun.Name() == "" { if tun.iface == nil || tun.Name() == "" {
@ -83,13 +90,9 @@ func (tun *TunAdapter) setupAddress(addr string) error {
return errors.New("Can't configure IPv6 address as TUN adapter is not present") return errors.New("Can't configure IPv6 address as TUN adapter is not present")
} }
if intf, ok := tun.iface.(*wgtun.NativeTun); ok { if intf, ok := tun.iface.(*wgtun.NativeTun); ok {
if ipaddr, ipnet, err := net.ParseCIDR(addr); err == nil { if ipnet, err := netip.ParsePrefix(addr); err == nil {
luid := winipcfg.LUID(intf.LUID()) luid := winipcfg.LUID(intf.LUID())
addresses := append([]net.IPNet{}, net.IPNet{ addresses := []netip.Prefix{ipnet}
IP: ipaddr,
Mask: ipnet.Mask,
})
err := luid.SetIPAddressesForFamily(windows.AF_INET6, addresses) err := luid.SetIPAddressesForFamily(windows.AF_INET6, addresses)
if err == windows.ERROR_OBJECT_ALREADY_EXISTS { if err == windows.ERROR_OBJECT_ALREADY_EXISTS {
cleanupAddressesOnDisconnectedInterfaces(windows.AF_INET6, addresses) cleanupAddressesOnDisconnectedInterfaces(windows.AF_INET6, addresses)
@ -112,24 +115,13 @@ func (tun *TunAdapter) setupAddress(addr string) error {
* SPDX-License-Identifier: MIT * SPDX-License-Identifier: MIT
* Copyright (C) 2019 WireGuard LLC. All Rights Reserved. * Copyright (C) 2019 WireGuard LLC. All Rights Reserved.
*/ */
func cleanupAddressesOnDisconnectedInterfaces(family winipcfg.AddressFamily, addresses []net.IPNet) { func cleanupAddressesOnDisconnectedInterfaces(family winipcfg.AddressFamily, addresses []netip.Prefix) {
if len(addresses) == 0 { if len(addresses) == 0 {
return return
} }
includedInAddresses := func(a net.IPNet) bool { addrHash := make(map[netip.Addr]bool, len(addresses))
// TODO: this makes the whole algorithm O(n^2). But we can't stick net.IPNet in a Go hashmap. Bummer! for i := range addresses {
for _, addr := range addresses { addrHash[addresses[i].Addr()] = true
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) interfaces, err := winipcfg.GetAdaptersAddresses(family, winipcfg.GAAFlagDefault)
if err != nil { if err != nil {
@ -140,11 +132,10 @@ func cleanupAddressesOnDisconnectedInterfaces(family winipcfg.AddressFamily, add
continue continue
} }
for address := iface.FirstUnicastAddress; address != nil; address = address.Next { for address := iface.FirstUnicastAddress; address != nil; address = address.Next {
ip := address.Address.IP() if ip, _ := netip.AddrFromSlice(address.Address.IP()); addrHash[ip] {
ipnet := net.IPNet{IP: ip, Mask: net.CIDRMask(int(address.OnLinkPrefixLength), 8*len(ip))} prefix := netip.PrefixFrom(ip, int(address.OnLinkPrefixLength))
if includedInAddresses(ipnet) { log.Printf("Cleaning up stale address %s from interface %s", prefix.String(), iface.FriendlyName())
log.Printf("Cleaning up stale address %s from interface %s", ipnet.String(), iface.FriendlyName()) iface.LUID.DeleteIPAddress(prefix)
iface.LUID.DeleteIPAddress(ipnet)
} }
} }
} }