mirror of https://github.com/k3s-io/k3s
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
161 lines
4.9 KiB
161 lines
4.9 KiB
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{} |
|
}
|
|
|