0dfb744630
* kgctl connect Use kgctl connect to connect your laptop to a cluster. Signed-off-by: leonnicolas <leonloechner@gmx.de> * cmd/kgctl: finish connect command This commit fixes some bugs and finishes the implementation of the `kgctl connect` command. Signed-off-by: Lucas Servén Marín <lserven@gmail.com> * e2e: add tests for kgctl connect Signed-off-by: Lucas Servén Marín <lserven@gmail.com> * docs: add documentation for `kgctl connect` Signed-off-by: Lucas Servén Marín <lserven@gmail.com> * pkg/mesh: move peer route generation to mesh Signed-off-by: Lucas Servén Marín <lserven@gmail.com> Co-authored-by: Lucas Servén Marín <lserven@gmail.com>
362 lines
10 KiB
Go
362 lines
10 KiB
Go
// Copyright 2021 the Kilo authors
|
|
//
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
|
|
package main
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"net"
|
|
"os"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/spf13/cobra"
|
|
"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
|
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
|
"k8s.io/apimachinery/pkg/runtime"
|
|
"k8s.io/apimachinery/pkg/runtime/schema"
|
|
"k8s.io/apimachinery/pkg/runtime/serializer/json"
|
|
|
|
"github.com/squat/kilo/pkg/k8s/apis/kilo/v1alpha1"
|
|
"github.com/squat/kilo/pkg/mesh"
|
|
"github.com/squat/kilo/pkg/wireguard"
|
|
)
|
|
|
|
const (
|
|
outputFormatJSON = "json"
|
|
outputFormatWireGuard = "wireguard"
|
|
outputFormatYAML = "yaml"
|
|
)
|
|
|
|
var (
|
|
availableOutputFormats = strings.Join([]string{
|
|
outputFormatJSON,
|
|
outputFormatWireGuard,
|
|
outputFormatYAML,
|
|
}, ", ")
|
|
allowedIPs []string
|
|
showConfOpts struct {
|
|
allowedIPs []net.IPNet
|
|
serializer *json.Serializer
|
|
output string
|
|
asPeer bool
|
|
}
|
|
)
|
|
|
|
func showConf() *cobra.Command {
|
|
cmd := &cobra.Command{
|
|
Use: "showconf",
|
|
Short: "Show the WireGuard configuration for a node or peer in the Kilo network",
|
|
PersistentPreRunE: runShowConf,
|
|
}
|
|
|
|
for _, subCmd := range []*cobra.Command{
|
|
showConfNode(),
|
|
showConfPeer(),
|
|
} {
|
|
cmd.AddCommand(subCmd)
|
|
}
|
|
cmd.PersistentFlags().BoolVar(&showConfOpts.asPeer, "as-peer", false, "Should the resource be shown as a peer? Useful to configure this resource as a peer of another WireGuard interface.")
|
|
cmd.PersistentFlags().StringVarP(&showConfOpts.output, "output", "o", "wireguard", fmt.Sprintf("The output format of the resource. Only valid when combined with 'as-peer'. Possible values: %s", availableOutputFormats))
|
|
cmd.PersistentFlags().StringSliceVar(&allowedIPs, "allowed-ips", []string{}, "Add the given IPs to the allowed IPs of the configuration. Only valid when combined with 'as-peer'.")
|
|
|
|
return cmd
|
|
}
|
|
|
|
func runShowConf(c *cobra.Command, args []string) error {
|
|
switch showConfOpts.output {
|
|
case outputFormatJSON:
|
|
showConfOpts.serializer = json.NewSerializer(json.DefaultMetaFactory, peerCreatorTyper{}, peerCreatorTyper{}, true)
|
|
case outputFormatWireGuard:
|
|
case outputFormatYAML:
|
|
showConfOpts.serializer = json.NewYAMLSerializer(json.DefaultMetaFactory, peerCreatorTyper{}, peerCreatorTyper{})
|
|
default:
|
|
return fmt.Errorf("output format %s unknown; posible values are: %s", showConfOpts.output, availableOutputFormats)
|
|
}
|
|
for i := range allowedIPs {
|
|
_, aip, err := net.ParseCIDR(allowedIPs[i])
|
|
if err != nil {
|
|
return fmt.Errorf("allowed-ips must contain only valid CIDRs; got %q", allowedIPs[i])
|
|
}
|
|
showConfOpts.allowedIPs = append(showConfOpts.allowedIPs, *aip)
|
|
}
|
|
return runRoot(c, args)
|
|
}
|
|
|
|
func showConfNode() *cobra.Command {
|
|
return &cobra.Command{
|
|
Use: "node [name]",
|
|
Short: "Show the WireGuard configuration for a node in the Kilo network",
|
|
RunE: runShowConfNode,
|
|
Args: cobra.ExactArgs(1),
|
|
}
|
|
}
|
|
|
|
func showConfPeer() *cobra.Command {
|
|
return &cobra.Command{
|
|
Use: "peer [name]",
|
|
Short: "Show the WireGuard configuration for a peer in the Kilo network",
|
|
RunE: runShowConfPeer,
|
|
Args: cobra.ExactArgs(1),
|
|
}
|
|
}
|
|
|
|
func runShowConfNode(_ *cobra.Command, args []string) error {
|
|
ns, err := opts.backend.Nodes().List()
|
|
if err != nil {
|
|
return fmt.Errorf("failed to list nodes: %w", err)
|
|
}
|
|
ps, err := opts.backend.Peers().List()
|
|
if err != nil {
|
|
return fmt.Errorf("failed to list peers: %w", err)
|
|
}
|
|
// Obtain the Granularity by looking at the annotation of the first node.
|
|
if opts.granularity, err = determineGranularity(opts.granularity, ns); err != nil {
|
|
return fmt.Errorf("failed to determine granularity: %w", err)
|
|
}
|
|
hostname := args[0]
|
|
subnet := mesh.DefaultKiloSubnet
|
|
nodes := make(map[string]*mesh.Node)
|
|
for _, n := range ns {
|
|
if n.Ready() {
|
|
nodes[n.Name] = n
|
|
}
|
|
if n.WireGuardIP != nil {
|
|
subnet = n.WireGuardIP
|
|
}
|
|
}
|
|
subnet.IP = subnet.IP.Mask(subnet.Mask)
|
|
if len(nodes) == 0 {
|
|
return errors.New("did not find any valid Kilo nodes in the cluster")
|
|
}
|
|
if _, ok := nodes[hostname]; !ok {
|
|
return fmt.Errorf("did not find any node named %q in the cluster", hostname)
|
|
}
|
|
|
|
peers := make(map[string]*mesh.Peer)
|
|
for _, p := range ps {
|
|
if p.Ready() {
|
|
peers[p.Name] = p
|
|
}
|
|
}
|
|
|
|
t, err := mesh.NewTopology(nodes, peers, opts.granularity, hostname, int(opts.port), wgtypes.Key{}, subnet, nodes[hostname].PersistentKeepalive, nil)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to create topology: %w", err)
|
|
}
|
|
|
|
var found bool
|
|
for _, p := range t.PeerConf("").Peers {
|
|
if p.PublicKey == nodes[hostname].Key {
|
|
found = true
|
|
break
|
|
}
|
|
}
|
|
if !found {
|
|
_, err := os.Stderr.WriteString(fmt.Sprintf("Node %q is not a leader node\n", hostname))
|
|
return err
|
|
}
|
|
|
|
if !showConfOpts.asPeer {
|
|
c, err := t.Conf().Bytes()
|
|
if err != nil {
|
|
return fmt.Errorf("failed to generate configuration: %w", err)
|
|
}
|
|
_, err = os.Stdout.Write(c)
|
|
return err
|
|
}
|
|
|
|
switch showConfOpts.output {
|
|
case outputFormatJSON:
|
|
fallthrough
|
|
case outputFormatYAML:
|
|
p := t.AsPeer()
|
|
if p == nil {
|
|
return errors.New("cannot generate config from nil peer")
|
|
}
|
|
p.AllowedIPs = append(p.AllowedIPs, showConfOpts.allowedIPs...)
|
|
p.DeduplicateIPs()
|
|
k8sp := translatePeer(p)
|
|
k8sp.Name = hostname
|
|
return showConfOpts.serializer.Encode(k8sp, os.Stdout)
|
|
case outputFormatWireGuard:
|
|
p := t.AsPeer()
|
|
if p == nil {
|
|
return errors.New("cannot generate config from nil peer")
|
|
}
|
|
p.AllowedIPs = append(p.AllowedIPs, showConfOpts.allowedIPs...)
|
|
p.DeduplicateIPs()
|
|
c, err := (&wireguard.Conf{
|
|
Peers: []wireguard.Peer{*p},
|
|
}).Bytes()
|
|
if err != nil {
|
|
return fmt.Errorf("failed to generate configuration: %w", err)
|
|
}
|
|
_, err = os.Stdout.Write(c)
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func runShowConfPeer(_ *cobra.Command, args []string) error {
|
|
ns, err := opts.backend.Nodes().List()
|
|
if err != nil {
|
|
return fmt.Errorf("failed to list nodes: %w", err)
|
|
}
|
|
ps, err := opts.backend.Peers().List()
|
|
if err != nil {
|
|
return fmt.Errorf("failed to list peers: %w", err)
|
|
}
|
|
// Obtain the Granularity by looking at the annotation of the first node.
|
|
if opts.granularity, err = determineGranularity(opts.granularity, ns); err != nil {
|
|
return fmt.Errorf("failed to determine granularity: %w", err)
|
|
}
|
|
var hostname string
|
|
subnet := mesh.DefaultKiloSubnet
|
|
nodes := make(map[string]*mesh.Node)
|
|
for _, n := range ns {
|
|
if n.Ready() {
|
|
nodes[n.Name] = n
|
|
hostname = n.Name
|
|
}
|
|
if n.WireGuardIP != nil {
|
|
subnet = n.WireGuardIP
|
|
}
|
|
}
|
|
subnet.IP = subnet.IP.Mask(subnet.Mask)
|
|
if len(nodes) == 0 {
|
|
return errors.New("did not find any valid Kilo nodes in the cluster")
|
|
}
|
|
|
|
peer := args[0]
|
|
peers := make(map[string]*mesh.Peer)
|
|
for _, p := range ps {
|
|
if p.Ready() {
|
|
peers[p.Name] = p
|
|
}
|
|
}
|
|
if _, ok := peers[peer]; !ok {
|
|
return fmt.Errorf("did not find any peer named %q in the cluster", peer)
|
|
}
|
|
|
|
pka := time.Duration(0)
|
|
if p := peers[peer].PersistentKeepaliveInterval; p != nil {
|
|
pka = *p
|
|
}
|
|
t, err := mesh.NewTopology(nodes, peers, opts.granularity, hostname, mesh.DefaultKiloPort, wgtypes.Key{}, subnet, pka, nil)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to create topology: %w", err)
|
|
}
|
|
if !showConfOpts.asPeer {
|
|
c, err := t.PeerConf(peer).Bytes()
|
|
if err != nil {
|
|
return fmt.Errorf("failed to generate configuration: %w", err)
|
|
}
|
|
_, err = os.Stdout.Write(c)
|
|
return err
|
|
}
|
|
|
|
switch showConfOpts.output {
|
|
case outputFormatJSON:
|
|
fallthrough
|
|
case outputFormatYAML:
|
|
p := peers[peer]
|
|
p.AllowedIPs = append(p.AllowedIPs, showConfOpts.allowedIPs...)
|
|
p.DeduplicateIPs()
|
|
k8sp := translatePeer(&p.Peer)
|
|
k8sp.Name = peer
|
|
return showConfOpts.serializer.Encode(k8sp, os.Stdout)
|
|
case outputFormatWireGuard:
|
|
p := &peers[peer].Peer
|
|
p.AllowedIPs = append(p.AllowedIPs, showConfOpts.allowedIPs...)
|
|
p.DeduplicateIPs()
|
|
c, err := (&wireguard.Conf{
|
|
Peers: []wireguard.Peer{*p},
|
|
}).Bytes()
|
|
if err != nil {
|
|
return fmt.Errorf("failed to generate configuration: %w", err)
|
|
}
|
|
_, err = os.Stdout.Write(c)
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// translatePeer translates a wireguard.Peer to a Peer CRD.
|
|
// TODO this function has many similarities to peerBackend.Set(name, peer)
|
|
func translatePeer(peer *wireguard.Peer) *v1alpha1.Peer {
|
|
if peer == nil {
|
|
return &v1alpha1.Peer{}
|
|
}
|
|
var aips []string
|
|
for _, aip := range peer.AllowedIPs {
|
|
// Skip any invalid IPs.
|
|
// TODO all IPs should be valid, so no need to skip here?
|
|
if aip.String() == (&net.IPNet{}).String() {
|
|
continue
|
|
}
|
|
aips = append(aips, aip.String())
|
|
}
|
|
var endpoint *v1alpha1.PeerEndpoint
|
|
if peer.Endpoint.Port() > 0 || !peer.Endpoint.HasDNS() {
|
|
endpoint = &v1alpha1.PeerEndpoint{
|
|
DNSOrIP: v1alpha1.DNSOrIP{
|
|
IP: peer.Endpoint.IP().String(),
|
|
DNS: peer.Endpoint.DNS(),
|
|
},
|
|
Port: uint32(peer.Endpoint.Port()),
|
|
}
|
|
}
|
|
var key string
|
|
if peer.PublicKey != (wgtypes.Key{}) {
|
|
key = peer.PublicKey.String()
|
|
}
|
|
var psk string
|
|
if peer.PresharedKey != nil {
|
|
psk = peer.PresharedKey.String()
|
|
}
|
|
var pka int
|
|
if peer.PersistentKeepaliveInterval != nil && *peer.PersistentKeepaliveInterval > time.Duration(0) {
|
|
pka = int(*peer.PersistentKeepaliveInterval)
|
|
}
|
|
return &v1alpha1.Peer{
|
|
TypeMeta: metav1.TypeMeta{
|
|
Kind: v1alpha1.PeerKind,
|
|
APIVersion: v1alpha1.SchemeGroupVersion.String(),
|
|
},
|
|
Spec: v1alpha1.PeerSpec{
|
|
AllowedIPs: aips,
|
|
Endpoint: endpoint,
|
|
PersistentKeepalive: pka,
|
|
PresharedKey: psk,
|
|
PublicKey: key,
|
|
},
|
|
}
|
|
}
|
|
|
|
type peerCreatorTyper struct{}
|
|
|
|
func (p peerCreatorTyper) New(_ schema.GroupVersionKind) (runtime.Object, error) {
|
|
return &v1alpha1.Peer{}, nil
|
|
}
|
|
|
|
func (p peerCreatorTyper) ObjectKinds(_ runtime.Object) ([]schema.GroupVersionKind, bool, error) {
|
|
return []schema.GroupVersionKind{v1alpha1.PeerGVK}, false, nil
|
|
}
|
|
|
|
func (p peerCreatorTyper) Recognizes(_ schema.GroupVersionKind) bool {
|
|
return true
|
|
}
|