From eed2c8809c4ba7ebbdd37a899f25c5d2a0bef518 Mon Sep 17 00:00:00 2001 From: Pavel Sviderski Date: Thu, 29 Aug 2024 15:38:13 +1000 Subject: [PATCH] generate user key for new cluster and add it to peers on bootstrap machine --- cmd/uncloud/machine/add.go | 1 + internal/cli/cluster.go | 41 ++++++++++++++++++------ internal/cli/config/cluster.go | 2 ++ internal/cli/config/machine.go | 11 ++++--- internal/cli/user.go | 51 ++++++++++++++++++++++++++++++ internal/machine/config.go | 3 +- internal/machine/network/config.go | 17 +++++----- 7 files changed, 104 insertions(+), 22 deletions(-) create mode 100644 internal/cli/user.go diff --git a/cmd/uncloud/machine/add.go b/cmd/uncloud/machine/add.go index 158becaf..463b43e1 100644 --- a/cmd/uncloud/machine/add.go +++ b/cmd/uncloud/machine/add.go @@ -61,6 +61,7 @@ func add(ctx context.Context, uncli *cli.CLI, host string, opts addOptions) erro if err != nil { return fmt.Errorf("create default cluster: %w", err) } + fmt.Printf("Created %q cluster\n", cluster.Name) } else { return fmt.Errorf("get current cluster: %w", err) } diff --git a/internal/cli/cluster.go b/internal/cli/cluster.go index f31e51c8..9cef88e4 100644 --- a/internal/cli/cluster.go +++ b/internal/cli/cluster.go @@ -9,6 +9,7 @@ import ( "uncloud/internal/cli/config" "uncloud/internal/cmdexec" "uncloud/internal/machine" + "uncloud/internal/machine/network" "uncloud/internal/secret" ) @@ -30,7 +31,7 @@ func (c *Cluster) toConfig() *config.Cluster { } } -func (cli *CLI) CreateCluster(name string, privateKey ed25519.PrivateKey) (*Cluster, error) { +func (cli *CLI) CreateCluster(name string, privateKey ed25519.PrivateKey, userPrivateKey secret.Secret) (*Cluster, error) { if _, ok := cli.config.Clusters[name]; ok { return nil, fmt.Errorf("cluster %q already exists", name) } @@ -41,13 +42,22 @@ func (cli *CLI) CreateCluster(name string, privateKey ed25519.PrivateKey) (*Clus return nil, fmt.Errorf("generate cluster secret: %w", err) } } + if userPrivateKey == nil { + user, err := NewUser(nil) + if err != nil { + return nil, fmt.Errorf("generate user: %w", err) + } + userPrivateKey = user.PrivateKey() + } c := &Cluster{ Name: name, privateKey: privateKey, config: cli.config, } - cli.config.Clusters[name] = c.toConfig() + cfg := c.toConfig() + cfg.UserKey = userPrivateKey + cli.config.Clusters[name] = cfg if err := cli.config.Save(); err != nil { return nil, err } @@ -56,7 +66,7 @@ func (cli *CLI) CreateCluster(name string, privateKey ed25519.PrivateKey) (*Clus } func (cli *CLI) CreateDefaultCluster() (*Cluster, error) { - c, err := cli.CreateCluster("default", nil) + c, err := cli.CreateCluster("default", nil, nil) if err != nil { return nil, err } @@ -116,7 +126,19 @@ func (c *Cluster) AddMachine(ctx context.Context, name, user, host string, port _ = exec.Close() }() - mcfg, err := machine.NewBootstrapConfig(name, netip.Prefix{}) + wgUserKey := c.config.Clusters[c.Name].UserKey + if wgUserKey == nil { + return "", errors.New("cluster user_key must be set in the config") + } + wgUser, err := NewUser(wgUserKey) + if err != nil { + return "", fmt.Errorf("create user from key: %w", err) + } + userPeerCfg := network.PeerConfig{ + Subnet: netip.PrefixFrom(wgUser.Address(), 128), + PublicKey: wgUser.PublicKey(), + } + mcfg, err := machine.NewBootstrapConfig(name, netip.Prefix{}, userPeerCfg) if err != nil { return "", fmt.Errorf("generate machine bootstrap config: %w", err) } @@ -153,13 +175,14 @@ func (c *Cluster) AddMachine(ctx context.Context, name, user, host string, port if err != nil { return "", fmt.Errorf("start uncloudd: %w: %s", err, out) } - fmt.Println("uncloudd started") + fmt.Println("uncloudd daemon started") connConfig := config.MachineConnection{ - User: user, - Host: host, - Port: port, - SSHKey: sshKeyPath, + User: user, + Host: host, + Port: port, + SSHKey: sshKeyPath, + PublicKey: mcfg.Network.PublicKey, } c.config.Clusters[c.Name].Machines = append(c.config.Clusters[c.Name].Machines, connConfig) if err = c.config.Save(); err != nil { diff --git a/internal/cli/config/cluster.go b/internal/cli/config/cluster.go index 99b9a663..5fc5ab96 100644 --- a/internal/cli/config/cluster.go +++ b/internal/cli/config/cluster.go @@ -6,4 +6,6 @@ type Cluster struct { Name string `toml:"-"` Machines []MachineConnection `toml:"machines"` Secret secret.Secret `toml:"secret"` + // UserKey is the user's WireGuard private key used to connect to cluster machines. + UserKey secret.Secret `toml:"user_key"` } diff --git a/internal/cli/config/machine.go b/internal/cli/config/machine.go index 96a81b09..c502485d 100644 --- a/internal/cli/config/machine.go +++ b/internal/cli/config/machine.go @@ -1,8 +1,11 @@ package config +import "uncloud/internal/secret" + type MachineConnection struct { - User string `toml:"user,omitempty"` - Host string `toml:"host"` - Port int `toml:"port"` - SSHKey string `toml:"ssh_key,omitempty"` + User string `toml:"user,omitempty"` + Host string `toml:"host"` + Port int `toml:"port"` + SSHKey string `toml:"ssh_key,omitempty"` + PublicKey secret.Secret `toml:"public_key,omitempty"` } diff --git a/internal/cli/user.go b/internal/cli/user.go new file mode 100644 index 00000000..24fa7ab9 --- /dev/null +++ b/internal/cli/user.go @@ -0,0 +1,51 @@ +package cli + +import ( + "fmt" + "golang.zx2c4.com/wireguard/wgctrl/wgtypes" + "net/netip" + "uncloud/internal/secret" +) + +type User struct { + privateKey wgtypes.Key +} + +func NewUser(privateKey secret.Secret) (*User, error) { + var ( + wgKey wgtypes.Key + err error + ) + if privateKey == nil { + wgKey, err = wgtypes.GeneratePrivateKey() + if err != nil { + return nil, fmt.Errorf("generate key for user: %w", err) + } + privateKey = wgKey[:] + } else { + wgKey, err = wgtypes.NewKey(privateKey) + if err != nil { + return nil, fmt.Errorf("invalid key: %w", err) + } + privateKey = wgKey[:] + } + return &User{ + privateKey: wgKey, + }, nil +} + +func (u *User) PrivateKey() secret.Secret { + return u.privateKey[:] +} + +func (u *User) PublicKey() secret.Secret { + pubKey := u.privateKey.PublicKey() + return pubKey[:] +} + +func (u *User) Address() netip.Addr { + pubKey := u.PublicKey() + bytes := [16]byte{0xfd, 0xcc} + copy(bytes[2:], pubKey[:14]) + return netip.AddrFrom16(bytes) +} diff --git a/internal/machine/config.go b/internal/machine/config.go index 43ec3b56..ac719e70 100644 --- a/internal/machine/config.go +++ b/internal/machine/config.go @@ -48,7 +48,7 @@ func ConfigPath(dataDir string) string { } // NewBootstrapConfig returns a new machine configuration that should be applied to the first machine in a cluster. -func NewBootstrapConfig(name string, subnet netip.Prefix) (*Config, error) { +func NewBootstrapConfig(name string, subnet netip.Prefix, peers ...network.PeerConfig) (*Config, error) { mid, err := secret.NewID() if err != nil { return nil, fmt.Errorf("generate machine ID: %w", err) @@ -75,6 +75,7 @@ func NewBootstrapConfig(name string, subnet netip.Prefix) (*Config, error) { Subnet: subnet, PrivateKey: privKey, PublicKey: pubKey, + Peers: peers, }, }, nil } diff --git a/internal/machine/network/config.go b/internal/machine/network/config.go index 054f45b0..e96a0f21 100644 --- a/internal/machine/network/config.go +++ b/internal/machine/network/config.go @@ -19,13 +19,13 @@ type Config struct { Subnet netip.Prefix PrivateKey secret.Secret PublicKey secret.Secret - Peers []PeerConfig + Peers []PeerConfig `json:",omitempty"` } type PeerConfig struct { Subnet netip.Prefix - Endpoint netip.AddrPort - AllEndpoints []netip.AddrPort + Endpoint *netip.AddrPort `json:",omitempty"` + AllEndpoints []netip.AddrPort `json:",omitempty"` PublicKey secret.Secret } @@ -43,17 +43,18 @@ func (c Config) toDeviceConfig() (wgtypes.Config, error) { if kErr != nil { return wgtypes.Config{}, fmt.Errorf("parse peer public key: %w", kErr) } - endpoint := &net.UDPAddr{ - IP: peerConfig.Endpoint.Addr().AsSlice(), - Port: int(peerConfig.Endpoint.Port()), - } wgPeerConfigs[i] = wgtypes.PeerConfig{ PublicKey: peerPublicKey, - Endpoint: endpoint, ReplaceAllowedIPs: true, AllowedIPs: []net.IPNet{prefixToIPNet(peerConfig.Subnet)}, PersistentKeepaliveInterval: &persistentKeepalive, } + if peerConfig.Endpoint != nil { + wgPeerConfigs[i].Endpoint = &net.UDPAddr{ + IP: peerConfig.Endpoint.Addr().AsSlice(), + Port: int(peerConfig.Endpoint.Port()), + } + } } return wgtypes.Config{