Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
116 changes: 84 additions & 32 deletions cmd/init.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"fmt"
"net/netip"
"os"
"strings"

"github.com/encodeous/nylon/state"
"github.com/goccy/go-yaml"
Expand All @@ -28,6 +29,12 @@ type initOptions struct {
preDown []string
postUp []string
postDown []string
serve bool
connect string
token string
central string
addresses []string
prefixes []string
}

func newInitCmd() *cobra.Command {
Expand All @@ -38,39 +45,19 @@ func newInitCmd() *cobra.Command {
GroupID: "init",
Args: cobra.NoArgs,
RunE: func(cmd *cobra.Command, _ []string) error {
cfg, err := buildNodeConfig(opts)
if err != nil {
return err
cmd.SilenceUsage = true
if opts.serve {
return runInitServe(cmd, opts)
}

data, err := yaml.Marshal(cfg)
if err != nil {
return fmt.Errorf("encode node config: %w", err)
if opts.connect != "" {
return runInitConnect(cmd, opts)
}

flags := os.O_WRONLY | os.O_CREATE
if opts.force {
flags |= os.O_TRUNC
} else {
flags |= os.O_EXCL
}
file, err := os.OpenFile(opts.output, flags, 0o600)
cfg, err := buildNodeConfig(opts)
if err != nil {
if errors.Is(err, os.ErrExist) {
return fmt.Errorf("%s already exists (use --force to overwrite)", opts.output)
}
return fmt.Errorf("create %s: %w", opts.output, err)
}
if err = file.Chmod(0o600); err != nil {
_ = file.Close()
return fmt.Errorf("secure %s: %w", opts.output, err)
}
if _, err = file.Write(data); err != nil {
_ = file.Close()
return fmt.Errorf("write %s: %w", opts.output, err)
return err
}
if err = file.Close(); err != nil {
return fmt.Errorf("close %s: %w", opts.output, err)
if err = writeNodeConfig(cfg, opts.output, opts.force); err != nil {
return err
}

publicKey, err := cfg.Key.Pubkey().MarshalText()
Expand All @@ -83,8 +70,8 @@ func newInitCmd() *cobra.Command {
}

flags := cmd.Flags()
flags.StringVar(&opts.id, "id", "", "Unique node ID (required)")
flags.Uint16Var(&opts.port, "port", 57175, "UDP port Nylon listens on")
flags.StringVar(&opts.id, "id", "", "Unique node ID (defaults to hostname)")
flags.Uint16Var(&opts.port, "port", state.DefaultPort, "UDP port Nylon listens on")
flags.StringVar(&opts.key, "key", "", "Existing private key (a new key is generated if omitted)")
flags.StringVarP(&opts.output, "output", "o", DefaultNodeConfigPath, "Node config output path")
flags.BoolVar(&opts.force, "force", false, "Overwrite the output file if it exists")
Expand All @@ -99,9 +86,53 @@ func newInitCmd() *cobra.Command {
flags.StringSliceVar(&opts.preDown, "pre-down", nil, "Command to run before interface shutdown (repeatable)")
flags.StringSliceVar(&opts.postUp, "post-up", nil, "Command to run after interface startup (repeatable)")
flags.StringSliceVar(&opts.postDown, "post-down", nil, "Command to run after interface shutdown (repeatable)")
flags.BoolVar(&opts.serve, "serve", false, "Print a setup token and wait for a node to join with --connect")
flags.StringVar(&opts.connect, "connect", "", "Join the network through a node running --serve (host[:port])")
flags.StringVar(&opts.token, "token", "", "Setup token printed by --serve")
flags.StringVarP(&opts.central, "config", "c", DefaultConfigPath, "Central config path used by --serve and --connect")
flags.StringSliceVar(&opts.addresses, "address", nil, "Nylon address for this node in central config (repeatable)")
flags.StringSliceVar(&opts.prefixes, "prefix", nil, "IP prefix this node advertises in central config (repeatable)")
cmd.MarkFlagsMutuallyExclusive("serve", "connect")
cmd.MarkFlagsRequiredTogether("connect", "token")
return cmd
}

func writeNodeConfig(cfg *state.LocalCfg, path string, force bool) error {
data, err := yaml.Marshal(cfg)
if err != nil {
return fmt.Errorf("encode node config: %w", err)
}
return writeSecretFile(path, data, force)
}

func writeSecretFile(path string, data []byte, force bool) error {
flags := os.O_WRONLY | os.O_CREATE
if force {
flags |= os.O_TRUNC
} else {
flags |= os.O_EXCL
}
file, err := os.OpenFile(path, flags, 0o600)
if err != nil {
if errors.Is(err, os.ErrExist) {
return fmt.Errorf("%s already exists (use --force to overwrite)", path)
}
return fmt.Errorf("create %s: %w", path, err)
}
if err = file.Chmod(0o600); err != nil {
_ = file.Close()
return fmt.Errorf("secure %s: %w", path, err)
}
if _, err = file.Write(data); err != nil {
_ = file.Close()
return fmt.Errorf("write %s: %w", path, err)
}
if err = file.Close(); err != nil {
return fmt.Errorf("close %s: %w", path, err)
}
return nil
}

func buildNodeConfig(opts initOptions) (*state.LocalCfg, error) {
privateKey := state.GenerateKey()
if opts.key != "" {
Expand All @@ -110,9 +141,18 @@ func buildNodeConfig(opts initOptions) (*state.LocalCfg, error) {
}
}

id := opts.id
if id == "" {
host, err := os.Hostname()
if err != nil {
return nil, fmt.Errorf("--id is required: %w", err)
}
id = strings.ToLower(host)
}

cfg := &state.LocalCfg{
Key: privateKey,
Id: state.NodeId(opts.id),
Id: state.NodeId(id),
Port: opts.port,
UseSystemRouting: opts.useSystemRouting,
NoNetConfigure: opts.noNetConfigure,
Expand All @@ -139,6 +179,18 @@ func buildNodeConfig(opts initOptions) (*state.LocalCfg, error) {
return cfg, nil
}

func parseAddrs(values []string) ([]netip.Addr, error) {
addrs := make([]netip.Addr, 0, len(values))
for _, value := range values {
addr, err := netip.ParseAddr(value)
if err != nil {
return nil, fmt.Errorf("%q: %w", value, err)
}
addrs = append(addrs, addr)
}
return addrs, nil
}

func parsePrefixes(values []string) ([]netip.Prefix, error) {
prefixes := make([]netip.Prefix, 0, len(values))
for _, value := range values {
Expand Down
Loading
Loading