51 Commits

Author SHA1 Message Date
github-actions[bot]
7948580d1d chore: update vendorHash in flake.nix 2026-02-16 14:41:10 +00:00
bb88db92c1 Refactor codebase 2026-02-16 09:40:34 -05:00
323709b0a1 Ignore windows arm 2026-02-14 07:49:46 -05:00
renovate[bot]
65509016cb chore(deps): update dependency go to v1.26.0 (#169)
* Update dependency go to v1.26.0

* Add nix dev shell

* Go mod tidy

* Remove version in glangci-lint

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Dave Gallant <davegallant@proton.me>
2026-02-14 07:38:10 -05:00
renovate[bot]
9892ebe864 Update actions/checkout action to v6 (#172)
* Update actions/checkout action to v6

* chore: update vendorHash in flake.nix

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-02-14 07:15:06 -05:00
renovate[bot]
a3e448a658 Update cachix/install-nix-action action to v31 (#173)
* Update cachix/install-nix-action action to v31

* chore: update vendorHash in flake.nix

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-02-14 07:14:54 -05:00
github-actions[bot]
33a1b1b1d3 chore: update vendorHash in flake.nix 2026-02-14 12:13:48 +00:00
renovate[bot]
c03d27afcb Update module github.com/olekukonko/tablewriter to v1.1.3 (#171)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-14 07:11:23 -05:00
4027784d1d Update flake.nix vendor-hash in a workflow 2026-02-14 06:59:08 -05:00
renovate[bot]
34054180ed Update module github.com/olekukonko/tablewriter to v1 (#155)
* Update module github.com/olekukonko/tablewriter to v1

* Fix lint

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Dave Gallant <davegallant@proton.me>
2026-02-13 22:06:53 -05:00
renovate[bot]
7da150493c Update module golang.org/x/net to v0.50.0 (#170)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-13 21:58:48 -05:00
9e9992e643 Update flake.nix 2026-01-17 16:01:58 -05:00
renovate[bot]
ff1d10e9bd Update dependency go to v1.25.6 (#167)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-17 10:07:08 -05:00
renovate[bot]
9939da15cc Update module golang.org/x/net to v0.49.0 (#168)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-17 10:06:59 -05:00
a668484da6 Update flake.nix 2025-12-19 17:34:32 -05:00
renovate[bot]
9fa908f5fd Update module github.com/spf13/afero to v1.15.0 (#143)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-19 08:50:54 -05:00
renovate[bot]
e0444a7e42 Update dependency go to v1.25.5 (#163)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Dave Gallant <davegallant@proton.me>
2025-12-19 08:49:09 -05:00
renovate[bot]
3f7d49f78d Update module github.com/spf13/cobra to v1.10.2 (#166)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-19 08:48:01 -05:00
renovate[bot]
5ac7d495ab Update module golang.org/x/net to v0.48.0 (#164)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-19 08:47:52 -05:00
eda46dcce9 Update flake.nix 2025-11-25 08:33:36 -05:00
002fec4537 Use buildGo125Module 2025-11-25 08:31:14 -05:00
cd92f93201 Run go mod tidy 2025-11-25 08:27:42 -05:00
aa547e09d2 nix flake update 2025-11-25 08:22:07 -05:00
9545c9fd85 Update flake.nix 2025-11-24 21:30:01 -05:00
renovate[bot]
fb9c4800ae Update golangci/golangci-lint-action action to v9 (#161)
* Update golangci/golangci-lint-action action to v9

* Bump golangci-lint to 2.6.2

* Refactor to pass golangcli-lint

* Fix tests

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Dave Gallant <dave.gallant@gmail.com>
Co-authored-by: Dave Gallant <davegallant@proton.me>
2025-11-23 09:13:17 -05:00
903f93895a Update flake.nix 2025-11-23 08:35:48 -05:00
renovate[bot]
1de072bbbf Update dependency go to 1.25 (#156)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-22 07:24:38 -05:00
renovate[bot]
486fc180dd Update module github.com/spf13/cobra to v1.10.1 (#159)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-22 07:23:58 -05:00
renovate[bot]
52cadc8433 Update module github.com/stretchr/testify to v1.11.1 (#158)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-22 07:23:11 -05:00
renovate[bot]
106f8b39d2 Update actions/setup-go action to v6 (#160)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-22 07:22:36 -05:00
renovate[bot]
79c742f274 Update actions/checkout action to v6 (#162)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-22 07:22:23 -05:00
0167804252 Update flake.nix 2025-07-28 20:26:25 -04:00
renovate[bot]
acb938dfdd Update module github.com/stretchr/testify to v1.10.0 (#140)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-07-25 21:25:00 -04:00
Dave Gallant
5c069cb289 Add flake.nix 2025-04-06 22:05:19 -04:00
renovate[bot]
98bb23ee28 Update module github.com/rs/zerolog to v1.34.0 (#151)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-04-05 06:47:22 -04:00
9185801b91 Update README.md (#152) 2025-03-29 08:36:03 -04:00
renovate[bot]
4cb7af39c6 Update dependency go to 1.24 (#150)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-03-14 22:29:41 -04:00
renovate[bot]
552a6e3543 Update module github.com/spf13/cobra to v1.9.1 (#147)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-02-21 22:38:55 -05:00
renovate[bot]
8811763d7e Update module github.com/spf13/cobra to v1.9.0 (#146)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-02-15 22:04:52 -05:00
renovate[bot]
4bd470b95e Update module golang.org/x/net to v0.35.0 (#145)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-02-14 21:24:46 -05:00
renovate[bot]
9fb2a3f171 Update module golang.org/x/net to v0.34.0 (#144)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-01-10 21:52:17 -05:00
renovate[bot]
4e8cc6b9b7 Update module golang.org/x/net to v0.33.0 [SECURITY] (#142)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-12-18 23:48:59 -05:00
renovate[bot]
7afc0dd235 Update module golang.org/x/net to v0.32.0 (#141)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-12-06 22:01:19 -05:00
renovate[bot]
a4391ba6ee Update module golang.org/x/net to v0.31.0 (#139)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-08 19:46:14 -05:00
Dave Gallant
4c66b19a7d Add "386" goarch to .goreleaser.yaml 2024-10-04 22:16:39 -04:00
renovate[bot]
3a03348840 Update module golang.org/x/net to v0.30.0 (#138)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-04 21:40:37 -04:00
renovate[bot]
361d54c96f Update module golang.org/x/net to v0.29.0 (#135)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-09-06 20:45:37 -04:00
renovate[bot]
f5d788a271 Update module golang.org/x/net to v0.28.0 (#134)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-08-10 09:05:24 -04:00
Dave Gallant
4b202ae94c Add new asciinema to README.md 2024-07-29 20:50:35 -04:00
Dave Gallant
a72c298292 Update README.md (#133) 2024-07-28 21:29:44 -04:00
Dave Gallant
3e819c5c55 Add initial support and docs for Windows (#132)
- Add initial support for Windows
- Update docs
- Fix issue with openvpn 2.6 data-ciphers
2024-07-28 21:09:57 -04:00
19 changed files with 510 additions and 297 deletions

1
.envrc Normal file
View File

@@ -0,0 +1 @@
use flake

View File

@@ -13,13 +13,11 @@ jobs:
name: validate name: validate
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/setup-go@v5 - uses: actions/setup-go@v6
with: with:
go-version: 1.19 go-version: 1.26
- uses: actions/checkout@v4 - uses: actions/checkout@v6
- name: golangci-lint - name: golangci-lint
uses: golangci/golangci-lint-action@v6 uses: golangci/golangci-lint-action@v9
with:
version: v1.49
- name: test - name: test
run: make test run: make test

View File

@@ -11,19 +11,18 @@ jobs:
steps: steps:
- -
name: Checkout name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v6
with: with:
fetch-depth: 0 fetch-depth: 0
- -
name: Set up Go name: Set up Go
uses: actions/setup-go@v5 uses: actions/setup-go@v6
with: with:
go-version: "1.22" go-version: "1.26"
- -
name: Run GoReleaser name: Run GoReleaser
uses: goreleaser/goreleaser-action@v6 uses: goreleaser/goreleaser-action@v6
with: with:
version: '~> v2'
args: release --clean args: release --clean
env: env:
GITHUB_TOKEN: ${{ secrets.PERSONAL_GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.PERSONAL_GITHUB_TOKEN }}

View File

@@ -0,0 +1,58 @@
name: Update vendorHash in flake.nix
on:
push:
paths:
- 'go.mod'
- 'go.sum'
workflow_dispatch:
permissions:
contents: write
jobs:
update-hash:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: cachix/install-nix-action@v31
with:
nix_path: nixpkgs=channel:nixpkgs-unstable
- name: Calculate new vendorHash
id: hash
run: |
# Set vendorHash to empty string to trigger hash mismatch
sed -i 's|vendorHash = .*|vendorHash = "";|' flake.nix
# Try to build and extract the expected hash from error message
BUILD_OUTPUT=$(nix build .#vpngate 2>&1 || true)
HASH=$(echo "$BUILD_OUTPUT" | grep -oP 'got:\s*\K(sha256-[a-zA-Z0-9+/]+={0,2})' | head -1)
if [ -z "$HASH" ]; then
echo "Build output:"
echo "$BUILD_OUTPUT"
echo "Failed to extract hash from build output"
exit 1
fi
echo "hash=$HASH" >> $GITHUB_OUTPUT
echo "Calculated hash: $HASH"
- name: Update flake.nix with correct hash
run: |
sed -i "s|vendorHash = \"\";|vendorHash = \"${{ steps.hash.outputs.hash }}\";|" flake.nix
- name: Commit and push if changed
run: |
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
if git diff --quiet flake.nix; then
echo "No changes to commit"
else
git add flake.nix
git commit -m "chore: update vendorHash in flake.nix"
git push
fi

4
.gitignore vendored
View File

@@ -1,3 +1,7 @@
dist dist
.vscode .vscode
# direnv
.direnv
.envrc.local

View File

@@ -15,11 +15,15 @@ builds:
- darwin - darwin
- windows - windows
goarch: goarch:
- "386"
- amd64 - amd64
- arm - arm
- arm64 - arm64
goarm: goarm:
- "7" - "7"
ignore:
- goos: windows
goarch: arm
ldflags: ldflags:
- -s -w - -s -w
mod_timestamp: "{{ .CommitTimestamp }}" mod_timestamp: "{{ .CommitTimestamp }}"

View File

@@ -11,5 +11,5 @@ test: ## Run unit tests
.PHONY: test .PHONY: test
lint: ## Run lint lint: ## Run lint
@go get github.com/golangci/golangci-lint/cmd/golangci-lint@v1.51.0 @go get github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v2.6.2
golangci-lint run golangci-lint run

View File

@@ -2,11 +2,11 @@
This is a client for [vpngate.net](https://www.vpngate.net/). This is a client for [vpngate.net](https://www.vpngate.net/).
![vpngate](https://user-images.githubusercontent.com/4519234/104145615-b6f9f880-5395-11eb-812c-c6597a7aed0f.gif) ![vpngate](https://github.com/user-attachments/assets/dafa2702-8d68-4b5f-badb-2c53ddd68991)
This client fetches the list of available relay servers provided by vpngate.net, and allows you to filter and connect to a server of your liking. This client fetches the list of available relay servers provided by vpngate.net, and allows you to filter and connect to a server of your liking.
You can check out your current IP address and region at <https://ipinfo.io>, or run the following: You can check out your current IP address and region at <https://ipinfo.io/json>, or run the following:
```shell ```shell
curl ipinfo.io curl ipinfo.io
@@ -14,28 +14,34 @@ curl ipinfo.io
## Requirements ## Requirements
- OpenVPN <= 2.5 (configurations on vpngate.net do not seem work on OpenVPN 2.6+) - OpenVPN
- macOS, Linux, or Windows - macOS, Linux, or Windows
## Install ## Install
You can install vpngate in a few different ways, and differs slightly depending on your OS. You can install vpngate in a few different ways, and it will differ slightly depending on your OS.
### Homebrew ### Homebrew (macOS and linux)
vpngate can be installed with [homebrew](https://brew.sh/) (ensure that xcode is installed before installing homebrew by running `xcode-select --install`). vpngate can be installed with [homebrew](https://brew.sh/) (if on macOS, ensure that xcode is installed before installing homebrew by running `xcode-select --install`).
```shell ```shell
brew install openvpn davegallant/public/vpngate brew install openvpn davegallant/public/vpngate
``` ```
## Windows ### Windows
On Windows, install OpenVPN 2.5.x from the [official website](https://openvpn.net/community-downloads/). On Windows, install OpenVPN from the [official website](https://openvpn.net/community-downloads/).
You must run in the terminal (command prompt) as Administrator in order to be able to run the relevant OpenVPN commands. As there is no installer at the moment, you will need to download and extract the Windows release from the relevant Github release.
### from source Once the release is extracted, open Command Prompt *as Administrator*, and run vpngate.exe from the location where it was extracted.
<img width="278" alt="image" src="https://github.com/user-attachments/assets/fb47270d-82bb-4790-833a-377b874c8104">
<img width="565" alt="image" src="https://github.com/user-attachments/assets/42287904-6c00-48d1-bff3-9757cf250519">
### Build from source
Ensure that [go](https://golang.org/doc/install) is installed. Ensure that [go](https://golang.org/doc/install) is installed.

View File

@@ -6,7 +6,6 @@ import (
"math/rand" "math/rand"
"os" "os"
"strings" "strings"
"time"
"github.com/AlecAivazis/survey/v2" "github.com/AlecAivazis/survey/v2"
"github.com/rs/zerolog/log" "github.com/rs/zerolog/log"
@@ -32,83 +31,71 @@ func init() {
var connectCmd = &cobra.Command{ var connectCmd = &cobra.Command{
Use: "connect", Use: "connect",
Short: "Connect to a vpn server (survey selection appears if hostname is not provided)", Short: "Connect to a vpn server (survey selection appears if hostname is not provided)",
Long: `Connect to a vpn from a list of relay servers`, Long: `Connect to a vpn from a list of relay servers`,
Args: cobra.RangeArgs(0, 1), Args: cobra.RangeArgs(0, 1),
Run: func(cmd *cobra.Command, args []string) { Run: func(cmd *cobra.Command, args []string) {
vpnServers, err := vpn.GetList(flagProxy, flagSocks5Proxy) vpnServers, err := vpn.GetList(flagProxy, flagSocks5Proxy)
if err != nil { if err != nil {
log.Fatal().Msgf(err.Error()) log.Fatal().Msg(err.Error())
os.Exit(1)
} }
serverSelection := []string{} // Build server selection options and hostname lookup map
serverSelected := vpn.Server{} serverSelection := make([]string, len(*vpnServers))
serverMap := make(map[string]vpn.Server, len(*vpnServers))
for _, s := range *vpnServers { for i, s := range *vpnServers {
serverSelection = append(serverSelection, fmt.Sprintf("%s (%s)", s.HostName, s.CountryLong)) serverSelection[i] = fmt.Sprintf("%s (%s)", s.HostName, s.CountryLong)
serverMap[s.HostName] = s
} }
selection := "" selection := ""
var serverSelected vpn.Server
if !flagRandom {
if len(args) > 0 {
selection = args[0]
} else {
prompt := &survey.Select{ prompt := &survey.Select{
Message: "Choose a server:", Message: "Choose a server:",
Options: serverSelection, Options: serverSelection,
} }
if !flagRandom {
if len(args) > 0 {
selection = args[0]
} else {
err := survey.AskOne(prompt, &selection, survey.WithPageSize(10)) err := survey.AskOne(prompt, &selection, survey.WithPageSize(10))
if err != nil { if err != nil {
log.Error().Msg("Unable to obtain hostname from survey") log.Fatal().Msg("Unable to obtain hostname from survey")
os.Exit(1)
} }
} }
// Server lookup from selection could be more optimized with a hash map // Lookup server from selection using map for O(1) lookup
for _, s := range *vpnServers { hostname := extractHostname(selection)
if strings.Contains(selection, s.HostName) { if server, exists := serverMap[hostname]; exists {
serverSelected = s serverSelected = server
} } else {
}
if serverSelected.HostName == "" {
log.Fatal().Msgf("Server '%s' was not found", selection) log.Fatal().Msgf("Server '%s' was not found", selection)
os.Exit(1)
} }
} }
for { for {
if flagRandom { if flagRandom {
// Select a random server // Select a random server
rand.Seed(time.Now().UnixNano())
serverSelected = (*vpnServers)[rand.Intn(len(*vpnServers))] serverSelected = (*vpnServers)[rand.Intn(len(*vpnServers))]
} }
decodedConfig, err := base64.StdEncoding.DecodeString(serverSelected.OpenVpnConfigData) decodedConfig, err := base64.StdEncoding.DecodeString(serverSelected.OpenVpnConfigData)
if err != nil { if err != nil {
log.Fatal().Msgf(err.Error()) log.Fatal().Msg(err.Error())
os.Exit(1)
} }
tmpfile, err := os.CreateTemp("", "vpngate-openvpn-config-") tmpfile, err := os.CreateTemp("", "vpngate-openvpn-config-")
if err != nil { if err != nil {
log.Fatal().Msgf(err.Error()) log.Fatal().Msg(err.Error())
os.Exit(1)
} }
if _, err := tmpfile.Write(decodedConfig); err != nil { if _, err := tmpfile.Write(decodedConfig); err != nil {
log.Fatal().Msgf(err.Error()) log.Fatal().Msg(err.Error())
os.Exit(1)
} }
if err := tmpfile.Close(); err != nil { if err := tmpfile.Close(); err != nil {
log.Fatal().Msgf(err.Error()) log.Fatal().Msg(err.Error())
os.Exit(1)
} }
log.Info().Msgf("Connecting to %s (%s) in %s", serverSelected.HostName, serverSelected.IPAddr, serverSelected.CountryLong) log.Info().Msgf("Connecting to %s (%s) in %s", serverSelected.HostName, serverSelected.IPAddr, serverSelected.CountryLong)
@@ -116,12 +103,22 @@ var connectCmd = &cobra.Command{
err = vpn.Connect(tmpfile.Name()) err = vpn.Connect(tmpfile.Name())
if err != nil && !flagReconnect { if err != nil && !flagReconnect {
log.Fatal().Msgf(err.Error()) // VPN connection failed and reconnect is disabled
os.Exit(1) _ = os.Remove(tmpfile.Name())
} else { log.Fatal().Msg("VPN connection failed")
os.Remove(tmpfile.Name())
} }
// Always try to clean up temporary file
_ = os.Remove(tmpfile.Name())
} }
}, },
} }
// extractHostname extracts the hostname from the selection string (format: "hostname (country)")
func extractHostname(selection string) string {
parts := strings.Split(selection, " (")
if len(parts) > 0 {
return parts[0]
}
return selection
}

View File

@@ -4,7 +4,7 @@ import (
"os" "os"
"strconv" "strconv"
"github.com/olekukonko/tablewriter" tw "github.com/olekukonko/tablewriter"
"github.com/rs/zerolog/log" "github.com/rs/zerolog/log"
"github.com/davegallant/vpngate/pkg/vpn" "github.com/davegallant/vpngate/pkg/vpn"
@@ -26,16 +26,21 @@ var listCmd = &cobra.Command{
vpnServers, err := vpn.GetList(flagProxy, flagSocks5Proxy) vpnServers, err := vpn.GetList(flagProxy, flagSocks5Proxy)
if err != nil { if err != nil {
log.Fatal().Msgf(err.Error()) log.Fatal().Msg(err.Error())
os.Exit(1)
} }
table := tablewriter.NewWriter(os.Stdout) table := tw.NewWriter(os.Stdout)
table.SetHeader([]string{"#", "HostName", "Country", "Ping", "Score"}) table.Header([]string{"#", "HostName", "Country", "Ping", "Score"})
for i, v := range *vpnServers { for i, v := range *vpnServers {
table.Append([]string{strconv.Itoa(i + 1), v.HostName, v.CountryLong, v.Ping, strconv.Itoa(v.Score)}) err := table.Append([]string{strconv.Itoa(i + 1), v.HostName, v.CountryLong, v.Ping, strconv.Itoa(v.Score)})
if err != nil {
log.Fatal().Msg(err.Error())
}
}
err = table.Render()
if err != nil {
log.Fatal().Msg(err.Error())
} }
table.Render() // Send output
}, },
} }

61
flake.lock generated Normal file
View File

@@ -0,0 +1,61 @@
{
"nodes": {
"flake-utils": {
"inputs": {
"systems": "systems"
},
"locked": {
"lastModified": 1731533236,
"narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "11707dc2f618dd54ca8739b309ec4fc024de578b",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "flake-utils",
"type": "github"
}
},
"nixpkgs": {
"locked": {
"lastModified": 1770843696,
"narHash": "sha256-LovWTGDwXhkfCOmbgLVA10bvsi/P8eDDpRudgk68HA8=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "2343bbb58f99267223bc2aac4fc9ea301a155a16",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixpkgs-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"root": {
"inputs": {
"flake-utils": "flake-utils",
"nixpkgs": "nixpkgs"
}
},
"systems": {
"locked": {
"lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"owner": "nix-systems",
"repo": "default",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"type": "github"
},
"original": {
"owner": "nix-systems",
"repo": "default",
"type": "github"
}
}
},
"root": "root",
"version": 7
}

55
flake.nix Normal file
View File

@@ -0,0 +1,55 @@
{
description = "vpngate - VPN server connector";
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable";
flake-utils.url = "github:numtide/flake-utils";
};
outputs =
{
self,
nixpkgs,
flake-utils,
}:
let
vpngate =
pkgs:
pkgs.buildGo125Module rec {
name = "vpngate";
src = ./.;
vendorHash = "sha256-FNpeIIIrINm/3neCkuX/kFWWGCCEN8Duz1iSFAki+54=";
nativeBuildInputs = pkgs.lib.optionals pkgs.stdenv.isLinux [ pkgs.makeWrapper ];
env.CGO_ENABLED = 0;
doCheck = false;
};
flakeForSystem =
nixpkgs: system:
let
pkgs = nixpkgs.legacyPackages.${system};
vg = vpngate pkgs;
in
{
packages = {
default = vg;
vpngate = vg;
};
devShells.default = pkgs.mkShell {
name = "vpngate-dev";
description = "Development environment for vpngate";
packages = with pkgs; [
go_1_26
gopls
gotools
golangci-lint
];
shellHook = ''
echo "Welcome to the vpngate dev environment"
go version
'';
};
};
in
flake-utils.lib.eachDefaultSystem (system: flakeForSystem nixpkgs system);
}

44
go.mod
View File

@@ -1,37 +1,41 @@
module github.com/davegallant/vpngate module github.com/davegallant/vpngate
go 1.19 go 1.24.0
toolchain go1.26.0
require ( require (
github.com/AlecAivazis/survey/v2 v2.3.7 github.com/AlecAivazis/survey/v2 v2.3.7
github.com/jszwec/csvutil v1.10.0 github.com/jszwec/csvutil v1.10.0
github.com/juju/errors v1.0.0 github.com/juju/errors v1.0.0
github.com/nxadm/tail v1.4.11 github.com/olekukonko/tablewriter v1.1.3
github.com/olekukonko/tablewriter v0.0.5 github.com/rs/zerolog v1.34.0
github.com/rs/zerolog v1.33.0 github.com/spf13/cobra v1.10.2
github.com/spf13/afero v1.11.0 github.com/stretchr/testify v1.11.1
github.com/spf13/cobra v1.8.1 golang.org/x/net v0.50.0
github.com/stretchr/testify v1.9.0
golang.org/x/net v0.27.0
) )
require ( require (
github.com/clipperhouse/displaywidth v0.6.2 // indirect
github.com/clipperhouse/stringish v0.1.1 // indirect
github.com/clipperhouse/uax29/v2 v2.3.0 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect github.com/davecgh/go-spew v1.1.1 // indirect
github.com/fsnotify/fsnotify v1.6.0 // indirect github.com/fatih/color v1.18.0 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect
github.com/kr/pretty v0.3.0 // indirect github.com/kr/pretty v0.3.1 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-colorable v0.1.14 // indirect
github.com/mattn/go-isatty v0.0.19 // indirect github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-runewidth v0.0.15 // indirect github.com/mattn/go-runewidth v0.0.19 // indirect
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect
github.com/olekukonko/cat v0.0.0-20250911104152-50322a0618f6 // indirect
github.com/olekukonko/errors v1.1.0 // indirect
github.com/olekukonko/ll v0.1.4-0.20260115111900-9e59c2286df0 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/rivo/uniseg v0.4.4 // indirect github.com/rogpeppe/go-internal v1.14.1 // indirect
github.com/rogpeppe/go-internal v1.9.0 // indirect github.com/spf13/pflag v1.0.10 // indirect
github.com/spf13/pflag v1.0.5 // indirect golang.org/x/sys v0.41.0 // indirect
golang.org/x/sys v0.22.0 // indirect golang.org/x/term v0.40.0 // indirect
golang.org/x/term v0.22.0 // indirect golang.org/x/text v0.34.0 // indirect
golang.org/x/text v0.16.0 // indirect
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect
) )

111
go.sum
View File

@@ -2,17 +2,22 @@ github.com/AlecAivazis/survey/v2 v2.3.7 h1:6I/u8FvytdGsgonrYsVn2t8t4QiRnh6QSTqkk
github.com/AlecAivazis/survey/v2 v2.3.7/go.mod h1:xUTIdE4KCOIjsBAE1JYsUPoCqYdZ1reCfTwbto0Fduo= github.com/AlecAivazis/survey/v2 v2.3.7/go.mod h1:xUTIdE4KCOIjsBAE1JYsUPoCqYdZ1reCfTwbto0Fduo=
github.com/Netflix/go-expect v0.0.0-20220104043353-73e0943537d2 h1:+vx7roKuyA63nhn5WAunQHLTznkw5W8b1Xc0dNjp83s= github.com/Netflix/go-expect v0.0.0-20220104043353-73e0943537d2 h1:+vx7roKuyA63nhn5WAunQHLTznkw5W8b1Xc0dNjp83s=
github.com/Netflix/go-expect v0.0.0-20220104043353-73e0943537d2/go.mod h1:HBCaDeC1lPdgDeDbhX8XFpy1jqjK0IBG8W5K+xYqA0w= github.com/Netflix/go-expect v0.0.0-20220104043353-73e0943537d2/go.mod h1:HBCaDeC1lPdgDeDbhX8XFpy1jqjK0IBG8W5K+xYqA0w=
github.com/clipperhouse/displaywidth v0.6.2 h1:ZDpTkFfpHOKte4RG5O/BOyf3ysnvFswpyYrV7z2uAKo=
github.com/clipperhouse/displaywidth v0.6.2/go.mod h1:R+kHuzaYWFkTm7xoMmK1lFydbci4X2CicfbGstSGg0o=
github.com/clipperhouse/stringish v0.1.1 h1:+NSqMOr3GR6k1FdRhhnXrLfztGzuG+VuFDfatpWHKCs=
github.com/clipperhouse/stringish v0.1.1/go.mod h1:v/WhFtE1q0ovMta2+m+UbpZ+2/HEXNWYXQgCt4hdOzA=
github.com/clipperhouse/uax29/v2 v2.3.0 h1:SNdx9DVUqMoBuBoW3iLOj4FQv3dN5mDtuqwuhIGpJy4=
github.com/clipperhouse/uax29/v2 v2.3.0/go.mod h1:Wn1g7MK6OoeDT0vL+Q0SQLDz/KpfsVRgg6W7ihQeh4g=
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/creack/pty v1.1.17 h1:QeVUsEDNrLBW4tMgZHvxy18sKtr6VI492kBhUfhDJNI= github.com/creack/pty v1.1.17 h1:QeVUsEDNrLBW4tMgZHvxy18sKtr6VI492kBhUfhDJNI=
github.com/creack/pty v1.1.17/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= github.com/creack/pty v1.1.17/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY= github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw= github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU=
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/hinshun/vt10x v0.0.0-20220119200601-820417d04eec h1:qv2VnGeEQHchGaZ/u7lxST/RaJw+cv273q79D81Xbog= github.com/hinshun/vt10x v0.0.0-20220119200601-820417d04eec h1:qv2VnGeEQHchGaZ/u7lxST/RaJw+cv273q79D81Xbog=
github.com/hinshun/vt10x v0.0.0-20220119200601-820417d04eec/go.mod h1:Q48J4R4DvxnHolD5P8pOtXigYlRuPLGl6moFx3ulM68= github.com/hinshun/vt10x v0.0.0-20220119200601-820417d04eec/go.mod h1:Q48J4R4DvxnHolD5P8pOtXigYlRuPLGl6moFx3ulM68=
@@ -24,68 +29,62 @@ github.com/juju/errors v1.0.0 h1:yiq7kjCLll1BiaRuNY53MGI0+EQ3rF6GB+wvboZDefM=
github.com/juju/errors v1.0.0/go.mod h1:B5x9thDqx0wIMH3+aLIMP9HjItInYWObRovoCFM5Qe8= github.com/juju/errors v1.0.0/go.mod h1:B5x9thDqx0wIMH3+aLIMP9HjItInYWObRovoCFM5Qe8=
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs= github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs=
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8= github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA=
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/mattn/go-runewidth v0.0.19 h1:v++JhqYnZuu5jSKrk9RbgF5v4CGUjqRfBm05byFGLdw=
github.com/mattn/go-runewidth v0.0.19/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs=
github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE= github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE=
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d h1:5PJl274Y63IEHC+7izoQE9x6ikvDFZS2mDVS3drnohI= github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d h1:5PJl274Y63IEHC+7izoQE9x6ikvDFZS2mDVS3drnohI=
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE= github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE=
github.com/nxadm/tail v1.4.11 h1:8feyoE3OzPrcshW5/MJ4sGESc5cqmGkGCWlco4l0bqY= github.com/olekukonko/cat v0.0.0-20250911104152-50322a0618f6 h1:zrbMGy9YXpIeTnGj4EljqMiZsIcE09mmF8XsD5AYOJc=
github.com/nxadm/tail v1.4.11/go.mod h1:OTaG3NK980DZzxbRq6lEuzgU+mug70nY11sMd4JXXHc= github.com/olekukonko/cat v0.0.0-20250911104152-50322a0618f6/go.mod h1:rEKTHC9roVVicUIfZK7DYrdIoM0EOr8mK1Hj5s3JjH0=
github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec= github.com/olekukonko/errors v1.1.0 h1:RNuGIh15QdDenh+hNvKrJkmxxjV4hcS50Db478Ou5sM=
github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY= github.com/olekukonko/errors v1.1.0/go.mod h1:ppzxA5jBKcO1vIpCXQ9ZqgDh8iwODz6OXIGKU8r5m4Y=
github.com/olekukonko/ll v0.1.4-0.20260115111900-9e59c2286df0 h1:jrYnow5+hy3WRDCBypUFvVKNSPPCdqgSXIE9eJDD8LM=
github.com/olekukonko/ll v0.1.4-0.20260115111900-9e59c2286df0/go.mod h1:b52bVQRRPObe+yyBl0TxNfhesL0nedD4Cht0/zx55Ew=
github.com/olekukonko/tablewriter v1.1.3 h1:VSHhghXxrP0JHl+0NnKid7WoEmd9/urKRJLysb70nnA=
github.com/olekukonko/tablewriter v1.1.3/go.mod h1:9VU0knjhmMkXjnMKrZ3+L2JhhtsQ/L38BbL3CRNE8tM=
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.4.4 h1:8TfxU8dW6PdqD27gjM8MVNuicgxIjxpm4K7x4jp8sis=
github.com/rivo/uniseg v0.4.4/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
github.com/rs/zerolog v1.33.0 h1:1cU2KZkvPxNyfgEmhHAz/1A9Bz+llsdYzklWFzgp0r8= github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
github.com/rs/zerolog v1.33.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss= github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0=
github.com/rs/zerolog v1.34.0 h1:k43nTLIwcTVQAncfCw4KZ2VY6ukYoZaBPNOE8txlOeY=
github.com/rs/zerolog v1.34.0/go.mod h1:bJsvje4Z08ROH4Nhs5iH600c3IkWhwp44iRc54W6wYQ=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8= github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU=
github.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY= github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4=
github.com/spf13/cobra v1.8.0 h1:7aJaZx1B85qltLMc546zn58BxxfZdR/W22ej9CFoEf0= github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/cobra v1.8.0/go.mod h1:WXLWApfZ71AjXPya3WOlMsY9yMs7YeiHhFVlvLyhcho= github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk=
github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM= github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y=
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.19.0 h1:zTwKpTd2XuCqf8huc7Fo2iSy+4RHPd10s4KzeTnVr1c= golang.org/x/net v0.50.0 h1:ucWh9eiCGyDR3vtzso0WMQinm2Dnt8cFMuQa9K33J60=
golang.org/x/net v0.19.0/go.mod h1:CfAk/cbD4CthTvqiEl8NpboMuiuOYsAr/7NOjZJtv1U= golang.org/x/net v0.50.0/go.mod h1:UgoSli3F/pBgdJBHCTc+tp3gmrU4XswgGRgtnwWTfyM=
golang.org/x/net v0.23.0 h1:7EYJ93RZ9vYSZAIb2x3lnuvqO5zneoD6IvWjuhfxjTs=
golang.org/x/net v0.23.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg=
golang.org/x/net v0.27.0 h1:5K3Njcw06/l2y9vpGCSdcxWOYHOUk3dVNGDXN+FvAys=
golang.org/x/net v0.27.0/go.mod h1:dDi0PyhWNoiUOrAS8uXv/vnScO4wnHQO4mj9fn/RytE=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
@@ -95,41 +94,27 @@ golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc= golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=
golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4=
golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI=
golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.15.0 h1:y/Oo/a/q3IXu26lQgl04j/gjuBDOBlx7X6Om1j2CPW4= golang.org/x/term v0.40.0 h1:36e4zGLqU4yhjlmxEaagx2KuYbJq3EwY8K943ZsHcvg=
golang.org/x/term v0.15.0/go.mod h1:BDl952bC7+uMoWR75FIrCDx79TPU9oHkTZ9yRbYOrX0= golang.org/x/term v0.40.0/go.mod h1:w2P8uVp06p2iyKKuvXIm7N/y0UCRt3UfJTfZ7oOpglM=
golang.org/x/term v0.18.0 h1:FcHjZXDMxI8mM3nwhX9HlKop4C0YQvCVCdwYl2wOtE8=
golang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58=
golang.org/x/term v0.22.0 h1:BbsgPEJULsl2fV/AT3v15Mjva5yXKQDyKf+TbDz7QJk=
golang.org/x/term v0.22.0/go.mod h1:F3qCibpT5AMpCRfhfT53vVJwhLtIVHhB9XDjfFvnMI4=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA=
golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4=
golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

View File

@@ -1,45 +1,59 @@
package exec package exec
import ( import (
"bytes" "bufio"
"os" "io"
"os/exec" "os/exec"
"strings"
"github.com/juju/errors"
"github.com/rs/zerolog/log" "github.com/rs/zerolog/log"
) )
// Run executes a command in workDir and returns stdout and error. // Run executes a command in workDir and logs its output.
// The spawned process will exit upon termination of this application // If the command fails to start or setup fails, an error is logged and returned.
// to ensure a clean exit // If the command exits with a non-zero status, the error is returned without logging
func Run(path string, workDir string, args ...string) (string, error) { // (this allows the caller to decide how to handle it).
func Run(path string, workDir string, args ...string) error {
_, err := exec.LookPath(path) _, err := exec.LookPath(path)
if err != nil { if err != nil {
log.Error().Msgf("%s is required, please install it", path) log.Error().Msgf("%s is required, please install it", path)
os.Exit(1) return err
} }
cmd := exec.Command(path, args...) cmd := exec.Command(path, args...)
cmd.Dir = workDir cmd.Dir = workDir
stdout := &bytes.Buffer{}
stderr := &bytes.Buffer{} log.Debug().Strs("command", cmd.Args).Msg("Executing command")
cmd.Stdout = stdout
cmd.Stderr = stderr stdout, err := cmd.StdoutPipe()
log.Debug().Msgf("Executing " + strings.Join(cmd.Args, " "))
err = cmd.Run()
output := strings.TrimSpace(stdout.String())
errOut := strings.TrimSpace(stderr.String())
if output != "" {
log.Debug().Msgf(output)
}
if errOut != "" {
log.Debug().Msgf(errOut)
}
if _, ok := err.(*exec.ExitError); !ok {
return output, errors.Trace(err)
}
if err != nil { if err != nil {
return output, errors.Annotatef(err, path, cmd.Args, errOut) log.Error().Msgf("Failed to get stdout pipe: %v", err)
return err
} }
return output, nil
stderr, err := cmd.StderrPipe()
if err != nil {
log.Error().Msgf("Failed to get stderr pipe: %v", err)
return err
}
if err := cmd.Start(); err != nil {
log.Error().Msgf("Failed to start command: %v", err)
return err
}
// Combine stdout and stderr into a single reader
combined := io.MultiReader(stdout, stderr)
scanner := bufio.NewScanner(combined)
for scanner.Scan() {
log.Debug().Msg(scanner.Text())
}
if err := scanner.Err(); err != nil {
log.Error().Msgf("Error reading output: %v", err)
return err
}
// cmd.Wait() returns an error if the command exits with non-zero status
// We return this without logging since it's expected behavior for some commands
return cmd.Wait()
} }

View File

@@ -2,16 +2,17 @@ package util
import ( import (
"time" "time"
"github.com/rs/zerolog/log" "github.com/rs/zerolog/log"
) )
func Retry(attempts int, delay time.Duration,fn func() error) error { func Retry(attempts int, delay time.Duration, fn func() error) error {
var err error var err error
for i := 0; i < attempts; i++ { for i := 0; i < attempts; i++ {
if err = fn(); err == nil { if err = fn(); err == nil {
return nil return nil
} }
log.Error().Msgf("Retrying after %d seconds. An error occured: %s", delay, err) log.Error().Msgf("Retrying after %v. An error occurred: %s", delay, err)
time.Sleep(delay) time.Sleep(delay)
} }
return err return err

View File

@@ -4,37 +4,42 @@ import (
"encoding/json" "encoding/json"
"io" "io"
"os" "os"
"path" "path/filepath"
"time" "time"
"github.com/rs/zerolog/log"
"github.com/spf13/afero"
) )
const serverCachefile = "servers.json" const serverCachefile = "servers.json"
func getCacheDir() string { func getCacheDir() (string, error) {
homeDir, err := os.UserHomeDir() homeDir, err := os.UserHomeDir()
if err != nil { if err != nil {
log.Error().Msgf("Failed to get user's home directory: %s ", err) return "", err
return ""
} }
cacheDir := path.Join(homeDir, ".vpngate", "cache") cacheDir := filepath.Join(homeDir, ".vpngate", "cache")
return cacheDir return cacheDir, nil
} }
func createCacheDir() error { func createCacheDir() error {
cacheDir := getCacheDir() cacheDir, err := getCacheDir()
AppFs := afero.NewOsFs() if err != nil {
return AppFs.MkdirAll(cacheDir, 0o700) return err
}
return os.MkdirAll(cacheDir, 0o700)
} }
func getVpnListCache() (*[]Server, error) { func getVpnListCache() (*[]Server, error) {
cacheFile := path.Join(getCacheDir(), serverCachefile) cacheDir, err := getCacheDir()
if err != nil {
return nil, err
}
cacheFile := filepath.Join(cacheDir, serverCachefile)
serversFile, err := os.Open(cacheFile) serversFile, err := os.Open(cacheFile)
if err != nil { if err != nil {
return nil, err return nil, err
} }
defer func() {
_ = serversFile.Close()
}()
byteValue, err := io.ReadAll(serversFile) byteValue, err := io.ReadAll(serversFile)
if err != nil { if err != nil {
@@ -44,7 +49,6 @@ func getVpnListCache() (*[]Server, error) {
var servers []Server var servers []Server
err = json.Unmarshal(byteValue, &servers) err = json.Unmarshal(byteValue, &servers)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -53,8 +57,7 @@ func getVpnListCache() (*[]Server, error) {
} }
func writeVpnListToCache(servers []Server) error { func writeVpnListToCache(servers []Server) error {
err := createCacheDir() if err := createCacheDir(); err != nil {
if err != nil {
return err return err
} }
@@ -63,20 +66,26 @@ func writeVpnListToCache(servers []Server) error {
return err return err
} }
cacheFile := path.Join(getCacheDir(), serverCachefile) cacheDir, err := getCacheDir()
if err != nil {
err = os.WriteFile(cacheFile, f, 0o644)
return err return err
}
cacheFile := filepath.Join(cacheDir, serverCachefile)
return os.WriteFile(cacheFile, f, 0o644)
} }
func vpnListCacheIsExpired() bool { func vpnListCacheIsExpired() bool {
file, err := os.Stat(path.Join(getCacheDir(), serverCachefile)) cacheDir, err := getCacheDir()
if err != nil {
return true
}
file, err := os.Stat(filepath.Join(cacheDir, serverCachefile))
if err != nil { if err != nil {
return true return true
} }
lastModified := file.ModTime() lastModified := file.ModTime()
return (time.Since(lastModified)) > time.Duration(24*time.Hour) return time.Since(lastModified) > 24*time.Hour
} }

View File

@@ -1,38 +1,17 @@
package vpn package vpn
import ( import (
"os"
"runtime" "runtime"
"github.com/davegallant/vpngate/pkg/exec" "github.com/davegallant/vpngate/pkg/exec"
"github.com/juju/errors"
"github.com/nxadm/tail"
"github.com/rs/zerolog/log"
) )
// Connect to a specified OpenVPN configuration // Connect to a specified OpenVPN configuration
func Connect(configPath string) error { func Connect(configPath string) error {
tmpLogFile, err := os.CreateTemp("", "vpngate-openvpn-log-")
if err != nil {
return errors.Annotate(err, "Unable to create a temporary log file")
}
defer os.Remove(tmpLogFile.Name())
go func() {
// Tail the temporary openvpn log file
t, err := tail.TailFile(tmpLogFile.Name(), tail.Config{Follow: true})
if err != nil {
log.Error().Msgf("%s", err)
}
for line := range t.Lines {
log.Debug().Msg(line.Text)
}
}()
executable := "openvpn" executable := "openvpn"
if runtime.GOOS == "windows" { if runtime.GOOS == "windows" {
executable = "C:\\Program Files\\OpenVPN\\bin\\openvpn.exe" executable = "C:\\Program Files\\OpenVPN\\bin\\openvpn.exe"
} }
_, err = exec.Run(executable, ".", "--verb", "4", "--log", tmpLogFile.Name(), "--config", configPath)
return err return exec.Run(executable, ".", "--verb", "4", "--config", configPath, "--data-ciphers", "AES-128-CBC")
} }

View File

@@ -2,10 +2,12 @@ package vpn
import ( import (
"bytes" "bytes"
"context"
"io" "io"
"net"
"net/http" "net/http"
"net/url" "net/url"
"os" "time"
"github.com/jszwec/csvutil" "github.com/jszwec/csvutil"
"github.com/rs/zerolog/log" "github.com/rs/zerolog/log"
@@ -17,9 +19,11 @@ import (
const ( const (
vpnList = "https://www.vpngate.net/api/iphone/" vpnList = "https://www.vpngate.net/api/iphone/"
httpClientTimeout = 30 * time.Second
dialTimeout = 10 * time.Second
) )
// Server holds in formation about a vpn relay server // Server holds information about a vpn relay server
type Server struct { type Server struct {
HostName string `csv:"#HostName"` HostName string `csv:"#HostName"`
CountryLong string `csv:"CountryLong"` CountryLong string `csv:"CountryLong"`
@@ -30,20 +34,14 @@ type Server struct {
Ping string `csv:"Ping"` Ping string `csv:"Ping"`
} }
func streamToBytes(stream io.Reader) []byte { // parseVpnList parses the VPN server list from CSV format
buf := new(bytes.Buffer)
_, err := buf.ReadFrom(stream)
if err != nil {
log.Error().Msg("Unable to stream bytes")
}
return buf.Bytes()
}
// parse csv
func parseVpnList(r io.Reader) (*[]Server, error) { func parseVpnList(r io.Reader) (*[]Server, error) {
var servers []Server var servers []Server
serverList := streamToBytes(r) serverList, err := io.ReadAll(r)
if err != nil {
return nil, errors.Annotate(err, "Unable to read stream")
}
// Trim known invalid rows // Trim known invalid rows
serverList = bytes.TrimPrefix(serverList, []byte("*vpn_servers\r\n")) serverList = bytes.TrimPrefix(serverList, []byte("*vpn_servers\r\n"))
@@ -57,84 +55,119 @@ func parseVpnList(r io.Reader) (*[]Server, error) {
return &servers, nil return &servers, nil
} }
// createHTTPClient creates an HTTP client with optional proxy configuration
func createHTTPClient(httpProxy string, socks5Proxy string) (*http.Client, error) {
if httpProxy != "" {
proxyURL, err := url.Parse(httpProxy)
if err != nil {
return nil, errors.Annotatef(err, "Error parsing HTTP proxy: %s", httpProxy)
}
transport := &http.Transport{
Proxy: http.ProxyURL(proxyURL),
}
return &http.Client{
Transport: transport,
Timeout: httpClientTimeout,
}, nil
}
if socks5Proxy != "" {
dialer, err := proxy.SOCKS5("tcp", socks5Proxy, nil, proxy.Direct)
if err != nil {
return nil, errors.Annotatef(err, "Error creating SOCKS5 dialer: %v", err)
}
// Create a DialContext function from the SOCKS5 dialer
dialContext := func(ctx context.Context, network, addr string) (net.Conn, error) {
// Check if context is already done
select {
case <-ctx.Done():
return nil, ctx.Err()
default:
}
// Use the dialer with a timeout
conn, err := dialer.Dial(network, addr)
if err != nil {
return nil, err
}
// Respect context cancellation after connection
go func() {
<-ctx.Done()
_ = conn.Close()
}()
return conn, nil
}
httpTransport := &http.Transport{
DialContext: dialContext,
}
return &http.Client{
Transport: httpTransport,
Timeout: httpClientTimeout,
}, nil
}
return &http.Client{
Timeout: httpClientTimeout,
Transport: &http.Transport{
DialContext: (&net.Dialer{
Timeout: dialTimeout,
}).DialContext,
},
}, nil
}
// GetList returns a list of vpn servers // GetList returns a list of vpn servers
func GetList(httpProxy string, socks5Proxy string) (*[]Server, error) { func GetList(httpProxy string, socks5Proxy string) (*[]Server, error) {
cacheExpired := vpnListCacheIsExpired() cacheExpired := vpnListCacheIsExpired()
var servers *[]Server // Try to use cached list if not expired
var client *http.Client
if !cacheExpired { if !cacheExpired {
servers, err := getVpnListCache() servers, err := getVpnListCache()
if err == nil {
if err != nil {
log.Info().Msg("Unable to retrieve vpn list from cache")
} else {
return servers, nil return servers, nil
} }
log.Info().Msg("Unable to retrieve vpn list from cache")
} else { } else {
log.Info().Msg("The vpn server list cache has expired") log.Info().Msg("The vpn server list cache has expired")
} }
log.Info().Msg("Fetching the latest server list") log.Info().Msg("Fetching the latest server list")
if httpProxy != "" { client, err := createHTTPClient(httpProxy, socks5Proxy)
proxyURL, err := url.Parse(httpProxy)
if err != nil { if err != nil {
log.Error().Msgf("Error parsing proxy: %s", err) return nil, err
os.Exit(1)
}
transport := &http.Transport{
Proxy: http.ProxyURL(proxyURL),
} }
client = &http.Client{ var servers *[]Server
Transport: transport,
}
} else if socks5Proxy != "" { err = util.Retry(5, 1, func() error {
dialer, err := proxy.SOCKS5("tcp", socks5Proxy, nil, proxy.Direct) resp, err := client.Get(vpnList)
if err != nil {
log.Error().Msgf("Error creating SOCKS5 dialer: %v", err)
os.Exit(1)
}
httpTransport := &http.Transport{
Dial: dialer.Dial,
}
client = &http.Client{
Transport: httpTransport,
}
} else {
client = &http.Client{}
}
var r *http.Response
err := util.Retry(5, 1, func() error {
var err error
r, err = client.Get(vpnList)
if err != nil { if err != nil {
return err return err
} }
defer r.Body.Close() defer func() {
_ = resp.Body.Close()
}()
if r.StatusCode != 200 { if resp.StatusCode != http.StatusOK {
return errors.Annotatef(err, "Unexpected status code when retrieving vpn list: %d", r.StatusCode) return errors.Annotatef(err, "Unexpected status code when retrieving vpn list: %d", resp.StatusCode)
} }
servers, err = parseVpnList(r.Body) parsedServers, err := parseVpnList(resp.Body)
if err != nil { if err != nil {
return err return err
} }
err = writeVpnListToCache(*servers) servers = parsedServers
if err != nil { // Cache the servers for future use
log.Warn().Msgf("Unable to write servers to cache: %s", err) cacheErr := writeVpnListToCache(*servers)
if cacheErr != nil {
log.Warn().Msgf("Unable to write servers to cache: %s", cacheErr)
} }
return nil return nil
}) })