mirror of
https://github.com/yggdrasil-network/yggdrasil-go.git
synced 2024-11-23 18:15:24 +00:00
Merge pull request #1034 from yggdrasil-network/neil/futurelink2
Link refactoring, admin socket changes, TLS changes
This commit is contained in:
commit
2565cbf11b
@ -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,122 +37,15 @@ 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
|
|
||||||
// filesystem.
|
|
||||||
var conf []byte
|
|
||||||
var err error
|
|
||||||
if useconffile != "" {
|
|
||||||
// Read the file from the filesystem
|
|
||||||
conf, err = os.ReadFile(useconffile)
|
|
||||||
} else {
|
|
||||||
// Read the file from stdin.
|
|
||||||
conf, err = io.ReadAll(os.Stdin)
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
// 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
|
|
||||||
// with -genconf.
|
|
||||||
func doGenconf(isjson bool) string {
|
|
||||||
cfg := defaults.GenerateConfig()
|
|
||||||
var bs []byte
|
|
||||||
var err error
|
|
||||||
if isjson {
|
|
||||||
bs, err = json.MarshalIndent(cfg, "", " ")
|
|
||||||
} else {
|
|
||||||
bs, err = hjson.Marshal(cfg)
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
return string(bs)
|
|
||||||
}
|
|
||||||
|
|
||||||
func setLogLevel(loglevel string, logger *log.Logger) {
|
|
||||||
levels := [...]string{"error", "warn", "info", "debug", "trace"}
|
|
||||||
loglevel = strings.ToLower(loglevel)
|
|
||||||
|
|
||||||
contains := func() bool {
|
|
||||||
for _, l := range levels {
|
|
||||||
if l == loglevel {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
if !contains() { // set default log level
|
|
||||||
logger.Infoln("Loglevel parse failed. Set default level(info)")
|
|
||||||
loglevel = "info"
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, l := range levels {
|
|
||||||
logger.EnableLevel(l)
|
|
||||||
if l == loglevel {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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")
|
genconf := flag.Bool("genconf", false, "print a new config to stdout")
|
||||||
useconf := flag.Bool("useconf", false, "read HJSON/JSON config from stdin")
|
useconf := flag.Bool("useconf", false, "read HJSON/JSON config from stdin")
|
||||||
useconffile := flag.String("useconffile", "", "read HJSON/JSON config from specified file path")
|
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")
|
normaliseconf := flag.Bool("normaliseconf", false, "use in combination with either -useconf or -useconffile, outputs your configuration normalised")
|
||||||
|
exportkey := flag.Bool("exportkey", false, "use in combination with either -useconf or -useconffile, outputs your private key in PEM format")
|
||||||
|
exportcsr := flag.Bool("exportcsr", false, "use in combination with either -useconf or -useconffile, outputs your self-signed certificate request in PEM format")
|
||||||
|
exportcert := flag.Bool("exportcert", false, "use in combination with either -useconf or -useconffile, outputs your self-signed certificate in PEM format")
|
||||||
confjson := flag.Bool("json", false, "print configuration from -genconf or -normaliseconf as JSON instead of HJSON")
|
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)")
|
autoconf := flag.Bool("autoconf", false, "automatic mode (dynamic IP, peer with IPv6 neighbors)")
|
||||||
ver := flag.Bool("version", false, "prints the version of this build")
|
ver := flag.Bool("version", false, "prints the version of this build")
|
||||||
@ -168,34 +54,26 @@ func getArgs() yggArgs {
|
|||||||
getsnet := flag.Bool("subnet", false, "returns the IPv6 subnet 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")
|
loglevel := flag.String("loglevel", "info", "loglevel to enable")
|
||||||
flag.Parse()
|
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.
|
// Catch interrupts from the operating system to exit gracefully.
|
||||||
func run(args yggArgs, ctx context.Context) {
|
ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
|
||||||
|
|
||||||
|
// Capture the service being stopped on Windows.
|
||||||
|
minwinsvc.SetOnExit(cancel)
|
||||||
|
|
||||||
// Create a new logger that logs output to stdout.
|
// Create a new logger that logs output to stdout.
|
||||||
var logger *log.Logger
|
var logger *log.Logger
|
||||||
switch args.logto {
|
switch *logto {
|
||||||
case "stdout":
|
case "stdout":
|
||||||
logger = log.New(os.Stdout, "", log.Flags())
|
logger = log.New(os.Stdout, "", log.Flags())
|
||||||
|
|
||||||
case "syslog":
|
case "syslog":
|
||||||
if syslogger, err := gsyslog.NewLogger(gsyslog.LOG_NOTICE, "DAEMON", version.BuildName()); err == nil {
|
if syslogger, err := gsyslog.NewLogger(gsyslog.LOG_NOTICE, "DAEMON", version.BuildName()); err == nil {
|
||||||
logger = log.New(syslogger, "", log.Flags())
|
logger = log.New(syslogger, "", log.Flags())
|
||||||
}
|
}
|
||||||
|
|
||||||
default:
|
default:
|
||||||
if logfd, err := os.OpenFile(args.logto, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644); err == nil {
|
if logfd, err := os.OpenFile(*logto, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644); err == nil {
|
||||||
logger = log.New(logfd, "", log.Flags())
|
logger = log.New(logfd, "", log.Flags())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -203,87 +81,115 @@ func run(args yggArgs, ctx context.Context) {
|
|||||||
logger = log.New(os.Stdout, "", log.Flags())
|
logger = log.New(os.Stdout, "", log.Flags())
|
||||||
logger.Warnln("Logging defaulting to stdout")
|
logger.Warnln("Logging defaulting to stdout")
|
||||||
}
|
}
|
||||||
|
if *normaliseconf {
|
||||||
if args.normaliseconf {
|
|
||||||
setLogLevel("error", logger)
|
setLogLevel("error", logger)
|
||||||
} else {
|
} else {
|
||||||
setLogLevel(args.loglevel, logger)
|
setLogLevel(*loglevel, logger)
|
||||||
}
|
}
|
||||||
|
|
||||||
var cfg *config.NodeConfig
|
cfg := config.GenerateConfig()
|
||||||
var err error
|
var err error
|
||||||
switch {
|
switch {
|
||||||
case args.ver:
|
case *ver:
|
||||||
fmt.Println("Build name:", version.BuildName())
|
fmt.Println("Build name:", version.BuildName())
|
||||||
fmt.Println("Build version:", version.BuildVersion())
|
fmt.Println("Build version:", version.BuildVersion())
|
||||||
return
|
return
|
||||||
case args.autoconf:
|
|
||||||
|
case *autoconf:
|
||||||
// Use an autoconf-generated config, this will give us random keys and
|
// Use an autoconf-generated config, this will give us random keys and
|
||||||
// port numbers, and will use an automatically selected TUN interface.
|
// port numbers, and will use an automatically selected TUN interface.
|
||||||
cfg = defaults.GenerateConfig()
|
|
||||||
case args.useconffile != "" || args.useconf:
|
case *useconf:
|
||||||
// Read the configuration from either stdin or from the filesystem
|
if _, err := cfg.ReadFrom(os.Stdin); err != nil {
|
||||||
cfg = readConfig(logger, args.useconf, args.useconffile, args.normaliseconf)
|
panic(err)
|
||||||
// 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.
|
case *useconffile != "":
|
||||||
fmt.Println(doGenconf(args.confjson))
|
f, err := os.Open(*useconffile)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
if _, err := cfg.ReadFrom(f); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
_ = f.Close()
|
||||||
|
|
||||||
|
case *genconf:
|
||||||
|
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
|
return
|
||||||
|
|
||||||
default:
|
default:
|
||||||
// No flags were provided, therefore print the list of flags to stdout.
|
|
||||||
fmt.Println("Usage:")
|
fmt.Println("Usage:")
|
||||||
flag.PrintDefaults()
|
flag.PrintDefaults()
|
||||||
|
|
||||||
if args.getaddr || args.getsnet {
|
if *getaddr || *getsnet {
|
||||||
fmt.Println("\nError: You need to specify some config data using -useconf or -useconffile.")
|
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
|
privateKey := ed25519.PrivateKey(cfg.PrivateKey)
|
||||||
// if we don't.
|
publicKey := privateKey.Public().(ed25519.PublicKey)
|
||||||
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 {
|
switch {
|
||||||
case args.getaddr:
|
case *getaddr:
|
||||||
if key := getNodeKey(); key != nil {
|
addr := address.AddrForKey(publicKey)
|
||||||
addr := address.AddrForKey(key)
|
ip := net.IP(addr[:])
|
||||||
ip := net.IP(addr[:])
|
fmt.Println(ip.String())
|
||||||
fmt.Println(ip.String())
|
|
||||||
}
|
|
||||||
return
|
return
|
||||||
case args.getsnet:
|
|
||||||
if key := getNodeKey(); key != nil {
|
case *getsnet:
|
||||||
snet := address.SubnetForKey(key)
|
snet := address.SubnetForKey(publicKey)
|
||||||
ipnet := net.IPNet{
|
ipnet := net.IPNet{
|
||||||
IP: append(snet[:], 0, 0, 0, 0, 0, 0, 0, 0),
|
IP: append(snet[:], 0, 0, 0, 0, 0, 0, 0, 0),
|
||||||
Mask: net.CIDRMask(len(snet)*8, 128),
|
Mask: net.CIDRMask(len(snet)*8, 128),
|
||||||
}
|
|
||||||
fmt.Println(ipnet.String())
|
|
||||||
}
|
}
|
||||||
|
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
|
||||||
|
|
||||||
|
case *exportcsr:
|
||||||
|
pem, err := cfg.GenerateCertificateSigningRequest()
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
fmt.Println(string(pem))
|
||||||
|
return
|
||||||
|
|
||||||
|
case *exportcert:
|
||||||
|
pem, err := cfg.MarshalPEMCertificate()
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
fmt.Println(string(pem))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -291,10 +197,6 @@ func run(args yggArgs, ctx context.Context) {
|
|||||||
|
|
||||||
// Setup the Yggdrasil node itself.
|
// Setup the Yggdrasil node itself.
|
||||||
{
|
{
|
||||||
sk, err := hex.DecodeString(cfg.PrivateKey)
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
options := []core.SetupOption{
|
options := []core.SetupOption{
|
||||||
core.NodeInfo(cfg.NodeInfo),
|
core.NodeInfo(cfg.NodeInfo),
|
||||||
core.NodeInfoPrivacy(cfg.NodeInfoPrivacy),
|
core.NodeInfoPrivacy(cfg.NodeInfoPrivacy),
|
||||||
@ -310,6 +212,9 @@ func run(args yggArgs, ctx context.Context) {
|
|||||||
options = append(options, core.Peer{URI: peer, SourceInterface: intf})
|
options = append(options, core.Peer{URI: peer, SourceInterface: intf})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
for _, root := range cfg.RootCertificates {
|
||||||
|
options = append(options, core.RootCertificate(*root))
|
||||||
|
}
|
||||||
for _, allowed := range cfg.AllowedPublicKeys {
|
for _, allowed := range cfg.AllowedPublicKeys {
|
||||||
k, err := hex.DecodeString(allowed)
|
k, err := hex.DecodeString(allowed)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -317,7 +222,7 @@ func run(args yggArgs, ctx context.Context) {
|
|||||||
}
|
}
|
||||||
options = append(options, core.AllowedPublicKey(k[:]))
|
options = append(options, core.AllowedPublicKey(k[:]))
|
||||||
}
|
}
|
||||||
if n.core, err = core.New(sk[:], logger, options...); err != nil {
|
if n.core, err = core.New(cfg.Certificate, logger, options...); err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -369,15 +274,6 @@ func run(args yggArgs, ctx context.Context) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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.
|
// Block until we are told to shut down.
|
||||||
<-ctx.Done()
|
<-ctx.Done()
|
||||||
|
|
||||||
@ -388,21 +284,28 @@ func run(args yggArgs, ctx context.Context) {
|
|||||||
n.core.Stop()
|
n.core.Stop()
|
||||||
}
|
}
|
||||||
|
|
||||||
func main() {
|
func setLogLevel(loglevel string, logger *log.Logger) {
|
||||||
args := getArgs()
|
levels := [...]string{"error", "warn", "info", "debug", "trace"}
|
||||||
|
loglevel = strings.ToLower(loglevel)
|
||||||
|
|
||||||
// Catch interrupts from the operating system to exit gracefully.
|
contains := func() bool {
|
||||||
ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
|
for _, l := range levels {
|
||||||
|
if l == loglevel {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
// Capture the service being stopped on Windows.
|
if !contains() { // set default log level
|
||||||
minwinsvc.SetOnExit(cancel)
|
logger.Infoln("Loglevel parse failed. Set default level(info)")
|
||||||
|
loglevel = "info"
|
||||||
|
}
|
||||||
|
|
||||||
// Start the node, block and then wait for it to shut down.
|
for _, l := range levels {
|
||||||
var wg sync.WaitGroup
|
logger.EnableLevel(l)
|
||||||
wg.Add(1)
|
if l == loglevel {
|
||||||
go func() {
|
break
|
||||||
defer wg.Done()
|
}
|
||||||
run(args, ctx)
|
}
|
||||||
}()
|
|
||||||
wg.Wait()
|
|
||||||
}
|
}
|
||||||
|
@ -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,7 +38,6 @@ 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 getDHT")
|
||||||
fmt.Println(" - ", os.Args[0], "-endpoint=unix:///var/run/ygg.sock getDHT")
|
fmt.Println(" - ", os.Args[0], "-endpoint=unix:///var/run/ygg.sock getDHT")
|
||||||
@ -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
|
||||||
|
@ -174,17 +174,30 @@ 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()
|
||||||
|
@ -11,7 +11,6 @@ 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/version"
|
"github.com/yggdrasil-network/yggdrasil-go/src/version"
|
||||||
@ -44,16 +43,12 @@ func (m *Yggdrasil) StartJSON(configjson []byte) error {
|
|||||||
logger.EnableLevel("error")
|
logger.EnableLevel("error")
|
||||||
logger.EnableLevel("warn")
|
logger.EnableLevel("warn")
|
||||||
logger.EnableLevel("info")
|
logger.EnableLevel("info")
|
||||||
m.config = defaults.GenerateConfig()
|
m.config = config.GenerateConfig()
|
||||||
if err := json.Unmarshal(configjson, &m.config); err != nil {
|
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})
|
||||||
@ -70,7 +65,11 @@ 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 _, root := range m.config.RootCertificates {
|
||||||
|
options = append(options, core.RootCertificate(*root))
|
||||||
|
}
|
||||||
|
var err error
|
||||||
|
m.core, err = core.New(m.config.Certificate, logger, options...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
@ -165,7 +164,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
|
||||||
|
3
go.mod
3
go.mod
@ -8,9 +8,8 @@ require (
|
|||||||
github.com/cheggaaa/pb/v3 v3.0.8
|
github.com/cheggaaa/pb/v3 v3.0.8
|
||||||
github.com/gologme/log v1.2.0
|
github.com/gologme/log v1.2.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/vishvananda/netlink v1.1.0
|
github.com/vishvananda/netlink v1.1.0
|
||||||
golang.org/x/mobile v0.0.0-20221110043201-43a038452099
|
golang.org/x/mobile v0.0.0-20221110043201-43a038452099
|
||||||
golang.org/x/net v0.9.0
|
golang.org/x/net v0.9.0
|
||||||
|
6
go.sum
6
go.sum
@ -19,8 +19,8 @@ github.com/gologme/log v1.2.0 h1:Ya5Ip/KD6FX7uH0S31QO87nCCSucKtF44TLbTtO7V4c=
|
|||||||
github.com/gologme/log v1.2.0/go.mod h1:gq31gQ8wEHkR+WekdWsqDuf8pXTUZA9BnnzTuPz1Y9U=
|
github.com/gologme/log v1.2.0/go.mod h1:gq31gQ8wEHkR+WekdWsqDuf8pXTUZA9BnnzTuPz1Y9U=
|
||||||
github.com/hashicorp/go-syslog v1.0.0 h1:KaodqZuhUoZereWVIYmpUgZysurB1kBLX2j0MwMrUAE=
|
github.com/hashicorp/go-syslog v1.0.0 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/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/mattn/go-colorable v0.1.8 h1:c1ghPdyEDarC70ftn0y+A/Ee++9zz8ljHG1b13eJ0s8=
|
github.com/mattn/go-colorable v0.1.8 h1:c1ghPdyEDarC70ftn0y+A/Ee++9zz8ljHG1b13eJ0s8=
|
||||||
@ -32,8 +32,6 @@ github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m
|
|||||||
github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
|
github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
|
||||||
github.com/mattn/go-runewidth v0.0.13 h1:lTGmDsbAYt5DmK6OnoV7EuIF1wEIFAcxld6ypU4OSgU=
|
github.com/mattn/go-runewidth v0.0.13 h1:lTGmDsbAYt5DmK6OnoV7EuIF1wEIFAcxld6ypU4OSgU=
|
||||||
github.com/mattn/go-runewidth v0.0.13/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/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
|
@ -35,10 +35,10 @@ 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"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type handler struct {
|
type handler struct {
|
||||||
@ -309,18 +309,22 @@ func (a *AdminSocket) handleRequest(conn net.Conn) {
|
|||||||
|
|
||||||
defer conn.Close()
|
defer conn.Close()
|
||||||
|
|
||||||
defer func() {
|
/*
|
||||||
r := recover()
|
defer func() {
|
||||||
if r != nil {
|
r := recover()
|
||||||
a.log.Debugln("Admin socket error:", r)
|
if r != nil {
|
||||||
if err := encoder.Encode(&ErrorResponse{
|
fmt.Println("ERROR:", r)
|
||||||
Error: "Check your syntax and input types",
|
a.log.Debugln("Admin socket error:", r)
|
||||||
}); err != nil {
|
if err := encoder.Encode(&ErrorResponse{
|
||||||
a.log.Debugln("Admin socket JSON encode error:", err)
|
Error: "Check your syntax and input types",
|
||||||
|
}); err != nil {
|
||||||
|
fmt.Println("ERROR 2:", 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")
|
||||||
}
|
}
|
||||||
|
@ -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,31 +17,43 @@ type GetPeersResponse struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type PeerEntry struct {
|
type PeerEntry struct {
|
||||||
IPAddress string `json:"address"`
|
URI string `json:"remote,omitempty"`
|
||||||
PublicKey string `json:"key"`
|
Up bool `json:"up"`
|
||||||
Port uint64 `json:"port"`
|
Inbound bool `json:"inbound"`
|
||||||
Priority uint64 `json:"priority"`
|
IPAddress string `json:"address,omitempty"`
|
||||||
Remote string `json:"remote"`
|
PublicKey string `json:"key"`
|
||||||
RXBytes DataUnit `json:"bytes_recvd"`
|
Port uint64 `json:"port"`
|
||||||
TXBytes DataUnit `json:"bytes_sent"`
|
Priority uint64 `json:"priority"`
|
||||||
Uptime float64 `json:"uptime"`
|
RXBytes DataUnit `json:"bytes_recvd,omitempty"`
|
||||||
|
TXBytes DataUnit `json:"bytes_sent,omitempty"`
|
||||||
|
Uptime float64 `json:"uptime,omitempty"`
|
||||||
|
LastError string `json:"last_error,omitempty"`
|
||||||
|
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{
|
Port: p.Port,
|
||||||
IPAddress: net.IP(addr[:]).String(),
|
Up: p.Up,
|
||||||
PublicKey: hex.EncodeToString(p.Key),
|
Inbound: p.Inbound,
|
||||||
Port: p.Port,
|
Priority: uint64(p.Priority), // can't be uint8 thanks to gobind
|
||||||
Priority: uint64(p.Priority), // can't be uint8 thanks to gobind
|
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 {
|
||||||
|
@ -17,26 +17,45 @@ configuration option that is not provided.
|
|||||||
package config
|
package config
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"crypto/ed25519"
|
"crypto/ed25519"
|
||||||
|
"crypto/rand"
|
||||||
|
"crypto/tls"
|
||||||
|
"crypto/x509"
|
||||||
|
"crypto/x509/pkix"
|
||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
|
"encoding/json"
|
||||||
|
"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 {
|
||||||
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."`
|
PrivateKey KeyBytes `comment:"Your private key. DO NOT share this with anyone!"`
|
||||||
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."`
|
PrivateKeyPath string `json:",omitempty"`
|
||||||
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."`
|
Certificate *tls.Certificate `json:"-"`
|
||||||
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."`
|
CertificatePath string `json:",omitempty"`
|
||||||
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."`
|
RootCertificates []*x509.Certificate `json:"-"`
|
||||||
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."`
|
RootCertificatePaths []string `json:",omitempty"`
|
||||||
PublicKey string `comment:"Your public key. Your peers may ask you for this to put\ninto their AllowedPublicKeys configuration."`
|
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."`
|
||||||
PrivateKey string `comment:"Your private key. DO NOT share this with anyone!"`
|
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."`
|
||||||
IfName string `comment:"Local network interface name for TUN adapter, or \"auto\" to select\nan interface automatically, or \"none\" to run without TUN."`
|
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."`
|
||||||
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."`
|
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."`
|
||||||
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."`
|
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."`
|
||||||
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."`
|
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."`
|
||||||
|
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."`
|
||||||
|
NodeInfoPrivacy bool `comment:"By default, nodeinfo contains some defaults including the platform,\narchitecture and Yggdrasil version. These can help when surveying\nthe network and diagnosing network routing problems. Enabling\nnodeinfo privacy prevents this, so that only items specified in\n\"NodeInfo\" are sent back if specified."`
|
||||||
|
NodeInfo map[string]interface{} `comment:"Optional node info. This must be a { \"key\": \"value\", ... } map\nor set as null. This is entirely optional but, if set, is visible\nto the whole network on request."`
|
||||||
}
|
}
|
||||||
|
|
||||||
type MulticastInterfaceConfig struct {
|
type MulticastInterfaceConfig struct {
|
||||||
@ -47,14 +66,260 @@ type MulticastInterfaceConfig struct {
|
|||||||
Priority uint64 // really uint8, but gobind won't export it
|
Priority uint64 // really uint8, but gobind won't export it
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if cfg.CertificatePath != "" {
|
||||||
|
if cfg.PrivateKeyPath == "" {
|
||||||
|
return fmt.Errorf("CertificatePath requires PrivateKeyPath")
|
||||||
|
}
|
||||||
|
cfg.Certificate = nil
|
||||||
|
f, err := os.ReadFile(cfg.CertificatePath)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := cfg.UnmarshalPEMCertificate(f); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if cfg.Certificate == nil {
|
||||||
|
if err := cfg.GenerateSelfSignedCertificate(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
cfg.RootCertificates = cfg.RootCertificates[:0]
|
||||||
|
for _, path := range cfg.RootCertificatePaths {
|
||||||
|
f, err := os.ReadFile(path)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := cfg.UnmarshalRootCertificate(f); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cfg *NodeConfig) UnmarshalRootCertificate(b []byte) error {
|
||||||
|
p, _ := pem.Decode(b)
|
||||||
|
if p == nil {
|
||||||
|
return fmt.Errorf("failed to parse PEM file")
|
||||||
|
}
|
||||||
|
if p.Type != "CERTIFICATE" {
|
||||||
|
return fmt.Errorf("unexpected PEM type %q", p.Type)
|
||||||
|
}
|
||||||
|
cert, err := x509.ParseCertificate(p.Bytes)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to load X.509 keypair: %w", err)
|
||||||
|
}
|
||||||
|
if !cert.IsCA {
|
||||||
|
return fmt.Errorf("supplied root certificate is not a certificate authority")
|
||||||
|
}
|
||||||
|
cfg.RootCertificates = append(cfg.RootCertificates, cert)
|
||||||
|
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) GenerateCertificateSigningRequest() ([]byte, error) {
|
||||||
|
template := &x509.CertificateRequest{
|
||||||
|
Subject: pkix.Name{
|
||||||
|
CommonName: hex.EncodeToString(cfg.PrivateKey),
|
||||||
|
},
|
||||||
|
SignatureAlgorithm: x509.PureEd25519,
|
||||||
|
}
|
||||||
|
|
||||||
|
csrBytes, err := x509.CreateCertificateRequest(rand.Reader, template, ed25519.PrivateKey(cfg.PrivateKey))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
pemBytes := bytes.NewBuffer(nil)
|
||||||
|
if err := pem.Encode(pemBytes, &pem.Block{Type: "CERTIFICATE REQUEST", Bytes: csrBytes}); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return pemBytes.Bytes(), 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) UnmarshalPEMCertificate(b []byte) error {
|
||||||
|
tlsCert, err := tls.LoadX509KeyPair(cfg.CertificatePath, cfg.PrivateKeyPath)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to load X.509 keypair: %w", err)
|
||||||
|
}
|
||||||
|
cfg.Certificate = &tlsCert
|
||||||
|
return 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
|
||||||
}
|
}
|
||||||
|
@ -1,54 +1,54 @@
|
|||||||
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
|
/*
|
||||||
nodeConfig.NewKeys()
|
var nodeConfig NodeConfig
|
||||||
|
nodeConfig.NewKeys()
|
||||||
|
|
||||||
publicKey1, err := hex.DecodeString(nodeConfig.PublicKey)
|
publicKey1, err := hex.DecodeString(nodeConfig.PublicKey)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal("can not decode generated public key")
|
t.Fatal("can not decode generated public key")
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(publicKey1) == 0 {
|
if len(publicKey1) == 0 {
|
||||||
t.Fatal("empty public key generated")
|
t.Fatal("empty public key generated")
|
||||||
}
|
}
|
||||||
|
|
||||||
privateKey1, err := hex.DecodeString(nodeConfig.PrivateKey)
|
privateKey1, err := hex.DecodeString(nodeConfig.PrivateKey)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal("can not decode generated private key")
|
t.Fatal("can not decode generated private key")
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(privateKey1) == 0 {
|
if len(privateKey1) == 0 {
|
||||||
t.Fatal("empty private key generated")
|
t.Fatal("empty private key generated")
|
||||||
}
|
}
|
||||||
|
|
||||||
nodeConfig.NewKeys()
|
nodeConfig.NewKeys()
|
||||||
|
|
||||||
publicKey2, err := hex.DecodeString(nodeConfig.PublicKey)
|
publicKey2, err := hex.DecodeString(nodeConfig.PublicKey)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal("can not decode generated public key")
|
t.Fatal("can not decode generated public key")
|
||||||
}
|
}
|
||||||
|
|
||||||
if bytes.Equal(publicKey2, publicKey1) {
|
if bytes.Equal(publicKey2, publicKey1) {
|
||||||
t.Fatal("same public key generated")
|
t.Fatal("same public key generated")
|
||||||
}
|
}
|
||||||
|
|
||||||
privateKey2, err := hex.DecodeString(nodeConfig.PrivateKey)
|
privateKey2, err := hex.DecodeString(nodeConfig.PrivateKey)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal("can not decode generated private key")
|
t.Fatal("can not decode generated private key")
|
||||||
}
|
}
|
||||||
|
|
||||||
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
34
src/config/defaults.go
Normal 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
|
||||||
|
}
|
@ -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.
|
@ -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.
|
@ -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.
|
@ -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.
|
@ -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.
|
@ -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.
|
159
src/core/api.go
159
src/core/api.go
@ -6,9 +6,9 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"net"
|
"net"
|
||||||
"net/url"
|
"net/url"
|
||||||
"sync/atomic"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/Arceliar/ironwood/network"
|
||||||
"github.com/Arceliar/phony"
|
"github.com/Arceliar/phony"
|
||||||
"github.com/yggdrasil-network/yggdrasil-go/src/address"
|
"github.com/yggdrasil-network/yggdrasil-go/src/address"
|
||||||
)
|
)
|
||||||
@ -19,15 +19,19 @@ type SelfInfo struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type PeerInfo struct {
|
type PeerInfo struct {
|
||||||
Key ed25519.PublicKey
|
URI string
|
||||||
Root ed25519.PublicKey
|
Up bool
|
||||||
Coords []uint64
|
Inbound bool
|
||||||
Port uint64
|
LastError error
|
||||||
Priority uint8
|
LastErrorTime time.Time
|
||||||
Remote string
|
Key ed25519.PublicKey
|
||||||
RXBytes uint64
|
Root ed25519.PublicKey
|
||||||
TXBytes uint64
|
Coords []uint64
|
||||||
Uptime time.Duration
|
Port uint64
|
||||||
|
Priority uint8
|
||||||
|
RXBytes uint64
|
||||||
|
TXBytes uint64
|
||||||
|
Uptime time.Duration
|
||||||
}
|
}
|
||||||
|
|
||||||
type TreeEntryInfo struct {
|
type TreeEntryInfo struct {
|
||||||
@ -61,35 +65,39 @@ func (c *Core) GetSelf() SelfInfo {
|
|||||||
|
|
||||||
func (c *Core) GetPeers() []PeerInfo {
|
func (c *Core) GetPeers() []PeerInfo {
|
||||||
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
|
|
||||||
}
|
|
||||||
})
|
|
||||||
ps := c.PacketConn.PacketConn.Debug.GetPeers()
|
|
||||||
for _, p := range ps {
|
|
||||||
var info PeerInfo
|
|
||||||
info.Key = p.Key
|
|
||||||
info.Root = p.Root
|
|
||||||
info.Port = p.Port
|
|
||||||
info.Priority = p.Priority
|
|
||||||
if p.Conn != nil {
|
|
||||||
info.Remote = p.Conn.RemoteAddr().String()
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
if name := names[p.Conn]; name != "" {
|
|
||||||
info.Remote = name
|
|
||||||
}
|
|
||||||
}
|
|
||||||
peers = append(peers, info)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
c.links.RLock()
|
||||||
|
defer c.links.RUnlock()
|
||||||
|
for info, state := range c.links._links {
|
||||||
|
var peerinfo PeerInfo
|
||||||
|
var conn net.Conn
|
||||||
|
phony.Block(state, func() {
|
||||||
|
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 = info.linkType == linkTypeIncoming
|
||||||
|
peerinfo.RXBytes = c.rx
|
||||||
|
peerinfo.TXBytes = 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)
|
||||||
|
}
|
||||||
|
|
||||||
return peers
|
return peers
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -139,16 +147,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
|
||||||
@ -187,49 +186,34 @@ 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(uri string, sourceInterface string) error {
|
||||||
var err error
|
return fmt.Errorf("not implemented yet")
|
||||||
phony.Block(c, func() {
|
/*
|
||||||
peer := Peer{uri, sourceInterface}
|
var err error
|
||||||
linkInfo, ok := c.config._peers[peer]
|
phony.Block(c, func() {
|
||||||
if !ok {
|
peer := Peer{uri, sourceInterface}
|
||||||
err = fmt.Errorf("peer not configured")
|
linkInfo, ok := c.config._peers[peer]
|
||||||
return
|
if !ok {
|
||||||
}
|
err = fmt.Errorf("peer not configured")
|
||||||
if ok && linkInfo != nil {
|
return
|
||||||
c.links.Act(nil, func() {
|
}
|
||||||
if link := c.links._links[*linkInfo]; link != nil {
|
if ok && linkInfo != nil {
|
||||||
_ = link.close()
|
c.links.Act(nil, func() {
|
||||||
}
|
if link := c.links._links[*linkInfo]; link != nil {
|
||||||
})
|
_ = link.conn.Close()
|
||||||
}
|
}
|
||||||
delete(c.config._peers, peer)
|
})
|
||||||
})
|
}
|
||||||
return err
|
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,
|
||||||
@ -241,8 +225,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 {
|
||||||
|
114
src/core/core.go
114
src/core/core.go
@ -3,6 +3,9 @@ package core
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"crypto/ed25519"
|
"crypto/ed25519"
|
||||||
|
"crypto/tls"
|
||||||
|
"crypto/x509"
|
||||||
|
"encoding/hex"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"net"
|
"net"
|
||||||
@ -36,7 +39,9 @@ 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
|
||||||
|
roots *x509.CertPool // 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
|
||||||
@ -45,48 +50,84 @@ type Core struct {
|
|||||||
pathNotify func(ed25519.PublicKey)
|
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.
|
var err error
|
||||||
if len(secret) != ed25519.PrivateKeySize {
|
c.config._listeners = map[ListenAddress]struct{}{}
|
||||||
|
c.config._allowedPublicKeys = map[[32]byte]struct{}{}
|
||||||
|
for _, opt := range opts {
|
||||||
|
switch opt.(type) {
|
||||||
|
case Peer, ListenAddress:
|
||||||
|
// We can't do peers yet as the links aren't set up.
|
||||||
|
continue
|
||||||
|
default:
|
||||||
|
if err = c._applyOption(opt); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to apply configuration option %T: %w", opt, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if cert == nil || cert.PrivateKey == nil {
|
||||||
|
return nil, fmt.Errorf("no private key supplied")
|
||||||
|
}
|
||||||
|
var ok bool
|
||||||
|
if c.secret, ok = cert.PrivateKey.(ed25519.PrivateKey); !ok {
|
||||||
|
return nil, fmt.Errorf("private key must be ed25519")
|
||||||
|
}
|
||||||
|
if len(c.secret) != ed25519.PrivateKeySize {
|
||||||
return nil, fmt.Errorf("private key is incorrect length")
|
return nil, fmt.Errorf("private key is incorrect length")
|
||||||
}
|
}
|
||||||
c.secret = make(ed25519.PrivateKey, ed25519.PrivateKeySize)
|
c.public = c.secret.Public().(ed25519.PublicKey)
|
||||||
copy(c.secret, secret)
|
|
||||||
c.public = secret.Public().(ed25519.PublicKey)
|
if c.config.tls, err = c.generateTLSConfig(cert); err != nil {
|
||||||
var err error
|
return nil, fmt.Errorf("error generating TLS config: %w", err)
|
||||||
|
}
|
||||||
keyXform := func(key ed25519.PublicKey) ed25519.PublicKey {
|
keyXform := func(key ed25519.PublicKey) ed25519.PublicKey {
|
||||||
return address.SubnetForKey(key).GetKey()
|
return address.SubnetForKey(key).GetKey()
|
||||||
}
|
}
|
||||||
if c.PacketConn, err = iwe.NewPacketConn(c.secret,
|
if c.PacketConn, err = iwe.NewPacketConn(
|
||||||
|
c.secret,
|
||||||
iwn.WithBloomTransform(keyXform),
|
iwn.WithBloomTransform(keyXform),
|
||||||
iwn.WithPeerMaxMessageSize(65535*2),
|
iwn.WithPeerMaxMessageSize(65535*2),
|
||||||
iwn.WithPathNotify(c.doPathNotify),
|
iwn.WithPathNotify(c.doPathNotify),
|
||||||
); err != nil {
|
); err != nil {
|
||||||
return nil, fmt.Errorf("error creating encryption: %w", err)
|
return nil, fmt.Errorf("error creating encryption: %w", err)
|
||||||
}
|
}
|
||||||
c.config._peers = map[Peer]*linkInfo{}
|
address, subnet := c.Address(), c.Subnet()
|
||||||
c.config._listeners = map[ListenAddress]struct{}{}
|
c.log.Infof("Your public key is %s", hex.EncodeToString(c.public))
|
||||||
c.config._allowedPublicKeys = map[[32]byte]struct{}{}
|
c.log.Infof("Your IPv6 address is %s", address.String())
|
||||||
for _, opt := range opts {
|
c.log.Infof("Your IPv6 subnet is %s", subnet.String())
|
||||||
c._applyOption(opt)
|
if c.config.roots != nil {
|
||||||
}
|
c.log.Println("Yggdrasil is running in TLS-only mode")
|
||||||
if c.log == nil {
|
|
||||||
c.log = log.New(io.Discard, "", 0)
|
|
||||||
}
|
}
|
||||||
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)
|
||||||
}
|
}
|
||||||
@ -100,42 +141,11 @@ 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
|
|
||||||
// configure them. The loop ensures that disconnected peers will eventually
|
|
||||||
// be reconnected with.
|
|
||||||
func (c *Core) _addPeerLoop() {
|
|
||||||
select {
|
|
||||||
case <-c.ctx.Done():
|
|
||||||
return
|
|
||||||
default:
|
|
||||||
}
|
|
||||||
// Add peers from the Peers section
|
|
||||||
for peer := range c.config._peers {
|
|
||||||
go func(peer string, intf string) {
|
|
||||||
u, err := url.Parse(peer)
|
|
||||||
if err != nil {
|
|
||||||
c.log.Errorln("Failed to parse peer url:", peer, err)
|
|
||||||
}
|
|
||||||
if err := c.CallPeer(u, intf); err != nil {
|
|
||||||
c.log.Errorln("Failed to add peer:", err)
|
|
||||||
}
|
|
||||||
}(peer.URI, peer.SourceInterface) // TODO: this should be acted and not in a goroutine?
|
|
||||||
}
|
|
||||||
|
|
||||||
c.addPeerTimer = time.AfterFunc(time.Minute, func() {
|
|
||||||
c.Act(nil, c._addPeerLoop)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Core) RetryPeersNow() {
|
func (c *Core) RetryPeersNow() {
|
||||||
if c.addPeerTimer != nil && !c.addPeerTimer.Stop() {
|
// TODO: figure out a way to retrigger peer connections.
|
||||||
<-c.addPeerTimer.C
|
|
||||||
}
|
|
||||||
c.Act(nil, c._addPeerLoop)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Stop shuts down the Yggdrasil node.
|
// Stop shuts down the Yggdrasil node.
|
||||||
@ -159,6 +169,10 @@ func (c *Core) _close() error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *Core) isTLSOnly() bool {
|
||||||
|
return c.config.roots != nil
|
||||||
|
}
|
||||||
|
|
||||||
func (c *Core) MTU() uint64 {
|
func (c *Core) MTU() uint64 {
|
||||||
const sessionTypeOverhead = 1
|
const sessionTypeOverhead = 1
|
||||||
MTU := c.PacketConn.MTU() - sessionTypeOverhead
|
MTU := c.PacketConn.MTU() - sessionTypeOverhead
|
||||||
|
@ -2,7 +2,6 @@ package core
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"crypto/ed25519"
|
|
||||||
"math/rand"
|
"math/rand"
|
||||||
"net/url"
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
@ -10,6 +9,7 @@ import (
|
|||||||
"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)
|
||||||
|
|
||||||
@ -76,7 +87,7 @@ func WaitConnected(nodeA, nodeB *Core) bool {
|
|||||||
}
|
}
|
||||||
*/
|
*/
|
||||||
if len(nodeA.GetTree()) > 1 && len(nodeB.GetTree()) > 1 {
|
if len(nodeA.GetTree()) > 1 && len(nodeB.GetTree()) > 1 {
|
||||||
time.Sleep(3*time.Second) // FIXME hack, there's still stuff happening internally
|
time.Sleep(3 * time.Second) // FIXME hack, there's still stuff happening internally
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
568
src/core/link.go
568
src/core/link.go
@ -2,14 +2,18 @@ package core
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"context"
|
||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
|
"math"
|
||||||
"net"
|
"net"
|
||||||
|
"net/netip"
|
||||||
"net/url"
|
"net/url"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
"sync"
|
||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@ -17,51 +21,70 @@ import (
|
|||||||
"github.com/yggdrasil-network/yggdrasil-go/src/address"
|
"github.com/yggdrasil-network/yggdrasil-go/src/address"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type linkType int
|
||||||
|
|
||||||
|
const (
|
||||||
|
linkTypePersistent linkType = iota // Statically configured
|
||||||
|
linkTypeEphemeral // Multicast discovered
|
||||||
|
linkTypeIncoming // Incoming connection
|
||||||
|
)
|
||||||
|
|
||||||
type links struct {
|
type links struct {
|
||||||
phony.Inbox
|
core *Core
|
||||||
core *Core
|
tcp *linkTCP // TCP interface support
|
||||||
tcp *linkTCP // TCP interface support
|
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
|
sync.RWMutex // Protects the below
|
||||||
_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(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
|
linkType linkType // Type of link, i.e. outbound/inbound, persistent/ephemeral
|
||||||
}
|
|
||||||
|
|
||||||
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
|
phony.Inbox
|
||||||
links *links
|
ctx context.Context //
|
||||||
conn *linkConn
|
cancel context.CancelFunc //
|
||||||
options linkOptions
|
kick chan struct{} // Attempt to reconnect now, if backing off
|
||||||
info linkInfo
|
info linkInfo //
|
||||||
incoming bool
|
linkProto string // Protocol carrier of link, e.g. TCP, AWDL
|
||||||
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
|
||||||
}
|
}
|
||||||
|
|
||||||
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
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -103,197 +126,299 @@ func (l *links) shutdown() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (l *links) isConnectedTo(info linkInfo) bool {
|
func (l *links) isConnectedTo(info linkInfo) bool {
|
||||||
var isConnected bool
|
l.RLock()
|
||||||
phony.Block(l, func() {
|
link, ok := l._links[info]
|
||||||
_, isConnected = l._links[info]
|
l.RUnlock()
|
||||||
})
|
if !ok {
|
||||||
return isConnected
|
return false
|
||||||
|
}
|
||||||
|
return link._conn != nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (l *links) call(u *url.URL, sintf string, errch chan<- error) (info linkInfo, err error) {
|
type linkError string
|
||||||
info = linkInfoFor(u.Scheme, sintf, u.Host)
|
|
||||||
if l.isConnectedTo(info) {
|
func (e linkError) Error() string { return string(e) }
|
||||||
if errch != nil {
|
|
||||||
close(errch) // already connected, no error
|
const ErrLinkAlreadyConfigured = linkError("peer is already configured")
|
||||||
|
const ErrLinkPriorityInvalid = linkError("priority value is invalid")
|
||||||
|
const ErrLinkPinnedKeyInvalid = linkError("pinned public key is invalid")
|
||||||
|
const ErrLinkUnrecognisedSchema = linkError("link schema unknown")
|
||||||
|
|
||||||
|
func (l *links) add(u *url.URL, sintf string, linkType linkType) error {
|
||||||
|
// 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,
|
||||||
|
linkType: linkType,
|
||||||
|
}
|
||||||
|
l.RLock()
|
||||||
|
state, ok := l._links[info]
|
||||||
|
l.RUnlock()
|
||||||
|
if ok && state != nil {
|
||||||
|
select {
|
||||||
|
case state.kick <- struct{}{}:
|
||||||
|
default:
|
||||||
}
|
}
|
||||||
return info, nil
|
return ErrLinkAlreadyConfigured
|
||||||
}
|
}
|
||||||
options := linkOptions{
|
|
||||||
pinnedEd25519Keys: map[keyArray]struct{}{},
|
// 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.
|
||||||
|
ctx, cancel := context.WithCancel(l.core.ctx)
|
||||||
|
state = &link{
|
||||||
|
info: info,
|
||||||
|
linkProto: strings.ToUpper(u.Scheme),
|
||||||
|
ctx: ctx,
|
||||||
|
cancel: cancel,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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 {
|
return ErrLinkPinnedKeyInvalid
|
||||||
close(errch)
|
|
||||||
}
|
|
||||||
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 {
|
return ErrLinkPriorityInvalid
|
||||||
close(errch)
|
|
||||||
}
|
|
||||||
return info, fmt.Errorf("priority invalid: %w", err)
|
|
||||||
}
|
}
|
||||||
options.priority = uint8(pi)
|
options.priority = uint8(pi)
|
||||||
}
|
}
|
||||||
switch info.linkType {
|
|
||||||
case "tcp":
|
|
||||||
go func() {
|
|
||||||
if errch != nil {
|
|
||||||
defer close(errch)
|
|
||||||
}
|
|
||||||
if err := l.tcp.dial(u, options, sintf); err != nil && err != io.EOF {
|
|
||||||
l.core.log.Warnf("Failed to dial TCP %s: %s\n", u.Host, err)
|
|
||||||
if errch != nil {
|
|
||||||
errch <- err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
case "socks":
|
// Store the state of the link, try to connect and then run
|
||||||
go func() {
|
// the handler.
|
||||||
if errch != nil {
|
l.Lock()
|
||||||
defer close(errch)
|
l._links[info] = state
|
||||||
}
|
l.Unlock()
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
case "tls":
|
// Track how many consecutive connection failures we have had,
|
||||||
// SNI headers must contain hostnames and not IP addresses, so we must make sure
|
// as we will back off exponentially rather than hammering the
|
||||||
// that we do not populate the SNI with an IP literal. We do this by splitting
|
// remote node endlessly.
|
||||||
// the host-port combo from the query option and then seeing if it parses to an
|
var backoff int
|
||||||
// IP address successfully or not.
|
|
||||||
var tlsSNI string
|
// backoffNow is called when there's a connection error. It
|
||||||
if sni := u.Query().Get("sni"); sni != "" {
|
// will wait for the specified amount of time and then return
|
||||||
if net.ParseIP(sni) == nil {
|
// true, unless the peering context was cancelled (due to a
|
||||||
tlsSNI = sni
|
// 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 <-time.After(duration):
|
||||||
|
return true
|
||||||
|
case <-state.kick:
|
||||||
|
return true
|
||||||
|
case <-ctx.Done():
|
||||||
|
return false
|
||||||
}
|
}
|
||||||
// If the SNI is not configured still because the above failed then we'll try
|
|
||||||
// again but this time we'll use the host part of the peering URI instead.
|
|
||||||
if tlsSNI == "" {
|
|
||||||
if host, _, err := net.SplitHostPort(u.Host); err == nil && net.ParseIP(host) == nil {
|
|
||||||
tlsSNI = host
|
|
||||||
}
|
|
||||||
}
|
|
||||||
go func() {
|
|
||||||
if errch != nil {
|
|
||||||
defer close(errch)
|
|
||||||
}
|
|
||||||
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":
|
|
||||||
go func() {
|
|
||||||
if errch != nil {
|
|
||||||
defer close(errch)
|
|
||||||
}
|
|
||||||
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:
|
|
||||||
if errch != nil {
|
|
||||||
close(errch)
|
|
||||||
}
|
|
||||||
return info, errors.New("unknown call scheme: " + u.Scheme)
|
|
||||||
}
|
}
|
||||||
return info, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l *links) listen(u *url.URL, sintf string) (*Listener, error) {
|
// The goroutine is responsible for attempting the connection
|
||||||
var listener *Listener
|
// and then running the handler. If the connection is persistent
|
||||||
var err error
|
// then the loop will run endlessly, using backoffs as needed.
|
||||||
switch u.Scheme {
|
// Otherwise the loop will end, cleaning up the link entry.
|
||||||
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() {
|
go func() {
|
||||||
if err := intf.handler(dial); err != nil {
|
defer func() {
|
||||||
l.core.log.Errorf("Link handler %s error (%s): %s", name, conn.RemoteAddr(), err)
|
l.Lock()
|
||||||
|
defer l.Unlock()
|
||||||
|
delete(l._links, info)
|
||||||
|
}()
|
||||||
|
for {
|
||||||
|
conn, err := l.connect(u, info, options)
|
||||||
|
if err != nil {
|
||||||
|
if linkType == linkTypePersistent {
|
||||||
|
phony.Block(state, func() {
|
||||||
|
state._err = err
|
||||||
|
state._errtime = time.Now()
|
||||||
|
})
|
||||||
|
if backoffNow() {
|
||||||
|
continue
|
||||||
|
} else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
lc := &linkConn{
|
||||||
|
Conn: conn,
|
||||||
|
up: time.Now(),
|
||||||
|
}
|
||||||
|
phony.Block(state, func() {
|
||||||
|
state._conn = lc
|
||||||
|
state._err = nil
|
||||||
|
state._errtime = time.Time{}
|
||||||
|
})
|
||||||
|
if err = l.handler(&info, options, lc); err != nil && err != io.EOF {
|
||||||
|
l.core.log.Debugf("Link %s error: %s\n", info.uri, err)
|
||||||
|
} else {
|
||||||
|
backoff = 0
|
||||||
|
}
|
||||||
|
_ = conn.Close()
|
||||||
|
phony.Block(state, func() {
|
||||||
|
state._conn = nil
|
||||||
|
if state._err = err; state._err != nil {
|
||||||
|
state._errtime = time.Now()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
if linkType == linkTypePersistent {
|
||||||
|
if backoffNow() {
|
||||||
|
continue
|
||||||
|
} else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
break
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (intf *link) handler(dial *linkDial) error {
|
func (l *links) listen(u *url.URL, sintf string) (*Listener, error) {
|
||||||
defer intf.conn.Close() // nolint:errcheck
|
ctx, cancel := context.WithCancel(l.core.ctx)
|
||||||
|
var protocol linkProtocol
|
||||||
// Don't connect to this link more than once.
|
switch strings.ToLower(u.Scheme) {
|
||||||
if intf.links.isConnectedTo(intf.info) {
|
case "tcp":
|
||||||
return nil
|
protocol = l.tcp
|
||||||
|
case "tls":
|
||||||
|
protocol = l.tls
|
||||||
|
case "unix":
|
||||||
|
protocol = l.unix
|
||||||
|
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,
|
||||||
|
}
|
||||||
|
go func() {
|
||||||
|
l.core.log.Printf("%s listener started on %s", strings.ToUpper(u.Scheme), listener.Addr())
|
||||||
|
defer l.core.log.Printf("%s listener stopped on %s", strings.ToUpper(u.Scheme), listener.Addr())
|
||||||
|
for {
|
||||||
|
conn, err := listener.Accept()
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
go func(conn net.Conn) {
|
||||||
|
defer conn.Close()
|
||||||
|
pu := *u
|
||||||
|
pu.Host = conn.RemoteAddr().String()
|
||||||
|
lu := urlForLinkInfo(pu)
|
||||||
|
info := linkInfo{
|
||||||
|
uri: lu.String(),
|
||||||
|
sintf: sintf,
|
||||||
|
linkType: linkTypeEphemeral, // TODO: should be incoming
|
||||||
|
}
|
||||||
|
if l.isConnectedTo(info) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
l.RLock()
|
||||||
|
state, ok := l._links[info]
|
||||||
|
l.RUnlock()
|
||||||
|
if !ok || state == nil {
|
||||||
|
state = &link{
|
||||||
|
info: info,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
lc := &linkConn{
|
||||||
|
Conn: conn,
|
||||||
|
up: time.Now(),
|
||||||
|
}
|
||||||
|
var options linkOptions
|
||||||
|
phony.Block(state, func() {
|
||||||
|
state._conn = lc
|
||||||
|
state._err = nil
|
||||||
|
state.linkProto = strings.ToUpper(u.Scheme)
|
||||||
|
})
|
||||||
|
l.Lock()
|
||||||
|
l._links[info] = state
|
||||||
|
l.Unlock()
|
||||||
|
if err = l.handler(&info, options, lc); err != nil && err != io.EOF {
|
||||||
|
l.core.log.Debugf("Link %s error: %s\n", u.Host, err)
|
||||||
|
}
|
||||||
|
l.Lock()
|
||||||
|
delete(l._links, info)
|
||||||
|
l.Unlock()
|
||||||
|
}(conn)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
return li, nil
|
||||||
|
}
|
||||||
|
|
||||||
// Mark the connection as in progress.
|
func (l *links) connect(u *url.URL, info linkInfo, options linkOptions) (net.Conn, error) {
|
||||||
phony.Block(intf.links, func() {
|
var dialer linkProtocol
|
||||||
intf.links._links[intf.info] = nil
|
switch strings.ToLower(u.Scheme) {
|
||||||
})
|
case "tcp":
|
||||||
|
dialer = l.tcp
|
||||||
// When we're done, clean up the connection entry.
|
case "tls":
|
||||||
defer phony.Block(intf.links, func() {
|
// SNI headers must contain hostnames and not IP addresses, so we must make sure
|
||||||
delete(intf.links._links, intf.info)
|
// that we do not populate the SNI with an IP literal. We do this by splitting
|
||||||
})
|
// the host-port combo from the query option and then seeing if it parses to an
|
||||||
|
// IP address successfully or not.
|
||||||
|
if sni := u.Query().Get("sni"); sni != "" {
|
||||||
|
if net.ParseIP(sni) == nil {
|
||||||
|
options.tlsSNI = sni
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// If the SNI is not configured still because the above failed then we'll try
|
||||||
|
// again but this time we'll use the host part of the peering URI instead.
|
||||||
|
if options.tlsSNI == "" {
|
||||||
|
if host, _, err := net.SplitHostPort(u.Host); err == nil && net.ParseIP(host) == nil {
|
||||||
|
options.tlsSNI = host
|
||||||
|
}
|
||||||
|
}
|
||||||
|
dialer = l.tls
|
||||||
|
case "socks":
|
||||||
|
dialer = l.socks
|
||||||
|
case "unix":
|
||||||
|
dialer = l.unix
|
||||||
|
default:
|
||||||
|
return nil, ErrLinkUnrecognisedSchema
|
||||||
|
}
|
||||||
|
return dialer.dial(u, info, options)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *links) handler(info *linkInfo, options linkOptions, conn net.Conn) error {
|
||||||
meta := version_getBaseMetadata()
|
meta := version_getBaseMetadata()
|
||||||
meta.publicKey = intf.links.core.public
|
meta.publicKey = l.core.public
|
||||||
metaBytes := meta.encode()
|
metaBytes := meta.encode()
|
||||||
if err := intf.conn.SetDeadline(time.Now().Add(time.Second * 6)); err != nil {
|
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 {
|
if _, err = io.ReadFull(conn, metaBytes); err != nil {
|
||||||
return fmt.Errorf("read handshake: %w", err)
|
return fmt.Errorf("read handshake: %w", err)
|
||||||
}
|
}
|
||||||
if err = intf.conn.SetDeadline(time.Time{}); err != nil {
|
if err = conn.SetDeadline(time.Time{}); err != nil {
|
||||||
return fmt.Errorf("failed to clear handshake deadline: %w", err)
|
return fmt.Errorf("failed to clear handshake deadline: %w", err)
|
||||||
}
|
}
|
||||||
meta = version_metadata{}
|
meta = version_metadata{}
|
||||||
@ -302,23 +427,14 @@ func (intf *link) handler(dial *linkDial) error {
|
|||||||
return errors.New("failed to decode metadata")
|
return errors.New("failed to decode metadata")
|
||||||
}
|
}
|
||||||
if !meta.check() {
|
if !meta.check() {
|
||||||
var connectError string
|
return fmt.Errorf("remote node incompatible version (local %s, remote %s)",
|
||||||
if intf.incoming {
|
|
||||||
connectError = "Rejected incoming connection"
|
|
||||||
} else {
|
|
||||||
connectError = "Failed to connect"
|
|
||||||
}
|
|
||||||
intf.links.core.log.Debugf("%s: %s is incompatible version (local %s, remote %s)",
|
|
||||||
connectError,
|
|
||||||
intf.lname,
|
|
||||||
fmt.Sprintf("%d.%d", base.majorVer, base.minorVer),
|
fmt.Sprintf("%d.%d", base.majorVer, base.minorVer),
|
||||||
fmt.Sprintf("%d.%d", meta.majorVer, meta.minorVer),
|
fmt.Sprintf("%d.%d", meta.majorVer, meta.minorVer),
|
||||||
)
|
)
|
||||||
return errors.New("remote node is incompatible version")
|
|
||||||
}
|
}
|
||||||
// Check if the remote side matches the keys we expected. This is a bit of a weak
|
// Check 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.publicKey)
|
copy(key[:], meta.publicKey)
|
||||||
if _, allowed := pinned[key]; !allowed {
|
if _, allowed := pinned[key]; !allowed {
|
||||||
@ -326,7 +442,10 @@ func (intf *link) handler(dial *linkDial) error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
// 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.publicKey) {
|
if bytes.Equal(k[:], meta.publicKey) {
|
||||||
@ -334,71 +453,45 @@ func (intf *link) handler(dial *linkDial) error {
|
|||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if intf.incoming && !intf.force && !isallowed {
|
if info.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.publicKey))
|
||||||
}
|
}
|
||||||
|
|
||||||
phony.Block(intf.links, func() {
|
|
||||||
intf.links._links[intf.info] = intf
|
|
||||||
})
|
|
||||||
|
|
||||||
dir := "outbound"
|
dir := "outbound"
|
||||||
if intf.incoming {
|
if info.linkType == linkTypeIncoming {
|
||||||
dir = "inbound"
|
dir = "inbound"
|
||||||
}
|
}
|
||||||
remoteAddr := net.IP(address.AddrForKey(meta.publicKey)[:]).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",
|
l.core.log.Infof("Connected %s: %s, source %s",
|
||||||
dir, strings.ToUpper(intf.info.linkType), remoteStr, localStr)
|
dir, remoteStr, localStr)
|
||||||
|
|
||||||
err = intf.links.core.HandleConn(meta.publicKey, intf.conn, intf.options.priority)
|
err = l.core.HandleConn(meta.publicKey, conn, options.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 +514,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
|
|
||||||
}
|
|
||||||
|
@ -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(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.Password, _ = url.User.Password()
|
||||||
}
|
}
|
||||||
proxyAuth := &proxy.Auth{}
|
|
||||||
proxyAuth.User = url.User.Username()
|
|
||||||
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
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
@ -6,7 +6,6 @@ import (
|
|||||||
"net"
|
"net"
|
||||||
"net/url"
|
"net/url"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/Arceliar/phony"
|
"github.com/Arceliar/phony"
|
||||||
@ -15,19 +14,19 @@ 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
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -37,7 +36,7 @@ type tcpDialer struct {
|
|||||||
addr *net.TCPAddr
|
addr *net.TCPAddr
|
||||||
}
|
}
|
||||||
|
|
||||||
func (l *linkTCP) dialersFor(url *url.URL, options linkOptions, sintf string) ([]*tcpDialer, error) {
|
func (l *linkTCP) dialersFor(url *url.URL, info linkInfo) ([]*tcpDialer, error) {
|
||||||
host, p, err := net.SplitHostPort(url.Host)
|
host, p, err := net.SplitHostPort(url.Host)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@ -56,14 +55,10 @@ func (l *linkTCP) dialersFor(url *url.URL, options linkOptions, sintf string) ([
|
|||||||
IP: ip,
|
IP: ip,
|
||||||
Port: port,
|
Port: port,
|
||||||
}
|
}
|
||||||
dialer, err := l.dialerFor(addr, sintf)
|
dialer, err := l.dialerFor(addr, info.sintf)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
info := linkInfoFor("tcp", sintf, tcpIDFor(dialer.LocalAddr, addr))
|
|
||||||
if l.links.isConnectedTo(info) {
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
dialers = append(dialers, &tcpDialer{
|
dialers = append(dialers, &tcpDialer{
|
||||||
info: info,
|
info: info,
|
||||||
dialer: dialer,
|
dialer: dialer,
|
||||||
@ -73,13 +68,16 @@ func (l *linkTCP) dialersFor(url *url.URL, options linkOptions, sintf string) ([
|
|||||||
return dialers, nil
|
return dialers, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (l *linkTCP) dial(url *url.URL, options linkOptions, sintf string) error {
|
func (l *linkTCP) dial(url *url.URL, info linkInfo, options linkOptions) (net.Conn, error) {
|
||||||
dialers, err := l.dialersFor(url, options, sintf)
|
if l.core.isTLSOnly() {
|
||||||
|
return nil, fmt.Errorf("TCP peer prohibited in TLS-only mode")
|
||||||
|
}
|
||||||
|
dialers, err := l.dialersFor(url, info)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return nil, err
|
||||||
}
|
}
|
||||||
if len(dialers) == 0 {
|
if len(dialers) == 0 {
|
||||||
return nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
for _, d := range dialers {
|
for _, d := range dialers {
|
||||||
var conn net.Conn
|
var conn net.Conn
|
||||||
@ -88,86 +86,22 @@ func (l *linkTCP) dial(url *url.URL, options linkOptions, sintf string) error {
|
|||||||
l.core.log.Warnf("Failed to connect to %s: %s", d.addr, err)
|
l.core.log.Warnf("Failed to connect to %s: %s", d.addr, err)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
name := strings.TrimRight(strings.SplitN(url.String(), "?", 2)[0], "/")
|
return conn, nil
|
||||||
dial := &linkDial{
|
|
||||||
url: url,
|
|
||||||
sintf: sintf,
|
|
||||||
}
|
|
||||||
return l.handler(dial, name, d.info, conn, options, false, false)
|
|
||||||
}
|
}
|
||||||
return fmt.Errorf("failed to connect via %d address(es), last error: %w", len(dialers), err)
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (l *linkTCP) listen(url *url.URL, sintf string) (*Listener, error) {
|
func (l *linkTCP) listen(ctx context.Context, url *url.URL, sintf string) (net.Listener, error) {
|
||||||
ctx, cancel := context.WithCancel(l.core.ctx)
|
if l.core.isTLSOnly() {
|
||||||
|
return nil, fmt.Errorf("TCP listener prohibited in TLS-only mode")
|
||||||
|
}
|
||||||
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) {
|
||||||
@ -228,16 +162,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()
|
|
||||||
}
|
|
||||||
|
@ -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,27 +27,23 @@ 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(url *url.URL, info linkInfo, options linkOptions) (net.Conn, error) {
|
||||||
dialers, err := l.tcp.dialersFor(url, options, sintf)
|
dialers, err := l.tcp.dialersFor(url, info)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return nil, err
|
||||||
}
|
}
|
||||||
if len(dialers) == 0 {
|
if len(dialers) == 0 {
|
||||||
return nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
for _, d := range dialers {
|
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: d.dialer,
|
NetDialer: d.dialer,
|
||||||
Config: tlsconfig,
|
Config: tlsconfig,
|
||||||
@ -66,18 +53,12 @@ func (l *linkTLS) dial(url *url.URL, options linkOptions, sintf, sni string) err
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
name := strings.TrimRight(strings.SplitN(url.String(), "?", 2)[0], "/")
|
return conn, nil
|
||||||
dial := &linkDial{
|
|
||||||
url: url,
|
|
||||||
sintf: sintf,
|
|
||||||
}
|
|
||||||
return l.handler(dial, name, d.info, conn, options, false, false)
|
|
||||||
}
|
}
|
||||||
return fmt.Errorf("failed to connect via %d address(es), last error: %w", len(dialers), err)
|
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 {
|
||||||
@ -86,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)
|
|
||||||
}
|
}
|
||||||
|
@ -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(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(l.core.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
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
@ -2,12 +2,25 @@ package core
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"crypto/ed25519"
|
"crypto/ed25519"
|
||||||
|
"crypto/x509"
|
||||||
|
"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 RootCertificate:
|
||||||
|
cert := x509.Certificate(v)
|
||||||
|
if c.config.roots == nil {
|
||||||
|
c.config.roots = x509.NewCertPool()
|
||||||
|
}
|
||||||
|
c.config.roots.AddCert(&cert)
|
||||||
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,12 +32,14 @@ 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 {
|
||||||
isSetupOption()
|
isSetupOption()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type RootCertificate x509.Certificate
|
||||||
type ListenAddress string
|
type ListenAddress string
|
||||||
type Peer struct {
|
type Peer struct {
|
||||||
URI string
|
URI string
|
||||||
@ -34,6 +49,7 @@ type NodeInfo map[string]interface{}
|
|||||||
type NodeInfoPrivacy bool
|
type NodeInfoPrivacy bool
|
||||||
type AllowedPublicKey ed25519.PublicKey
|
type AllowedPublicKey ed25519.PublicKey
|
||||||
|
|
||||||
|
func (a RootCertificate) isSetupOption() {}
|
||||||
func (a ListenAddress) isSetupOption() {}
|
func (a ListenAddress) isSetupOption() {}
|
||||||
func (a Peer) isSetupOption() {}
|
func (a Peer) isSetupOption() {}
|
||||||
func (a NodeInfo) isSetupOption() {}
|
func (a NodeInfo) isSetupOption() {}
|
||||||
|
63
src/core/tls.go
Normal file
63
src/core/tls.go
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
package core
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/tls"
|
||||||
|
"crypto/x509"
|
||||||
|
"fmt"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (c *Core) generateTLSConfig(cert *tls.Certificate) (*tls.Config, error) {
|
||||||
|
config := &tls.Config{
|
||||||
|
Certificates: []tls.Certificate{*cert},
|
||||||
|
ClientAuth: tls.RequireAnyClientCert,
|
||||||
|
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(rawCerts [][]byte, _ [][]*x509.Certificate) error {
|
||||||
|
if c.config.roots == nil {
|
||||||
|
// If there's no certificate pool configured then we will
|
||||||
|
// accept all TLS certificates.
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if len(rawCerts) == 0 {
|
||||||
|
return fmt.Errorf("expected at least one certificate")
|
||||||
|
}
|
||||||
|
|
||||||
|
opts := x509.VerifyOptions{
|
||||||
|
Roots: c.config.roots,
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, rawCert := range rawCerts {
|
||||||
|
if i == 0 {
|
||||||
|
// The first certificate is the leaf certificate. All other
|
||||||
|
// certificates in the list are intermediates, so add them
|
||||||
|
// into the VerifyOptions.
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
cert, err := x509.ParseCertificate(rawCert)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to parse intermediate certificate: %w", err)
|
||||||
|
}
|
||||||
|
opts.Intermediates.AddCert(cert)
|
||||||
|
}
|
||||||
|
|
||||||
|
cert, err := x509.ParseCertificate(rawCerts[0])
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to parse leaf certificate: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = cert.Verify(opts)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Core) verifyTLSConnection(cs tls.ConnectionState) error {
|
||||||
|
return nil
|
||||||
|
}
|
@ -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
|
|
||||||
}
|
|
28
src/multicast/advertisement.go
Normal file
28
src/multicast/advertisement.go
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
package multicast
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/ed25519"
|
||||||
|
"encoding/binary"
|
||||||
|
"fmt"
|
||||||
|
)
|
||||||
|
|
||||||
|
type multicastAdvertisement struct {
|
||||||
|
PublicKey ed25519.PublicKey
|
||||||
|
Port uint16
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *multicastAdvertisement) MarshalBinary() ([]byte, error) {
|
||||||
|
b := make([]byte, 0, ed25519.PublicKeySize+2)
|
||||||
|
b = append(b, m.PublicKey...)
|
||||||
|
b = binary.BigEndian.AppendUint16(b, m.Port)
|
||||||
|
return b, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *multicastAdvertisement) UnmarshalBinary(b []byte) error {
|
||||||
|
if len(b) < ed25519.PublicKeySize+2 {
|
||||||
|
return fmt.Errorf("invalid multicast beacon")
|
||||||
|
}
|
||||||
|
m.PublicKey = b[:ed25519.PublicKeySize]
|
||||||
|
m.Port = binary.BigEndian.Uint16(b[ed25519.PublicKeySize:])
|
||||||
|
return nil
|
||||||
|
}
|
@ -1,10 +1,7 @@
|
|||||||
package multicast
|
package multicast
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
|
||||||
"context"
|
"context"
|
||||||
"crypto/ed25519"
|
|
||||||
"encoding/binary"
|
|
||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net"
|
"net"
|
||||||
@ -248,7 +245,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,7 +292,7 @@ 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)
|
urlString := fmt.Sprintf("tls://[%s]:%d", addrIP, info.port)
|
||||||
u, err := url.Parse(urlString)
|
u, err := url.Parse(urlString)
|
||||||
@ -321,17 +318,18 @@ 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 {
|
PublicKey: m.core.PublicKey(),
|
||||||
a.Zone = ""
|
Port: uint16(addr.Port),
|
||||||
destAddr.Zone = iface.Name
|
}
|
||||||
msg := append([]byte(nil), m.core.GetSelf().Key...)
|
msg, err := adv.MarshalBinary()
|
||||||
msg = append(msg, a.IP...)
|
if err != nil {
|
||||||
pbs := make([]byte, 2)
|
continue
|
||||||
binary.BigEndian.PutUint16(pbs, uint16(a.Port))
|
}
|
||||||
msg = append(msg, pbs...)
|
destAddr.Zone = iface.Name
|
||||||
_, _ = m.sock.WriteTo(msg, nil, destAddr)
|
if _, err = m.sock.WriteTo(msg, nil, destAddr); err != nil {
|
||||||
|
m.log.Warn("Failed to send multicast beacon:", err)
|
||||||
}
|
}
|
||||||
if linfo.interval.Seconds() < 15 {
|
if linfo.interval.Seconds() < 15 {
|
||||||
linfo.interval += time.Second
|
linfo.interval += time.Second
|
||||||
@ -351,7 +349,7 @@ func (m *Multicast) listen() {
|
|||||||
}
|
}
|
||||||
bs := make([]byte, 2048)
|
bs := make([]byte, 2048)
|
||||||
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 +367,27 @@ 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
|
if adv.PublicKey.Equal(m.core.PublicKey()) {
|
||||||
key = append(key, bs[:ed25519.PublicKeySize]...)
|
|
||||||
if bytes.Equal(key, m.core.GetSelf().Key) {
|
|
||||||
continue // don't bother trying to peer with self
|
|
||||||
}
|
|
||||||
begin := ed25519.PublicKeySize
|
|
||||||
end := nBytes - 2
|
|
||||||
if end <= begin {
|
|
||||||
continue // malformed address
|
|
||||||
}
|
|
||||||
ip := bs[begin:end]
|
|
||||||
port := binary.BigEndian.Uint16(bs[end:nBytes])
|
|
||||||
anAddr := net.TCPAddr{IP: ip, Port: int(port)}
|
|
||||||
addr, err := net.ResolveTCPAddr("tcp6", anAddr.String())
|
|
||||||
if err != nil {
|
|
||||||
continue
|
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 = ""
|
v := &url.Values{}
|
||||||
pin := fmt.Sprintf("/?key=%s&priority=%d", hex.EncodeToString(key), info.priority)
|
v.Add("key", hex.EncodeToString(adv.PublicKey))
|
||||||
u, err := url.Parse("tls://" + addr.String() + pin)
|
v.Add("priority", fmt.Sprintf("%d", info.priority))
|
||||||
if err != nil {
|
u := &url.URL{
|
||||||
m.log.Debugln("Call from multicast failed, parse error:", addr.String(), err)
|
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)
|
||||||
|
@ -14,8 +14,8 @@ import (
|
|||||||
"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"
|
"github.com/yggdrasil-network/yggdrasil-go/src/ipv6rwc"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -43,7 +43,7 @@ type TunAdapter struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 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,20 +72,20 @@ 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
|
||||||
|
@ -9,7 +9,7 @@ import (
|
|||||||
"log"
|
"log"
|
||||||
"net"
|
"net"
|
||||||
|
|
||||||
"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
|
||||||
|
Loading…
Reference in New Issue
Block a user