From 39123a1ee1201ed67e75158115c21a04ae7b1f0e Mon Sep 17 00:00:00 2001 From: "jan.hajek@zerops.io" Date: Sat, 30 Mar 2024 21:28:23 +0100 Subject: [PATCH] vpn refactored --- src/cmd/root.go | 2 +- src/cmd/vpnDown.go | 10 ++--- src/cmd/vpnUp.go | 79 ++++++++++++++++------------------------ src/i18n/en.go | 4 +- src/i18n/i18n.go | 4 +- src/uxBlock/spinner.go | 5 ++- src/uxHelpers/spinner.go | 10 ++++- src/wg/darwin.go | 60 ++++++++++++++++++++++++++++++ src/wg/linux.go | 60 ++++++++++++++++++++++++++++++ src/wg/wg.go | 60 ++++++++++++++++++++++++++++++ src/wg/windows.go | 62 +++++++++++++++++++++++++++++++ 11 files changed, 298 insertions(+), 58 deletions(-) create mode 100644 src/wg/darwin.go create mode 100644 src/wg/linux.go create mode 100644 src/wg/wg.go create mode 100644 src/wg/windows.go diff --git a/src/cmd/root.go b/src/cmd/root.go index 1c19c60b..9c48ead9 100644 --- a/src/cmd/root.go +++ b/src/cmd/root.go @@ -82,7 +82,7 @@ func rootCmd() *cmdBuilder.Cmd { } } - if isVpnUp(ctx) { + if isVpnUp(ctx, cmdData.UxBlocks, 1) { body.AddStringsRow(i18n.T(i18n.StatusInfoVpnStatus), i18n.T(i18n.VpnCheckingConnectionIsActive)) } else { body.AddStringsRow(i18n.T(i18n.StatusInfoVpnStatus), i18n.T(i18n.VpnCheckingConnectionIsNotActive)) diff --git a/src/cmd/vpnDown.go b/src/cmd/vpnDown.go index 3641e184..6541892f 100644 --- a/src/cmd/vpnDown.go +++ b/src/cmd/vpnDown.go @@ -3,9 +3,6 @@ package cmd import ( "context" "os" - "os/exec" - - "github.com/pkg/errors" "github.com/zeropsio/zcli/src/cmdBuilder" "github.com/zeropsio/zcli/src/cmdRunner" @@ -14,6 +11,7 @@ import ( "github.com/zeropsio/zcli/src/i18n" "github.com/zeropsio/zcli/src/uxBlock" "github.com/zeropsio/zcli/src/uxBlock/styles" + "github.com/zeropsio/zcli/src/wg" ) func vpnDownCmd() *cmdBuilder.Cmd { @@ -27,9 +25,9 @@ func vpnDownCmd() *cmdBuilder.Cmd { } func disconnectVpn(ctx context.Context, uxBlocks uxBlock.UxBlocks) error { - _, err := exec.LookPath("wg-quick") + err := wg.CheckWgInstallation() if err != nil { - return errors.New(i18n.T(i18n.VpnWgQuickIsNotInstalled)) + return err } filePath, fileMode, err := constants.WgConfigFilePath() @@ -44,7 +42,7 @@ func disconnectVpn(ctx context.Context, uxBlocks uxBlock.UxBlocks) error { } defer f.Close() - c := exec.CommandContext(ctx, "wg-quick", "down", filePath) + c := wg.DownCmd(ctx, filePath) _, err = cmdRunner.Run(c) if err != nil { return err diff --git a/src/cmd/vpnUp.go b/src/cmd/vpnUp.go index 90aefd33..04aa61aa 100644 --- a/src/cmd/vpnUp.go +++ b/src/cmd/vpnUp.go @@ -3,11 +3,10 @@ package cmd import ( "context" "os" - "os/exec" - "text/template" "time" "github.com/pkg/errors" + "github.com/zeropsio/zcli/src/uxBlock" "golang.zx2c4.com/wireguard/wgctrl/wgtypes" "github.com/zeropsio/zcli/src/cliStorage" @@ -21,6 +20,7 @@ import ( "github.com/zeropsio/zcli/src/nettools" "github.com/zeropsio/zcli/src/uxBlock/styles" "github.com/zeropsio/zcli/src/uxHelpers" + "github.com/zeropsio/zcli/src/wg" "github.com/zeropsio/zerops-go/dto/input/body" "github.com/zeropsio/zerops-go/dto/input/path" "github.com/zeropsio/zerops-go/types" @@ -40,7 +40,7 @@ func vpnUpCmd() *cmdBuilder.Cmd { LoggedUserRunFunc(func(ctx context.Context, cmdData *cmdBuilder.LoggedUserCmdData) error { uxBlocks := cmdData.UxBlocks - if isVpnUp(ctx) { + if isVpnUp(ctx, uxBlocks, 1) { if cmdData.Params.GetBool("auto-disconnect") { err := disconnectVpn(ctx, uxBlocks) if err != nil { @@ -92,28 +92,12 @@ func vpnUpCmd() *cmdBuilder.Cmd { return err } - f, err := file.Open(filePath, os.O_RDWR|os.O_CREATE, fileMode) + f, err := file.Open(filePath, os.O_RDWR|os.O_CREATE|os.O_TRUNC, fileMode) if err != nil { return err } - err = func() error { - defer f.Close() - - templ := template.Must(template.New("wg template").Parse(vpnTmpl)) - - return templ.Execute(f, map[string]interface{}{ - "PrivateKey": privateKey.String(), - "PublicKey": vpnSettings.Project.PublicKey, - "AssignedIpv4Address": vpnSettings.Peer.Ipv4.AssignedIpAddress, - "AssignedIpv6Address": vpnSettings.Peer.Ipv6.AssignedIpAddress, - "Ipv4NetworkGateway": vpnSettings.Project.Ipv4.Network.Gateway, - "ProjectIpv4Network": vpnSettings.Project.Ipv4.Network.Network, - "ProjectIpv6Network": vpnSettings.Project.Ipv6.Network.Network, - "Ipv4Network": vpnSettings.Peer.Ipv4.Network.Network, - "Ipv6Network": vpnSettings.Peer.Ipv6.Network.Network, - "ProjectIpv4SharedEndpoint": vpnSettings.Project.Ipv4.SharedEndpoint, - }) - }() + + err = wg.GenerateConfig(f, privateKey, vpnSettings) if err != nil { return err } @@ -136,18 +120,19 @@ func vpnUpCmd() *cmdBuilder.Cmd { return err } - _, err = exec.LookPath("wg-quick") + err = wg.CheckWgInstallation() if err != nil { - return errors.New(i18n.T(i18n.VpnWgQuickIsNotInstalled)) + return err } - c := exec.CommandContext(ctx, "wg-quick", "up", filePath) + c := wg.UpCmd(ctx, filePath) _, err = cmdRunner.Run(c) if err != nil { return err } - if isVpnUp(ctx) { + // wait for the vpn to be up + if isVpnUp(ctx, uxBlocks, 6) { uxBlocks.PrintInfo(styles.InfoLine(i18n.T(i18n.VpnUp))) } else { uxBlocks.PrintWarning(styles.WarningLine(i18n.T(i18n.VpnPingFailed))) @@ -157,11 +142,28 @@ func vpnUpCmd() *cmdBuilder.Cmd { }) } -func isVpnUp(ctx context.Context) bool { - ctx, cancel := context.WithTimeout(ctx, time.Second) - defer cancel() +func isVpnUp(ctx context.Context, uxBlocks uxBlock.UxBlocks, attempts int) bool { + p := []uxHelpers.Process{ + { + F: func(ctx context.Context) error { + for i := 0; i < attempts; i++ { + err := nettools.Ping(ctx, vpnCheckAddress) + if err == nil { + return nil + } + + time.Sleep(time.Millisecond * 500) + } + return errors.New(i18n.T(i18n.VpnPingFailed)) + }, + RunningMessage: i18n.T(i18n.VpnCheckingConnection), + ErrorMessageMessage: "", + SuccessMessage: "", + }, + } + + err := uxHelpers.ProcessCheckWithSpinner(ctx, uxBlocks, p) - err := nettools.Ping(ctx, vpnCheckAddress) return err == nil } @@ -186,20 +188,3 @@ func getOrCreatePrivateVpnKey(cmdData *cmdBuilder.LoggedUserCmdData) (wgtypes.Ke return vpnKey, nil } - -var vpnTmpl = ` -[Interface] -PrivateKey = {{.PrivateKey}} - -Address = {{if .AssignedIpv4Address}}{{.AssignedIpv4Address}}/32{{end}}, {{.AssignedIpv6Address}}/128 -DNS = {{.Ipv4NetworkGateway}}, zerops - -[Peer] -PublicKey = {{.PublicKey}} - -AllowedIPs = {{if .ProjectIpv4Network}}{{.ProjectIpv4Network}},{{end}} {{.ProjectIpv6Network}}, {{if .Ipv4Network}}{{.Ipv4Network}}, {{end}}{{.Ipv6Network}} - -Endpoint = {{.ProjectIpv4SharedEndpoint}} - -PersistentKeepalive = 5 -` diff --git a/src/i18n/en.go b/src/i18n/en.go index 60073a2b..a3c3879a 100644 --- a/src/i18n/en.go +++ b/src/i18n/en.go @@ -171,6 +171,7 @@ var en = map[string]string{ VpnPrivateKeyCreated: "VPN private key created", VpnDisconnectionPrompt: "VPN is active, do you want to disconnect?", VpnDisconnectionPromptNo: "VPN is active, you can disconnect using the 'zcli vpn down' command", + VpnCheckingConnection: "Checking VPN connection", VpnPingFailed: fmt.Sprintf("Wireguard adapter was created, but we are not able to establish a connection,"+ "this could indicate a problem on our side. Please contact our support team %s.", CustomerSupportLink), @@ -180,7 +181,8 @@ var en = map[string]string{ VpnDown: "VPN disconnected", // vpn shared - VpnWgQuickIsNotInstalled: "wg-quick is not installed, please install it and try again", + VpnWgQuickIsNotInstalled: "wg-quick is not installed, please visit https://www.wireguard.com/install/", + VpnWgQuickIsNotInstalledWindows: "wireguard is not installed, please visit https://www.wireguard.com/install/", // flags description RegionFlag: "Choose one of Zerops regions. Use the \"zcli region list\" command to list all Zerops regions.", diff --git a/src/i18n/i18n.go b/src/i18n/i18n.go index c39aef0f..c15a43a9 100644 --- a/src/i18n/i18n.go +++ b/src/i18n/i18n.go @@ -166,6 +166,7 @@ const ( VpnPrivateKeyCreated = "VpnPrivateKeyCreated" VpnDisconnectionPrompt = "VpnDisconnectionPrompt" VpnDisconnectionPromptNo = "VpnDisconnectionPromptNo" + VpnCheckingConnection = "VpnCheckingConnection" VpnPingFailed = "VpnPingFailed" // vpn down @@ -174,7 +175,8 @@ const ( VpnDown = "VpnDown" // vpn shared - VpnWgQuickIsNotInstalled = "VpnWgQuickIsNotInstalled" + VpnWgQuickIsNotInstalled = "VpnWgQuickIsNotInstalled" + VpnWgQuickIsNotInstalledWindows = "VpnWgQuickIsNotInstalledWindows" // flags description RegionFlag = "RegionFlag" diff --git a/src/uxBlock/spinner.go b/src/uxBlock/spinner.go index 64e58e3e..d7e74cc9 100644 --- a/src/uxBlock/spinner.go +++ b/src/uxBlock/spinner.go @@ -120,7 +120,10 @@ func (m *spinnerModel) View() string { if m.canceled { s += "canceled\n" } else { - s += spinner.view() + spinner.line.String() + "\n" + line := spinner.line.String() + if line != "" { + s += spinner.view() + spinner.line.String() + "\n" + } } } diff --git a/src/uxHelpers/spinner.go b/src/uxHelpers/spinner.go index c308c34c..c307ef5a 100644 --- a/src/uxHelpers/spinner.go +++ b/src/uxHelpers/spinner.go @@ -39,12 +39,20 @@ func ProcessCheckWithSpinner( defer wg.Done() err := process.F(ctx) if err != nil { - spinner.Finish(styles.ErrorLine(process.ErrorMessageMessage)) + if process.ErrorMessageMessage == "" { + spinner.Finish(styles.NewLine()) + } else { + spinner.Finish(styles.ErrorLine(process.ErrorMessageMessage)) + } once.Do(func() { returnErr = err }) return } + if process.SuccessMessage == "" { + spinner.Finish(styles.NewLine()) + return + } spinner.Finish(styles.SuccessLine(process.SuccessMessage)) }(processList[i], spinners[i]) } diff --git a/src/wg/darwin.go b/src/wg/darwin.go new file mode 100644 index 00000000..ec0add01 --- /dev/null +++ b/src/wg/darwin.go @@ -0,0 +1,60 @@ +//go:build darwin +// +build darwin + +package wg + +import ( + "context" + "io" + "os/exec" + "text/template" + + "github.com/pkg/errors" + "github.com/zeropsio/zcli/src/i18n" + "github.com/zeropsio/zerops-go/dto/output" + "golang.zx2c4.com/wireguard/wgctrl/wgtypes" +) + +func CheckWgInstallation() error { + _, err := exec.LookPath("wg-quick") + if err != nil { + return errors.New(i18n.T(i18n.VpnWgQuickIsNotInstalled)) + } + + return nil +} + +func GenerateConfig(f io.Writer, privateKey wgtypes.Key, vpnSettings output.ProjectVpnItem) error { + data, err := defaultTemplateData(privateKey, vpnSettings) + if err != nil { + return err + } + + return template.Must(template.New("wg template").Parse(vpnTmpl)).Execute(f, data) +} + +func UpCmd(ctx context.Context, filePath string) (err *exec.Cmd) { + return exec.CommandContext(ctx, "wg-quick", "up", filePath) +} + +func DownCmd(ctx context.Context, filePath string) (err *exec.Cmd) { + return exec.CommandContext(ctx, "wg-quick", "down", filePath) +} + +var vpnTmpl = ` +[Interface] +PrivateKey = {{.PrivateKey}} + +Address = {{if .AssignedIpv4Address}}{{.AssignedIpv4Address}}/32{{end}}, {{.AssignedIpv6Address}}/128 +PostUp = echo "nameserver {{.Ipv4NetworkGateway}}" > /etc/resolver/zerops +PostDown = rm /etc/resolver/zerops + +[Peer] +PublicKey = {{.PublicKey}} + +AllowedIPs = {{if .ProjectIpv4Network}}{{.ProjectIpv4Network}},{{end}} {{.ProjectIpv6Network}}, {{if .Ipv4Network}}{{.Ipv4Network}}, {{end}}{{.Ipv6Network}} + +Endpoint = {{.ProjectIpv4SharedEndpoint}} + +PersistentKeepalive = 5 +` diff --git a/src/wg/linux.go b/src/wg/linux.go new file mode 100644 index 00000000..b7629a95 --- /dev/null +++ b/src/wg/linux.go @@ -0,0 +1,60 @@ +//go:build linux +// +build linux + +package wg + +import ( + "context" + "io" + "os/exec" + "text/template" + + "github.com/pkg/errors" + "github.com/zeropsio/zcli/src/i18n" + "github.com/zeropsio/zerops-go/dto/output" + "golang.zx2c4.com/wireguard/wgctrl/wgtypes" +) + +func CheckWgInstallation() error { + _, err := exec.LookPath("wg-quick") + if err != nil { + return errors.New(i18n.T(i18n.VpnWgQuickIsNotInstalled)) + } + + return nil +} + +func GenerateConfig(f io.Writer, privateKey wgtypes.Key, vpnSettings output.ProjectVpnItem) error { + data, err := defaultTemplateData(privateKey, vpnSettings) + if err != nil { + return err + } + + return template.Must(template.New("wg template").Parse(vpnTmpl)).Execute(f, data) +} + +func UpCmd(ctx context.Context, filePath string) (err *exec.Cmd) { + return exec.CommandContext(ctx, "wg-quick", "up", filePath) +} + +func DownCmd(ctx context.Context, filePath string) (err *exec.Cmd) { + return exec.CommandContext(ctx, "wg-quick", "down", filePath) +} + +var vpnTmpl = ` +[Interface] +PrivateKey = {{.PrivateKey}} + +Address = {{if .AssignedIpv4Address}}{{.AssignedIpv4Address}}/32{{end}}, {{.AssignedIpv6Address}}/128 +PostUp = resolvectl dns %i {{.Ipv4NetworkGateway}} +PostUp = resolvectl domain %i zerops + +[Peer] +PublicKey = {{.PublicKey}} + +AllowedIPs = {{if .ProjectIpv4Network}}{{.ProjectIpv4Network}},{{end}} {{.ProjectIpv6Network}}, {{if .Ipv4Network}}{{.Ipv4Network}}, {{end}}{{.Ipv6Network}} + +Endpoint = {{.ProjectIpv4SharedEndpoint}} + +PersistentKeepalive = 5 +` diff --git a/src/wg/wg.go b/src/wg/wg.go new file mode 100644 index 00000000..a7d888b0 --- /dev/null +++ b/src/wg/wg.go @@ -0,0 +1,60 @@ +package wg + +import ( + "net" + + "github.com/pkg/errors" + "github.com/zeropsio/zerops-go/dto/output" + "golang.zx2c4.com/wireguard/wgctrl/wgtypes" +) + +func defaultTemplateData(privateKey wgtypes.Key, vpnSettings output.ProjectVpnItem) (map[string]string, error) { + projectIpv4Network := "" + if vpnSettings.Project.Ipv4.Network.Network != "" { + _, n, err := net.ParseCIDR(string(vpnSettings.Project.Ipv4.Network.Network)) + if err != nil { + return nil, errors.Wrap(err, "failed to parse projectIpv4Network network") + } + projectIpv4Network = n.String() + } + + projectIpv6Network := "" + if vpnSettings.Project.Ipv6.Network.Network != "" { + _, n, err := net.ParseCIDR(string(vpnSettings.Project.Ipv6.Network.Network)) + if err != nil { + return nil, errors.Wrap(err, "failed to parse projectIpv6Network network") + } + projectIpv6Network = n.String() + } + + ipv4Network := "" + if vpnSettings.Peer.Ipv4.Network.Network != "" { + _, n, err := net.ParseCIDR(string(vpnSettings.Peer.Ipv4.Network.Network)) + if err != nil { + return nil, errors.Wrap(err, "failed to parse Ipv4Network network") + } + ipv4Network = n.String() + } + + ipv6Network := "" + if vpnSettings.Peer.Ipv6.Network.Network != "" { + _, n, err := net.ParseCIDR(string(vpnSettings.Peer.Ipv6.Network.Network)) + if err != nil { + return nil, errors.Wrap(err, "failed to parse Ipv6Network network") + } + ipv6Network = n.String() + } + + return map[string]string{ + "PrivateKey": privateKey.String(), + "PublicKey": string(vpnSettings.Project.PublicKey), + "AssignedIpv4Address": string(vpnSettings.Peer.Ipv4.AssignedIpAddress), + "AssignedIpv6Address": string(vpnSettings.Peer.Ipv6.AssignedIpAddress), + "Ipv4NetworkGateway": string(vpnSettings.Project.Ipv4.Network.Gateway), + "ProjectIpv4Network": projectIpv4Network, + "ProjectIpv6Network": projectIpv6Network, + "Ipv4Network": ipv4Network, + "Ipv6Network": ipv6Network, + "ProjectIpv4SharedEndpoint": string(vpnSettings.Project.Ipv4.SharedEndpoint), + }, nil +} diff --git a/src/wg/windows.go b/src/wg/windows.go new file mode 100644 index 00000000..4c7a3516 --- /dev/null +++ b/src/wg/windows.go @@ -0,0 +1,62 @@ +//go:build windows +// +build windows + +package wg + +import ( + "context" + "io" + "os/exec" + "text/template" + + "github.com/pkg/errors" + "github.com/zeropsio/zcli/src/i18n" + "github.com/zeropsio/zerops-go/dto/output" + "golang.zx2c4.com/wireguard/wgctrl/wgtypes" +) + +func CheckWgInstallation() error { + _, err := exec.LookPath("wireguard") + if err != nil { + return errors.New(i18n.T(i18n.VpnWgQuickIsNotInstalledWindows)) + } + + return nil +} + +func GenerateConfig(f io.Writer, privateKey wgtypes.Key, vpnSettings output.ProjectVpnItem) error { + data, err := defaultTemplateData(privateKey, vpnSettings) + if err != nil { + return err + } + + return template.Must(template.New("wg template").Parse(vpnTmpl)).Execute(f, data) +} + +func UpCmd(ctx context.Context, filePath string) (err *exec.Cmd) { + return exec.CommandContext(ctx, "wireguard", "/installtunnelservice", filePath) +} + +func DownCmd(ctx context.Context, filePath string) (err *exec.Cmd) { + return exec.CommandContext(ctx, "wireguard", "/uninstalltunnelservice", filePath) +} + +var vpnTmpl = ` +[Interface] +PrivateKey = {{.PrivateKey}} + +Address = {{if .AssignedIpv4Address}}{{.AssignedIpv4Address}}/32{{end}}, {{.AssignedIpv6Address}}/128 +DNS = {{.Ipv4NetworkGateway}}, zerops +### Alternative DNS +# PostUp = powershell -command "Add-DnsClientNrptRule -Namespace 'zerops' -NameServers '{{.Ipv4NetworkGateway}}'" +# PostDown = powershell -command "Get-DnsClientNrptRule | Where { $_.Namespace -match '.*zerops' } | Remove-DnsClientNrptRule -force" + +[Peer] +PublicKey = {{.PublicKey}} + +AllowedIPs = {{if .ProjectIpv4Network}}{{.ProjectIpv4Network}},{{end}} {{.ProjectIpv6Network}}, {{if .Ipv4Network}}{{.Ipv4Network}}, {{end}}{{.Ipv6Network}} + +Endpoint = {{.ProjectIpv4SharedEndpoint}} + +PersistentKeepalive = 5 +`