package main import ( "bytes" "encoding/xml" "flag" "fmt" "io" "lux/crypto" "lux/host" "lux/node" "lux/proto" "lux/rpc" "net" "net/netip" "os" "os/exec" "os/signal" "strings" "syscall" "time" "github.com/op/go-logging" ) var isNode bool var isHost bool var isRpc bool var configPath string var bootstrap bool var justNodeId bool type LogConfig struct { XMLName xml.Name `xml:"log"` Level string `xml:"level,attr"` LogPath string `xml:",innerxml"` } type UpdateHookConfig struct { XMLName xml.Name `xml:"hook"` HostID string `xml:"id"` Script string `xml:"script"` } type NodeConfig struct { XMLName xml.Name `xml:"node"` KeyStore string `xml:"keystore"` ID string `xml:"id"` Interior []string `xml:"interior"` Exterior []string `xml:"exterior"` Neighbors []struct { XMLName xml.Name `xml:"neighbor"` ID string `xml:"id"` Address string `xml:"address"` } `xml:"neighbor"` Sync int `xml:"sync"` RPCEndpoints []string `xml:"rpc"` DNS []string `xml:"dns"` Log LogConfig `xml:"log"` Hooks []UpdateHookConfig `xml:"hook"` } func setupLogging(log LogConfig) { var level logging.Level switch log.Level { case "critical": level = logging.CRITICAL case "error": level = logging.ERROR case "warning": level = logging.WARNING case "notice": level = logging.NOTICE case "info": level = logging.INFO case "debug": level = logging.DEBUG default: level = logging.INFO } // if log tag has file path, then it open file in append mode, // otherwise it will use default stdout logger if log.LogPath != "" { logFile, err := os.OpenFile(log.LogPath, os.O_WRONLY|os.O_CREATE|os.O_APPEND, os.FileMode(0600)) if err != nil { fmt.Fprintf(os.Stderr, "failed to open log file: %v\n", err) os.Exit(1) } logging.SetBackend(logging.NewLogBackend(logFile, "", 0)) } logging.SetLevel(level, "") } func bootstrapNode() { xmlBytes, err := os.ReadFile(configPath) if err != nil { fmt.Fprintln(os.Stderr, err) os.Exit(1) } var config NodeConfig if err := xml.Unmarshal(xmlBytes, &config); err != nil { fmt.Fprintf(os.Stderr, "failed to parse xml: %v", err) os.Exit(1) } // create keystore, generate node key ks := crypto.NewLuxKeyStore(config.KeyStore) nodeKey, err := crypto.NewLuxKey(proto.LuxTypeNode) if err != nil { fmt.Fprintln(os.Stderr, err) os.Exit(1) } if err := ks.Put(nodeKey); err != nil { fmt.Fprintln(os.Stderr, err) os.Exit(1) } if justNodeId { fmt.Println(nodeKey.Id.String()) } else { fmt.Printf("Your node key ID is: %s\nAdd %s to your node config!\n", nodeKey.Id.String(), nodeKey.Id.String()) } } var log = logging.MustGetLogger("main") type hookUpdateSubscriber struct { Hooks map[proto.LuxID]UpdateHookConfig } func (subscriber *hookUpdateSubscriber) HandleStateUpdate(state node.LuxHostState) { hook, ok := subscriber.Hooks[state.HostId] if !ok { return } // spawn executable and pipe xml host state into it's stdin xmlBytes, err := xml.Marshal(&rpc.LuxRpcHost{ HostID: state.HostId.String(), Hostname: state.State.Hostname, State: state.State.IntoRpc(), }) if err != nil { log.Errorf("failed to marshal host update for hook: %v\n", err) return } buffer := bytes.Buffer{} buffer.Write(xmlBytes) cmd := exec.Command(hook.Script) cmd.Stdin = &buffer cmd.Stdout = io.Discard cmd.Stderr = io.Discard if err := cmd.Run(); err != nil { log.Warningf("failed to execute script %s: %v\n", hook.Script, err) } } func nodeMain() { xmlBytes, err := os.ReadFile(configPath) if err != nil { fmt.Fprintln(os.Stderr, err) os.Exit(1) } var config NodeConfig if err := xml.Unmarshal(xmlBytes, &config); err != nil { log.Criticalf("failed to parse xml: %v", err) os.Exit(1) } // setup logging setupLogging(config.Log) // check presense of keystore and id in config if config.KeyStore == "" { log.Critical("no keystore path specified!") os.Exit(1) } if config.ID == "" { log.Critical("no ID in config! You need to --bootstrap") os.Exit(1) } nodeId, err := proto.ParseLuxID(config.ID) if err != nil { log.Criticalf("failed to parse node id: %v", err) os.Exit(1) } // load keystore ks := crypto.NewLuxKeyStore(config.KeyStore) if err := ks.Load(); err != nil { log.Criticalf("failed to laod keystore: %v", err) os.Exit(1) } nodeKey, ok := ks.Get(nodeId) if !ok { log.Critical("node key is not present in key store!") os.Exit(1) } // create node node := node.NewLuxNode(nodeKey, ks) // add interior exterior channels for _, interior := range config.Interior { if err := node.AddInterior(interior); err != nil { log.Criticalf("failed to add interior %s: %v", interior, err) os.Exit(1) } } for _, exterior := range config.Exterior { if err := node.AddExterior(exterior); err != nil { log.Criticalf("failed to add exterior %s: %v", exterior, err) os.Exit(1) } } // add neighbors for _, neighbor := range config.Neighbors { neighId, err := proto.ParseLuxID(neighbor.ID) if err != nil { log.Criticalf("failed to parse neigh id %s: %v", neighbor.ID, err) os.Exit(1) } if err := node.AddNeighbor(neighId, neighbor.Address); err != nil { log.Criticalf("failed to add neighbor %s: %v", neighbor.ID, err) os.Exit(1) } } // create rpc server sv := rpc.NewLuxRpcServer() sv.RegisterController(&node) // parse and and spawn rpc endpoints for _, rpcPath := range config.RPCEndpoints { if strings.HasPrefix(rpcPath, "unix://") { path := rpcPath[7:] if err := sv.AddEndpoint("unix", path, rpc.LuxRpcTypeRoot); err != nil { log.Criticalf("failed to add root rpc %s: %v", path, err) os.Exit(1) } } else if strings.HasPrefix(rpcPath, "tcp://") { path := rpcPath[6:] if err := sv.AddEndpoint("tcp", path, rpc.LuxRpcTypeQuery); err != nil { log.Criticalf("failed to add query rpc %s: %v", path, err) os.Exit(1) } } else { log.Criticalf("unknown rpc type %s. It must be either unix:// or tcp://\n", rpcPath) os.Exit(1) } } // add dns server frontends for _, dnsListen := range config.DNS { if err := node.AddDnsFrontend(dnsListen); err != nil { log.Criticalf("failed to spawn dns %s: %v\n", dnsListen, err) os.Exit(1) } } // add update hooks hook := hookUpdateSubscriber{ Hooks: make(map[proto.LuxID]UpdateHookConfig), } for _, item := range config.Hooks { id, err := proto.ParseLuxID(item.HostID) if err != nil { log.Criticalf("failed to parse hook host id %s: %v\n", item.HostID, err) os.Exit(1) } hook.Hooks[id] = item } node.AddSubscriber(&hook) // start node node.Start() // node state sync scheduled task stopChan := make(chan struct{}) if config.Sync != 0 { ticker := time.NewTicker(time.Duration(config.Sync) * time.Minute) go func() { for { select { case <-ticker.C: err := node.MulticastSync() if err != nil { log.Errorf("MulticastSync err: %v", err) } case <-stopChan: ticker.Stop() return } } }() } else { log.Info("sync interval is not set. Node will not sync with neighbors") } // register go channel to receive unix signals, // while hogging main thread to read them and take action. // its important to keep main thread alive for process to run sigs := make(chan os.Signal, 1) signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM) signaling: for { sig := <-sigs log.Debug(sig) switch sig { case syscall.SIGINT, syscall.SIGTERM: break signaling } } // stop daemon close(stopChan) node.Stop() } type HostCommand struct { XMLName xml.Name `xml:"command"` Executable string `xml:",innerxml"` } type HostConfig struct { XMLName xml.Name `xml:"host"` KeyStore string `xml:"keystore"` ID string `xml:"id"` Hostname string `xml:"hostname"` Log LogConfig `xml:"log"` Heartbeat int `xml:"heartbeat"` Options []struct { XMLName xml.Name `xml:"option"` Type string `xml:"type,attr"` WAN struct { XMLName xml.Name `xml:"wan"` Method string `xml:"method,attr"` Addr4 string `xml:"addr4"` Addr6 string `xml:"addr6"` Command HostCommand `xml:"command"` } `xml:"wan"` } `xml:"option"` Nodes []struct { XMLName xml.Name `xml:"node"` ID string `xml:"id"` Exterior string `xml:"exterior"` } `xml:"node"` } type HostStaticWAN struct { addr4 netip.Addr addr6 netip.Addr } func NewHostStaticWAN(ip4 string, ip6 string) (HostStaticWAN, error) { addr4, err := netip.ParseAddr(ip4) if err != nil { return HostStaticWAN{}, err } addr6, err := netip.ParseAddr(ip6) if err != nil { return HostStaticWAN{}, err } return HostStaticWAN{addr4, addr6}, nil } func (wan *HostStaticWAN) Provide() (host.LuxOption, error) { return &host.LuxOptionWAN{Addr4: wan.addr4, Addr6: wan.addr6}, nil } type HostIdentWAN struct{} func dialIdentMe(identIp netip.Addr) (netip.Addr, error) { var network string if identIp.Is6() { network = "tcp6" } else { network = "tcp4" } cl, err := net.DialTCP(network, nil, net.TCPAddrFromAddrPort(netip.AddrPortFrom(identIp, 80))) if err != nil { return netip.Addr{}, err } defer cl.Close() // I dont want to bundle http package for embedded reasons, // so I will just bang http headers directly const req = "GET / HTTP/1.1\r\nHost: ident.me\r\nUser-Agent: LUX-v1\r\nAccept: */*\r\n\r\n" _, err = cl.Write([]byte(req)) if err != nil { return netip.Addr{}, err } res := make([]byte, 1024) n, err := cl.Read(res) if err != nil { return netip.Addr{}, err } // parse http headers to get response sep := []byte{0x0D, 0x0A, 0x0D, 0x0A} ipStr := "" for i := 0; i < n-4; i++ { if bytes.Equal(res[i:i+4], sep) { ipStr = string(res[i+4 : n]) } } // parse ip if ipStr == "" { return netip.Addr{}, fmt.Errorf("IP not found in ident.me response: %s", string(res)) } // to prevent panics parsedIp := net.ParseIP(ipStr) if parsedIp == nil { return netip.Addr{}, fmt.Errorf("failed to parse ident.me IP") } return proto.LuxProtoIPToAddr(parsedIp), nil } func (*HostIdentWAN) Provide() (host.LuxOption, error) { wan := host.NewLuxOptionWAN() // so first we gonna resolve ident.me and see if there is IPv6 addrs, err := net.LookupHost("ident.me") if err != nil { return &wan, err } for _, addr := range addrs { ip := net.ParseIP(addr) if ip.To4() == nil && wan.Addr6.IsUnspecified() { // we gonna resolve IPv6 addr, err := dialIdentMe(proto.LuxProtoIPToAddr(ip)) if err == nil { // we got IPv6 wan.Addr6 = addr } } else if wan.Addr4.IsUnspecified() { addr, err := dialIdentMe(proto.LuxProtoIPToAddr(ip)) if err != nil { // if no IPv4 its considered catastrophic error return &wan, err } else { wan.Addr4 = addr } } } return &wan, nil } type HostNetif struct{} func (*HostNetif) Provide() (host.LuxOption, error) { netif := host.NewLuxOptionNetIf() if err := netif.EnumerateNetlink(); err != nil { return nil, err } return &netif, nil } func hostMain() { xmlBytes, err := os.ReadFile(configPath) if err != nil { fmt.Fprintf(os.Stderr, "failed to open config: %v\n", err) os.Exit(1) } var config HostConfig if err := xml.Unmarshal(xmlBytes, &config); err != nil { fmt.Fprintf(os.Stderr, "failed to parse host config: %v\n", err) os.Exit(1) } ks := crypto.NewLuxKeyStore(config.KeyStore) if err := ks.Load(); err != nil { fmt.Fprintf(os.Stderr, "failed to load keystore: %v\n", err) os.Exit(1) } hostId, err := proto.ParseLuxID(config.ID) if err != nil { fmt.Fprintf(os.Stderr, "failed to parse host ID: %v\n", err) os.Exit(1) } hostKey, ok := ks.Get(hostId) if !ok { fmt.Fprintln(os.Stderr, "host key is not present in keystore!") os.Exit(1) } if config.Hostname == "" { fmt.Fprintln(os.Stderr, "no hostname specified!") os.Exit(1) } if config.Heartbeat == 0 { fmt.Fprintln(os.Stderr, "no minute interval provided in !") os.Exit(1) } // setup logging setupLogging(config.Log) // create host host := host.NewLuxHost(config.Hostname, hostKey, ks) // populate option providers for _, option := range config.Options { if option.Type == "wan" { wan := &option.WAN if wan.Method == "static" { provider, err := NewHostStaticWAN(wan.Addr4, wan.Addr6) if err != nil { fmt.Fprintf(os.Stderr, "failed to create static wan provider: %v\n", err) os.Exit(1) } host.AddOptionProvider(&provider) } else if wan.Method == "identme" { host.AddOptionProvider(&HostIdentWAN{}) } } else if option.Type == "netif" { host.AddOptionProvider(&HostNetif{}) } } // add nodes for _, node := range config.Nodes { nodeId, err := proto.ParseLuxID(node.ID) if err != nil { fmt.Fprintf(os.Stderr, "failed to parse node id: %v\n", err) os.Exit(1) } if err := host.AddNode(nodeId, node.Exterior); err != nil { fmt.Fprintf(os.Stderr, "failed to add node: %v\n", err) os.Exit(1) } } // start host host.Start() // start heartbeat timer stopChan := make(chan struct{}) ticker := time.NewTicker(time.Duration(config.Heartbeat) * time.Minute) go func() { for { select { case <-ticker.C: if err := host.Heartbeat(); err != nil { fmt.Fprintf(os.Stderr, "failed to heartbeat: %v\n", err) } case <-stopChan: ticker.Stop() return } } }() // handle signals in main thread sigs := make(chan os.Signal, 1) signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM) signaling: for { sig := <-sigs switch sig { case syscall.SIGINT: break signaling case syscall.SIGTERM: break signaling } } // stop host close(stopChan) host.Stop() } func printRpcHost(host rpc.LuxRpcHost) { fmt.Printf("%s\n", host.HostID) fmt.Printf("|hostname: %s\n", host.Hostname) fmt.Printf("|wan:\n|\taddr4\t%s\n|\taddr6\t%s\n", host.State.WAN.Addr4, host.State.WAN.Addr6) for _, netif := range host.State.NetIf.Interfaces { fmt.Printf("|netif %d: %s\n", netif.Index, netif.Name) for _, addr := range netif.Addrs { fmt.Printf("|\t%s\t%s\t\n", addr.Type, addr.Addr) } } } var rpcPath string var rpcNewHost string var rpcNewNode string var rpcQueryHost string var rpcQueryHostname string var rpcGetRoutes bool var rpcGetKeys bool var rpcGetHosts bool var rpcXml bool func rpcMain() { var cl rpc.LuxRpcClient var err error if strings.HasPrefix(rpcPath, "unix://") { cl, err = rpc.LuxDialRpc("unix", rpcPath[7:]) } else if strings.HasPrefix(rpcPath, "tcp://") { cl, err = rpc.LuxDialRpc("tcp", rpcPath[6:]) } else { fmt.Fprintln(os.Stderr, "unknown RPC network (must be unix:// or tcp://)") os.Exit(1) } if err != nil { fmt.Fprintf(os.Stderr, "failed to dial RPC: %v\n", err) os.Exit(1) } defer cl.Close() // now we send requests counter := 0 if rpcGetRoutes { rpcRes, rpcErr, err := cl.Execute(rpc.LuxRpcRequest{ RequestID: counter, Controller: "router", Command: "get", }) if err != nil { fmt.Fprintf(os.Stderr, "failed to send request: %v\n", err) os.Exit(1) } counter++ if rpcErr.ErrorCode != 0 { // we got error fmt.Fprintf(os.Stderr, "RPC error %d: %s\n", rpcErr.ErrorCode, rpcErr.Message) os.Exit(1) } if rpcXml { xmlBytes, err := xml.Marshal(&rpcRes) if err != nil { fmt.Fprintf(os.Stderr, "failed to marshal rpc output: %v\n", err) os.Exit(1) } fmt.Println(string(xmlBytes)) return } // pretty print routes for _, route := range rpcRes.Routes { fmt.Println(route.String()) } } if rpcGetKeys { rpcRes, rpcErr, err := cl.Execute(rpc.LuxRpcRequest{ RequestID: counter, Controller: "ks", Command: "get", }) if err != nil { fmt.Fprintf(os.Stderr, "failed to send request: %v\n", err) os.Exit(1) } counter++ if rpcErr.ErrorCode != 0 { // we got error fmt.Fprintf(os.Stderr, "RPC error %d: %s\n", rpcErr.ErrorCode, rpcErr.Message) os.Exit(1) } if rpcXml { xmlBytes, err := xml.Marshal(&rpcRes) if err != nil { fmt.Fprintf(os.Stderr, "failed to marshal rpc output: %v\n", err) os.Exit(1) } fmt.Println(string(xmlBytes)) return } // pretty print keys for _, node := range rpcRes.Keystore.Nodes { fmt.Printf("node %s\n", node.ID) } for _, host := range rpcRes.Keystore.Hosts { fmt.Printf("host %s\n", host.ID) } } if rpcNewHost != "" { rpcRes, rpcErr, err := cl.Execute(rpc.LuxRpcRequest{ RequestID: counter, Controller: "node", Command: "new-host", }) if err != nil { fmt.Fprintf(os.Stderr, "failed to send request: %v\n", err) os.Exit(1) } counter++ if rpcErr.ErrorCode != 0 { // we got error fmt.Fprintf(os.Stderr, "RPC error %d: %s\n", rpcErr.ErrorCode, rpcErr.Message) os.Exit(1) } if rpcXml { xmlBytes, err := xml.Marshal(&rpcRes) if err != nil { fmt.Fprintf(os.Stderr, "failed to marshal rpc output: %v\n", err) os.Exit(1) } fmt.Println(string(xmlBytes)) return } // deserialize keystore _, err = crypto.LuxKeyStoreFromRpc(rpcRes.Keystore, rpcNewHost) if err != nil { fmt.Fprintf(os.Stderr, "failed to save host keystore: %v\n", err) } fmt.Printf("New host ID: %s\n", rpcRes.NewHostID) } if rpcNewNode != "" { rpcRes, rpcErr, err := cl.Execute(rpc.LuxRpcRequest{ RequestID: counter, Controller: "node", Command: "new-node", }) if err != nil { fmt.Fprintf(os.Stderr, "failed to send request: %v\n", err) os.Exit(1) } counter++ if rpcErr.ErrorCode != 0 { // we got error fmt.Fprintf(os.Stderr, "RPC error %d: %s\n", rpcErr.ErrorCode, rpcErr.Message) os.Exit(1) } if rpcXml { xmlBytes, err := xml.Marshal(&rpcRes) if err != nil { fmt.Fprintf(os.Stderr, "failed to marshal rpc output: %v\n", err) os.Exit(1) } fmt.Println(string(xmlBytes)) return } // deserialize keystore _, err = crypto.LuxKeyStoreFromRpc(rpcRes.Keystore, rpcNewNode) if err != nil { fmt.Fprintf(os.Stderr, "failed to save node keystore: %v\n", err) } fmt.Printf("New neighbor node ID: %s\n", rpcRes.NewNodeID) } if rpcQueryHost != "" || rpcQueryHostname != "" { var rpcReq rpc.LuxRpcRequest if rpcQueryHost != "" { rpcReq = rpc.LuxRpcRequest{ RequestID: counter, Controller: "node", Command: "query", Hosts: []rpc.LuxRpcHost{ {HostID: rpcQueryHost}, }, } } else { rpcReq = rpc.LuxRpcRequest{ RequestID: counter, Controller: "node", Command: "query", Hosts: []rpc.LuxRpcHost{ {Hostname: rpcQueryHostname}, }, } } rpcRes, rpcErr, err := cl.Execute(rpcReq) if err != nil { fmt.Fprintf(os.Stderr, "failed to send request: %v\n", err) os.Exit(1) } counter++ if rpcErr.ErrorCode != 0 { // we got error fmt.Fprintf(os.Stderr, "RPC error %d: %s\n", rpcErr.ErrorCode, rpcErr.Message) os.Exit(1) } if rpcXml { xmlBytes, err := xml.Marshal(&rpcRes) if err != nil { fmt.Fprintf(os.Stderr, "failed to marshal rpc output: %v\n", err) os.Exit(1) } fmt.Println(string(xmlBytes)) return } // print state printRpcHost(rpcRes.Hosts[0]) } if rpcGetHosts { rpcRes, rpcErr, err := cl.Execute(rpc.LuxRpcRequest{ RequestID: counter, Controller: "node", Command: "get-hosts", }) if err != nil { fmt.Fprintf(os.Stderr, "failed to send request: %v\n", err) os.Exit(1) } counter++ if rpcErr.ErrorCode != 0 { // we got error fmt.Fprintf(os.Stderr, "RPC error %d: %s\n", rpcErr.ErrorCode, rpcErr.Message) os.Exit(1) } if rpcXml { xmlBytes, err := xml.Marshal(&rpcRes) if err != nil { fmt.Fprintf(os.Stderr, "failed to marshal rpc output: %v\n", err) os.Exit(1) } fmt.Println(string(xmlBytes)) return } for _, host := range rpcRes.Hosts { printRpcHost(host) } } } func main() { // first, we need to determine who we are: node, host or rpc. // determine by executable name (lux binary will be symlinked to lux-node, lux-host, luc-rpc), // or by explicit cli flag (--node, --host, --rpc) flag.BoolVar(&isNode, "node", false, "LUX node") flag.BoolVar(&isHost, "host", false, "LUX host") flag.StringVar(&configPath, "config", "", "node or host config") flag.BoolVar(&bootstrap, "bootstrap", false, "bootstrap node keystore. config must be specified") flag.BoolVar(&justNodeId, "just-node-id", false, "when bootstrapping only output node id to stdout") flag.StringVar(&rpcPath, "rpc", "", "Run as RPC client, specify path to RPC UNIX socket or TCP socket, must be in unix:// or tcp:// form") flag.StringVar(&rpcNewHost, "rpc-new-host", "", "RPC node create new host, specifies path for new keystore") flag.StringVar(&rpcNewNode, "rpc-new-node", "", "RPC node create new node, specifies path for new keystore") flag.StringVar(&rpcQueryHost, "rpc-query-host", "", "RPC node query host state by ID") flag.StringVar(&rpcQueryHostname, "rpc-query-hostname", "", "RPC node querty host state by hostname") flag.BoolVar(&rpcGetRoutes, "rpc-get-routes", false, "RPC node list established routes") flag.BoolVar(&rpcGetKeys, "rpc-get-keys", false, "RPC node list keys") flag.BoolVar(&rpcGetHosts, "rpc-get-hosts", false, "RPC node list hosts") flag.BoolVar(&rpcXml, "rpc-xml", false, "output RPC results in XML") flag.Parse() if rpcPath != "" { isRpc = true } else if !isNode && !isHost { // determine by argv[0] if strings.Contains(os.Args[0], "node") { isNode = true } else if strings.Contains(os.Args[0], "host") { isHost = true } } if (isNode || isHost) && configPath == "" { fmt.Fprintln(os.Stderr, "must provide config path") os.Exit(1) } else if isRpc && rpcPath == "" { fmt.Fprintln(os.Stderr, "must provide RPC socket path") os.Exit(1) } if isNode && bootstrap { bootstrapNode() return } if isNode { nodeMain() } else if isHost { hostMain() } else if isRpc { rpcMain() } }