Write WireGuard config file on init

This commit is contained in:
katexochen 2022-03-29 11:38:14 +02:00 committed by Paul Meyer
parent 5cf8f83ed8
commit 66fe34ee32
8 changed files with 268 additions and 25 deletions

View File

@ -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
}

View File

@ -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)
}
})
}
}

View File

@ -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 {

View File

@ -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
}

View File

@ -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())
}
})
}
}

3
go.mod
View File

@ -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

3
go.sum
View File

@ -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=

View File

@ -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),