kilo/cmd/kgctl/showconf.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 %v 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: %v", err)
}
ps, err := opts.backend.Peers().List()
if err != nil {
return fmt.Errorf("failed to list peers: %v", err)
}
// Obtain the Granularity by looking at the annotation of the first node.
if opts.granularity, err = optainGranularity(opts.granularity, ns); err != nil {
return fmt.Errorf("failed to obtain 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: %v", 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: %v", 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: %v", 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: %v", err)
}
ps, err := opts.backend.Peers().List()
if err != nil {
return fmt.Errorf("failed to list peers: %v", err)
}
// Obtain the Granularity by looking at the annotation of the first node.
if opts.granularity, err = optainGranularity(opts.granularity, ns); err != nil {
return fmt.Errorf("failed to obtain 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: %v", err)
}
if !showConfOpts.asPeer {
c, err := t.PeerConf(peer).Bytes()
if err != nil {
return fmt.Errorf("failed to generate configuration: %v", 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: %v", 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
}