Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,12 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [v1.0.59] - 2026-01-23

### Added
- `vpn config` command to Generate VPN configuration file without connecting
- `vpn up` command now supports flags to skip DNS configuration

## [v1.0.58] - 2026-01-21

### Fixed
Expand Down
2 changes: 1 addition & 1 deletion src/cmd/logout.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ func logoutCmd() *cmdBuilder.Cmd {
return err
}
if vpnActive {
_ = disconnectVpn(ctx, uxBlocks)
_ = disconnectVpn(ctx, uxBlocks, false, false)
}
uxBlocks.PrintInfo(styles.SuccessLine(i18n.T(i18n.LogoutSuccess)))

Expand Down
1 change: 1 addition & 0 deletions src/cmd/vpn.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ func vpnCmd() *cmdBuilder.Cmd {
HelpFlag(i18n.T(i18n.CmdHelpVpn)).
AddChildrenCmd(vpnUpCmd()).
AddChildrenCmd(vpnDownCmd()).
AddChildrenCmd(vpnConfigCmd()).
AddChildrenCmd(vpnClearCmd()).
AddChildrenCmd(vpnKeyCmd())
}
2 changes: 1 addition & 1 deletion src/cmd/vpnClear.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ func vpnClearCmd() *cmdBuilder.Cmd {
return err
}

if vpnDownErr := disconnectVpn(ctx, cmdData.UxBlocks); vpnDownErr != nil {
if vpnDownErr := disconnectVpn(ctx, cmdData.UxBlocks, false, false); vpnDownErr != nil {
cmdData.UxBlocks.PrintWarningTextf("vpn down: %s", vpnDownErr)
}

Expand Down
121 changes: 121 additions & 0 deletions src/cmd/vpnConfig.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
package cmd

import (
"context"
"os"
"time"

"github.com/zeropsio/zcli/src/cliStorage"
"github.com/zeropsio/zcli/src/cmdBuilder"
"github.com/zeropsio/zcli/src/constants"
"github.com/zeropsio/zcli/src/entity"
"github.com/zeropsio/zcli/src/file"
"github.com/zeropsio/zcli/src/i18n"
"github.com/zeropsio/zcli/src/uxBlock/styles"
"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"
"github.com/zeropsio/zerops-go/types/uuid"
)

func vpnConfigCmd() *cmdBuilder.Cmd {
return cmdBuilder.NewCmd().
Use("config").
Short("Generate VPN configuration file without connecting").
ScopeLevel(cmdBuilder.ScopeProject()).
Arg(cmdBuilder.ProjectArgName, cmdBuilder.OptionalArg()).
IntFlag(vpnFlagMtu, 1420, i18n.T(i18n.VpnMtuFlag)).
BoolFlag(vpnFlagSkipDnsSetup, false, "skip DNS configuration - you will need to use IP addresses to connect to services instead of domain names").
StringFlag(vpnFlagOutput, "", "output file path (use '-' for stdout, empty for default location)").
HelpFlag("Generate WireGuard VPN configuration file for the project without establishing connection").
LoggedUserRunFunc(func(ctx context.Context, cmdData *cmdBuilder.LoggedUserCmdData) error {
dnsSetup := !cmdData.Params.GetBool(vpnFlagSkipDnsSetup)

uxBlocks := cmdData.UxBlocks
project, err := cmdData.Project.Expect("project is null")
if err != nil {
return err
}

privateKey, err := getOrCreatePrivateVpnKey(project, cmdData)
if err != nil {
return err
}

publicKey := privateKey.PublicKey()

postProjectResponse, err := cmdData.RestApiClient.PostProjectVpn(
ctx,
path.ProjectId{Id: project.Id},
body.PostProjectVpn{PublicKey: types.String(publicKey.String())},
)
if err != nil {
return err
}

vpnSettings, err := postProjectResponse.Output()
if err != nil {
return err
}

outputPath := cmdData.Params.GetString(vpnFlagOutput)

// Determine output destination
var f *os.File
var filePath string
var fileMode os.FileMode

switch outputPath {
case "-":
// Output to stdout
f = os.Stdout
filePath = "stdout"
case "":
// Use default location
filePath, fileMode, err = constants.WgConfigFilePath()
if err != nil {
return err
}
f, err = file.Open(filePath, os.O_RDWR|os.O_CREATE|os.O_TRUNC, fileMode)
if err != nil {
return err
}
defer f.Close()
default:
// Use custom file path
filePath = outputPath
fileMode = 0600
f, err = os.OpenFile(filePath, os.O_RDWR|os.O_CREATE|os.O_TRUNC, fileMode)
if err != nil {
return err
}
defer f.Close()
}

if err := wg.GenerateConfig(f, privateKey, vpnSettings, cmdData.Params.GetInt(vpnFlagMtu), dnsSetup); err != nil {
return err
}

if outputPath != "-" {
uxBlocks.PrintInfo(styles.InfoWithValueLine(i18n.T(i18n.VpnConfigSaved), filePath))
}

if _, err = cmdData.CliStorage.Update(func(data cliStorage.Data) cliStorage.Data {
if data.ProjectVpnKeyRegistry == nil {
data.ProjectVpnKeyRegistry = make(map[uuid.ProjectId]entity.VpnKey)
}
data.ProjectVpnKeyRegistry[project.Id] = entity.VpnKey{
ProjectId: project.Id,
Key: privateKey.String(),
CreatedAt: time.Now(),
}

return data
}); err != nil {
return err
}

return nil
})
}
10 changes: 7 additions & 3 deletions src/cmd/vpnDown.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,17 @@ func vpnDownCmd() *cmdBuilder.Cmd {
Use("down").
Short(i18n.T(i18n.CmdDescVpnDown)).
HelpFlag(i18n.T(i18n.CmdHelpVpnDown)).
BoolFlag(vpnFlagSkipDnsSetup, false, "skip DNS configuration - you will need to use IP addresses to connect to services instead of domain names").
BoolFlag(vpnFlagSkipCheckInstallation, false, "skip WireGuard installation check").
LoggedUserRunFunc(func(ctx context.Context, cmdData *cmdBuilder.LoggedUserCmdData) error {
return disconnectVpn(ctx, cmdData.UxBlocks)
checkInstallation := !cmdData.Params.GetBool(vpnFlagSkipCheckInstallation)
dnsSetup := !cmdData.Params.GetBool(vpnFlagSkipDnsSetup)
return disconnectVpn(ctx, cmdData.UxBlocks, dnsSetup, checkInstallation)
})
}

func disconnectVpn(ctx context.Context, uxBlocks uxBlock.UxBlocks) error {
err := wg.CheckWgInstallation()
func disconnectVpn(ctx context.Context, uxBlocks uxBlock.UxBlocks, dnsSetup, checkInstallation bool) error {
err := wg.CheckWgInstallation(checkInstallation, dnsSetup)
if err != nil {
return err
}
Expand Down
45 changes: 31 additions & 14 deletions src/cmd/vpnUp.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,16 +28,33 @@ import (

const vpnCheckAddress = "logger.core.zerops"

const (
vpnFlagMtu = "mtu"
vpnFlagAutoDisconnect = "auto-disconnect"
vpnFlagSkipDnsSetup = "skip-dns-setup"
vpnFlagSkipVpnTest = "skip-vpn-test"
vpnFlagSkipCheckInstallation = "skip-check-installation"
vpnFlagSkipConnect = "skip-connect"
vpnFlagOutput = "output"
)

func vpnUpCmd() *cmdBuilder.Cmd {
return cmdBuilder.NewCmd().
Use("up").
Short(i18n.T(i18n.CmdDescVpnUp)).
ScopeLevel(cmdBuilder.ScopeProject()).
Arg(cmdBuilder.ProjectArgName, cmdBuilder.OptionalArg()).
IntFlag("mtu", 1420, i18n.T(i18n.VpnMtuFlag)).
BoolFlag("auto-disconnect", false, i18n.T(i18n.VpnAutoDisconnectFlag)).
IntFlag(vpnFlagMtu, 1420, i18n.T(i18n.VpnMtuFlag)).
BoolFlag(vpnFlagAutoDisconnect, false, i18n.T(i18n.VpnAutoDisconnectFlag)).
BoolFlag(vpnFlagSkipDnsSetup, false, "skip DNS configuration - you will need to use IP addresses to connect to services instead of domain names").
BoolFlag(vpnFlagSkipVpnTest, false, "skip VPN connectivity test after connection is established").
BoolFlag(vpnFlagSkipCheckInstallation, false, "skip WireGuard installation check").
HelpFlag(i18n.T(i18n.CmdHelpVpnUp)).
LoggedUserRunFunc(func(ctx context.Context, cmdData *cmdBuilder.LoggedUserCmdData) error {
dnsSetup := !cmdData.Params.GetBool(vpnFlagSkipDnsSetup)
vpnTest := !cmdData.Params.GetBool(vpnFlagSkipVpnTest)
checkInstallation := !cmdData.Params.GetBool(vpnFlagSkipCheckInstallation)

uxBlocks := cmdData.UxBlocks
project, err := cmdData.Project.Expect("project is null")
if err != nil {
Expand All @@ -49,8 +66,8 @@ func vpnUpCmd() *cmdBuilder.Cmd {
return err
}
if vpnActive {
if cmdData.Params.GetBool("auto-disconnect") {
if err := disconnectVpn(ctx, uxBlocks); err != nil {
if cmdData.Params.GetBool(vpnFlagAutoDisconnect) {
if err := disconnectVpn(ctx, uxBlocks, dnsSetup, checkInstallation); err != nil {
return err
}
} else {
Expand All @@ -66,7 +83,7 @@ func vpnUpCmd() *cmdBuilder.Cmd {
return nil
}

if err := disconnectVpn(ctx, uxBlocks); err != nil {
if err := disconnectVpn(ctx, uxBlocks, dnsSetup, checkInstallation); err != nil {
return err
}
}
Expand Down Expand Up @@ -104,8 +121,7 @@ func vpnUpCmd() *cmdBuilder.Cmd {
}
defer f.Close()

err = wg.GenerateConfig(f, privateKey, vpnSettings, cmdData.Params.GetInt("mtu"))
if err != nil {
if err := wg.GenerateConfig(f, privateKey, vpnSettings, cmdData.Params.GetInt(vpnFlagMtu), dnsSetup); err != nil {
return err
}

Expand All @@ -127,8 +143,7 @@ func vpnUpCmd() *cmdBuilder.Cmd {
return err
}

err = wg.CheckWgInstallation()
if err != nil {
if err := wg.CheckWgInstallation(checkInstallation, dnsSetup); err != nil {
return err
}

Expand All @@ -138,11 +153,13 @@ func vpnUpCmd() *cmdBuilder.Cmd {
return err
}

// wait for the vpn to be up
if isVpnUp(ctx, uxBlocks, 6) {
uxBlocks.PrintInfo(styles.SuccessLine(i18n.T(i18n.VpnUp)))
} else {
uxBlocks.PrintWarning(styles.WarningLine(i18n.T(i18n.VpnPingFailed)))
if vpnTest && dnsSetup {
// wait for the vpn to be up
if isVpnUp(ctx, uxBlocks, 6) {
uxBlocks.PrintInfo(styles.SuccessLine(i18n.T(i18n.VpnUp)))
} else {
uxBlocks.PrintWarning(styles.WarningLine(i18n.T(i18n.VpnPingFailed)))
}
}

return nil
Expand Down
11 changes: 8 additions & 3 deletions src/wg/darwin.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,10 @@ import (

const wgRunDir = "/var/run/wireguard/"

func CheckWgInstallation() error {
func CheckWgInstallation(checkInstallation, _ bool) error {
if !checkInstallation {
return nil
}
_, err := exec.LookPath("wg-quick")
if err != nil {
return errors.New(i18n.T(i18n.VpnWgQuickIsNotInstalled))
Expand All @@ -29,8 +32,8 @@ func CheckWgInstallation() error {
return nil
}

func GenerateConfig(f io.Writer, privateKey wgtypes.Key, vpnSettings output.ProjectVpnItem, mtu int) error {
data, err := defaultTemplateData(privateKey, vpnSettings, mtu)
func GenerateConfig(f io.Writer, privateKey wgtypes.Key, vpnSettings output.ProjectVpnItem, mtu int, dnsSetup bool) error {
data, err := defaultTemplateData(privateKey, vpnSettings, mtu, dnsSetup)
if err != nil {
return err
}
Expand Down Expand Up @@ -71,11 +74,13 @@ PrivateKey = {{.PrivateKey}}
MTU = {{.Mtu}}

Address = {{if .AssignedIpv4Address}}{{.AssignedIpv4Address}}/32{{end}}, {{.AssignedIpv6Address}}/128
{{if .DnsSetup -}}
PostUp = mkdir -p /etc/resolver
PostUp = echo "nameserver {{.Ipv4NetworkGateway}}" > /etc/resolver/zerops
PostUp = echo "domain zerops" >> /etc/resolver/zerops
PostUp = echo "search zerops" >> /etc/resolver/zerops
PostDown = rm /etc/resolver/zerops
{{end}}

[Peer]
PublicKey = {{.PublicKey}}
Expand Down
18 changes: 12 additions & 6 deletions src/wg/linux.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,19 +18,24 @@ import (
"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
)

func CheckWgInstallation() error {
func CheckWgInstallation(checkInstallation, dnsSetup bool) error {
if !checkInstallation {
return nil
}
if _, err := exec.LookPath("wg-quick"); err != nil {
return errors.New(i18n.T(i18n.VpnWgQuickIsNotInstalled))
}
// Debian does not have it by default anymore
if _, err := exec.LookPath("resolvectl"); err != nil {
return errors.New(i18n.T(i18n.VpnResolveCtlIsNotInstalled))
if dnsSetup {
if _, err := exec.LookPath("resolvectl"); err != nil {
return errors.New(i18n.T(i18n.VpnResolveCtlIsNotInstalled))
}
}
return nil
}

func GenerateConfig(f io.Writer, privateKey wgtypes.Key, vpnSettings output.ProjectVpnItem, mtu int) error {
data, err := defaultTemplateData(privateKey, vpnSettings, mtu)
func GenerateConfig(f io.Writer, privateKey wgtypes.Key, vpnSettings output.ProjectVpnItem, mtu int, dnsSetup bool) error {
data, err := defaultTemplateData(privateKey, vpnSettings, mtu, dnsSetup)
if err != nil {
return err
}
Expand Down Expand Up @@ -63,9 +68,10 @@ PrivateKey = {{.PrivateKey}}
MTU = {{.Mtu}}

Address = {{if .AssignedIpv4Address}}{{.AssignedIpv4Address}}/32{{end}}, {{.AssignedIpv6Address}}/128
{{if .DnsSetup -}}
PostUp = resolvectl dns %i {{.Ipv4NetworkGateway}}
PostUp = resolvectl domain %i zerops

{{end}}
[Peer]
PublicKey = {{.PublicKey}}

Expand Down
Loading