k3s/pkg/vpn/vpn.go
Manuel Buil 12459fca97 Add extraArgs to tailscale
Signed-off-by: Manuel Buil <mbuil@suse.com>
2023-09-25 17:04:50 +02:00

162 lines
4.9 KiB
Go

package vpn
import (
"encoding/json"
"fmt"
"net"
"net/url"
"strings"
"github.com/k3s-io/k3s/pkg/util"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
)
const (
tailscaleIf = "tailscale0"
)
type TailscaleOutput struct {
TailscaleIPs []string `json:"TailscaleIPs"`
}
// VPNInfo includes node information of the VPN. It is a general struct in case we want to add more vpn integrations
type VPNInfo struct {
IPv4Address net.IP
IPv6Address net.IP
NodeID string
ProviderName string
VPNInterface string
}
// vpnCliAuthInfo includes auth information of the VPN. It is a general struct in case we want to add more vpn integrations
type vpnCliAuthInfo struct {
Name string
JoinKey string
ControlServerURL string
ExtraCLIFlags []string
}
// StartVPN starts the VPN interface. General function in case we want to add more vpn integrations
func StartVPN(vpnAuthConfigFile string) error {
authInfo, err := getVPNAuthInfo(vpnAuthConfigFile)
if err != nil {
return err
}
logrus.Infof("Starting VPN: %s", authInfo.Name)
switch authInfo.Name {
case "tailscale":
args := []string{
"up", "--authkey", authInfo.JoinKey, "--timeout=30s", "--reset",
}
if authInfo.ControlServerURL != "" {
args = append(args, "--login-server", authInfo.ControlServerURL)
}
if len(authInfo.ExtraCLIFlags) > 0 {
args = append(args, authInfo.ExtraCLIFlags...)
}
logrus.Debugf("Flags passed to tailscale up: %v", args)
output, err := util.ExecCommand("tailscale", args)
if err != nil {
return errors.Wrap(err, "tailscale up failed: "+output)
}
logrus.Debugf("Output from tailscale up: %v", output)
return nil
default:
return fmt.Errorf("Requested VPN: %s is not supported. We currently only support tailscale", authInfo.Name)
}
}
// GetVPNInfo returns a VPNInfo object with details about the VPN. General function in case we want to add more vpn integrations
func GetVPNInfo(vpnAuth string) (VPNInfo, error) {
authInfo, err := getVPNAuthInfo(vpnAuth)
if err != nil {
return VPNInfo{}, err
}
if authInfo.Name == "tailscale" {
return getTailscaleInfo()
}
return VPNInfo{}, nil
}
// getVPNAuthInfo returns the required authInfo object
func getVPNAuthInfo(vpnAuth string) (vpnCliAuthInfo, error) {
var authInfo vpnCliAuthInfo
// Separate extraArgs which will be passed directly to the vpn binary command
vpnCommand, extraArgs := processCLIArgs(vpnAuth)
authInfo.ExtraCLIFlags = extraArgs
vpnParameters := strings.Split(vpnCommand, ",")
for _, vpnKeyValues := range vpnParameters {
vpnKeyValue := strings.Split(vpnKeyValues, "=")
switch vpnKeyValue[0] {
case "name":
authInfo.Name = vpnKeyValue[1]
case "joinKey":
authInfo.JoinKey = vpnKeyValue[1]
case "controlServerURL":
authInfo.ControlServerURL = vpnKeyValue[1]
default:
return vpnCliAuthInfo{}, fmt.Errorf("VPN Error. The passed VPN auth info includes an unknown parameter: %v", vpnKeyValue[0])
}
}
if err := isVPNConfigOK(authInfo); err != nil {
return authInfo, err
}
return authInfo, nil
}
// isVPNConfigOK checks that the config is complete
func isVPNConfigOK(authInfo vpnCliAuthInfo) error {
if authInfo.Name == "tailscale" {
if authInfo.JoinKey == "" {
return errors.New("VPN Error. Tailscale requires a JoinKey")
}
if authInfo.ControlServerURL != "" {
if _, err := url.Parse(authInfo.ControlServerURL); err != nil {
return fmt.Errorf("VPN Error. Invalid control server URL for Tailscale: %w", err)
}
}
return nil
}
return errors.New("Requested VPN: " + authInfo.Name + " is not supported. We currently only support tailscale")
}
// getTailscaleInfo returns the IPs of the interface
func getTailscaleInfo() (VPNInfo, error) {
output, err := util.ExecCommand("tailscale", []string{"status", "--json"})
if err != nil {
return VPNInfo{}, fmt.Errorf("failed to run tailscale status --json: %v", err)
}
logrus.Debugf("Output from tailscale status --json: %v", output)
var tailscaleOutput TailscaleOutput
err = json.Unmarshal([]byte(output), &tailscaleOutput)
if err != nil {
return VPNInfo{}, fmt.Errorf("failed to unmarshal tailscale output: %v", err)
}
// Errors are ignored because the interface might not have ipv4 or ipv6 addresses (that's the only possible error)
ipv4Address, _ := util.GetFirst4String(tailscaleOutput.TailscaleIPs)
ipv6Address, _ := util.GetFirst6String(tailscaleOutput.TailscaleIPs)
return VPNInfo{IPv4Address: net.ParseIP(ipv4Address), IPv6Address: net.ParseIP(ipv6Address), NodeID: "", ProviderName: "tailscale", VPNInterface: tailscaleIf}, nil
}
// processCLIArgs separates the extraArgs part from the command.
// Note that tailscale flags of type list are comma separated and don't accept spaces, thus we can use strings.Fields to separate flags
func processCLIArgs(command string) (string, []string) {
subCommands := strings.Split(command, ",extraArgs=")
if len(subCommands) > 1 {
return subCommands[0], strings.Fields(subCommands[1])
}
return subCommands[0], []string{}
}