From 7b41cec27f6ded81efe6d96b5a645145766379de Mon Sep 17 00:00:00 2001 From: Vasyl Gello Date: Wed, 15 Nov 2023 18:58:02 +0200 Subject: [PATCH] Port "Refactor node creation" Signed-off-by: Vasyl Gello --- cmd/yggdrasil/main.go | 276 ++++++------------------------------- src/config/config.go | 82 +++++++++++ src/setup/setup.go | 309 ++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 435 insertions(+), 232 deletions(-) create mode 100644 src/setup/setup.go diff --git a/cmd/yggdrasil/main.go b/cmd/yggdrasil/main.go index a225755c..1b68d274 100644 --- a/cmd/yggdrasil/main.go +++ b/cmd/yggdrasil/main.go @@ -2,16 +2,11 @@ package main import ( "context" - "crypto/ed25519" - "encoding/hex" "encoding/json" "flag" "fmt" - "net" "os" "os/signal" - "regexp" - "strings" "syscall" "github.com/gologme/log" @@ -19,40 +14,14 @@ import ( "github.com/hjson/hjson-go/v4" "github.com/kardianos/minwinsvc" - "github.com/yggdrasil-network/yggdrasil-go/src/address" - "github.com/yggdrasil-network/yggdrasil-go/src/admin" "github.com/yggdrasil-network/yggdrasil-go/src/config" - "github.com/yggdrasil-network/yggdrasil-go/src/ipv6rwc" - - "github.com/yggdrasil-network/yggdrasil-go/src/core" - "github.com/yggdrasil-network/yggdrasil-go/src/multicast" - "github.com/yggdrasil-network/yggdrasil-go/src/tun" + "github.com/yggdrasil-network/yggdrasil-go/src/setup" "github.com/yggdrasil-network/yggdrasil-go/src/version" ) -type node struct { - core *core.Core - tun *tun.TunAdapter - multicast *multicast.Multicast - admin *admin.AdminSocket -} - // The main function is responsible for configuring and starting Yggdrasil. func main() { - genconf := flag.Bool("genconf", false, "print a new config to stdout") - useconf := flag.Bool("useconf", false, "read HJSON/JSON config from stdin") - useconffile := flag.String("useconffile", "", "read HJSON/JSON config from specified file path") - normaliseconf := flag.Bool("normaliseconf", false, "use in combination with either -useconf or -useconffile, outputs your configuration normalised") - exportkey := flag.Bool("exportkey", false, "use in combination with either -useconf or -useconffile, outputs your private key in PEM format") - confjson := flag.Bool("json", false, "print configuration from -genconf or -normaliseconf as JSON instead of HJSON") - autoconf := flag.Bool("autoconf", false, "automatic mode (dynamic IP, peer with IPv6 neighbors)") - ver := flag.Bool("version", false, "prints the version of this build") - logto := flag.String("logto", "stdout", "file path to log to, \"syslog\" or \"stdout\"") - getaddr := flag.Bool("address", false, "use in combination with either -useconf or -useconffile, outputs your IPv6 address") - getsnet := flag.Bool("subnet", false, "use in combination with either -useconf or -useconffile, outputs your IPv6 subnet") - getpkey := flag.Bool("publickey", false, "use in combination with either -useconf or -useconffile, outputs your public key") - loglevel := flag.String("loglevel", "info", "loglevel to enable") - flag.Parse() + args := setup.ParseArguments() // Catch interrupts from the operating system to exit gracefully. ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM) @@ -62,7 +31,7 @@ func main() { // Create a new logger that logs output to stdout. var logger *log.Logger - switch *logto { + switch args.LogTo { case "stdout": logger = log.New(os.Stdout, "", log.Flags()) @@ -72,7 +41,7 @@ func main() { } default: - if logfd, err := os.OpenFile(*logto, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644); err == nil { + if logfd, err := os.OpenFile(args.LogTo, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644); err == nil { logger = log.New(logfd, "", log.Flags()) } } @@ -80,227 +49,70 @@ func main() { logger = log.New(os.Stdout, "", log.Flags()) logger.Warnln("Logging defaulting to stdout") } - if *normaliseconf { - setLogLevel("error", logger) - } else { - setLogLevel(*loglevel, logger) - } - cfg := config.GenerateConfig() + var cfg *config.NodeConfig var err error switch { - case *ver: + case args.Version: fmt.Println("Build name:", version.BuildName()) fmt.Println("Build version:", version.BuildVersion()) return - case *autoconf: + case args.AutoConf: // Use an autoconf-generated config, this will give us random keys and - // port numbers, and will use an automatically selected TUN interface. + // port numbers, and will use an automatically selected TUN/TAP interface. + cfg = config.GenerateConfig() - case *useconf: - if _, err := cfg.ReadFrom(os.Stdin); err != nil { - panic(err) - } - - case *useconffile != "": - f, err := os.Open(*useconffile) - if err != nil { - panic(err) - } - if _, err := cfg.ReadFrom(f); err != nil { - panic(err) - } - _ = f.Close() - - case *genconf: - cfg.AdminListen = "" - var bs []byte - if *confjson { - bs, err = json.MarshalIndent(cfg, "", " ") - } else { - bs, err = hjson.Marshal(cfg) - } - if err != nil { - panic(err) - } - fmt.Println(string(bs)) - return - - default: - fmt.Println("Usage:") - flag.PrintDefaults() - - if *getaddr || *getsnet { - fmt.Println("\nError: You need to specify some config data using -useconf or -useconffile.") - } - return - } - - privateKey := ed25519.PrivateKey(cfg.PrivateKey) - publicKey := privateKey.Public().(ed25519.PublicKey) - - switch { - case *getaddr: - addr := address.AddrForKey(publicKey) - ip := net.IP(addr[:]) - fmt.Println(ip.String()) - return - - case *getsnet: - snet := address.SubnetForKey(publicKey) - ipnet := net.IPNet{ - IP: append(snet[:], 0, 0, 0, 0, 0, 0, 0, 0), - Mask: net.CIDRMask(len(snet)*8, 128), - } - fmt.Println(ipnet.String()) - return - - case *getpkey: - fmt.Println(hex.EncodeToString(publicKey)) - return - - case *normaliseconf: - cfg.AdminListen = "" - var bs []byte - if *confjson { - bs, err = json.MarshalIndent(cfg, "", " ") - } else { - bs, err = hjson.Marshal(cfg) - } - if err != nil { - panic(err) - } - fmt.Println(string(bs)) - return - - case *exportkey: - pem, err := cfg.MarshalPEMPrivateKey() - if err != nil { - panic(err) - } - fmt.Println(string(pem)) - return - } - - n := &node{} - - // Setup the Yggdrasil node itself. - { - options := []core.SetupOption{ - core.NodeInfo(cfg.NodeInfo), - core.NodeInfoPrivacy(cfg.NodeInfoPrivacy), - } - for _, addr := range cfg.Listen { - options = append(options, core.ListenAddress(addr)) - } - for _, peer := range cfg.Peers { - options = append(options, core.Peer{URI: peer}) - } - for intf, peers := range cfg.InterfacePeers { - for _, peer := range peers { - options = append(options, core.Peer{URI: peer, SourceInterface: intf}) + case args.UseConfFile != "" || args.UseConf: + // Read the configuration from either stdin or from the filesystem + cfg = setup.ReadConfig(logger, args.UseConf, args.UseConfFile, args.NormaliseConf) + // If the -normaliseconf option was specified then remarshal the above + // configuration and print it back to stdout. This lets the user update + // their configuration file with newly mapped names (like above) or to + // convert from plain JSON to commented HJSON. + if args.NormaliseConf { + var bs []byte + if args.ConfJSON { + bs, err = json.MarshalIndent(cfg, "", " ") + } else { + bs, err = hjson.Marshal(cfg) } - } - for _, allowed := range cfg.AllowedPublicKeys { - k, err := hex.DecodeString(allowed) if err != nil { panic(err) } - options = append(options, core.AllowedPublicKey(k[:])) + fmt.Println(string(bs)) + return } - if n.core, err = core.New(cfg.Certificate, logger, options...); err != nil { - panic(err) - } - address, subnet := n.core.Address(), n.core.Subnet() - logger.Infof("Your public key is %s", hex.EncodeToString(n.core.PublicKey())) - logger.Infof("Your IPv6 address is %s", address.String()) - logger.Infof("Your IPv6 subnet is %s", subnet.String()) + + case args.GenConf: + // Generate a new configuration and print it to stdout. + fmt.Printf("%s\n", config.GenerateConfigJSON(args.ConfJSON)) + return + + default: + // No flags were provided, therefore print the list of flags to stdout. + fmt.Println("Usage:") + flag.PrintDefaults() } - // Setup the admin socket. - { - options := []admin.SetupOption{ - admin.ListenAddress(cfg.AdminListen), - } - if cfg.LogLookups { - options = append(options, admin.LogLookups{}) - } - if n.admin, err = admin.New(n.core, logger, options...); err != nil { - panic(err) - } - if n.admin != nil { - n.admin.SetupAdminHandlers() - } - } + // Create a new standalone node + n := setup.NewNode(cfg, logger) + n.SetLogLevel(args.LogLevel) - // Setup the multicast module. - { - options := []multicast.SetupOption{} - for _, intf := range cfg.MulticastInterfaces { - options = append(options, multicast.MulticastInterface{ - Regex: regexp.MustCompile(intf.Regex), - Beacon: intf.Beacon, - Listen: intf.Listen, - Port: intf.Port, - Priority: uint8(intf.Priority), - Password: intf.Password, - }) - } - if n.multicast, err = multicast.New(n.core, logger, options...); err != nil { - panic(err) - } - if n.admin != nil && n.multicast != nil { - n.multicast.SetupAdminHandlers(n.admin) - } + // Now start Yggdrasil - this starts the router, switch and other core + // components needed for Yggdrasil to operate + if err = n.Run(args); err != nil { + logger.Fatalln(err) } // Setup the TUN module. - { - options := []tun.SetupOption{ - tun.InterfaceName(cfg.IfName), - tun.InterfaceMTU(cfg.IfMTU), - } - if n.tun, err = tun.New(ipv6rwc.NewReadWriteCloser(n.core), logger, options...); err != nil { - panic(err) - } - if n.admin != nil && n.tun != nil { - n.tun.SetupAdminHandlers(n.admin) - } + if err = n.SetupTun(); err != nil { + panic(err) } // Block until we are told to shut down. <-ctx.Done() // Shut down the node. - _ = n.admin.Stop() - _ = n.multicast.Stop() - _ = n.tun.Stop() - n.core.Stop() -} - -func setLogLevel(loglevel string, logger *log.Logger) { - 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 - } - } + n.Close() } diff --git a/src/config/config.go b/src/config/config.go index e899a35d..78fcf123 100644 --- a/src/config/config.go +++ b/src/config/config.go @@ -33,6 +33,7 @@ import ( "time" "github.com/hjson/hjson-go/v4" + "github.com/mitchellh/mapstructure" "golang.org/x/text/encoding/unicode" ) @@ -89,6 +90,22 @@ func GenerateConfig() *NodeConfig { return cfg } +func GenerateConfigJSON(isjson bool) []byte { + // Generates a new configuration and returns it in HJSON or JSON format. + cfg := 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 bs +} + func (cfg *NodeConfig) ReadFrom(r io.Reader) (int64, error) { conf, err := io.ReadAll(r) if err != nil { @@ -258,3 +275,68 @@ func (k *KeyBytes) UnmarshalJSON(b []byte) error { *k, err = hex.DecodeString(s) return err } + +func ReadConfig(conf []byte) *NodeConfig { + // 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() + var dat map[string]interface{} + if err := hjson.Unmarshal(conf, &dat); err != nil { + panic(err) + } + // Check if we have old field names + if old, ok := dat["SigningPrivateKey"]; ok { + if _, ok := dat["PrivateKey"]; !ok { + if privstr, err := hex.DecodeString(old.(string)); err == nil { + priv := ed25519.PrivateKey(privstr) + pub := priv.Public().(ed25519.PublicKey) + dat["PrivateKey"] = hex.EncodeToString(priv[:]) + dat["PublicKey"] = hex.EncodeToString(pub[:]) + } + } + } + if oldmc, ok := dat["MulticastInterfaces"]; ok { + if oldmcvals, ok := oldmc.([]interface{}); ok { + var newmc []MulticastInterfaceConfig + for _, oldmcval := range oldmcvals { + if str, ok := oldmcval.(string); ok { + newmc = append(newmc, MulticastInterfaceConfig{ + Regex: str, + Beacon: true, + Listen: true, + }) + } + } + if newmc != nil { + if oldport, ok := dat["LinkLocalTCPPort"]; ok { + // numbers parse to float64 by default + if port, ok := oldport.(float64); ok { + for idx := range newmc { + newmc[idx].Port = uint16(port) + } + } + } + dat["MulticastInterfaces"] = newmc + } + } + } + // 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) + } + if err := cfg.postprocessConfig(); err != nil { + panic(err) + } + return cfg +} diff --git a/src/setup/setup.go b/src/setup/setup.go new file mode 100644 index 00000000..00715e3e --- /dev/null +++ b/src/setup/setup.go @@ -0,0 +1,309 @@ +package setup + +import ( + "bytes" + "context" + "crypto/ed25519" + "encoding/hex" + "flag" + "fmt" + "io/ioutil" + "net" + "os" + "regexp" + "strings" + + "github.com/gologme/log" + "github.com/yggdrasil-network/yggdrasil-go/src/address" + "github.com/yggdrasil-network/yggdrasil-go/src/admin" + "github.com/yggdrasil-network/yggdrasil-go/src/config" + "github.com/yggdrasil-network/yggdrasil-go/src/core" + "github.com/yggdrasil-network/yggdrasil-go/src/ipv6rwc" + "github.com/yggdrasil-network/yggdrasil-go/src/multicast" + "github.com/yggdrasil-network/yggdrasil-go/src/tun" + "golang.org/x/text/encoding/unicode" +) + +type Node struct { + core *core.Core + ctx context.Context + cancel context.CancelFunc + logger *log.Logger + config *config.NodeConfig + tun *tun.TunAdapter + multicast *multicast.Multicast + admin *admin.AdminSocket +} + +func NewNode(cfg *config.NodeConfig, logger *log.Logger) *Node { + ctx, cancel := context.WithCancel(context.Background()) + return &Node{ + ctx: ctx, + cancel: cancel, + logger: logger, + config: cfg, + tun: &tun.TunAdapter{}, + multicast: &multicast.Multicast{}, + admin: &admin.AdminSocket{}, + } +} + +func (n *Node) Close() { + n.cancel() + _ = n.admin.Stop() + _ = n.multicast.Stop() + _ = n.tun.Stop() + _ = n.core.Close() +} + +func (n *Node) Done() <-chan struct{} { + return n.ctx.Done() +} + +func (n *Node) Admin() *admin.AdminSocket { + return n.admin +} + +// The main function is responsible for configuring and starting Yggdrasil. +func (n *Node) Run(args Arguments) error { + var err error = nil + // Have we got a working configuration? If we don't then it probably means + // that neither -autoconf, -useconf or -useconffile were set above. Stop + // if we don't. + if n.config == nil { + return fmt.Errorf("no configuration supplied") + } + // Have we been asked for the node address yet? If so, print it and then stop. + getNodeKey := func() ed25519.PublicKey { + return ed25519.PrivateKey(n.config.PrivateKey).Public().(ed25519.PublicKey) + } + switch { + case args.GetAddr: + if key := getNodeKey(); key != nil { + addr := address.AddrForKey(key) + ip := net.IP(addr[:]) + fmt.Println(ip.String()) + } + return nil + case args.GetSubnet: + if key := getNodeKey(); key != nil { + snet := address.SubnetForKey(key) + ipnet := net.IPNet{ + IP: append(snet[:], 0, 0, 0, 0, 0, 0, 0, 0), + Mask: net.CIDRMask(len(snet)*8, 128), + } + fmt.Println(ipnet.String()) + } + return nil + case args.GetPKey: + if key := getNodeKey(); key != nil { + fmt.Println(hex.EncodeToString(key)) + } + return nil + case args.ExportKey: + pem, err := n.config.MarshalPEMPrivateKey() + if err != nil { + return err + } + fmt.Println(string(pem)) + return nil + default: + } + + // Setup the Yggdrasil node itself. + { + options := []core.SetupOption{ + core.NodeInfo(n.config.NodeInfo), + core.NodeInfoPrivacy(n.config.NodeInfoPrivacy), + } + for _, addr := range n.config.Listen { + options = append(options, core.ListenAddress(addr)) + } + for _, peer := range n.config.Peers { + options = append(options, core.Peer{URI: peer}) + } + for intf, peers := range n.config.InterfacePeers { + for _, peer := range peers { + options = append(options, core.Peer{URI: peer, SourceInterface: intf}) + } + } + for _, allowed := range n.config.AllowedPublicKeys { + k, err := hex.DecodeString(allowed) + if err != nil { + return err + } + options = append(options, core.AllowedPublicKey(k[:])) + } + if n.core, err = core.New(n.config.Certificate, n.logger, options...); err != nil { + return err + } + address, subnet := n.core.Address(), n.core.Subnet() + n.logger.Infof("Your public key is %s", hex.EncodeToString(n.core.PublicKey())) + n.logger.Infof("Your IPv6 address is %s", address.String()) + n.logger.Infof("Your IPv6 subnet is %s", subnet.String()) + } + + // Setup the admin socket. + { + options := []admin.SetupOption{ + admin.ListenAddress(n.config.AdminListen), + } + if n.config.LogLookups { + options = append(options, admin.LogLookups{}) + } + if n.admin, err = admin.New(n.core, n.logger, options...); err != nil { + return err + } + if n.admin != nil { + n.admin.SetupAdminHandlers() + } + } + + // Setup the multicast module. + { + options := []multicast.SetupOption{} + for _, intf := range n.config.MulticastInterfaces { + options = append(options, multicast.MulticastInterface{ + Regex: regexp.MustCompile(intf.Regex), + Beacon: intf.Beacon, + Listen: intf.Listen, + Port: intf.Port, + Priority: uint8(intf.Priority), + Password: intf.Password, + }) + } + if n.multicast, err = multicast.New(n.core, n.logger, options...); err != nil { + return err + } + if n.admin != nil && n.multicast != nil { + n.multicast.SetupAdminHandlers(n.admin) + } + } + + return nil +} + +func (n *Node) SetLogLevel(loglevel string) { + 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 + n.logger.Infoln("Loglevel parse failed. Set default level(info)") + loglevel = "info" + } + + for _, l := range levels { + n.logger.EnableLevel(l) + if l == loglevel { + break + } + } +} + +func (n *Node) SetupTun() error { + var err error = nil + + options := []tun.SetupOption{ + tun.InterfaceName(n.config.IfName), + tun.InterfaceMTU(n.config.IfMTU), + } + + if n.tun, err = tun.New(ipv6rwc.NewReadWriteCloser(n.core), n.logger, options...); err != nil { + return err + } + + if n.admin != nil && n.tun != nil { + n.tun.SetupAdminHandlers(n.admin) + } + + return nil +} + +func ReadConfig(log *log.Logger, useconf bool, useconffile string, normaliseconf bool) *config.NodeConfig { + // Use a configuration file. If -useconf, the configuration will be read + // from stdin. If -useconffile, the configuration will be read from the + // filesystem. + var conf []byte + var err error + if useconffile != "" { + // Read the file from the filesystem + conf, err = ioutil.ReadFile(useconffile) + } else { + // Read the file from stdin. + conf, err = ioutil.ReadAll(os.Stdin) + } + 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) + } + } + return config.ReadConfig(conf) +} + +type Arguments struct { + GenConf bool + UseConf bool + NormaliseConf bool + ExportKey bool + ConfJSON bool + AutoConf bool + Version bool + GetAddr bool + GetSubnet bool + GetPKey bool + UseConfFile string + LogTo string + LogLevel string +} + +func ParseArguments() Arguments { + genconf := flag.Bool("genconf", false, "print a new config to stdout") + useconf := flag.Bool("useconf", false, "read HJSON/JSON config from stdin") + useconffile := flag.String("useconffile", "", "read HJSON/JSON config from specified file path") + normaliseconf := flag.Bool("normaliseconf", false, "use in combination with either -useconf or -useconffile, outputs your configuration normalised") + exportkey := flag.Bool("exportkey", false, "use in combination with either -useconf or -useconffile, outputs your private key in PEM format") + confjson := flag.Bool("json", false, "print configuration from -genconf or -normaliseconf as JSON instead of HJSON") + autoconf := flag.Bool("autoconf", false, "automatic mode (dynamic IP, peer with IPv6 neighbors)") + ver := flag.Bool("version", false, "prints the version of this build") + logto := flag.String("logto", "stdout", "file path to log to, \"syslog\" or \"stdout\"") + getaddr := flag.Bool("address", false, "returns the IPv6 address as derived from the supplied configuration") + getsnet := flag.Bool("subnet", false, "returns the IPv6 subnet as derived from the supplied configuration") + getpkey := flag.Bool("publickey", false, "use in combination with either -useconf or -useconffile, outputs your public key") + loglevel := flag.String("loglevel", "info", "loglevel to enable") + flag.Parse() + return Arguments{ + GenConf: *genconf, + UseConf: *useconf, + UseConfFile: *useconffile, + NormaliseConf: *normaliseconf, + ExportKey: *exportkey, + ConfJSON: *confjson, + AutoConf: *autoconf, + Version: *ver, + LogTo: *logto, + GetAddr: *getaddr, + GetSubnet: *getsnet, + GetPKey: *getpkey, + LogLevel: *loglevel, + } +}