-
Notifications
You must be signed in to change notification settings - Fork 2.4k
/
vpn.go
161 lines (138 loc) · 4.92 KB
/
vpn.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
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{}
}