mirror of
https://github.com/edgelesssys/constellation.git
synced 2025-08-14 01:35:34 -04:00
Create cli/internal package
This commit is contained in:
parent
917be71d89
commit
aee4d44b45
12 changed files with 5 additions and 5 deletions
148
cli/internal/proto/client.go
Normal file
148
cli/internal/proto/client.go
Normal file
|
@ -0,0 +1,148 @@
|
|||
package proto
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"io"
|
||||
|
||||
"github.com/edgelesssys/constellation/coordinator/atls"
|
||||
"github.com/edgelesssys/constellation/coordinator/pubapi/pubproto"
|
||||
"github.com/edgelesssys/constellation/coordinator/state"
|
||||
kms "github.com/edgelesssys/constellation/kms/server/setup"
|
||||
"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/credentials"
|
||||
)
|
||||
|
||||
// Client wraps a PubAPI client and the connection to it.
|
||||
type Client struct {
|
||||
conn *grpc.ClientConn
|
||||
pubapi pubproto.APIClient
|
||||
}
|
||||
|
||||
// Connect connects the client to a given server, using the handed
|
||||
// Validators for the attestation of the connection.
|
||||
// The connection must be closed using Close(). If connect is
|
||||
// called on a client that already has a connection, the old
|
||||
// connection is closed.
|
||||
func (c *Client) Connect(endpoint string, validators []atls.Validator) error {
|
||||
tlsConfig, err := atls.CreateAttestationClientTLSConfig(nil, validators)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
conn, err := grpc.Dial(endpoint, grpc.WithTransportCredentials(credentials.NewTLS(tlsConfig)))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if c.conn != nil {
|
||||
c.conn.Close()
|
||||
}
|
||||
c.conn = conn
|
||||
c.pubapi = pubproto.NewAPIClient(conn)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Close closes the grpc connection of the client.
|
||||
// Close is idempotent and can be called on non connected clients
|
||||
// without returning an error.
|
||||
func (c *Client) Close() error {
|
||||
if c.conn == nil {
|
||||
return nil
|
||||
}
|
||||
if err := c.conn.Close(); err != nil {
|
||||
return err
|
||||
}
|
||||
c.conn = nil
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetState returns the state of the connected server.
|
||||
func (c *Client) GetState(ctx context.Context) (state.State, error) {
|
||||
if c.pubapi == nil {
|
||||
return state.Uninitialized, errors.New("client is not connected")
|
||||
}
|
||||
|
||||
resp, err := c.pubapi.GetState(ctx, &pubproto.GetStateRequest{})
|
||||
if err != nil {
|
||||
return state.Uninitialized, err
|
||||
}
|
||||
return state.State(resp.State), nil
|
||||
}
|
||||
|
||||
// Activate activates the Constellation coordinator via a grpc call.
|
||||
// The handed IP addresses must be the private IP addresses of running AWS or GCP instances,
|
||||
// and the userPublicKey is the VPN key of the users WireGuard interface.
|
||||
func (c *Client) Activate(ctx context.Context, userPublicKey, masterSecret []byte, nodeIPs, coordinatorIPs, autoscalingNodeGroups []string, cloudServiceAccountURI string, sshUserKeys []*pubproto.SSHUserKey) (ActivationResponseClient, error) {
|
||||
if c.pubapi == nil {
|
||||
return nil, errors.New("client is not connected")
|
||||
}
|
||||
if len(userPublicKey) == 0 {
|
||||
return nil, errors.New("parameter userPublicKey is empty")
|
||||
}
|
||||
if len(nodeIPs) == 0 {
|
||||
return nil, errors.New("parameter ips is empty")
|
||||
}
|
||||
|
||||
pubKey, err := wgtypes.ParseKey(string(userPublicKey))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
req := &pubproto.ActivateAsCoordinatorRequest{
|
||||
AdminVpnPubKey: pubKey[:],
|
||||
NodePublicIps: nodeIPs,
|
||||
CoordinatorPublicIps: coordinatorIPs,
|
||||
AutoscalingNodeGroups: autoscalingNodeGroups,
|
||||
MasterSecret: masterSecret,
|
||||
KmsUri: kms.ClusterKMSURI,
|
||||
StorageUri: kms.NoStoreURI,
|
||||
KeyEncryptionKeyId: "",
|
||||
UseExistingKek: false,
|
||||
CloudServiceAccountUri: cloudServiceAccountURI,
|
||||
SshUserKeys: sshUserKeys,
|
||||
}
|
||||
|
||||
client, err := c.pubapi.ActivateAsCoordinator(ctx, req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return NewActivationRespClient(client), nil
|
||||
}
|
||||
|
||||
// ActivationResponseClient has methods to read messages from a stream of
|
||||
// ActivateAsCoordinatorResponses.
|
||||
type ActivationResponseClient interface {
|
||||
// NextLog reads responses from the response stream and returns the
|
||||
// first received log.
|
||||
// If AdminConfig responses are received before the first log response
|
||||
// occurs, the state of the client is updated with those configs. An
|
||||
// io.EOF error is returned at the end of the stream.
|
||||
NextLog() (string, error)
|
||||
|
||||
// WriteLogStream reads responses from the response stream and
|
||||
// writes log responses to the handed writer.
|
||||
// Occurring AdminConfig responses update the state of the client.
|
||||
WriteLogStream(io.Writer) error
|
||||
|
||||
// GetKubeconfig returns the kubeconfig that was received in the
|
||||
// latest AdminConfig response or an error if the field is empty.
|
||||
GetKubeconfig() (string, error)
|
||||
|
||||
// GetCoordinatorVpnKey returns the Coordinator's VPN key that was
|
||||
// received in the latest AdminConfig response or an error if the field
|
||||
// is empty.
|
||||
GetCoordinatorVpnKey() (string, error)
|
||||
|
||||
// GetClientVpnIp returns the client VPN IP that was received
|
||||
// in the latest AdminConfig response or an error if the field is empty.
|
||||
GetClientVpnIp() (string, error)
|
||||
|
||||
// GetOwnerID returns the owner identifier, derived from the client's master secret
|
||||
// or an error if the field is empty.
|
||||
GetOwnerID() (string, error)
|
||||
|
||||
// GetClusterID returns the cluster's unique identifier
|
||||
// or an error if the field is empty.
|
||||
GetClusterID() (string, error)
|
||||
}
|
212
cli/internal/proto/client_test.go
Normal file
212
cli/internal/proto/client_test.go
Normal file
|
@ -0,0 +1,212 @@
|
|||
package proto
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"net"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/edgelesssys/constellation/coordinator/pubapi/pubproto"
|
||||
"github.com/edgelesssys/constellation/coordinator/state"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/connectivity"
|
||||
"google.golang.org/grpc/credentials/insecure"
|
||||
"google.golang.org/grpc/test/bufconn"
|
||||
)
|
||||
|
||||
func TestClose(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
require := require.New(t)
|
||||
|
||||
client := Client{}
|
||||
|
||||
// Create a connection.
|
||||
listener := bufconn.Listen(4)
|
||||
defer listener.Close()
|
||||
ctx := context.Background()
|
||||
conn, err := grpc.DialContext(ctx, "", grpc.WithContextDialer(func(context.Context, string) (net.Conn, error) {
|
||||
return listener.Dial()
|
||||
}), grpc.WithTransportCredentials(insecure.NewCredentials()))
|
||||
require.NoError(err)
|
||||
defer conn.Close()
|
||||
|
||||
// Wait for connection to reach 'connecting' state.
|
||||
// Connection is not yet usable in this state, but we just need
|
||||
// any stable non 'shutdown' state to validate that the state
|
||||
// previous to calling close isn't already 'shutdown'.
|
||||
err = func() error {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond)
|
||||
defer cancel()
|
||||
for {
|
||||
if ctx.Err() != nil {
|
||||
return ctx.Err()
|
||||
}
|
||||
if connectivity.Connecting == conn.GetState() {
|
||||
return nil
|
||||
}
|
||||
time.Sleep(5 * time.Millisecond)
|
||||
}
|
||||
}()
|
||||
require.NoError(err)
|
||||
|
||||
client.conn = conn
|
||||
|
||||
// Close connection.
|
||||
assert.NoError(client.Close())
|
||||
assert.Empty(client.conn)
|
||||
assert.Equal(connectivity.Shutdown, conn.GetState())
|
||||
|
||||
// Close closed connection.
|
||||
assert.NoError(client.Close())
|
||||
assert.Empty(client.conn)
|
||||
assert.Equal(connectivity.Shutdown, conn.GetState())
|
||||
}
|
||||
|
||||
func TestGetState(t *testing.T) {
|
||||
someErr := errors.New("some error")
|
||||
|
||||
testCases := map[string]struct {
|
||||
pubAPIClient pubproto.APIClient
|
||||
wantErr bool
|
||||
wantState state.State
|
||||
}{
|
||||
"success": {
|
||||
pubAPIClient: &stubPubAPIClient{getStateState: state.IsNode},
|
||||
wantState: state.IsNode,
|
||||
},
|
||||
"getState error": {
|
||||
pubAPIClient: &stubPubAPIClient{getStateErr: someErr},
|
||||
wantErr: true,
|
||||
},
|
||||
"uninitialized": {
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for name, tc := range testCases {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
|
||||
client := Client{}
|
||||
if tc.pubAPIClient != nil {
|
||||
client.pubapi = tc.pubAPIClient
|
||||
}
|
||||
|
||||
state, err := client.GetState(context.Background())
|
||||
|
||||
if tc.wantErr {
|
||||
assert.Error(err)
|
||||
} else {
|
||||
assert.NoError(err)
|
||||
assert.Equal(tc.wantState, state)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestActivate(t *testing.T) {
|
||||
testKey := base64.StdEncoding.EncodeToString([]byte("32bytesWireGuardKeyForTheTesting"))
|
||||
someErr := errors.New("failed")
|
||||
|
||||
testCases := map[string]struct {
|
||||
pubAPIClient *stubPubAPIClient
|
||||
userPublicKey string
|
||||
ips []string
|
||||
wantErr bool
|
||||
}{
|
||||
"normal activation": {
|
||||
pubAPIClient: &stubPubAPIClient{},
|
||||
userPublicKey: testKey,
|
||||
ips: []string{"192.0.2.1", "192.0.2.1", "192.0.2.1"},
|
||||
wantErr: false,
|
||||
},
|
||||
"client without pubAPIClient": {
|
||||
userPublicKey: testKey,
|
||||
ips: []string{"192.0.2.1", "192.0.2.1", "192.0.2.1"},
|
||||
wantErr: true,
|
||||
},
|
||||
"empty public key parameter": {
|
||||
pubAPIClient: &stubPubAPIClient{},
|
||||
userPublicKey: "",
|
||||
ips: []string{"192.0.2.1", "192.0.2.1", "192.0.2.1"},
|
||||
wantErr: true,
|
||||
},
|
||||
"invalid public key parameter": {
|
||||
pubAPIClient: &stubPubAPIClient{},
|
||||
userPublicKey: "invalid Key",
|
||||
ips: []string{"192.0.2.1", "192.0.2.1", "192.0.2.1"},
|
||||
wantErr: true,
|
||||
},
|
||||
"empty ips parameter": {
|
||||
pubAPIClient: &stubPubAPIClient{},
|
||||
userPublicKey: testKey,
|
||||
ips: []string{},
|
||||
wantErr: true,
|
||||
},
|
||||
"fail ActivateAsCoordinator": {
|
||||
pubAPIClient: &stubPubAPIClient{activateAsCoordinatorErr: someErr},
|
||||
userPublicKey: testKey,
|
||||
ips: []string{"192.0.2.1", "192.0.2.1", "192.0.2.1"},
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for name, tc := range testCases {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
|
||||
client := Client{}
|
||||
if tc.pubAPIClient != nil {
|
||||
client.pubapi = tc.pubAPIClient
|
||||
}
|
||||
_, err := client.Activate(context.Background(), []byte(tc.userPublicKey), []byte("Constellation"), tc.ips, nil, nil, "serviceaccount://test", nil)
|
||||
if tc.wantErr {
|
||||
assert.Error(err)
|
||||
} else {
|
||||
assert.NoError(err)
|
||||
assert.Equal("32bytesWireGuardKeyForTheTesting", string(tc.pubAPIClient.activateAsCoordinatorReqKey))
|
||||
assert.Equal(tc.ips, tc.pubAPIClient.activateAsCoordinatorReqIPs)
|
||||
assert.Equal("Constellation", string(tc.pubAPIClient.activateAsCoordinatorMasterSecret))
|
||||
assert.Equal("serviceaccount://test", tc.pubAPIClient.activateCloudServiceAccountURI)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
type stubPubAPIClient struct {
|
||||
getStateState state.State
|
||||
getStateErr error
|
||||
activateAsCoordinatorErr error
|
||||
activateAdditionalNodesErr error
|
||||
activateAsCoordinatorReqKey []byte
|
||||
activateAsCoordinatorReqIPs []string
|
||||
activateAsCoordinatorMasterSecret []byte
|
||||
activateAdditionalNodesReqIPs []string
|
||||
activateCloudServiceAccountURI string
|
||||
pubproto.APIClient
|
||||
}
|
||||
|
||||
func (s *stubPubAPIClient) GetState(ctx context.Context, in *pubproto.GetStateRequest, opts ...grpc.CallOption) (*pubproto.GetStateResponse, error) {
|
||||
return &pubproto.GetStateResponse{State: uint32(s.getStateState)}, s.getStateErr
|
||||
}
|
||||
|
||||
func (s *stubPubAPIClient) ActivateAsCoordinator(ctx context.Context, in *pubproto.ActivateAsCoordinatorRequest,
|
||||
opts ...grpc.CallOption,
|
||||
) (pubproto.API_ActivateAsCoordinatorClient, error) {
|
||||
s.activateAsCoordinatorReqKey = in.AdminVpnPubKey
|
||||
s.activateAsCoordinatorReqIPs = in.NodePublicIps
|
||||
s.activateAsCoordinatorMasterSecret = in.MasterSecret
|
||||
s.activateCloudServiceAccountURI = in.CloudServiceAccountUri
|
||||
return dummyActivateAsCoordinatorClient{}, s.activateAsCoordinatorErr
|
||||
}
|
||||
|
||||
func (s *stubPubAPIClient) ActivateAdditionalNodes(ctx context.Context, in *pubproto.ActivateAdditionalNodesRequest,
|
||||
opts ...grpc.CallOption,
|
||||
) (pubproto.API_ActivateAdditionalNodesClient, error) {
|
||||
s.activateAdditionalNodesReqIPs = in.NodePublicIps
|
||||
return dummyActivateAdditionalNodesClient{}, s.activateAdditionalNodesErr
|
||||
}
|
67
cli/internal/proto/recover.go
Normal file
67
cli/internal/proto/recover.go
Normal file
|
@ -0,0 +1,67 @@
|
|||
package proto
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
|
||||
"github.com/edgelesssys/constellation/coordinator/atls"
|
||||
"github.com/edgelesssys/constellation/state/keyservice/keyproto"
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/credentials"
|
||||
)
|
||||
|
||||
// KeyClient wraps a KeyAPI client and the connection to it.
|
||||
type KeyClient struct {
|
||||
conn *grpc.ClientConn
|
||||
keyapi keyproto.APIClient
|
||||
}
|
||||
|
||||
// Connect connects the client to a given server, using the handed
|
||||
// Validators for the attestation of the connection.
|
||||
// The connection must be closed using Close(). If connect is
|
||||
// called on a client that already has a connection, the old
|
||||
// connection is closed.
|
||||
func (c *KeyClient) Connect(endpoint string, validators []atls.Validator) error {
|
||||
tlsConfig, err := atls.CreateAttestationClientTLSConfig(nil, validators)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
conn, err := grpc.Dial(endpoint, grpc.WithTransportCredentials(credentials.NewTLS(tlsConfig)))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_ = c.Close()
|
||||
c.conn = conn
|
||||
c.keyapi = keyproto.NewAPIClient(conn)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Close closes the grpc connection of the client.
|
||||
// Close is idempotent and can be called on non connected clients
|
||||
// without returning an error.
|
||||
func (c *KeyClient) Close() error {
|
||||
if c.conn == nil {
|
||||
return nil
|
||||
}
|
||||
if err := c.conn.Close(); err != nil {
|
||||
return err
|
||||
}
|
||||
c.conn = nil
|
||||
return nil
|
||||
}
|
||||
|
||||
// PushStateDiskKey pushes the state disk key to a constellation instance in recovery mode.
|
||||
// The state disk key must be derived from the UUID of the state disk and the master key.
|
||||
func (c *KeyClient) PushStateDiskKey(ctx context.Context, stateDiskKey []byte) error {
|
||||
if c.keyapi == nil {
|
||||
return errors.New("client is not connected")
|
||||
}
|
||||
|
||||
req := &keyproto.PushStateDiskKeyRequest{
|
||||
StateDiskKey: stateDiskKey,
|
||||
}
|
||||
|
||||
_, err := c.keyapi.PushStateDiskKey(ctx, req)
|
||||
return err
|
||||
}
|
117
cli/internal/proto/respclients.go
Normal file
117
cli/internal/proto/respclients.go
Normal file
|
@ -0,0 +1,117 @@
|
|||
package proto
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
|
||||
"github.com/edgelesssys/constellation/coordinator/pubapi/pubproto"
|
||||
"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
|
||||
)
|
||||
|
||||
// ActivationRespClient has methods to read messages from a stream of
|
||||
// ActivateAsCoordinatorResponses. It wraps an API_ActivateAsCoordinatorClient.
|
||||
type ActivationRespClient struct {
|
||||
client pubproto.API_ActivateAsCoordinatorClient
|
||||
kubeconfig string
|
||||
coordinatorVpnKey string
|
||||
clientVpnIp string
|
||||
ownerID string
|
||||
clusterID string
|
||||
}
|
||||
|
||||
// NewActivationRespClient creates a new ActivationRespClient with the handed
|
||||
// API_ActivateAsCoordinatorClient.
|
||||
func NewActivationRespClient(client pubproto.API_ActivateAsCoordinatorClient) *ActivationRespClient {
|
||||
return &ActivationRespClient{
|
||||
client: client,
|
||||
}
|
||||
}
|
||||
|
||||
// NextLog reads responses from the response stream and returns the
|
||||
// first received log.
|
||||
func (a *ActivationRespClient) NextLog() (string, error) {
|
||||
for {
|
||||
resp, err := a.client.Recv()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
switch x := resp.Content.(type) {
|
||||
case *pubproto.ActivateAsCoordinatorResponse_Log:
|
||||
return x.Log.Message, nil
|
||||
case *pubproto.ActivateAsCoordinatorResponse_AdminConfig:
|
||||
config := x.AdminConfig
|
||||
a.kubeconfig = string(config.Kubeconfig)
|
||||
|
||||
coordinatorVpnKey, err := wgtypes.NewKey(config.CoordinatorVpnPubKey)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
a.coordinatorVpnKey = coordinatorVpnKey.String()
|
||||
a.clientVpnIp = config.AdminVpnIp
|
||||
a.ownerID = base64.StdEncoding.EncodeToString(config.OwnerId)
|
||||
a.clusterID = base64.StdEncoding.EncodeToString(config.ClusterId)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// WriteLogStream reads responses from the response stream and
|
||||
// writes log responses to the handed writer.
|
||||
func (a *ActivationRespClient) WriteLogStream(w io.Writer) error {
|
||||
log, err := a.NextLog()
|
||||
for err == nil {
|
||||
fmt.Fprintln(w, log)
|
||||
log, err = a.NextLog()
|
||||
}
|
||||
if !errors.Is(err, io.EOF) {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetKubeconfig returns the kubeconfig that was received in the
|
||||
// latest AdminConfig response or an error if the field is empty.
|
||||
func (a *ActivationRespClient) GetKubeconfig() (string, error) {
|
||||
if a.kubeconfig == "" {
|
||||
return "", errors.New("kubeconfig is empty")
|
||||
}
|
||||
return a.kubeconfig, nil
|
||||
}
|
||||
|
||||
// GetCoordinatorVpnKey returns the Coordinator's VPN key that was
|
||||
// received in the latest AdminConfig response or an error if the field
|
||||
// is empty.
|
||||
func (a *ActivationRespClient) GetCoordinatorVpnKey() (string, error) {
|
||||
if a.coordinatorVpnKey == "" {
|
||||
return "", errors.New("coordinator public VPN key is empty")
|
||||
}
|
||||
return a.coordinatorVpnKey, nil
|
||||
}
|
||||
|
||||
// GetClientVpnIp returns the client VPN IP that was received
|
||||
// in the latest AdminConfig response or an error if the field is empty.
|
||||
func (a *ActivationRespClient) GetClientVpnIp() (string, error) {
|
||||
if a.clientVpnIp == "" {
|
||||
return "", errors.New("client VPN IP is empty")
|
||||
}
|
||||
return a.clientVpnIp, nil
|
||||
}
|
||||
|
||||
// GetOwnerID returns the owner identifier, derived from the client's master secret
|
||||
// or an error if the field is empty.
|
||||
func (a *ActivationRespClient) GetOwnerID() (string, error) {
|
||||
if a.ownerID == "" {
|
||||
return "", errors.New("secret identifier is empty")
|
||||
}
|
||||
return a.ownerID, nil
|
||||
}
|
||||
|
||||
// GetClusterID returns the cluster's unique identifier
|
||||
// or an error if the field is empty.
|
||||
func (a *ActivationRespClient) GetClusterID() (string, error) {
|
||||
if a.clusterID == "" {
|
||||
return "", errors.New("cluster identifier is empty")
|
||||
}
|
||||
return a.clusterID, nil
|
||||
}
|
241
cli/internal/proto/respclients_test.go
Normal file
241
cli/internal/proto/respclients_test.go
Normal file
|
@ -0,0 +1,241 @@
|
|||
package proto
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"io"
|
||||
"testing"
|
||||
|
||||
"github.com/edgelesssys/constellation/coordinator/pubapi/pubproto"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"google.golang.org/grpc"
|
||||
)
|
||||
|
||||
// dummyActivateAsCoordinatorClient is a dummy and panics if Recv() is called.
|
||||
type dummyActivateAsCoordinatorClient struct {
|
||||
grpc.ClientStream
|
||||
}
|
||||
|
||||
func (c dummyActivateAsCoordinatorClient) Recv() (*pubproto.ActivateAsCoordinatorResponse, error) {
|
||||
panic("i'm a dummy, Recv() not implemented")
|
||||
}
|
||||
|
||||
// dummyActivateAsCoordinatorClient is a dummy and panics if Recv() is called.
|
||||
type dummyActivateAdditionalNodesClient struct {
|
||||
grpc.ClientStream
|
||||
}
|
||||
|
||||
func (c dummyActivateAdditionalNodesClient) Recv() (*pubproto.ActivateAdditionalNodesResponse, error) {
|
||||
panic("i'm a dummy, Recv() not implemented")
|
||||
}
|
||||
|
||||
// stubActivationAsCoordinatorClient recives responses from an predefined
|
||||
// response stream iterator or a stub error.
|
||||
type stubActivationAsCoordinatorClient struct {
|
||||
grpc.ClientStream
|
||||
|
||||
stream *stubActivateAsCoordinatorResponseIter
|
||||
recvErr error
|
||||
}
|
||||
|
||||
func (c stubActivationAsCoordinatorClient) Recv() (*pubproto.ActivateAsCoordinatorResponse, error) {
|
||||
if c.recvErr != nil {
|
||||
return nil, c.recvErr
|
||||
}
|
||||
return c.stream.Next()
|
||||
}
|
||||
|
||||
// stubActivateAsCoordinatorResponseIter is an iterator over a slice of
|
||||
// ActivateAsCoordinatorResponses. It returns the messages in the order
|
||||
// they occur in the slice and returns an io.EOF error when no response
|
||||
// is left.
|
||||
type stubActivateAsCoordinatorResponseIter struct {
|
||||
msgs []*pubproto.ActivateAsCoordinatorResponse
|
||||
}
|
||||
|
||||
// Next returns the next message from the message slice or an io.EOF error
|
||||
// if the message slice is empty.
|
||||
func (q *stubActivateAsCoordinatorResponseIter) Next() (*pubproto.ActivateAsCoordinatorResponse, error) {
|
||||
if len(q.msgs) == 0 {
|
||||
return nil, io.EOF
|
||||
}
|
||||
msg := q.msgs[0]
|
||||
q.msgs = q.msgs[1:]
|
||||
return msg, nil
|
||||
}
|
||||
|
||||
func TestNextLog(t *testing.T) {
|
||||
testClientVpnIp := "192.0.2.1"
|
||||
testCoordinatorVpnKey := []byte("32bytesWireGuardKeyForTheTesting")
|
||||
testCoordinatorVpnKey64 := []byte("MzJieXRlc1dpcmVHdWFyZEtleUZvclRoZVRlc3Rpbmc=")
|
||||
testKubeconfig := []byte("apiVersion:v1 kind:Config...")
|
||||
testConfigResp := &pubproto.ActivateAsCoordinatorResponse{
|
||||
Content: &pubproto.ActivateAsCoordinatorResponse_AdminConfig{
|
||||
AdminConfig: &pubproto.AdminConfig{
|
||||
AdminVpnIp: testClientVpnIp,
|
||||
CoordinatorVpnPubKey: testCoordinatorVpnKey,
|
||||
Kubeconfig: testKubeconfig,
|
||||
},
|
||||
},
|
||||
}
|
||||
testLogMessage := "some log message"
|
||||
testLogResp := &pubproto.ActivateAsCoordinatorResponse{
|
||||
Content: &pubproto.ActivateAsCoordinatorResponse_Log{
|
||||
Log: &pubproto.Log{
|
||||
Message: testLogMessage,
|
||||
},
|
||||
},
|
||||
}
|
||||
someErr := errors.New("failed")
|
||||
|
||||
testCases := map[string]struct {
|
||||
msgs []*pubproto.ActivateAsCoordinatorResponse
|
||||
wantLogLen int
|
||||
wantState bool
|
||||
recvErr error
|
||||
wantErr bool
|
||||
}{
|
||||
"some logs": {
|
||||
msgs: []*pubproto.ActivateAsCoordinatorResponse{testLogResp, testLogResp, testLogResp},
|
||||
wantLogLen: 3,
|
||||
},
|
||||
"only admin config": {
|
||||
msgs: []*pubproto.ActivateAsCoordinatorResponse{testConfigResp},
|
||||
wantState: true,
|
||||
},
|
||||
"logs and configs": {
|
||||
msgs: []*pubproto.ActivateAsCoordinatorResponse{testLogResp, testConfigResp, testLogResp, testConfigResp},
|
||||
wantLogLen: 2,
|
||||
wantState: true,
|
||||
},
|
||||
"no response": {
|
||||
msgs: []*pubproto.ActivateAsCoordinatorResponse{},
|
||||
wantLogLen: 0,
|
||||
},
|
||||
"recv fail": {
|
||||
recvErr: someErr,
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for name, tc := range testCases {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
|
||||
respClient := stubActivationAsCoordinatorClient{
|
||||
stream: &stubActivateAsCoordinatorResponseIter{
|
||||
msgs: tc.msgs,
|
||||
},
|
||||
recvErr: tc.recvErr,
|
||||
}
|
||||
client := NewActivationRespClient(respClient)
|
||||
|
||||
var logs []string
|
||||
var err error
|
||||
for err == nil {
|
||||
var log string
|
||||
log, err = client.NextLog()
|
||||
if err == nil {
|
||||
logs = append(logs, log)
|
||||
}
|
||||
}
|
||||
|
||||
assert.Error(err)
|
||||
if tc.wantErr {
|
||||
assert.NotErrorIs(err, io.EOF)
|
||||
return
|
||||
}
|
||||
|
||||
assert.ErrorIs(err, io.EOF)
|
||||
assert.Len(logs, tc.wantLogLen)
|
||||
|
||||
if tc.wantState {
|
||||
ip, err := client.GetClientVpnIp()
|
||||
assert.NoError(err)
|
||||
assert.Equal(testClientVpnIp, ip)
|
||||
config, err := client.GetKubeconfig()
|
||||
assert.NoError(err)
|
||||
assert.Equal(string(testKubeconfig), config)
|
||||
key, err := client.GetCoordinatorVpnKey()
|
||||
assert.NoError(err)
|
||||
assert.Equal(string(testCoordinatorVpnKey64), key)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestPrintLogStream(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
|
||||
//
|
||||
// 10 logs a 10 byte
|
||||
//
|
||||
var msgs []*pubproto.ActivateAsCoordinatorResponse
|
||||
for i := 0; i < 10; i++ {
|
||||
msgs = append(msgs, &pubproto.ActivateAsCoordinatorResponse{
|
||||
Content: &pubproto.ActivateAsCoordinatorResponse_Log{
|
||||
Log: &pubproto.Log{
|
||||
Message: "10BytesLog",
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
respClient := stubActivationAsCoordinatorClient{
|
||||
stream: &stubActivateAsCoordinatorResponseIter{
|
||||
msgs: msgs,
|
||||
},
|
||||
}
|
||||
client := NewActivationRespClient(respClient)
|
||||
out := &bytes.Buffer{}
|
||||
assert.NoError(client.WriteLogStream(out))
|
||||
assert.Equal(out.Len(), 10*11) // 10 messages * (len(message) + 1 newline)
|
||||
|
||||
//
|
||||
// Check error handling.
|
||||
//
|
||||
someErr := errors.New("failed")
|
||||
respClient = stubActivationAsCoordinatorClient{
|
||||
recvErr: someErr,
|
||||
}
|
||||
client = NewActivationRespClient(respClient)
|
||||
assert.Error(client.WriteLogStream(&bytes.Buffer{}))
|
||||
}
|
||||
|
||||
func TestGetKubeconfig(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
|
||||
client := NewActivationRespClient(dummyActivateAsCoordinatorClient{})
|
||||
_, err := client.GetKubeconfig()
|
||||
assert.Error(err)
|
||||
|
||||
client.kubeconfig = "apiVersion:v1 kind:Config..."
|
||||
config, err := client.GetKubeconfig()
|
||||
assert.NoError(err)
|
||||
assert.Equal("apiVersion:v1 kind:Config...", config)
|
||||
}
|
||||
|
||||
func TestGetCoordinatorVpnKey(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
|
||||
client := NewActivationRespClient(dummyActivateAsCoordinatorClient{})
|
||||
_, err := client.GetCoordinatorVpnKey()
|
||||
assert.Error(err)
|
||||
|
||||
client.coordinatorVpnKey = "32bytesWireGuardKeyForTheTesting"
|
||||
key, err := client.GetCoordinatorVpnKey()
|
||||
assert.NoError(err)
|
||||
assert.Equal("32bytesWireGuardKeyForTheTesting", key)
|
||||
}
|
||||
|
||||
func TestGetClientVpnIp(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
|
||||
client := NewActivationRespClient(dummyActivateAsCoordinatorClient{})
|
||||
_, err := client.GetClientVpnIp()
|
||||
assert.Error(err)
|
||||
|
||||
client.clientVpnIp = "192.0.2.1"
|
||||
ip, err := client.GetClientVpnIp()
|
||||
assert.NoError(err)
|
||||
assert.Equal("192.0.2.1", ip)
|
||||
}
|
143
cli/internal/status/status.go
Normal file
143
cli/internal/status/status.go
Normal file
|
@ -0,0 +1,143 @@
|
|||
package status
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"io"
|
||||
"time"
|
||||
|
||||
"github.com/edgelesssys/constellation/coordinator/atls"
|
||||
"github.com/edgelesssys/constellation/coordinator/pubapi/pubproto"
|
||||
"github.com/edgelesssys/constellation/coordinator/state"
|
||||
"google.golang.org/grpc"
|
||||
grpccodes "google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/credentials"
|
||||
grpcstatus "google.golang.org/grpc/status"
|
||||
)
|
||||
|
||||
// TODO(katexochen): Use protoClient for waiter?
|
||||
|
||||
// Waiter waits for PeerStatusServer to reach a specific state. The waiter needs
|
||||
// to be initialized before usage.
|
||||
type Waiter struct {
|
||||
initialized bool
|
||||
interval time.Duration
|
||||
newConn func(ctx context.Context, target string, opts ...grpc.DialOption) (ClientConn, error)
|
||||
newClient func(cc grpc.ClientConnInterface) pubproto.APIClient
|
||||
}
|
||||
|
||||
// NewWaiter returns a default Waiter with probing inteval of 10 seconds,
|
||||
// attested gRPC connection and PeerStatusClient.
|
||||
func NewWaiter() *Waiter {
|
||||
return &Waiter{
|
||||
interval: 10 * time.Second,
|
||||
newClient: pubproto.NewAPIClient,
|
||||
}
|
||||
}
|
||||
|
||||
// InitializeValidators initializes the validators for the attestation.
|
||||
func (w *Waiter) InitializeValidators(validators []atls.Validator) error {
|
||||
if len(validators) == 0 {
|
||||
return errors.New("no validators provided to initialize status waiter")
|
||||
}
|
||||
w.newConn = newAttestedConnGenerator(validators)
|
||||
w.initialized = true
|
||||
return nil
|
||||
}
|
||||
|
||||
// WaitFor waits for a PeerStatusServer, which is reachable under the given endpoint
|
||||
// to reach the specified state.
|
||||
func (w *Waiter) WaitFor(ctx context.Context, endpoint string, status ...state.State) error {
|
||||
if !w.initialized {
|
||||
return errors.New("waiter not initialized")
|
||||
}
|
||||
|
||||
ticker := time.NewTicker(w.interval)
|
||||
defer ticker.Stop()
|
||||
|
||||
// Check once before waiting
|
||||
resp, err := w.probe(ctx, endpoint)
|
||||
if err != nil && grpcstatus.Code(err) != grpccodes.Unavailable {
|
||||
return err
|
||||
}
|
||||
if resp != nil && containsState(state.State(resp.State), status...) {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Periodically check status again
|
||||
for {
|
||||
select {
|
||||
case <-ticker.C:
|
||||
resp, err := w.probe(ctx, endpoint)
|
||||
if grpcstatus.Code(err) == grpccodes.Unavailable {
|
||||
// The server isn't reachable yet.
|
||||
continue
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if containsState(state.State(resp.State), status...) {
|
||||
return nil
|
||||
}
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// probe sends a PeerStatusCheck request to a PeerStatusServer and returns the response.
|
||||
func (w *Waiter) probe(ctx context.Context, endpoint string) (*pubproto.GetStateResponse, error) {
|
||||
conn, err := w.newConn(ctx, endpoint)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
client := w.newClient(conn)
|
||||
return client.GetState(ctx, &pubproto.GetStateRequest{})
|
||||
}
|
||||
|
||||
// WaitForAll waits for a list of PeerStatusServers, which listen on the handed
|
||||
// endpoints, to reach the specified state.
|
||||
func (w *Waiter) WaitForAll(ctx context.Context, endpoints []string, status ...state.State) error {
|
||||
if !w.initialized {
|
||||
return errors.New("waiter not initialized")
|
||||
}
|
||||
|
||||
for _, endpoint := range endpoints {
|
||||
if err := w.WaitFor(ctx, endpoint, status...); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// newAttestedConnGenerator creates a function returning a default attested grpc connection.
|
||||
func newAttestedConnGenerator(validators []atls.Validator) func(ctx context.Context, target string, opts ...grpc.DialOption) (ClientConn, error) {
|
||||
return func(ctx context.Context, target string, opts ...grpc.DialOption) (ClientConn, error) {
|
||||
tlsConfig, err := atls.CreateAttestationClientTLSConfig(nil, validators)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return grpc.DialContext(
|
||||
ctx, target, grpc.WithTransportCredentials(credentials.NewTLS(tlsConfig)),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// ClientConn is the gRPC connection a PeerStatusClient uses to connect to a server.
|
||||
type ClientConn interface {
|
||||
grpc.ClientConnInterface
|
||||
io.Closer
|
||||
}
|
||||
|
||||
// containsState checks if current state is one of the given states.
|
||||
func containsState(s state.State, states ...state.State) bool {
|
||||
for _, state := range states {
|
||||
if state == s {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
218
cli/internal/status/status_test.go
Normal file
218
cli/internal/status/status_test.go
Normal file
|
@ -0,0 +1,218 @@
|
|||
package status
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/edgelesssys/constellation/coordinator/atls"
|
||||
"github.com/edgelesssys/constellation/coordinator/core"
|
||||
"github.com/edgelesssys/constellation/coordinator/pubapi/pubproto"
|
||||
"github.com/edgelesssys/constellation/coordinator/state"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"google.golang.org/grpc"
|
||||
)
|
||||
|
||||
func TestInitializeValidators(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
|
||||
waiter := Waiter{
|
||||
interval: time.Millisecond,
|
||||
newClient: stubNewClientFunc(&stubPeerStatusClient{state: state.IsNode}),
|
||||
}
|
||||
|
||||
// Uninitialized waiter fails.
|
||||
assert.Error(waiter.WaitFor(context.Background(), "someIP", state.IsNode))
|
||||
|
||||
// Initializing waiter with no validators fails
|
||||
assert.Error(waiter.InitializeValidators(nil))
|
||||
|
||||
// Initialized waiter succeeds
|
||||
assert.NoError(waiter.InitializeValidators([]atls.Validator{core.NewMockValidator()}))
|
||||
assert.NoError(waiter.WaitFor(context.Background(), "someIP", state.IsNode))
|
||||
}
|
||||
|
||||
func TestWaitForAndWaitForAll(t *testing.T) {
|
||||
var noErr error
|
||||
someErr := errors.New("failed")
|
||||
|
||||
testCases := map[string]struct {
|
||||
waiter Waiter
|
||||
waitForState []state.State
|
||||
wantErr bool
|
||||
}{
|
||||
"successful wait": {
|
||||
waiter: Waiter{
|
||||
initialized: true,
|
||||
interval: time.Millisecond,
|
||||
newConn: stubNewConnFunc(noErr),
|
||||
newClient: stubNewClientFunc(&stubPeerStatusClient{state: state.IsNode}),
|
||||
},
|
||||
waitForState: []state.State{state.IsNode},
|
||||
},
|
||||
"successful wait multi states": {
|
||||
waiter: Waiter{
|
||||
initialized: true,
|
||||
interval: time.Millisecond,
|
||||
newConn: stubNewConnFunc(noErr),
|
||||
newClient: stubNewClientFunc(&stubPeerStatusClient{state: state.IsNode}),
|
||||
},
|
||||
waitForState: []state.State{state.IsNode, state.ActivatingNodes},
|
||||
},
|
||||
"expect timeout": {
|
||||
waiter: Waiter{
|
||||
initialized: true,
|
||||
interval: time.Millisecond,
|
||||
newConn: stubNewConnFunc(noErr),
|
||||
newClient: stubNewClientFunc(&stubPeerStatusClient{state: state.AcceptingInit}),
|
||||
},
|
||||
waitForState: []state.State{state.IsNode},
|
||||
wantErr: true,
|
||||
},
|
||||
"fail to check call": {
|
||||
waiter: Waiter{
|
||||
initialized: true,
|
||||
interval: time.Millisecond,
|
||||
newConn: stubNewConnFunc(noErr),
|
||||
newClient: stubNewClientFunc(&stubPeerStatusClient{checkErr: someErr}),
|
||||
},
|
||||
waitForState: []state.State{state.IsNode},
|
||||
wantErr: true,
|
||||
},
|
||||
"fail to create conn": {
|
||||
waiter: Waiter{
|
||||
initialized: true,
|
||||
interval: time.Millisecond,
|
||||
newConn: stubNewConnFunc(someErr),
|
||||
newClient: stubNewClientFunc(&stubPeerStatusClient{}),
|
||||
},
|
||||
waitForState: []state.State{state.IsNode},
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
t.Run("WaitFor", func(t *testing.T) {
|
||||
for name, tc := range testCases {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
|
||||
ctx := context.Background()
|
||||
ctx, cancel := context.WithTimeout(ctx, 50*time.Millisecond)
|
||||
defer cancel()
|
||||
|
||||
err := tc.waiter.WaitFor(ctx, "someIP", tc.waitForState...)
|
||||
|
||||
if tc.wantErr {
|
||||
assert.Error(err)
|
||||
} else {
|
||||
assert.NoError(err)
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("WaitForAll", func(t *testing.T) {
|
||||
for name, tc := range testCases {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
|
||||
ctx := context.Background()
|
||||
ctx, cancel := context.WithTimeout(ctx, 50*time.Millisecond)
|
||||
defer cancel()
|
||||
|
||||
endpoints := []string{"192.0.2.1", "192.0.2.2", "192.0.2.3"}
|
||||
err := tc.waiter.WaitForAll(ctx, endpoints, tc.waitForState...)
|
||||
|
||||
if tc.wantErr {
|
||||
assert.Error(err)
|
||||
} else {
|
||||
assert.NoError(err)
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func stubNewConnFunc(errStub error) func(ctx context.Context, target string, opts ...grpc.DialOption) (ClientConn, error) {
|
||||
return func(ctx context.Context, target string, opts ...grpc.DialOption) (ClientConn, error) {
|
||||
return &stubClientConn{}, errStub
|
||||
}
|
||||
}
|
||||
|
||||
type stubClientConn struct{}
|
||||
|
||||
func (c *stubClientConn) Invoke(ctx context.Context, method string, args any, reply any, opts ...grpc.CallOption) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *stubClientConn) NewStream(ctx context.Context, desc *grpc.StreamDesc, method string, opts ...grpc.CallOption) (grpc.ClientStream, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (c *stubClientConn) Close() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func stubNewClientFunc(stubClient pubproto.APIClient) func(cc grpc.ClientConnInterface) pubproto.APIClient {
|
||||
return func(cc grpc.ClientConnInterface) pubproto.APIClient {
|
||||
return stubClient
|
||||
}
|
||||
}
|
||||
|
||||
type stubPeerStatusClient struct {
|
||||
state state.State
|
||||
checkErr error
|
||||
pubproto.APIClient
|
||||
}
|
||||
|
||||
func (c *stubPeerStatusClient) GetState(ctx context.Context, in *pubproto.GetStateRequest, opts ...grpc.CallOption) (*pubproto.GetStateResponse, error) {
|
||||
resp := &pubproto.GetStateResponse{State: uint32(c.state)}
|
||||
return resp, c.checkErr
|
||||
}
|
||||
|
||||
func TestContainsState(t *testing.T) {
|
||||
testCases := map[string]struct {
|
||||
s state.State
|
||||
states []state.State
|
||||
success bool
|
||||
}{
|
||||
"is state": {
|
||||
s: state.IsNode,
|
||||
states: []state.State{
|
||||
state.IsNode,
|
||||
},
|
||||
success: true,
|
||||
},
|
||||
"is state multi": {
|
||||
s: state.AcceptingInit,
|
||||
states: []state.State{
|
||||
state.AcceptingInit,
|
||||
state.ActivatingNodes,
|
||||
},
|
||||
success: true,
|
||||
},
|
||||
"is not state": {
|
||||
s: state.NodeWaitingForClusterJoin,
|
||||
states: []state.State{
|
||||
state.AcceptingInit,
|
||||
},
|
||||
},
|
||||
"is not state multi": {
|
||||
s: state.NodeWaitingForClusterJoin,
|
||||
states: []state.State{
|
||||
state.AcceptingInit,
|
||||
state.ActivatingNodes,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for name, tc := range testCases {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
|
||||
res := containsState(tc.s, tc.states...)
|
||||
assert.Equal(tc.success, res)
|
||||
})
|
||||
}
|
||||
}
|
101
cli/internal/vpn/vpn.go
Normal file
101
cli/internal/vpn/vpn.go
Normal file
|
@ -0,0 +1,101 @@
|
|||
package vpn
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"time"
|
||||
|
||||
wgquick "github.com/nmiculinic/wg-quick-go"
|
||||
"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
|
||||
)
|
||||
|
||||
const (
|
||||
interfaceName = "wg0"
|
||||
wireguardPort = 51820
|
||||
)
|
||||
|
||||
type ConfigHandler struct {
|
||||
up func(cfg *wgquick.Config, iface string) error
|
||||
}
|
||||
|
||||
func NewConfigHandler() *ConfigHandler {
|
||||
return &ConfigHandler{up: wgquick.Up}
|
||||
}
|
||||
|
||||
func (h *ConfigHandler) Create(coordinatorPubKey, coordinatorPubIP, clientPrivKey, clientVPNIP string, mtu int) (*wgquick.Config, error) {
|
||||
return NewWGQuickConfig(coordinatorPubKey, coordinatorPubIP, clientPrivKey, clientVPNIP, mtu)
|
||||
}
|
||||
|
||||
// Apply applies the generated WireGuard quick config.
|
||||
func (h *ConfigHandler) Apply(conf *wgquick.Config) error {
|
||||
return h.up(conf, interfaceName)
|
||||
}
|
||||
|
||||
// GetBytes returns the the bytes of the config.
|
||||
func (h *ConfigHandler) Marshal(conf *wgquick.Config) ([]byte, error) {
|
||||
data, err := conf.MarshalText()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("marshal wg-quick config: %w", err)
|
||||
}
|
||||
return data, nil
|
||||
}
|
||||
|
||||
// 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 wgtypes.Config{}, fmt.Errorf("parsing coordinator public key: %w", err)
|
||||
}
|
||||
|
||||
var endpoint *net.UDPAddr
|
||||
if ip := net.ParseIP(coordinatorPubIP); ip != nil {
|
||||
endpoint = &net.UDPAddr{IP: ip, Port: wireguardPort}
|
||||
} else {
|
||||
endpoint = nil
|
||||
}
|
||||
clientPrivKeyParsed, err := wgtypes.ParseKey(clientPrivKey)
|
||||
if err != nil {
|
||||
return wgtypes.Config{}, fmt.Errorf("parsing client private key: %w", err)
|
||||
}
|
||||
listenPort := wireguardPort
|
||||
|
||||
keepAlive := 10 * time.Second
|
||||
return wgtypes.Config{
|
||||
PrivateKey: &clientPrivKeyParsed,
|
||||
ListenPort: &listenPort,
|
||||
ReplacePeers: false,
|
||||
Peers: []wgtypes.PeerConfig{
|
||||
{
|
||||
PublicKey: coordinatorPubKeyParsed,
|
||||
UpdateOnly: false,
|
||||
Endpoint: endpoint,
|
||||
AllowedIPs: []net.IPNet{*allowedIPs},
|
||||
PersistentKeepaliveInterval: &keepAlive,
|
||||
},
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
// NewWGQuickConfig create a new WireGuard wg-quick configuration file and mashals it to bytes.
|
||||
func NewWGQuickConfig(coordinatorPubKey, coordinatorPubIP, clientPrivKey, clientVPNIP string, mtu int) (*wgquick.Config, error) {
|
||||
config, err := newConfig(coordinatorPubKey, coordinatorPubIP, clientPrivKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
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}}},
|
||||
MTU: mtu,
|
||||
}
|
||||
return &quickfile, nil
|
||||
}
|
160
cli/internal/vpn/vpn_test.go
Normal file
160
cli/internal/vpn/vpn_test.go
Normal file
|
@ -0,0 +1,160 @@
|
|||
package vpn
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
wgquick "github.com/nmiculinic/wg-quick-go"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
|
||||
)
|
||||
|
||||
func TestCreate(t *testing.T) {
|
||||
require := require.New(t)
|
||||
|
||||
testKey, err := wgtypes.GeneratePrivateKey()
|
||||
require.NoError(err)
|
||||
|
||||
testCases := map[string]struct {
|
||||
coordinatorPubKey string
|
||||
coordinatorPubIP string
|
||||
clientPrivKey string
|
||||
clientVPNIP string
|
||||
wantErr bool
|
||||
}{
|
||||
"valid config": {
|
||||
clientPrivKey: testKey.String(),
|
||||
clientVPNIP: "192.0.2.1",
|
||||
coordinatorPubKey: testKey.PublicKey().String(),
|
||||
coordinatorPubIP: "192.0.2.1",
|
||||
},
|
||||
"valid missing endpoint": {
|
||||
clientPrivKey: testKey.String(),
|
||||
clientVPNIP: "192.0.2.1",
|
||||
coordinatorPubKey: testKey.PublicKey().String(),
|
||||
},
|
||||
"invalid coordinator pub key": {
|
||||
clientPrivKey: testKey.String(),
|
||||
clientVPNIP: "192.0.2.1",
|
||||
coordinatorPubIP: "192.0.2.1",
|
||||
wantErr: true,
|
||||
},
|
||||
"invalid client priv key": {
|
||||
clientVPNIP: "192.0.2.1",
|
||||
coordinatorPubKey: testKey.PublicKey().String(),
|
||||
coordinatorPubIP: "192.0.2.1",
|
||||
wantErr: true,
|
||||
},
|
||||
"invalid client ip": {
|
||||
clientPrivKey: testKey.String(),
|
||||
coordinatorPubKey: testKey.PublicKey().String(),
|
||||
coordinatorPubIP: "192.0.2.1",
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for name, tc := range testCases {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
|
||||
handler := &ConfigHandler{}
|
||||
const mtu = 2
|
||||
|
||||
quickConfig, err := handler.Create(tc.coordinatorPubKey, tc.coordinatorPubIP, tc.clientPrivKey, tc.clientVPNIP, mtu)
|
||||
|
||||
if tc.wantErr {
|
||||
assert.Error(err)
|
||||
} else {
|
||||
assert.NoError(err)
|
||||
assert.Equal(tc.clientPrivKey, quickConfig.PrivateKey.String())
|
||||
assert.Equal(tc.clientVPNIP, quickConfig.Address[0].IP.String())
|
||||
|
||||
if tc.coordinatorPubIP != "" {
|
||||
assert.Equal(tc.coordinatorPubIP, quickConfig.Peers[0].Endpoint.IP.String())
|
||||
}
|
||||
assert.Equal(mtu, quickConfig.MTU)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestApply(t *testing.T) {
|
||||
testKey, err := wgtypes.GeneratePrivateKey()
|
||||
require.NoError(t, err)
|
||||
|
||||
testCases := map[string]struct {
|
||||
quickConfig *wgquick.Config
|
||||
upErr error
|
||||
wantErr bool
|
||||
}{
|
||||
"valid": {
|
||||
quickConfig: &wgquick.Config{Config: wgtypes.Config{PrivateKey: &testKey}},
|
||||
},
|
||||
"invalid apply": {
|
||||
quickConfig: &wgquick.Config{Config: wgtypes.Config{PrivateKey: &testKey}},
|
||||
upErr: errors.New("some err"),
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for name, tc := range testCases {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
|
||||
var ifaceSpy string
|
||||
var cfgSpy *wgquick.Config
|
||||
upSpy := func(cfg *wgquick.Config, iface string) error {
|
||||
ifaceSpy = iface
|
||||
cfgSpy = cfg
|
||||
return tc.upErr
|
||||
}
|
||||
|
||||
handler := &ConfigHandler{up: upSpy}
|
||||
|
||||
err := handler.Apply(tc.quickConfig)
|
||||
|
||||
if tc.wantErr {
|
||||
assert.Error(err)
|
||||
} else {
|
||||
assert.NoError(err)
|
||||
assert.Equal(interfaceName, ifaceSpy)
|
||||
assert.Equal(tc.quickConfig, cfgSpy)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestMarshal(t *testing.T) {
|
||||
require := require.New(t)
|
||||
|
||||
testKey, err := wgtypes.GeneratePrivateKey()
|
||||
require.NoError(err)
|
||||
|
||||
testCases := map[string]struct {
|
||||
quickConfig *wgquick.Config
|
||||
wantErr bool
|
||||
}{
|
||||
"valid": {
|
||||
quickConfig: &wgquick.Config{Config: wgtypes.Config{PrivateKey: &testKey}},
|
||||
},
|
||||
"invalid config": {
|
||||
quickConfig: &wgquick.Config{Config: wgtypes.Config{}},
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
for name, tc := range testCases {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
handler := &ConfigHandler{}
|
||||
|
||||
data, err := handler.Marshal(tc.quickConfig)
|
||||
if tc.wantErr {
|
||||
assert.Error(err)
|
||||
} else {
|
||||
assert.NoError(err)
|
||||
assert.Greater(len(data), 0)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue