mirror of
https://github.com/davegallant/vpngate.git
synced 2026-03-03 18:16:35 +00:00
Compare commits
24 Commits
v0.3.2
...
7948580d1d
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7948580d1d | ||
| bb88db92c1 | |||
| 323709b0a1 | |||
|
|
65509016cb | ||
|
|
9892ebe864 | ||
|
|
a3e448a658 | ||
|
|
33a1b1b1d3 | ||
|
|
c03d27afcb | ||
| 4027784d1d | |||
|
|
34054180ed | ||
|
|
7da150493c | ||
| 9e9992e643 | |||
|
|
ff1d10e9bd | ||
|
|
9939da15cc | ||
| a668484da6 | |||
|
|
9fa908f5fd | ||
|
|
e0444a7e42 | ||
|
|
3f7d49f78d | ||
|
|
5ac7d495ab | ||
| eda46dcce9 | |||
| 002fec4537 | |||
| cd92f93201 | |||
| aa547e09d2 | |||
| 9545c9fd85 |
4
.github/workflows/golangci-lint.yml
vendored
4
.github/workflows/golangci-lint.yml
vendored
@@ -15,11 +15,9 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- uses: actions/setup-go@v6
|
- uses: actions/setup-go@v6
|
||||||
with:
|
with:
|
||||||
go-version: 1.25
|
go-version: 1.26
|
||||||
- uses: actions/checkout@v6
|
- uses: actions/checkout@v6
|
||||||
- name: golangci-lint
|
- name: golangci-lint
|
||||||
uses: golangci/golangci-lint-action@v9
|
uses: golangci/golangci-lint-action@v9
|
||||||
with:
|
|
||||||
version: v2.6.2
|
|
||||||
- name: test
|
- name: test
|
||||||
run: make test
|
run: make test
|
||||||
|
|||||||
3
.github/workflows/release.yml
vendored
3
.github/workflows/release.yml
vendored
@@ -18,12 +18,11 @@ jobs:
|
|||||||
name: Set up Go
|
name: Set up Go
|
||||||
uses: actions/setup-go@v6
|
uses: actions/setup-go@v6
|
||||||
with:
|
with:
|
||||||
go-version: "1.25"
|
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 }}
|
||||||
|
|||||||
58
.github/workflows/update-vendor-hash.yml
vendored
Normal file
58
.github/workflows/update-vendor-hash.yml
vendored
Normal 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
4
.gitignore
vendored
@@ -1,3 +1,7 @@
|
|||||||
dist
|
dist
|
||||||
|
|
||||||
.vscode
|
.vscode
|
||||||
|
|
||||||
|
# direnv
|
||||||
|
.direnv
|
||||||
|
.envrc.local
|
||||||
|
|||||||
@@ -21,6 +21,9 @@ builds:
|
|||||||
- arm64
|
- arm64
|
||||||
goarm:
|
goarm:
|
||||||
- "7"
|
- "7"
|
||||||
|
ignore:
|
||||||
|
- goos: windows
|
||||||
|
goarch: arm
|
||||||
ldflags:
|
ldflags:
|
||||||
- -s -w
|
- -s -w
|
||||||
mod_timestamp: "{{ .CommitTimestamp }}"
|
mod_timestamp: "{{ .CommitTimestamp }}"
|
||||||
|
|||||||
@@ -31,7 +31,6 @@ 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),
|
||||||
@@ -39,49 +38,43 @@ var connectCmd = &cobra.Command{
|
|||||||
vpnServers, err := vpn.GetList(flagProxy, flagSocks5Proxy)
|
vpnServers, err := vpn.GetList(flagProxy, flagSocks5Proxy)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal().Msg(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
|
||||||
serverSelected = (*vpnServers)[rand.Intn(len(*vpnServers))]
|
serverSelected = (*vpnServers)[rand.Intn(len(*vpnServers))]
|
||||||
@@ -90,23 +83,19 @@ var connectCmd = &cobra.Command{
|
|||||||
decodedConfig, err := base64.StdEncoding.DecodeString(serverSelected.OpenVpnConfigData)
|
decodedConfig, err := base64.StdEncoding.DecodeString(serverSelected.OpenVpnConfigData)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal().Msg(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().Msg(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().Msg(err.Error())
|
log.Fatal().Msg(err.Error())
|
||||||
os.Exit(1)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := tmpfile.Close(); err != nil {
|
if err := tmpfile.Close(); err != nil {
|
||||||
log.Fatal().Msg(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)
|
||||||
@@ -114,16 +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().Msg(err.Error())
|
// VPN connection failed and reconnect is disabled
|
||||||
os.Exit(1)
|
_ = os.Remove(tmpfile.Name())
|
||||||
} else {
|
log.Fatal().Msg("VPN connection failed")
|
||||||
err = os.Remove(tmpfile.Name())
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal().Msg(err.Error())
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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
|
||||||
|
}
|
||||||
17
cmd/list.go
17
cmd/list.go
@@ -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"
|
||||||
@@ -27,15 +27,20 @@ var listCmd = &cobra.Command{
|
|||||||
vpnServers, err := vpn.GetList(flagProxy, flagSocks5Proxy)
|
vpnServers, err := vpn.GetList(flagProxy, flagSocks5Proxy)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal().Msg(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
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
6
flake.lock
generated
6
flake.lock
generated
@@ -20,11 +20,11 @@
|
|||||||
},
|
},
|
||||||
"nixpkgs": {
|
"nixpkgs": {
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1743814133,
|
"lastModified": 1770843696,
|
||||||
"narHash": "sha256-drDyYyUmjeYGiHmwB9eOPTQRjmrq3Yz26knwmMPLZFk=",
|
"narHash": "sha256-LovWTGDwXhkfCOmbgLVA10bvsi/P8eDDpRudgk68HA8=",
|
||||||
"owner": "NixOS",
|
"owner": "NixOS",
|
||||||
"repo": "nixpkgs",
|
"repo": "nixpkgs",
|
||||||
"rev": "250b695f41e0e2f5afbf15c6b12480de1fe0001b",
|
"rev": "2343bbb58f99267223bc2aac4fc9ea301a155a16",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
|
|||||||
17
flake.nix
17
flake.nix
@@ -1,4 +1,6 @@
|
|||||||
{
|
{
|
||||||
|
description = "vpngate - VPN server connector";
|
||||||
|
|
||||||
inputs = {
|
inputs = {
|
||||||
nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable";
|
nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable";
|
||||||
flake-utils.url = "github:numtide/flake-utils";
|
flake-utils.url = "github:numtide/flake-utils";
|
||||||
@@ -13,10 +15,10 @@
|
|||||||
let
|
let
|
||||||
vpngate =
|
vpngate =
|
||||||
pkgs:
|
pkgs:
|
||||||
pkgs.buildGo123Module rec {
|
pkgs.buildGo125Module rec {
|
||||||
name = "vpngate";
|
name = "vpngate";
|
||||||
src = ./.;
|
src = ./.;
|
||||||
vendorHash = "sha256-CP2sFJdIde88WFJlAq29GlE7c1c0xJ6tHzrrasMzJo8=";
|
vendorHash = "sha256-FNpeIIIrINm/3neCkuX/kFWWGCCEN8Duz1iSFAki+54=";
|
||||||
nativeBuildInputs = pkgs.lib.optionals pkgs.stdenv.isLinux [ pkgs.makeWrapper ];
|
nativeBuildInputs = pkgs.lib.optionals pkgs.stdenv.isLinux [ pkgs.makeWrapper ];
|
||||||
env.CGO_ENABLED = 0;
|
env.CGO_ENABLED = 0;
|
||||||
doCheck = false;
|
doCheck = false;
|
||||||
@@ -33,12 +35,19 @@
|
|||||||
default = vg;
|
default = vg;
|
||||||
vpngate = vg;
|
vpngate = vg;
|
||||||
};
|
};
|
||||||
devShell = pkgs.mkShell {
|
devShells.default = pkgs.mkShell {
|
||||||
|
name = "vpngate-dev";
|
||||||
|
description = "Development environment for vpngate";
|
||||||
packages = with pkgs; [
|
packages = with pkgs; [
|
||||||
|
go_1_26
|
||||||
gopls
|
gopls
|
||||||
gotools
|
gotools
|
||||||
go_1_23
|
golangci-lint
|
||||||
];
|
];
|
||||||
|
shellHook = ''
|
||||||
|
echo "Welcome to the vpngate dev environment"
|
||||||
|
go version
|
||||||
|
'';
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
in
|
in
|
||||||
|
|||||||
214
go.mod
214
go.mod
@@ -2,226 +2,40 @@ module github.com/davegallant/vpngate
|
|||||||
|
|
||||||
go 1.24.0
|
go 1.24.0
|
||||||
|
|
||||||
toolchain go1.24.9
|
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/olekukonko/tablewriter v0.0.5
|
github.com/olekukonko/tablewriter v1.1.3
|
||||||
github.com/rs/zerolog v1.34.0
|
github.com/rs/zerolog v1.34.0
|
||||||
github.com/spf13/afero v1.14.0
|
github.com/spf13/cobra v1.10.2
|
||||||
github.com/spf13/cobra v1.10.1
|
|
||||||
github.com/stretchr/testify v1.11.1
|
github.com/stretchr/testify v1.11.1
|
||||||
golang.org/x/net v0.46.0
|
golang.org/x/net v0.50.0
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
4d63.com/gocheckcompilerdirectives v1.3.0 // indirect
|
github.com/clipperhouse/displaywidth v0.6.2 // indirect
|
||||||
4d63.com/gochecknoglobals v0.2.2 // indirect
|
github.com/clipperhouse/stringish v0.1.1 // indirect
|
||||||
codeberg.org/chavacava/garif v0.2.0 // indirect
|
github.com/clipperhouse/uax29/v2 v2.3.0 // indirect
|
||||||
dev.gaijin.team/go/exhaustruct/v4 v4.0.0 // indirect
|
|
||||||
dev.gaijin.team/go/golib v0.6.0 // indirect
|
|
||||||
github.com/4meepo/tagalign v1.4.3 // indirect
|
|
||||||
github.com/Abirdcfly/dupword v0.1.7 // indirect
|
|
||||||
github.com/AdminBenni/iota-mixing v1.0.0 // indirect
|
|
||||||
github.com/AlwxSin/noinlineerr v1.0.5 // indirect
|
|
||||||
github.com/Antonboom/errname v1.1.1 // indirect
|
|
||||||
github.com/Antonboom/nilnil v1.1.1 // indirect
|
|
||||||
github.com/Antonboom/testifylint v1.6.4 // indirect
|
|
||||||
github.com/BurntSushi/toml v1.5.0 // indirect
|
|
||||||
github.com/Djarvur/go-err113 v0.1.1 // indirect
|
|
||||||
github.com/Masterminds/semver/v3 v3.4.0 // indirect
|
|
||||||
github.com/MirrexOne/unqueryvet v1.2.1 // indirect
|
|
||||||
github.com/OpenPeeDeeP/depguard/v2 v2.2.1 // indirect
|
|
||||||
github.com/alecthomas/chroma/v2 v2.20.0 // indirect
|
|
||||||
github.com/alecthomas/go-check-sumtype v0.3.1 // indirect
|
|
||||||
github.com/alexkohler/nakedret/v2 v2.0.6 // indirect
|
|
||||||
github.com/alexkohler/prealloc v1.0.0 // indirect
|
|
||||||
github.com/alfatraining/structtag v1.0.0 // indirect
|
|
||||||
github.com/alingse/asasalint v0.0.11 // indirect
|
|
||||||
github.com/alingse/nilnesserr v0.2.0 // indirect
|
|
||||||
github.com/ashanbrown/forbidigo/v2 v2.3.0 // indirect
|
|
||||||
github.com/ashanbrown/makezero/v2 v2.1.0 // indirect
|
|
||||||
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
|
|
||||||
github.com/beorn7/perks v1.0.1 // indirect
|
|
||||||
github.com/bkielbasa/cyclop v1.2.3 // indirect
|
|
||||||
github.com/blizzy78/varnamelen v0.8.0 // indirect
|
|
||||||
github.com/bombsimon/wsl/v4 v4.7.0 // indirect
|
|
||||||
github.com/bombsimon/wsl/v5 v5.3.0 // indirect
|
|
||||||
github.com/breml/bidichk v0.3.3 // indirect
|
|
||||||
github.com/breml/errchkjson v0.4.1 // indirect
|
|
||||||
github.com/butuzov/ireturn v0.4.0 // indirect
|
|
||||||
github.com/butuzov/mirror v1.3.0 // indirect
|
|
||||||
github.com/catenacyber/perfsprint v0.10.0 // indirect
|
|
||||||
github.com/ccojocar/zxcvbn-go v1.0.4 // indirect
|
|
||||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
|
||||||
github.com/charithe/durationcheck v0.0.11 // indirect
|
|
||||||
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc // indirect
|
|
||||||
github.com/charmbracelet/lipgloss v1.1.0 // indirect
|
|
||||||
github.com/charmbracelet/x/ansi v0.8.0 // indirect
|
|
||||||
github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd // indirect
|
|
||||||
github.com/charmbracelet/x/term v0.2.1 // indirect
|
|
||||||
github.com/ckaznocha/intrange v0.3.1 // indirect
|
|
||||||
github.com/curioswitch/go-reassign v0.3.0 // indirect
|
|
||||||
github.com/daixiang0/gci v0.13.7 // indirect
|
|
||||||
github.com/dave/dst v0.27.3 // indirect
|
|
||||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||||
github.com/denis-tingaikin/go-header v0.5.0 // indirect
|
|
||||||
github.com/dlclark/regexp2 v1.11.5 // indirect
|
|
||||||
github.com/ettle/strcase v0.2.0 // indirect
|
|
||||||
github.com/fatih/color v1.18.0 // indirect
|
github.com/fatih/color v1.18.0 // indirect
|
||||||
github.com/fatih/structtag v1.2.0 // indirect
|
|
||||||
github.com/firefart/nonamedreturns v1.0.6 // indirect
|
|
||||||
github.com/fsnotify/fsnotify v1.5.4 // indirect
|
|
||||||
github.com/fzipp/gocyclo v0.6.0 // indirect
|
|
||||||
github.com/ghostiam/protogetter v0.3.17 // indirect
|
|
||||||
github.com/go-critic/go-critic v0.14.2 // indirect
|
|
||||||
github.com/go-toolsmith/astcast v1.1.0 // indirect
|
|
||||||
github.com/go-toolsmith/astcopy v1.1.0 // indirect
|
|
||||||
github.com/go-toolsmith/astequal v1.2.0 // indirect
|
|
||||||
github.com/go-toolsmith/astfmt v1.1.0 // indirect
|
|
||||||
github.com/go-toolsmith/astp v1.1.0 // indirect
|
|
||||||
github.com/go-toolsmith/strparse v1.1.0 // indirect
|
|
||||||
github.com/go-toolsmith/typep v1.1.0 // indirect
|
|
||||||
github.com/go-viper/mapstructure/v2 v2.4.0 // indirect
|
|
||||||
github.com/go-xmlfmt/xmlfmt v1.1.3 // indirect
|
|
||||||
github.com/gobwas/glob v0.2.3 // indirect
|
|
||||||
github.com/godoc-lint/godoc-lint v0.10.1 // indirect
|
|
||||||
github.com/gofrs/flock v0.13.0 // indirect
|
|
||||||
github.com/golang/protobuf v1.5.3 // indirect
|
|
||||||
github.com/golangci/asciicheck v0.5.0 // indirect
|
|
||||||
github.com/golangci/dupl v0.0.0-20250308024227-f665c8d69b32 // indirect
|
|
||||||
github.com/golangci/go-printf-func-name v0.1.1 // indirect
|
|
||||||
github.com/golangci/gofmt v0.0.0-20250106114630-d62b90e6713d // indirect
|
|
||||||
github.com/golangci/golangci-lint/v2 v2.6.2 // indirect
|
|
||||||
github.com/golangci/golines v0.0.0-20250217134842-442fd0091d95 // indirect
|
|
||||||
github.com/golangci/misspell v0.7.0 // indirect
|
|
||||||
github.com/golangci/plugin-module-register v0.1.2 // indirect
|
|
||||||
github.com/golangci/revgrep v0.8.0 // indirect
|
|
||||||
github.com/golangci/swaggoswag v0.0.0-20250504205917-77f2aca3143e // indirect
|
|
||||||
github.com/golangci/unconvert v0.0.0-20250410112200-a129a6e6413e // indirect
|
|
||||||
github.com/google/go-cmp v0.7.0 // indirect
|
|
||||||
github.com/gordonklaus/ineffassign v0.2.0 // indirect
|
|
||||||
github.com/gostaticanalysis/analysisutil v0.7.1 // indirect
|
|
||||||
github.com/gostaticanalysis/comment v1.5.0 // indirect
|
|
||||||
github.com/gostaticanalysis/forcetypeassert v0.2.0 // indirect
|
|
||||||
github.com/gostaticanalysis/nilerr v0.1.2 // indirect
|
|
||||||
github.com/hashicorp/go-immutable-radix/v2 v2.1.0 // indirect
|
|
||||||
github.com/hashicorp/go-version v1.7.0 // indirect
|
|
||||||
github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect
|
|
||||||
github.com/hashicorp/hcl v1.0.0 // indirect
|
|
||||||
github.com/hexops/gotextdiff v1.0.3 // indirect
|
|
||||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||||
github.com/jgautheron/goconst v1.8.2 // indirect
|
|
||||||
github.com/jingyugao/rowserrcheck v1.1.1 // indirect
|
|
||||||
github.com/jjti/go-spancheck v0.6.5 // indirect
|
|
||||||
github.com/julz/importas v0.2.0 // indirect
|
|
||||||
github.com/karamaru-alpha/copyloopvar v1.2.2 // 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/kisielk/errcheck v1.9.0 // indirect
|
|
||||||
github.com/kkHAIKE/contextcheck v1.1.6 // indirect
|
|
||||||
github.com/kr/pretty v0.3.1 // indirect
|
github.com/kr/pretty v0.3.1 // indirect
|
||||||
github.com/kulti/thelper v0.7.1 // indirect
|
|
||||||
github.com/kunwardeep/paralleltest v1.0.15 // indirect
|
|
||||||
github.com/lasiar/canonicalheader v1.1.2 // indirect
|
|
||||||
github.com/ldez/exptostd v0.4.5 // indirect
|
|
||||||
github.com/ldez/gomoddirectives v0.7.1 // indirect
|
|
||||||
github.com/ldez/grignotin v0.10.1 // indirect
|
|
||||||
github.com/ldez/tagliatelle v0.7.2 // indirect
|
|
||||||
github.com/ldez/usetesting v0.5.0 // indirect
|
|
||||||
github.com/leonklingele/grouper v1.1.2 // indirect
|
|
||||||
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
|
|
||||||
github.com/macabu/inamedparam v0.2.0 // indirect
|
|
||||||
github.com/magiconair/properties v1.8.6 // indirect
|
|
||||||
github.com/manuelarte/embeddedstructfieldcheck v0.4.0 // indirect
|
|
||||||
github.com/manuelarte/funcorder v0.5.0 // indirect
|
|
||||||
github.com/maratori/testableexamples v1.0.1 // indirect
|
|
||||||
github.com/maratori/testpackage v1.1.2 // indirect
|
|
||||||
github.com/matoous/godox v1.1.0 // indirect
|
|
||||||
github.com/mattn/go-colorable v0.1.14 // indirect
|
github.com/mattn/go-colorable v0.1.14 // indirect
|
||||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||||
github.com/mattn/go-runewidth v0.0.16 // indirect
|
github.com/mattn/go-runewidth v0.0.19 // indirect
|
||||||
github.com/matttproud/golang_protobuf_extensions v1.0.1 // indirect
|
|
||||||
github.com/mgechev/revive v1.12.0 // indirect
|
|
||||||
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect
|
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect
|
||||||
github.com/mitchellh/go-homedir v1.1.0 // indirect
|
github.com/olekukonko/cat v0.0.0-20250911104152-50322a0618f6 // indirect
|
||||||
github.com/mitchellh/mapstructure v1.5.0 // indirect
|
github.com/olekukonko/errors v1.1.0 // indirect
|
||||||
github.com/moricho/tparallel v0.3.2 // indirect
|
github.com/olekukonko/ll v0.1.4-0.20260115111900-9e59c2286df0 // indirect
|
||||||
github.com/muesli/termenv v0.16.0 // indirect
|
|
||||||
github.com/nakabonne/nestif v0.3.1 // indirect
|
|
||||||
github.com/nishanths/exhaustive v0.12.0 // indirect
|
|
||||||
github.com/nishanths/predeclared v0.2.2 // indirect
|
|
||||||
github.com/nunnatsa/ginkgolinter v0.21.2 // indirect
|
|
||||||
github.com/pelletier/go-toml v1.9.5 // indirect
|
|
||||||
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
|
|
||||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||||
github.com/polyfloyd/go-errorlint v1.8.0 // indirect
|
|
||||||
github.com/prometheus/client_golang v1.12.1 // indirect
|
|
||||||
github.com/prometheus/client_model v0.2.0 // indirect
|
|
||||||
github.com/prometheus/common v0.32.1 // indirect
|
|
||||||
github.com/prometheus/procfs v0.7.3 // indirect
|
|
||||||
github.com/quasilyte/go-ruleguard v0.4.5 // indirect
|
|
||||||
github.com/quasilyte/go-ruleguard/dsl v0.3.23 // indirect
|
|
||||||
github.com/quasilyte/gogrep v0.5.0 // indirect
|
|
||||||
github.com/quasilyte/regex/syntax v0.0.0-20210819130434-b3f0c404a727 // indirect
|
|
||||||
github.com/quasilyte/stdinfo v0.0.0-20220114132959-f7386bf02567 // indirect
|
|
||||||
github.com/raeperd/recvcheck v0.2.0 // indirect
|
|
||||||
github.com/rivo/uniseg v0.4.7 // indirect
|
|
||||||
github.com/rogpeppe/go-internal v1.14.1 // indirect
|
github.com/rogpeppe/go-internal v1.14.1 // indirect
|
||||||
github.com/ryancurrah/gomodguard v1.4.1 // indirect
|
|
||||||
github.com/ryanrolds/sqlclosecheck v0.5.1 // indirect
|
|
||||||
github.com/sanposhiho/wastedassign/v2 v2.1.0 // indirect
|
|
||||||
github.com/santhosh-tekuri/jsonschema/v6 v6.0.2 // indirect
|
|
||||||
github.com/sashamelentyev/interfacebloat v1.1.0 // indirect
|
|
||||||
github.com/sashamelentyev/usestdlibvars v1.29.0 // indirect
|
|
||||||
github.com/securego/gosec/v2 v2.22.10 // indirect
|
|
||||||
github.com/sirupsen/logrus v1.9.3 // indirect
|
|
||||||
github.com/sivchari/containedctx v1.0.3 // indirect
|
|
||||||
github.com/sonatard/noctx v0.4.0 // indirect
|
|
||||||
github.com/sourcegraph/go-diff v0.7.0 // indirect
|
|
||||||
github.com/spf13/cast v1.5.0 // indirect
|
|
||||||
github.com/spf13/jwalterweatherman v1.1.0 // indirect
|
|
||||||
github.com/spf13/pflag v1.0.10 // indirect
|
github.com/spf13/pflag v1.0.10 // indirect
|
||||||
github.com/spf13/viper v1.12.0 // indirect
|
golang.org/x/sys v0.41.0 // indirect
|
||||||
github.com/ssgreg/nlreturn/v2 v2.2.1 // indirect
|
golang.org/x/term v0.40.0 // indirect
|
||||||
github.com/stbenjam/no-sprintf-host-port v0.2.0 // indirect
|
golang.org/x/text v0.34.0 // indirect
|
||||||
github.com/stretchr/objx v0.5.2 // indirect
|
|
||||||
github.com/subosito/gotenv v1.4.1 // indirect
|
|
||||||
github.com/tetafro/godot v1.5.4 // indirect
|
|
||||||
github.com/timakin/bodyclose v0.0.0-20241222091800-1db5c5ca4d67 // indirect
|
|
||||||
github.com/timonwong/loggercheck v0.11.0 // indirect
|
|
||||||
github.com/tomarrell/wrapcheck/v2 v2.11.0 // indirect
|
|
||||||
github.com/tommy-muehle/go-mnd/v2 v2.5.1 // indirect
|
|
||||||
github.com/ultraware/funlen v0.2.0 // indirect
|
|
||||||
github.com/ultraware/whitespace v0.2.0 // indirect
|
|
||||||
github.com/uudashr/gocognit v1.2.0 // indirect
|
|
||||||
github.com/uudashr/iface v1.4.1 // indirect
|
|
||||||
github.com/xen0n/gosmopolitan v1.3.0 // indirect
|
|
||||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
|
|
||||||
github.com/yagipy/maintidx v1.0.0 // indirect
|
|
||||||
github.com/yeya24/promlinter v0.3.0 // indirect
|
|
||||||
github.com/ykadowak/zerologlint v0.1.5 // indirect
|
|
||||||
gitlab.com/bosi/decorder v0.4.2 // indirect
|
|
||||||
go-simpler.org/musttag v0.14.0 // indirect
|
|
||||||
go-simpler.org/sloglint v0.11.1 // indirect
|
|
||||||
go.augendre.info/arangolint v0.3.1 // indirect
|
|
||||||
go.augendre.info/fatcontext v0.9.0 // indirect
|
|
||||||
go.uber.org/automaxprocs v1.6.0 // indirect
|
|
||||||
go.uber.org/multierr v1.10.0 // indirect
|
|
||||||
go.uber.org/zap v1.27.0 // indirect
|
|
||||||
golang.org/x/exp/typeparams v0.0.0-20251023183803-a4bb9ffd2546 // indirect
|
|
||||||
golang.org/x/mod v0.29.0 // indirect
|
|
||||||
golang.org/x/sync v0.18.0 // indirect
|
|
||||||
golang.org/x/sys v0.37.0 // indirect
|
|
||||||
golang.org/x/term v0.36.0 // indirect
|
|
||||||
golang.org/x/text v0.30.0 // indirect
|
|
||||||
golang.org/x/tools v0.38.0 // indirect
|
|
||||||
google.golang.org/protobuf v1.36.8 // indirect
|
|
||||||
gopkg.in/ini.v1 v1.67.0 // indirect
|
|
||||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
|
||||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
honnef.co/go/tools v0.6.1 // indirect
|
|
||||||
mvdan.cc/gofumpt v0.9.2 // indirect
|
|
||||||
mvdan.cc/unparam v0.0.0-20251027182757-5beb8c8f8f15 // indirect
|
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -2,46 +2,58 @@ package exec
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bufio"
|
"bufio"
|
||||||
"os"
|
"io"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"strings"
|
|
||||||
|
|
||||||
"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
|
||||||
|
// (this allows the caller to decide how to handle it).
|
||||||
func Run(path string, workDir string, args ...string) error {
|
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
|
||||||
log.Debug().Msg("Executing " + strings.Join(cmd.Args, " "))
|
|
||||||
|
log.Debug().Strs("command", cmd.Args).Msg("Executing command")
|
||||||
|
|
||||||
stdout, err := cmd.StdoutPipe()
|
stdout, err := cmd.StdoutPipe()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal().Msgf("Failed to get stdout pipe: %v", err)
|
log.Error().Msgf("Failed to get stdout pipe: %v", err)
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 {
|
if err := cmd.Start(); err != nil {
|
||||||
log.Fatal().Msgf("Failed to start command: %v", err)
|
log.Error().Msgf("Failed to start command: %v", err)
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
scanner := bufio.NewScanner(stdout)
|
// Combine stdout and stderr into a single reader
|
||||||
|
combined := io.MultiReader(stdout, stderr)
|
||||||
|
scanner := bufio.NewScanner(combined)
|
||||||
for scanner.Scan() {
|
for scanner.Scan() {
|
||||||
log.Debug().Msg(scanner.Text())
|
log.Debug().Msg(scanner.Text())
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := scanner.Err(); err != nil {
|
if err := scanner.Err(); err != nil {
|
||||||
log.Fatal().Msgf("Error reading stdout: %v", err)
|
log.Error().Msgf("Error reading output: %v", err)
|
||||||
}
|
|
||||||
|
|
||||||
if err := cmd.Wait(); err != nil {
|
|
||||||
log.Fatal().Msgf("Command finished with error: %v", err)
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
return nil
|
|
||||||
|
// 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()
|
||||||
}
|
}
|
||||||
@@ -2,6 +2,7 @@ package util
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/rs/zerolog/log"
|
"github.com/rs/zerolog/log"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -11,7 +12,7 @@ func Retry(attempts int, delay time.Duration,fn func() error) error {
|
|||||||
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
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
@@ -1,28 +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"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// 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 func() {
|
|
||||||
_ = os.Remove(tmpLogFile.Name())
|
|
||||||
}()
|
|
||||||
|
|
||||||
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", "--config", configPath, "--data-ciphers", "AES-128-CBC")
|
return exec.Run(executable, ".", "--verb", "4", "--config", configPath, "--data-ciphers", "AES-128-CBC")
|
||||||
return err
|
|
||||||
}
|
}
|
||||||
153
pkg/vpn/list.go
153
pkg/vpn/list.go
@@ -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,6 +19,8 @@ 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 information about a vpn relay server
|
// Server holds information about a vpn relay server
|
||||||
@@ -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,86 +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 func() {
|
defer func() {
|
||||||
_ = r.Body.Close()
|
_ = 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
|
||||||
})
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user