diff --git a/cli/cmd/init.go b/cli/cmd/init.go index 9edda3819..6d22d34f3 100644 --- a/cli/cmd/init.go +++ b/cli/cmd/init.go @@ -135,6 +135,10 @@ func initialize(ctx context.Context, cmd *cobra.Command, protCl protoClient, vpn return err } + if err := result.writeWGQuickFile(fileHandler, config, string(flagArgs.userPrivKey)); err != nil { + return fmt.Errorf("write wg-quick file: %w", err) + } + if flagArgs.autoconfigureWG { if err := configureVpn(vpnCl, result.clientVpnIP, result.coordinatorPubKey, result.coordinatorPubIP, flagArgs.userPrivKey); err != nil { return err @@ -207,17 +211,34 @@ type activationResult struct { clusterID string } -func (res activationResult) writeOutput(w io.Writer, fileHandler file.Handler, config *config.Config) error { +// writeWGQuickFile writes the wg-quick file to the default path. +func (r activationResult) writeWGQuickFile(fileHandler file.Handler, config *config.Config, clientPrivKey string) error { + wgConf, err := vpn.NewConfig(r.coordinatorPubKey, r.coordinatorPubIP, clientPrivKey) + if err != nil { + return fmt.Errorf("create wg config: %w", err) + } + data, err := vpn.NewWGQuickConfig(wgConf, r.clientVpnIP) + if err != nil { + return fmt.Errorf("create wg-quick config: %w", err) + } + return fileHandler.Write(*config.WGQuickConfigPath, data, false) +} + +func (r activationResult) writeOutput(w io.Writer, fileHandler file.Handler, config *config.Config) error { fmt.Fprintln(w, "Your Constellation was successfully initialized.") - fmt.Fprintf(w, "Your WireGuard IP is %s\n", res.clientVpnIP) - fmt.Fprintf(w, "The Coordinator's public IP is %s\n", res.coordinatorPubIP) - fmt.Fprintf(w, "The Coordinator's public key is %s\n", res.coordinatorPubKey) - fmt.Fprintf(w, "The Constellation's owner identifier is %s\n", res.ownerID) - fmt.Fprintf(w, "The Constellation's unique identifier is %s\n", res.clusterID) - if err := fileHandler.Write(*config.AdminConfPath, []byte(res.kubeconfig), false); err != nil { + fmt.Fprintf(w, "Your WireGuard IP is %s\n", r.clientVpnIP) + fmt.Fprintf(w, "The Coordinator's public IP is %s\n", r.coordinatorPubIP) + fmt.Fprintf(w, "The Coordinator's public key is %s\n", r.coordinatorPubKey) + fmt.Fprintf(w, "The Constellation's owner identifier is %s\n", r.ownerID) + fmt.Fprintf(w, "The Constellation's unique identifier is %s\n", r.clusterID) + fmt.Fprintf(w, "Your WireGuard configuration file was written to %s\n", *config.WGQuickConfigPath) + if err := fileHandler.Write(*config.AdminConfPath, []byte(r.kubeconfig), false); err != nil { return err } fmt.Fprintf(w, "Your Constellation Kubernetes configuration was successfully written to %s\n", *config.AdminConfPath) + fmt.Fprintln(w, "\nYou can now connect to your Constellation by executing:") + fmt.Fprintf(w, "wg-quick up ./%s\n", *config.WGQuickConfigPath) + fmt.Fprintf(w, "export KUBECONFIG=\"$PWD/%s\"\n", *config.AdminConfPath) return nil } diff --git a/cli/cmd/init_test.go b/cli/cmd/init_test.go index af87de734..554e2ed24 100644 --- a/cli/cmd/init_test.go +++ b/cli/cmd/init_test.go @@ -20,6 +20,7 @@ import ( "github.com/spf13/cobra" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "golang.zx2c4.com/wireguard/wgctrl/wgtypes" ) func TestInitArgumentValidation(t *testing.T) { @@ -95,7 +96,7 @@ func TestInitialize(t *testing.T) { {log: "testlog2"}, { kubeconfig: "kubeconfig", - clientVpnIp: "vpnIp", + clientVpnIp: "192.0.2.2", coordinatorVpnKey: testKey, ownerID: "ownerID", clusterID: "clusterID", @@ -286,7 +287,7 @@ func TestInitialize(t *testing.T) { assert.Error(err) } else { require.NoError(err) - assert.Contains(out.String(), "vpnIp") + assert.Contains(out.String(), "192.0.2.2") assert.Contains(out.String(), "ownerID") assert.Contains(out.String(), "clusterID") } @@ -557,7 +558,7 @@ func TestAutoscaleFlag(t *testing.T) { {log: "testlog2"}, { kubeconfig: "kubeconfig", - clientVpnIp: "vpnIp", + clientVpnIp: "192.0.2.2", coordinatorVpnKey: testKey, ownerID: "ownerID", clusterID: "clusterID", @@ -659,3 +660,76 @@ func TestAutoscaleFlag(t *testing.T) { }) } } + +func TestWriteWGQuickFile(t *testing.T) { + require := require.New(t) + + testKey, err := wgtypes.GeneratePrivateKey() + require.NoError(err) + + testCases := map[string]struct { + coordinatorPubKey string + coordinatorPubIP string + clientVpnIp string + fileHandler file.Handler + config *config.Config + clientPrivKey string + wantErr bool + }{ + "write wg quick file": { + coordinatorPubKey: testKey.PublicKey().String(), + coordinatorPubIP: "192.0.2.1", + clientVpnIp: "192.0.2.2", + fileHandler: file.NewHandler(afero.NewMemMapFs()), + config: &config.Config{WGQuickConfigPath: func(s string) *string { return &s }("a.conf")}, + clientPrivKey: testKey.String(), + }, + "invalid coordinator public key": { + coordinatorPubIP: "192.0.2.1", + clientVpnIp: "192.0.2.2", + fileHandler: file.NewHandler(afero.NewMemMapFs()), + config: &config.Config{WGQuickConfigPath: func(s string) *string { return &s }("a.conf")}, + clientPrivKey: testKey.String(), + wantErr: true, + }, + "invalid client vpn ip": { + coordinatorPubKey: testKey.PublicKey().String(), + coordinatorPubIP: "192.0.2.1", + fileHandler: file.NewHandler(afero.NewMemMapFs()), + config: &config.Config{WGQuickConfigPath: func(s string) *string { return &s }("a.conf")}, + clientPrivKey: testKey.String(), + wantErr: true, + }, + "write fails": { + coordinatorPubKey: testKey.PublicKey().String(), + coordinatorPubIP: "192.0.2.1", + clientVpnIp: "192.0.2.2", + fileHandler: file.NewHandler(afero.NewReadOnlyFs(afero.NewMemMapFs())), + config: &config.Config{WGQuickConfigPath: func(s string) *string { return &s }("a.conf")}, + clientPrivKey: testKey.String(), + wantErr: true, + }, + } + + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + assert := assert.New(t) + + result := activationResult{ + coordinatorPubKey: tc.coordinatorPubKey, + coordinatorPubIP: tc.coordinatorPubIP, + clientVpnIP: tc.clientVpnIp, + } + err := result.writeWGQuickFile(tc.fileHandler, tc.config, tc.clientPrivKey) + + if tc.wantErr { + assert.Error(err) + } else { + assert.NoError(err) + file, err := tc.fileHandler.Read(*tc.config.WGQuickConfigPath) + assert.NoError(err) + assert.NotEmpty(file) + } + }) + } +} diff --git a/cli/cmd/terminate.go b/cli/cmd/terminate.go index a193bb41a..3d81a75c4 100644 --- a/cli/cmd/terminate.go +++ b/cli/cmd/terminate.go @@ -7,6 +7,7 @@ import ( "github.com/spf13/afero" "github.com/spf13/cobra" + "go.uber.org/multierr" azure "github.com/edgelesssys/constellation/cli/azure/client" ec2 "github.com/edgelesssys/constellation/cli/ec2/client" @@ -79,14 +80,20 @@ func terminate(cmd *cobra.Command, fileHandler file.Handler, config *config.Conf cmd.Println("Your Constellation was terminated successfully.") + var retErr error if err := fileHandler.Remove(*config.StatePath); err != nil { - return fmt.Errorf("failed to remove file '%s', please remove manually", *config.StatePath) + retErr = multierr.Append(err, fmt.Errorf("failed to remove file '%s', please remove manually", *config.StatePath)) } if err := fileHandler.Remove(*config.AdminConfPath); err != nil && !errors.Is(err, fs.ErrNotExist) { - return fmt.Errorf("failed to remove file '%s', please remove manually", *config.AdminConfPath) + retErr = multierr.Append(err, fmt.Errorf("failed to remove file '%s', please remove manually", *config.AdminConfPath)) } - return nil + + if err := fileHandler.Remove(*config.WGQuickConfigPath); err != nil && !errors.Is(err, fs.ErrNotExist) { + retErr = multierr.Append(err, fmt.Errorf("failed to remove file '%s', please remove manually", *config.WGQuickConfigPath)) + } + + return retErr } func terminateAzure(cmd *cobra.Command, cl azureclient, stat state.ConstellationState) error { diff --git a/cli/vpn/configurer.go b/cli/vpn/configurer.go index 1f98165fb..b1d04e5e0 100644 --- a/cli/vpn/configurer.go +++ b/cli/vpn/configurer.go @@ -1,9 +1,11 @@ package vpn import ( + "fmt" "net" "time" + wgquick "github.com/nmiculinic/wg-quick-go" "github.com/vishvananda/netlink" "golang.zx2c4.com/wireguard/wgctrl" "golang.zx2c4.com/wireguard/wgctrl/wgtypes" @@ -76,7 +78,8 @@ func New(netLink networkLink, vpn vpn) (*Configurer, error) { // WireGuard will listen on its default port. // The peer must have the IP 10.118.0.1 in the vpn. func (c *Configurer) Configure(clientVpnIp, coordinatorPubKey, coordinatorPubIP, clientPrivKey string) error { - if err := c.netLink.LinkAdd(&netlink.Wireguard{LinkAttrs: netlink.LinkAttrs{Name: interfaceName}}); err != nil { + wgLink := &netlink.Wireguard{LinkAttrs: netlink.LinkAttrs{Name: interfaceName}} + if err := c.netLink.LinkAdd(wgLink); err != nil { return err } @@ -95,14 +98,24 @@ func (c *Configurer) Configure(clientVpnIp, coordinatorPubKey, coordinatorPubIP, return err } - _, allowedIPs, err := net.ParseCIDR("10.118.0.1/32") + config, err := NewConfig(coordinatorPubKey, coordinatorPubIP, clientPrivKey) if err != nil { return err } + return c.vpn.ConfigureDevice(interfaceName, config) +} + +// NewConfig creates a new WireGuard configuration. +func NewConfig(coordinatorPubKey, coordinatorPubIP, clientPrivKey string) (wgtypes.Config, error) { + _, allowedIPs, err := net.ParseCIDR("10.118.0.1/32") + if err != nil { + return wgtypes.Config{}, fmt.Errorf("parsing CIDR: %w", err) + } + coordinatorPubKeyParsed, err := wgtypes.ParseKey(coordinatorPubKey) if err != nil { - return err + return wgtypes.Config{}, fmt.Errorf("parsing coordinator public key: %w", err) } var endpoint *net.UDPAddr @@ -113,12 +126,12 @@ func (c *Configurer) Configure(clientVpnIp, coordinatorPubKey, coordinatorPubIP, } clientPrivKeyParsed, err := wgtypes.ParseKey(clientPrivKey) if err != nil { - return err + return wgtypes.Config{}, fmt.Errorf("parsing client private key: %w", err) } listenPort := wireguardPort keepAlive := 10 * time.Second - err = c.vpn.ConfigureDevice(interfaceName, wgtypes.Config{ + return wgtypes.Config{ PrivateKey: &clientPrivKeyParsed, ListenPort: &listenPort, ReplacePeers: false, @@ -131,10 +144,22 @@ func (c *Configurer) Configure(clientVpnIp, coordinatorPubKey, coordinatorPubIP, PersistentKeepaliveInterval: &keepAlive, }, }, - }) - if err != nil { - return err - } - - return nil + }, nil +} + +// NewWGQuickConfig create a new WireGuard wg-quick configuration file and mashals it to bytes. +func NewWGQuickConfig(config wgtypes.Config, clientVPNIP string) ([]byte, error) { + clientIP := net.ParseIP(clientVPNIP) + if clientIP == nil { + return nil, fmt.Errorf("invalid client vpn ip '%s'", clientVPNIP) + } + quickfile := wgquick.Config{ + Config: config, + Address: []net.IPNet{{IP: clientIP, Mask: []byte{255, 255, 0, 0}}}, + } + data, err := quickfile.MarshalText() + if err != nil { + return nil, fmt.Errorf("marshal wg-quick config: %w", err) + } + return data, nil } diff --git a/cli/vpn/configurer_test.go b/cli/vpn/configurer_test.go index 202bfc563..55ce5ecb5 100644 --- a/cli/vpn/configurer_test.go +++ b/cli/vpn/configurer_test.go @@ -5,6 +5,7 @@ import ( "net" "testing" + wgquick "github.com/nmiculinic/wg-quick-go" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/vishvananda/netlink" @@ -68,7 +69,7 @@ func (s *stubVPN) ConfigureDevice(name string, cfg wgtypes.Config) error { return nil } -func TestVPNClient(t *testing.T) { +func TestConfigurer(t *testing.T) { assert := assert.New(t) require := require.New(t) @@ -98,3 +99,110 @@ func TestVPNClient(t *testing.T) { assert.Equal(net.JoinHostPort(coordinatorPubIp, "51820"), config.Peers[0].Endpoint.String()) assert.Equal("10.118.0.1/32", config.Peers[0].AllowedIPs[0].String()) } + +func TestNewConfig(t *testing.T) { + require := require.New(t) + + testKey, err := wgtypes.GeneratePrivateKey() + require.NoError(err) + + testCases := map[string]struct { + coordinatorPubKey wgtypes.Key + coordinatorPubIP string + clientPrivKey wgtypes.Key + wantErr bool + }{ + "valid": { + coordinatorPubKey: testKey.PublicKey(), + coordinatorPubIP: "192.0.2.1", + clientPrivKey: testKey, + }, + "empty coordinator pub ip": { + coordinatorPubKey: testKey.PublicKey(), + clientPrivKey: testKey, + }, + "empty coordinator public key": { + coordinatorPubKey: wgtypes.Key{}, + coordinatorPubIP: "192.0.2.1", + clientPrivKey: testKey, + wantErr: true, + }, + "empty client private key": { + coordinatorPubKey: testKey.PublicKey(), + coordinatorPubIP: "192.0.2.1", + clientPrivKey: wgtypes.Key{}, + wantErr: true, + }, + } + + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + assert := assert.New(t) + + var coordinatorPubKeyStr, clientPrivKeyStr string + if tc.coordinatorPubKey != (wgtypes.Key{}) { + coordinatorPubKeyStr = tc.coordinatorPubKey.String() + } + if tc.clientPrivKey != (wgtypes.Key{}) { + clientPrivKeyStr = tc.clientPrivKey.String() + } + config, err := NewConfig(coordinatorPubKeyStr, tc.coordinatorPubIP, clientPrivKeyStr) + + if tc.wantErr { + assert.Error(err) + } else { + assert.NoError(err) + assert.Equal(tc.coordinatorPubKey, config.Peers[0].PublicKey) + assert.Equal(tc.clientPrivKey, *config.PrivateKey) + } + }) + } +} + +func TestNewWGQuickConfig(t *testing.T) { + require := require.New(t) + + testKey, err := wgtypes.GeneratePrivateKey() + require.NoError(err) + testConfig := wgtypes.Config{ + PrivateKey: &testKey, + } + + testCases := map[string]struct { + config wgtypes.Config + clientVPNIP string + wantErr bool + }{ + "valid config": { + clientVPNIP: "192.0.2.1", + config: testConfig, + }, + "empty client vpn ip": { + config: testConfig, + wantErr: true, + }, + "config without private key": { + clientVPNIP: "192.0.2.1", + config: wgtypes.Config{}, + wantErr: true, + }, + } + + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + assert := assert.New(t) + + quickFile, err := NewWGQuickConfig(tc.config, tc.clientVPNIP) + + if tc.wantErr { + assert.Error(err) + } else { + assert.NoError(err) + var quickConfig wgquick.Config + assert.NoError(quickConfig.UnmarshalText(quickFile)) + assert.Equal(tc.config.PrivateKey, quickConfig.PrivateKey) + assert.Equal(tc.clientVPNIP, quickConfig.Address[0].IP.String()) + } + }) + } +} diff --git a/go.mod b/go.mod index b7716fe81..f41ae7bc4 100644 --- a/go.mod +++ b/go.mod @@ -32,6 +32,8 @@ replace ( k8s.io/sample-controller => k8s.io/sample-controller v0.23.1 ) +replace github.com/nmiculinic/wg-quick-go v0.1.3 => github.com/katexochen/wg-quick-go v0.1.3-beta.0 + require ( cloud.google.com/go/compute v1.5.0 cloud.google.com/go/iam v0.3.0 @@ -170,6 +172,7 @@ require ( github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00 // indirect + github.com/nmiculinic/wg-quick-go v0.1.3 github.com/opencontainers/go-digest v1.0.0 // indirect github.com/opencontainers/image-spec v1.0.2 // indirect github.com/opencontainers/runc v1.1.0 // indirect diff --git a/go.sum b/go.sum index da5637c0e..0cb66f522 100644 --- a/go.sum +++ b/go.sum @@ -959,6 +959,8 @@ github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8 github.com/jung-kurt/gofpdf v1.0.3-0.20190309125859-24315acbbda5/go.mod h1:7Id9E/uU8ce6rXgefFLlgrJj/GYY22cpxn+r32jIOes= github.com/k0kubun/go-ansi v0.0.0-20180517002512-3bf9e2903213/go.mod h1:vNUNkEQ1e29fT/6vq2aBdFsgNPmy8qMdSay1npru+Sw= github.com/karrick/godirwalk v1.16.1/go.mod h1:j4mkqPuvaLI8mp1DroR3P6ad7cyYd4c1qeJ3RV7ULlk= +github.com/katexochen/wg-quick-go v0.1.3-beta.0 h1:3udSRb7g2RdXWlFxaOPhVRdkY7uAkGy+30pGo8+5pKo= +github.com/katexochen/wg-quick-go v0.1.3-beta.0/go.mod h1:m3npTHwS7XHeXPF1XbUb/XhHURVZCXMpurHabylSA4I= github.com/kevinburke/ssh_config v0.0.0-20190725054713-01f96b0aa0cd/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM= github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00= @@ -1315,6 +1317,7 @@ github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeV github.com/sirupsen/logrus v1.0.4-0.20170822132746-89742aefa4b2/go.mod h1:pMByvHTf9Beacp5x1UXfOR9xyW/9antXMhjMPG0dEzc= github.com/sirupsen/logrus v1.0.6/go.mod h1:pMByvHTf9Beacp5x1UXfOR9xyW/9antXMhjMPG0dEzc= github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= +github.com/sirupsen/logrus v1.4.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q= github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88= diff --git a/internal/config/config.go b/internal/config/config.go index af852d9d1..55594eb0f 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -49,6 +49,7 @@ type Config struct { StatePath *string `json:"statepath,omitempty"` AdminConfPath *string `json:"adminconfpath,omitempty"` MasterSecretPath *string `json:"mastersecretpath,omitempty"` + WGQuickConfigPath *string `json:"wgquickconfigpath,omitempty"` CoordinatorPort *string `json:"coordinatorport,omitempty"` AutoscalingNodeGroupsMin *int `json:"autoscalingnodegroupsmin,omitempty"` AutoscalingNodeGroupsMax *int `json:"autoscalingnodegroupsmax,omitempty"` @@ -61,6 +62,7 @@ func Default() *Config { StatePath: proto.String("constellation-state.json"), AdminConfPath: proto.String("constellation-admin.conf"), MasterSecretPath: proto.String("constellation-mastersecret.base64"), + WGQuickConfigPath: proto.String("wg0.conf"), CoordinatorPort: proto.String(strconv.Itoa(coordinatorPort)), AutoscalingNodeGroupsMin: intPtr(1), AutoscalingNodeGroupsMax: intPtr(10),