diff --git a/.env.example b/.env.example index b90bf34..579db4e 100644 --- a/.env.example +++ b/.env.example @@ -1,7 +1,7 @@ XRAY_IMAGE=ghcr.io/xtls/xray-core:26.7.28 -OVPN_AGENT_IMAGE=alpine:3.23.4 -OVPN_TELEGRAM_BOT_IMAGE=alpine:3.23.4 -OVPN_WEB_IMAGE=nginx:1.29-alpine +OVPN_AGENT_IMAGE=alpine:3.24.2 +OVPN_TELEGRAM_BOT_IMAGE=alpine:3.24.2 +OVPN_WEB_IMAGE=nginx:1.30.5-alpine OVPN_AGENT_LOG_LEVEL=info # Bearer token that guards the ovpn-agent mutating endpoints. The CLI auto-generates and persists # one under ~/.ovpn/secrets/agent-token and renders it here on deploy; set it explicitly to manage @@ -15,11 +15,11 @@ OVPN_TLS_SELFSNI_CERT_DIR=/opt/ovpn/certs OVPN_CAMOUFLAGE_SITE_DIR=/opt/ovpn/camouflage-site OVPN_SECURITY_PROFILE=minimal OVPN_THREAT_DNS_SERVERS=9.9.9.9,149.112.112.112 -PROMETHEUS_IMAGE=prom/prometheus:v3.11.2 -ALERTMANAGER_IMAGE=prom/alertmanager:v0.32.0 -GRAFANA_IMAGE=grafana/grafana:12.4.3 -NODE_EXPORTER_IMAGE=prom/node-exporter:v1.11.1 -CADVISOR_IMAGE=ghcr.io/google/cadvisor:0.56.2 +PROMETHEUS_IMAGE=prom/prometheus:v3.15.0 +ALERTMANAGER_IMAGE=prom/alertmanager:v0.34.1 +GRAFANA_IMAGE=grafana/grafana:12.4.12 +NODE_EXPORTER_IMAGE=prom/node-exporter:v1.12.1 +CADVISOR_IMAGE=ghcr.io/google/cadvisor:0.60.6 GRAFANA_ADMIN_USER=ovpn GRAFANA_ADMIN_PASSWORD=change-me-now GRAFANA_PORT=3000 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d26e616..b551a3a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -95,6 +95,11 @@ jobs: set -euo pipefail go test -json -covermode=atomic -coverprofile=coverage.out ./... | tee test-report.jsonl + - name: Docker geodata permission regression + env: + OVPN_TEST_DOCKER: '1' + run: go test -v ./internal/deploy -run '^TestValidateConfigWithDockerPrivateGeodata$' + - name: Test embedded runtime asset path id: runtimeassets_test run: | @@ -143,16 +148,16 @@ jobs: - name: GolangCI-Lint id: golangci # golangci/golangci-lint-action v9.3.0 - uses: golangci/golangci-lint-action@d583c34f0599d37dbac4a198b9c83201be380893 + uses: golangci/golangci-lint-action@ba0d7d2ec06a0ea1cb5fa41b2e4a3ab91d21278a with: - version: v2.12.2 + version: v2.14.0 args: --timeout=5m - name: Install gosec id: install_gosec run: | echo "$(go env GOPATH)/bin" >> "$GITHUB_PATH" - go install github.com/securego/gosec/v2/cmd/gosec@v2.27.1 + go install github.com/securego/gosec/v2/cmd/gosec@v2.29.0 - name: Gosec gate (high confidence/high severity) id: gosec_gate diff --git a/.github/workflows/security.yml b/.github/workflows/security.yml index 8b143ee..fbceb87 100644 --- a/.github/workflows/security.yml +++ b/.github/workflows/security.yml @@ -72,7 +72,7 @@ jobs: - name: Install govulncheck run: | echo "$(go env GOPATH)/bin" >> "$GITHUB_PATH" - go install golang.org/x/vuln/cmd/govulncheck@v1.3.0 + go install golang.org/x/vuln/cmd/govulncheck@v1.8.0 - name: Run govulncheck run: govulncheck ./... @@ -86,7 +86,7 @@ jobs: actions: read security-events: write env: - TRIVY_VERSION: v0.71.0 + TRIVY_VERSION: v0.75.0 TRIVY_CACHE_DIR: .cache/trivy DOCKER_CONFIG: /tmp/trivy-docker-config @@ -174,8 +174,8 @@ jobs: id: upload_trivy_sarif if: ${{ always() && steps.sarif_policy.outputs.allowed == 'true' }} continue-on-error: true - # github/codeql-action v4.36.1 - uses: github/codeql-action/upload-sarif@8aad20d150bbac5944a9f9d289da16a4b0d87c1e + # github/codeql-action v4.38.2 + uses: github/codeql-action/upload-sarif@2892aa5e19bbd11bc0cff5427e3b750a04d9e3c2 with: sarif_file: trivy-results.sarif diff --git a/CHANGELOG.md b/CHANGELOG.md index 5adca42..9a18c71 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,17 @@ The format is based on Keep a Changelog and this repository uses plain semantic ## Unreleased +## 1.10.0 + +### Fixed +- Local config validation uses the CLI user's UID/GID to read owner-only geodata without changing file permissions (#44). +- Geodata permission errors now point to filesystem access instead of disabling the security profile. + +### Security +- Validation configs use private temporary directories and `0600` permissions, with automatic cleanup. +- Updated Go to `1.27.1`, dependencies, pinned Actions, security tools and runtime images; consolidated Dependabot #39, #41 and #43. +- Aligned the Xray Go module with runtime `26.7.28`, including its TLS pinning fix; retained patched gRPC `1.83.2`. + ## 1.9.0 ### Added diff --git a/README.md b/README.md index 22e3e7c..e489a6c 100644 --- a/README.md +++ b/README.md @@ -117,7 +117,7 @@ flowchart LR ## Versioning -- Current pinned version: `1.9.0` +- Current pinned version: `1.10.0` - Check locally: `./ovpn version` - Release source of truth: - `VERSION` diff --git a/VERSION b/VERSION index f8e233b..81c871d 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.9.0 +1.10.0 diff --git a/ansible/inventories/example/group_vars/all.yml b/ansible/inventories/example/group_vars/all.yml index c136115..fe67310 100644 --- a/ansible/inventories/example/group_vars/all.yml +++ b/ansible/inventories/example/group_vars/all.yml @@ -107,7 +107,7 @@ ovpn_disable_motd_news: true ovpn_prepare_base_stack: false ovpn_base_stack_compose_filename: "docker-compose.base.yml" ovpn_base_bundle_src: "" -ovpn_agent_image: "alpine:3.23.4" +ovpn_agent_image: "alpine:3.24.2" ovpn_agent_log_level: "info" # Xray/runtime defaults (kept in all.yml so bootstrap has required vars even diff --git a/docs/cli.md b/docs/cli.md index a2169b8..a2c4135 100644 --- a/docs/cli.md +++ b/docs/cli.md @@ -638,3 +638,19 @@ For a shared profile: Do not leave targeted debug running longer than needed. Use `debug list` during support and `debug stop` when finished. + +## Local config validation + +Validation uses local geodata and the CLI user's UID/GID. Files with owner-only +permissions remain readable without changing their permissions. Custom geodata +paths use `OVPN_PROXY_GEOSITE_PATH` and `OVPN_PROXY_GEOIP_PATH`. + +With Docker running and CLI version `1.10.0` or later, replace `` with +the existing local proxy name: + +```bash +OVPN_SECURITY_PROFILE=minimal ./ovpn config validate --server +``` + +Expected result: `config valid`. If access is denied, check local file ownership +and read permissions; keep the security profile enabled. diff --git a/docs/upgrades.md b/docs/upgrades.md index 2ea74f3..fa2d7b1 100644 --- a/docs/upgrades.md +++ b/docs/upgrades.md @@ -208,3 +208,12 @@ Quota semantics are rolling 30d. Updated public fields/metrics: - Metrics: - `ovpn_agent_user_window_30d_usage_bytes` - `ovpn_agent_user_window_30d_quota_bytes` + +## Dependency pins + +Go and library versions are defined in `go.mod`; runtime images are defined in +`internal/defaults/images.go`. Updated image defaults take effect on deployment. +Keep Xray API dependencies aligned with the pinned runtime revision. + +gRPC remains on patched `1.83.2` because [GO-2026-6443](https://pkg.go.dev/vuln/GO-2026-6443) +lists `1.84.0` as affected. diff --git a/examples/.env.example b/examples/.env.example index 0692f15..6129fa8 100644 --- a/examples/.env.example +++ b/examples/.env.example @@ -1,7 +1,7 @@ XRAY_IMAGE=ghcr.io/xtls/xray-core:26.7.28 -OVPN_AGENT_IMAGE=alpine:3.23.4 -OVPN_TELEGRAM_BOT_IMAGE=alpine:3.23.4 -OVPN_WEB_IMAGE=nginx:1.29-alpine +OVPN_AGENT_IMAGE=alpine:3.24.2 +OVPN_TELEGRAM_BOT_IMAGE=alpine:3.24.2 +OVPN_WEB_IMAGE=nginx:1.30.5-alpine OVPN_AGENT_LOG_LEVEL=info OVPN_AGENT_HOST_PORT=19000 OVPN_TELEGRAM_BOT_HOST_PORT=19001 @@ -11,11 +11,11 @@ OVPN_TLS_SELFSNI_CERT_DIR=/opt/ovpn/certs OVPN_CAMOUFLAGE_SITE_DIR=/opt/ovpn/camouflage-site OVPN_SECURITY_PROFILE=minimal OVPN_THREAT_DNS_SERVERS=9.9.9.9,149.112.112.112 -PROMETHEUS_IMAGE=prom/prometheus:v3.11.2 -ALERTMANAGER_IMAGE=prom/alertmanager:v0.32.0 -GRAFANA_IMAGE=grafana/grafana:12.4.3 -NODE_EXPORTER_IMAGE=prom/node-exporter:v1.11.1 -CADVISOR_IMAGE=ghcr.io/google/cadvisor:0.56.2 +PROMETHEUS_IMAGE=prom/prometheus:v3.15.0 +ALERTMANAGER_IMAGE=prom/alertmanager:v0.34.1 +GRAFANA_IMAGE=grafana/grafana:12.4.12 +NODE_EXPORTER_IMAGE=prom/node-exporter:v1.12.1 +CADVISOR_IMAGE=ghcr.io/google/cadvisor:0.60.6 GRAFANA_ADMIN_USER=ovpn GRAFANA_ADMIN_PASSWORD=change-me-now GRAFANA_PORT=3000 diff --git a/go.mod b/go.mod index 122bf04..976160a 100644 --- a/go.mod +++ b/go.mod @@ -1,56 +1,56 @@ module ovpn -go 1.26.5 +go 1.27.1 require ( github.com/google/uuid v1.6.0 github.com/jedib0t/go-pretty/v6 v6.8.3 - github.com/prometheus/client_golang v1.24.0 + github.com/prometheus/client_golang v1.24.1 github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e github.com/spf13/cobra v1.10.2 - github.com/xtls/xray-core v1.260327.0 - golang.org/x/crypto v0.54.0 - google.golang.org/grpc v1.82.1 - modernc.org/sqlite v1.54.0 + github.com/xtls/xray-core v1.260327.1-0.20260728075948-5ca6f4b7d4dc + golang.org/x/crypto v0.57.0 + google.golang.org/grpc v1.83.2 + google.golang.org/protobuf v1.36.12 + modernc.org/sqlite v1.60.1 ) require ( github.com/andybalholm/brotli v1.2.1 // indirect - github.com/apernet/quic-go v0.59.1-0.20260217092621-db4786c77a22 // indirect + github.com/apernet/quic-go v0.59.1-0.20260425001925-6c6cc9bcb716 // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/clipperhouse/uax29/v2 v2.7.0 // indirect - github.com/cloudflare/circl v1.6.3 // indirect + github.com/cloudflare/circl v1.6.4 // indirect github.com/dustin/go-humanize v1.0.1 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/juju/ratelimit v1.0.2 // indirect - github.com/klauspost/compress v1.19.0 // indirect - github.com/klauspost/cpuid/v2 v2.3.0 // indirect + github.com/klauspost/compress v1.20.1 // indirect + github.com/klauspost/cpuid/v2 v2.4.0 // indirect github.com/kylelemons/godebug v1.1.0 // indirect - github.com/mattn/go-isatty v0.0.22 // indirect + github.com/mattn/go-isatty v0.0.24 // indirect github.com/mattn/go-runewidth v0.0.23 // indirect github.com/miekg/dns v1.1.72 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/ncruces/go-strftime v1.0.0 // indirect - github.com/pires/go-proxyproto v0.12.0 // indirect - github.com/prometheus/client_model v0.6.2 // indirect - github.com/prometheus/common v0.70.0 // indirect + github.com/pires/go-proxyproto v0.15.0 // indirect + github.com/prometheus/client_model v0.6.3 // indirect + github.com/prometheus/common v0.72.0 // indirect github.com/prometheus/procfs v0.21.1 // indirect github.com/refraction-networking/utls v1.8.3-0.20260301010127-aa6edf4b11af // indirect github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect - github.com/sagernet/sing v0.5.1 // indirect github.com/spf13/pflag v1.0.10 // indirect github.com/xtls/reality v0.0.0-20260322125925-9234c772ba8f // indirect - golang.org/x/mod v0.37.0 // indirect - golang.org/x/net v0.56.0 // indirect - golang.org/x/sync v0.22.0 // indirect - golang.org/x/sys v0.47.0 // indirect - golang.org/x/text v0.40.0 // indirect - golang.org/x/tools v0.47.0 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20260511170946-3700d4141b60 // indirect - google.golang.org/protobuf v1.36.11 // indirect + go4.org/netipx v0.0.0-20231129151722-fdeea329fbba // indirect + golang.org/x/mod v0.41.0 // indirect + golang.org/x/net v0.59.0 // indirect + golang.org/x/sync v0.23.0 // indirect + golang.org/x/sys v0.48.0 // indirect + golang.org/x/text v0.42.0 // indirect + golang.org/x/tools v0.51.0 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20260706201446-f0a921348800 // indirect lukechampine.com/blake3 v1.4.1 // indirect - modernc.org/libc v1.74.1 // indirect + modernc.org/libc v1.77.1 // indirect modernc.org/mathutil v1.7.1 // indirect - modernc.org/memory v1.11.0 // indirect + modernc.org/memory v1.12.1 // indirect ) diff --git a/go.sum b/go.sum index 42cb1f6..60f67b7 100644 --- a/go.sum +++ b/go.sum @@ -1,18 +1,16 @@ github.com/andybalholm/brotli v1.2.1 h1:R+f5xP285VArJDRgowrfb9DqL18yVK0gKAW/F+eTWro= github.com/andybalholm/brotli v1.2.1/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY= -github.com/apernet/quic-go v0.59.1-0.20260217092621-db4786c77a22 h1:00ziBGnLWQEcR9LThDwvxOznJJquJ9bYUdmBFnawLMU= -github.com/apernet/quic-go v0.59.1-0.20260217092621-db4786c77a22/go.mod h1:Npbg8qBtAZlsAB3FWmqwlVh5jtVG6a4DlYsOylUpvzA= +github.com/apernet/quic-go v0.59.1-0.20260425001925-6c6cc9bcb716 h1:J1O+xpLuJWkdYbw5JPGwBqIHs2J8tiEP7Py9lPqkN2I= +github.com/apernet/quic-go v0.59.1-0.20260425001925-6c6cc9bcb716/go.mod h1:Npbg8qBtAZlsAB3FWmqwlVh5jtVG6a4DlYsOylUpvzA= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/clipperhouse/uax29/v2 v2.7.0 h1:+gs4oBZ2gPfVrKPthwbMzWZDaAFPGYK72F0NJv2v7Vk= github.com/clipperhouse/uax29/v2 v2.7.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJSwu5BF98AuoVM= -github.com/cloudflare/circl v1.6.3 h1:9GPOhQGF9MCYUeXyMYlqTR6a5gTrgR/fBLXvUgtVcg8= -github.com/cloudflare/circl v1.6.3/go.mod h1:2eXP6Qfat4O/Yhh8BznvKnJ+uzEoTQ6jVKJRn81BiS4= +github.com/cloudflare/circl v1.6.4 h1:pOXuDTCEYyzydgUpQ0CQz3LsinKjiSk6nNP5Lt5K64U= +github.com/cloudflare/circl v1.6.4/go.mod h1:YxarevkLlbaHuWsxG6vmYNWBEsSp4pnp7j+4VljMavY= github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= -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/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= github.com/ghodss/yaml v1.0.1-0.20220118164431-d8423dcdf344 h1:Arcl6UOIS/kgO2nW3A65HN+7CMjSDP/gofXL4CZt1V4= @@ -29,8 +27,8 @@ github.com/google/btree v1.1.2 h1:xf4v41cLI2Z6FxbKm+8Bu+m8ifhj15JuZ9sa0jZCMUU= github.com/google/btree v1.1.2/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= -github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e h1:ijClszYn+mADRFY17kjQEVQ1XRhq2/JR1M3sGqeJoxs= -github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA= +github.com/google/pprof v0.0.0-20260802141513-ef3492d7dac3 h1:LMLX+LgTNWpfvCBdFebv6EsYotImrt/Ppc5cXIriCSo= +github.com/google/pprof v0.0.0-20260802141513-ef3492d7dac3/go.mod h1:jl5iWTm0/hd5PjEYEOuwAJ57L/CibdZfrqZ5XA5GrCk= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= @@ -43,18 +41,18 @@ github.com/jedib0t/go-pretty/v6 v6.8.3 h1:yVSk5aemoYHCvcrtqyXklwqcgHQIQzmy/oUzFl github.com/jedib0t/go-pretty/v6 v6.8.3/go.mod h1:YwC5CE4fJ1HFUDeivSV1r//AmANFHyqczZk+U6BDALU= github.com/juju/ratelimit v1.0.2 h1:sRxmtRiajbvrcLQT7S+JbqU0ntsb9W2yhSdNN8tWfaI= github.com/juju/ratelimit v1.0.2/go.mod h1:qapgC/Gy+xNh9UxzV13HGGl/6UXNN+ct+vwSgWNm/qk= -github.com/klauspost/compress v1.19.0 h1:sXLILfc9jV2QYWkzFOPWStmcUVH2RHEB1JCdY2oVvCQ= -github.com/klauspost/compress v1.19.0/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ= -github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y= -github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= +github.com/klauspost/compress v1.20.1 h1:T7kKElXUMXrUJ2E9QhQhxFtcK5rPyLdsGZvdbLMPdiQ= +github.com/klauspost/compress v1.20.1/go.mod h1:LUdAzn7YLVvxLpc7y3V1m40wESHTgc1422pwwBSKYuI= +github.com/klauspost/cpuid/v2 v2.4.0 h1:S6Hrbc7+ywsr0r+RLapfGBHfyefhCTwEh3A0tV913Dw= +github.com/klauspost/cpuid/v2 v2.4.0/go.mod h1:19jmZ9mjzoF//ddRSUsv0zfBTJWh3QJh9FNxZTMrGxU= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= -github.com/mattn/go-isatty v0.0.22 h1:j8l17JJ9i6VGPUFUYoTUKPSgKe/83EYU2zBC7YNKMw4= -github.com/mattn/go-isatty v0.0.22/go.mod h1:ZXfXG4SQHsB/w3ZeOYbR0PrPwLy+n6xiMrJlRFqopa4= +github.com/mattn/go-isatty v0.0.24 h1:tGZZoVgT/KiqK1c8ocVLeDS8BSWMRd47J3Lbz7vsReI= +github.com/mattn/go-isatty v0.0.24/go.mod h1:nMCL3Zebbrt45jsMDgnfIwz6ydEQApk5oEI3HqDio6A= github.com/mattn/go-runewidth v0.0.23 h1:7ykA0T0jkPpzSvMS5i9uoNn2Xy3R383f9HDx3RybWcw= github.com/mattn/go-runewidth v0.0.23/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs= github.com/miekg/dns v1.1.72 h1:vhmr+TF2A3tuoGNkLDFK9zi36F2LS+hKTRW0Uf8kbzI= @@ -65,16 +63,22 @@ github.com/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOF github.com/ncruces/go-strftime v1.0.0/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls= github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8= github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= -github.com/pires/go-proxyproto v0.12.0 h1:TTCxD66dU898tahivkqc3hoceZp7P44FnorWyo9d5vM= -github.com/pires/go-proxyproto v0.12.0/go.mod h1:qUvfqUMEoX7T8g0q7TQLDnhMjdTrxnG0hvpMn+7ePNI= -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/prometheus/client_golang v1.24.0 h1:5XStIklKuAtJSNpdD3s8XJj/Yv78IQmE1kbNk87JrAI= -github.com/prometheus/client_golang v1.24.0/go.mod h1:QcsNdotprC2nS4BTM2ucbcqxd2CeXTEa9jW7zHO9iDE= -github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= -github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= -github.com/prometheus/common v0.70.0 h1:bcpru3tWPVnxGnETLgOV5jbp/JRXgYEyv65CuBLAMMI= -github.com/prometheus/common v0.70.0/go.mod h1:S/SFasQmgGiYH6C81LKCtYa8QACgthGg5zxL2udV7SY= +github.com/pion/dtls/v3 v3.1.4 h1:QhvtMflMfu9Kf0RcDC5BJBle4caPskByrKQR6uuYqpY= +github.com/pion/dtls/v3 v3.1.4/go.mod h1:cr/qotLISUw/9C1m83ZPNZtj9WnXkYLpfCptPqbkInc= +github.com/pion/logging v0.2.4 h1:tTew+7cmQ+Mc1pTBLKH2puKsOvhm32dROumOZ655zB8= +github.com/pion/logging v0.2.4/go.mod h1:DffhXTKYdNZU+KtJ5pyQDjvOAh/GsNSyv1lbkFbe3so= +github.com/pion/stun/v3 v3.1.6 h1:WnhsD0eHCiwCfKNkVx0VJJwr2Y3eV4Ueih3KJ+dfZy8= +github.com/pion/stun/v3 v3.1.6/go.mod h1:zRUghXSQU32Lx5orJsz3uYMkIihweXb3mu5gIns02fs= +github.com/pion/transport/v4 v4.0.2 h1:ifYlPqNwsy6aKQ9y8yzxXlHae5431ZrH2avkD/Rn6Tk= +github.com/pion/transport/v4 v4.0.2/go.mod h1:06hFI+jCFcok2X2MekVufNZ/uzNZXivGBPfviSVcjgM= +github.com/pires/go-proxyproto v0.15.0 h1:dTshmNbFm/D+0+sbrxUuddPOZ5Y0B7c5NhtsBkm6LqI= +github.com/pires/go-proxyproto v0.15.0/go.mod h1:OXsCrKwrK2tXS9YrI5tkHx5xaQlO8FH3lFW76orFh24= +github.com/prometheus/client_golang v1.24.1 h1:JnJkREXzWxUdCuPFpIWZiPispT9xVV59uiuyR2bPlnU= +github.com/prometheus/client_golang v1.24.1/go.mod h1:F+oSRECHg4sse5ucfYpYDeIv/hu68Zo0uoHKetWnzcE= +github.com/prometheus/client_model v0.6.3 h1:O0jaTVAYNxTHYInEPFJt5I3+sN8zqBtVMPTB1qyxiEo= +github.com/prometheus/client_model v0.6.3/go.mod h1:gpN5P9S7Rr6Yr92PiQ+Ixvhf6JZEkF1dnxsYL2aPBEM= +github.com/prometheus/common v0.72.0 h1:tAYsE+sPJxIncDAobm4H5aQjmox9ZxEIIqPbiffa8G4= +github.com/prometheus/common v0.72.0/go.mod h1:77NWqAQ2tXT7BIK40qjJdw5Acrsrg1TlHAnsQi3i6mk= github.com/prometheus/procfs v0.21.1 h1:GljZCt+zSTS+NZq88cyQ1LjZ+RCHp3uVuabBWA5+OJI= github.com/prometheus/procfs v0.21.1/go.mod h1:aB55Cww9pdSJVHk0hUf0inxWyyjPogFIjmHKYgMKmtY= github.com/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8= @@ -83,6 +87,8 @@ github.com/refraction-networking/utls v1.8.3-0.20260301010127-aa6edf4b11af h1:er github.com/refraction-networking/utls v1.8.3-0.20260301010127-aa6edf4b11af/go.mod h1:jkSOEkLqn+S/jtpEHPOsVv/4V4EVnelwbMQl4vCWXAM= github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= +github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs= +github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro= github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= @@ -97,102 +103,106 @@ github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiT github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk= github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= -github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= -github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/stretchr/testify v1.12.1 h1:EuwCh5fleGS7H32xRwO3wRGT7DxrDhLAT6FF8MpWDWE= +github.com/stretchr/testify v1.12.1/go.mod h1:MDEgiDPPsNp5cuIrHPPCyornHKgEVbtFUmoNlxoYthg= github.com/vishvananda/netlink v1.3.1 h1:3AEMt62VKqz90r0tmNhog0r/PpWKmrEShJU0wJW6bV0= github.com/vishvananda/netlink v1.3.1/go.mod h1:ARtKouGSTGchR8aMwmkzC0qiNPrrWO5JS/XMVl45+b4= github.com/vishvananda/netns v0.0.5 h1:DfiHV+j8bA32MFM7bfEunvT8IAqQ/NzSJHtcmW5zdEY= github.com/vishvananda/netns v0.0.5/go.mod h1:SpkAiCQRtJ6TvvxPnOSyH3BMl6unz3xZlaprSwhNNJM= +github.com/wlynxg/anet v0.0.5 h1:J3VJGi1gvo0JwZ/P1/Yc/8p63SoW98B5dHkYDmpgvvU= +github.com/wlynxg/anet v0.0.5/go.mod h1:eay5PRQr7fIVAMbTbchTnO9gG65Hg/uYGdc7mguHxoA= github.com/xtls/reality v0.0.0-20260322125925-9234c772ba8f h1:iy2JRioxmUpoJ3SzbFPyTxHZMbR/rSHP7dOOgYaq1O8= github.com/xtls/reality v0.0.0-20260322125925-9234c772ba8f/go.mod h1:DsJblcWDGt76+FVqBVwbwRhxyyNJsGV48gJLch0OOWI= -github.com/xtls/xray-core v1.260327.0 h1:g4TzxMwyPrxslZh6uD+FiG3lXKTrnNO+b4ky2OhogHE= -github.com/xtls/xray-core v1.260327.0/go.mod h1:OXMlhBloFry8mw0KwWLWLd3RQyXJzEYsCGlgsX36h60= +github.com/xtls/xray-core v1.260327.1-0.20260728075948-5ca6f4b7d4dc h1:fkOkmgHWbF2Q8MdV9VxrsyxRz4OndcrUXUkh1ANBTg0= +github.com/xtls/xray-core v1.260327.1-0.20260728075948-5ca6f4b7d4dc/go.mod h1:wukQoBGnQ6GaLTGuKwv8rCTgf80QxPj+6iznDZHQEWo= github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU= github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E= go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= -go.opentelemetry.io/otel v1.43.0 h1:mYIM03dnh5zfN7HautFE4ieIig9amkNANT+xcVxAj9I= -go.opentelemetry.io/otel v1.43.0/go.mod h1:JuG+u74mvjvcm8vj8pI5XiHy1zDeoCS2LB1spIq7Ay0= -go.opentelemetry.io/otel/metric v1.43.0 h1:d7638QeInOnuwOONPp4JAOGfbCEpYb+K6DVWvdxGzgM= -go.opentelemetry.io/otel/metric v1.43.0/go.mod h1:RDnPtIxvqlgO8GRW18W6Z/4P462ldprJtfxHxyKd2PY= -go.opentelemetry.io/otel/sdk v1.43.0 h1:pi5mE86i5rTeLXqoF/hhiBtUNcrAGHLKQdhg4h4V9Dg= -go.opentelemetry.io/otel/sdk v1.43.0/go.mod h1:P+IkVU3iWukmiit/Yf9AWvpyRDlUeBaRg6Y+C58QHzg= -go.opentelemetry.io/otel/sdk/metric v1.43.0 h1:S88dyqXjJkuBNLeMcVPRFXpRw2fuwdvfCGLEo89fDkw= -go.opentelemetry.io/otel/sdk/metric v1.43.0/go.mod h1:C/RJtwSEJ5hzTiUz5pXF1kILHStzb9zFlIEe85bhj6A= -go.opentelemetry.io/otel/trace v1.43.0 h1:BkNrHpup+4k4w+ZZ86CZoHHEkohws8AY+WTX09nk+3A= -go.opentelemetry.io/otel/trace v1.43.0/go.mod h1:/QJhyVBUUswCphDVxq+8mld+AvhXZLhe+8WVFxiFff0= +go.opentelemetry.io/otel v1.44.0 h1:JjwHmHpA4iZ3wBxluu2fbbE7j4kqlE8jXyAyPXH7HqU= +go.opentelemetry.io/otel v1.44.0/go.mod h1:BMgjTHL9WPRlRjL2oZCBTL4whCGtXch2H4BhOPIAyYc= +go.opentelemetry.io/otel/metric v1.44.0 h1:1w0gILTcHdr3YI+ixLyjemwrVnsMURbTZFrSYCdDdmc= +go.opentelemetry.io/otel/metric v1.44.0/go.mod h1:8O7hanEPBNgEMmybD3s2VBKcgWOCsA6tzHBPODAiquo= +go.opentelemetry.io/otel/sdk v1.44.0 h1:nHYwb9lK+fJPU/dnT6s7W7Z8itMWyqrnVfbheVYrZ58= +go.opentelemetry.io/otel/sdk v1.44.0/go.mod h1:Osuydd3Se74nqjAKxid74N5eC+jfEqfTegHRnq58oK0= +go.opentelemetry.io/otel/sdk/metric v1.44.0 h1:3LlKgI+VjbVsjNRFZJZAJ30WjXC5VkNRks6si09iEfI= +go.opentelemetry.io/otel/sdk/metric v1.44.0/go.mod h1:5B5pMARnXxKhltooO4xUuCBorl65a4EpnTalObqOigA= +go.opentelemetry.io/otel/trace v1.44.0 h1:jxF5CsGYCe74MCRx2X4g7WsY/VBKRqqpNvXlX/6gtIk= +go.opentelemetry.io/otel/trace v1.44.0/go.mod h1:oLl1jrMQAVo6v3GAggN+1VH9VIz9iUSvW53sW1Q8PIE= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.yaml.in/yaml/v2 v2.4.4 h1:tuyd0P+2Ont/d6e2rl3be67goVK4R6deVxCUX5vyPaQ= go.yaml.in/yaml/v2 v2.4.4/go.mod h1:gMZqIpDtDqOfM0uNfy0SkpRhvUryYH0Z6wdMYcacYXQ= go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= +go.yaml.in/yaml/v3 v3.0.5 h1:N6y/pJk8buWs9NY5ERU2HSMfm+IuD/OtfdAnq6kESPw= +go.yaml.in/yaml/v3 v3.0.5/go.mod h1:HVTZu1O7/Vkt2N+BFy8Zza+lnLsABggaTM2ZpNIGuKg= go4.org/netipx v0.0.0-20231129151722-fdeea329fbba h1:0b9z3AuHCjxk0x/opv64kcgZLBseWJUpBw5I82+2U4M= go4.org/netipx v0.0.0-20231129151722-fdeea329fbba/go.mod h1:PLyyIXexvUFg3Owu6p/WfdlivPbZJsZdgWZlrGope/Y= -golang.org/x/crypto v0.54.0 h1:YLIA59K4fiNzHzjnZt2tUJQjQtUWfWbeHBqKtk3eScw= -golang.org/x/crypto v0.54.0/go.mod h1:KWL8ny2AZdGR2cWmzeHrp2azQPGogOv+HeQaVEXC2dk= +golang.org/x/crypto v0.57.0 h1:3ZVCjf8Ggz7zneR/EHRVx68Ctf+2pmIMP2UFhh9cC6M= +golang.org/x/crypto v0.57.0/go.mod h1:Fdz0i5U6CoizGwLda9DttjSk6qlZo25zYNtR+ycvuZA= golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 h1:vr/HnozRka3pE4EsMEg1lgkXJkTFJCVUX+S/ZT6wYzM= golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842/go.mod h1:XtvwrStGgqGPLc4cjQfWqZHG1YFdYs6swckp8vpsjnc= -golang.org/x/mod v0.37.0 h1:vF1DjpVEshcIqoEaauuHebaLk1O1forxjxBaVn884JQ= -golang.org/x/mod v0.37.0/go.mod h1:m8S8VeM9r4dzDwjrKO0a1sZP3YjeMamRRlD+fmR2Q/0= -golang.org/x/net v0.56.0 h1:Rw8j/hFzGvJUZwNBXnAtf5sVDVt+65SK2C7IxCxZt5o= -golang.org/x/net v0.56.0/go.mod h1:D3Ku6r+V6JROoZK144D2XfMHFcMq/0zSfLelVTCFKec= -golang.org/x/sync v0.22.0 h1:SZjpbeLmrCk4xhRSZFNZW5gFUeCeFgjekvI/+gfScek= -golang.org/x/sync v0.22.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= -golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs= -golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= -golang.org/x/text v0.40.0 h1:Ub2Z6/xjgF1WrYQz2nuITOEegKFtiIy+rieRJ5lHZKs= -golang.org/x/text v0.40.0/go.mod h1:hpnzDAfGV753zIKo+wk3u1bVKCGPbrnF7+7LBF/UHVY= -golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE= -golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= -golang.org/x/tools v0.47.0 h1:7Kn5x/d1svx/PzryTsqeoZN4TZwqeH5pGWjefhLi/1Q= -golang.org/x/tools v0.47.0/go.mod h1:dFHnyTvFWY212G+h7ZY4Vsp/K3U4/7W9TyVaAul8uCA= +golang.org/x/mod v0.41.0 h1:qJmnOUb4YB+FsEuM3HcWucdZASCPGhsX6uljO6pog0c= +golang.org/x/mod v0.41.0/go.mod h1:Ek9pY8RKWXwsWvd3rQiHYtMqkjSUV+s1Rj7j4H5Ur6o= +golang.org/x/net v0.59.0 h1:5zfYln+w5XCxwrnMMJPufRgNoXEaGxl0wo5GqPXyues= +golang.org/x/net v0.59.0/go.mod h1:2DA/G1UfVbCpQPeWTmMPGY7Cs2PkBkwu743bVX5PIVg= +golang.org/x/sync v0.23.0 h1:KameEIfc1IkluZyXWLn39Wd4tURc6GbCiISGiZm2bQk= +golang.org/x/sync v0.23.0/go.mod h1:sUUOizhqBxiL6pEWpqNLUiaJn1ShEbZ6BBqskPbjZm0= +golang.org/x/sys v0.48.0 h1:bbX/i/6MgT9BVLM9RT1thmxL04yeTAhbEz4SyadbXoo= +golang.org/x/sys v0.48.0/go.mod h1:hNLxWAXmnKAxqDtdwIYC4bM9oQPEecfsnNMuSxOs3og= +golang.org/x/text v0.42.0 h1:JbOZXgfeCPU9gacVtYliJqOhD+zhrEqK4LfdpmlUZqI= +golang.org/x/text v0.42.0/go.mod h1:ojzP1Z+2QtioaF8DTtO8K5q7JWVVYwZKenzujK0Zd0E= +golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI= +golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4= +golang.org/x/tools v0.51.0 h1:k4Xc/1Om9jwkBJBo4NVLMSARBoWtK10mx+W5BnXCeAI= +golang.org/x/tools v0.51.0/go.mod h1:9eEncMayCV6zRMGhR5eZEC2iBx98qWcF1HZ9Z7wJOoA= golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 h1:B82qJJgjvYKsXS9jeunTOisW56dUokqW/FOteYJJ/yg= golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2/go.mod h1:deeaetjYA+DHMHg+sMSMI58GrEteJUUzzw7en6TJQcI= golang.zx2c4.com/wireguard v0.0.0-20250521234502-f333402bd9cb h1:whnFRlWMcXI9d+ZbWg+4sHnLp52d5yiIPUxMBSt4X9A= golang.zx2c4.com/wireguard v0.0.0-20250521234502-f333402bd9cb/go.mod h1:rpwXGsirqLqN2L0JDJQlwOboGHmptD5ZD6T2VmcqhTw= +golang.zx2c4.com/wireguard/windows v1.0.1 h1:eOxiDVbywPC+ZQqvdCK7x+ZwWXKbYv50TtH8ysFIbw8= +golang.zx2c4.com/wireguard/windows v1.0.1/go.mod h1:+fbT3FFdX4zzYDLwJh5+HPEcNN/3HyNdzhNSVsQM+zs= gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4= gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E= -google.golang.org/genproto/googleapis/rpc v0.0.0-20260511170946-3700d4141b60 h1:seT2EwLWM78plQ7wcDfuWBc/4FAEAXDDiaSol4ku4qo= -google.golang.org/genproto/googleapis/rpc v0.0.0-20260511170946-3700d4141b60/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= -google.golang.org/grpc v1.82.1 h1:NnAxzGRA0677vCa4BUkOAnO5+FfQqVl9iUXeD0IqcGE= -google.golang.org/grpc v1.82.1/go.mod h1:yzTZ1TB1Z3SG+LIYaI+WiE8D5+PZ3ArnrSp8zF3+/ZA= -google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= -google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260706201446-f0a921348800 h1:qEHAMpSaUhtD0p3NbEEI83HwNGFxEwaSJ1G9PLnCBZE= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260706201446-f0a921348800/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= +google.golang.org/grpc v1.83.2 h1:EManeRomTObA0BU7I8vXgg/78uE5MJ9M8B39EX2WscU= +google.golang.org/grpc v1.83.2/go.mod h1:YPI1hK3kDked6iHvgX3tR0y+nX/qpMFKhPgFsokw1S8= +google.golang.org/protobuf v1.36.12 h1:pJOKDDOyeXErUroCihFAd5LQuwXBSpVnKGrj5o/fwxc= +google.golang.org/protobuf v1.36.12/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/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/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= -gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= -gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gvisor.dev/gvisor v0.0.0-20260122175437-89a5d21be8f0 h1:Lk6hARj5UPY47dBep70OD/TIMwikJ5fGUGX0Rm3Xigk= gvisor.dev/gvisor v0.0.0-20260122175437-89a5d21be8f0/go.mod h1:QkHjoMIBaYtpVufgwv3keYAbln78mBoCuShZrPrer1Q= lukechampine.com/blake3 v1.4.1 h1:I3Smz7gso8w4/TunLKec6K2fn+kyKtDxr/xcQEN84Wg= lukechampine.com/blake3 v1.4.1/go.mod h1:QFosUxmjB8mnrWFSNwKmvxHpfY72bmD2tQ0kBMM3kwo= -modernc.org/cc/v4 v4.29.0 h1:CXgwL8cvxmyzBQZzbSl/6xFtMCryb6u8IOqDci39cgc= -modernc.org/cc/v4 v4.29.0/go.mod h1:OnovgIhbbMXMu1aISnJ0wvVD1KnW+cAUJkIrAWh+kVI= -modernc.org/ccgo/v4 v4.34.6 h1:sBgfIwyN0TQ9C5hwIeuqyeAKyMWnbvj2fvpF4L11uzU= -modernc.org/ccgo/v4 v4.34.6/go.mod h1:SZ8YcN9NG7XVsQYdm6jYBvi8PQP1qi+kqB6OhjqI3Fk= +modernc.org/cc/v4 v4.29.7 h1:q+NXGJ0bK3b4TXFYQQVr9pYETGnmwFWkrUzJnMya/Tg= +modernc.org/cc/v4 v4.29.7/go.mod h1:OnovgIhbbMXMu1aISnJ0wvVD1KnW+cAUJkIrAWh+kVI= +modernc.org/ccgo/v4 v4.36.1 h1:ZNIUZAryN0UgnJwtyxrdEzcFc3yD4Cu4AzjfPXsLsIE= +modernc.org/ccgo/v4 v4.36.1/go.mod h1:rrtGc2QkS239nYb/mQNuBMyjq3/y3ZXWbBjPoV3wqzA= modernc.org/fileutil v1.4.0 h1:j6ZzNTftVS054gi281TyLjHPp6CPHr2KCxEXjEbD6SM= modernc.org/fileutil v1.4.0/go.mod h1:EqdKFDxiByqxLk8ozOxObDSfcVOv/54xDs/DUHdvCUU= modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI= modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito= -modernc.org/gc/v3 v3.1.4 h1:2g65LGVSmFQrXeITAw97x7hCRvZFcyE1uDP+7Vng7JI= -modernc.org/gc/v3 v3.1.4/go.mod h1:HFK/6AGESC7Ex+EZJhJ2Gni6cTaYpSMmU/cT9RmlfYY= +modernc.org/gc/v3 v3.1.5 h1:21ldfPfRYE31Tb7B3mwAK8gy1AxP4+dKjrOQPfqakoc= +modernc.org/gc/v3 v3.1.5/go.mod h1:HFK/6AGESC7Ex+EZJhJ2Gni6cTaYpSMmU/cT9RmlfYY= modernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks= modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI= -modernc.org/libc v1.74.1 h1:bdR4VTKFMC4966QSNZ05XLGI/VwzVa2kTUX51Dm0riQ= -modernc.org/libc v1.74.1/go.mod h1:uH4t5bOx3G3g9Xcmj10YKlTcVISlRDwv8VoQJG9n8Os= +modernc.org/libc v1.77.1 h1:Ct8j47QtiZ1Enj2DtFXQtUqrPCAjdCmPjtCuvrYQ0Hs= +modernc.org/libc v1.77.1/go.mod h1:87/pZ4L6nD1zqW4nItuS12YO7hN1igAah34xjnQo/W0= modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU= modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg= -modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI= -modernc.org/memory v1.11.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw= +modernc.org/memory v1.12.1 h1:nFMiWrpStgZczNl6XI9GnIk/rWhYIyHGUaR04pGbp9g= +modernc.org/memory v1.12.1/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw= modernc.org/opt v0.2.0 h1:tGyef5ApycA7FSEOMraay9SaTk5zmbx7Tu+cJs4QKZg= modernc.org/opt v0.2.0/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns= modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w= modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE= -modernc.org/sqlite v1.54.0 h1:JCxR4qwkJvOaqAoYcgDoO25Nc+ROg6EJ2LfBVzdrgog= -modernc.org/sqlite v1.54.0/go.mod h1:4ntCLuNmnH8+GNqjka1wNg7KJd5/Hi5FYp8K+XQ7GZw= +modernc.org/sqlite v1.60.1 h1:/blz53O951KWFOso4QQvEs/Fq6cDBKLtMVrYNSeJVKw= +modernc.org/sqlite v1.60.1/go.mod h1:1dIoEagfDE72QytD5scH1lxARtaUgKgHC/NuApA27r0= modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0= modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A= modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y= diff --git a/internal/cli/config_cmd.go b/internal/cli/config_cmd.go index 3f8846f..838bfad 100644 --- a/internal/cli/config_cmd.go +++ b/internal/cli/config_cmd.go @@ -92,9 +92,13 @@ func (a *App) configCmd() *cobra.Command { return fmt.Errorf("json invalid: %w", err) } xrayImage := "ghcr.io/xtls/xray-core:" + normalizeXrayVersionTag(srv.XrayVersion) - configFile := filepath.Join(os.TempDir(), fmt.Sprintf("ovpn-validate-%s.json", srv.Name)) - defer os.Remove(configFile) - if err := os.WriteFile(configFile, jsonRaw, 0o644); err != nil { + configDir, err := os.MkdirTemp("", "ovpn-validate-") + if err != nil { + return err + } + defer os.RemoveAll(configDir) + configFile := filepath.Join(configDir, "config.json") + if err := os.WriteFile(configFile, jsonRaw, 0o600); err != nil { return err } if _, err := exec.LookPath("docker"); err == nil { diff --git a/internal/cli/config_cmd_test.go b/internal/cli/config_cmd_test.go index ed16567..830a6fc 100644 --- a/internal/cli/config_cmd_test.go +++ b/internal/cli/config_cmd_test.go @@ -2,9 +2,14 @@ package cli import ( "crypto/tls" + "fmt" "os" "path/filepath" + "runtime" + "strings" "testing" + + "ovpn/internal/model" ) func TestWriteValidationTLSSelfSNICertificate(t *testing.T) { @@ -27,3 +32,76 @@ func TestWriteValidationTLSSelfSNICertificate(t *testing.T) { t.Fatalf("expected private key mode 0600, got %o", info.Mode().Perm()) } } + +func TestConfigValidatePrivateInputsAndCleanup(t *testing.T) { + for _, exitCode := range []string{"0", "23"} { + t.Run("docker_exit_"+exitCode, func(t *testing.T) { + app := newGlobalUsersTestApp(t) + proxy := addServerBackendTestServer(t, app, "proxy-test", model.ServerRoleProxy, "proxy-pub", "") + backend := addServerBackendTestServer(t, app, "backend-test", model.ServerRoleVPN, "vpn-pub", "11111111-1111-1111-1111-111111111111") + if err := app.store.UpsertProxyBackend(app.ctx, &model.ProxyBackend{ProxyServerID: proxy.ID, BackendServerID: backend.ID, Enabled: true, Priority: 10}); err != nil { + t.Fatal(err) + } + dir := t.TempDir() + site, ip := filepath.Join(dir, "geosite.dat"), filepath.Join(dir, "geoip.dat") + for _, path := range []string{site, ip} { + if err := os.WriteFile(path, []byte("private geodata fixture"), 0o600); err != nil { + t.Fatal(err) + } + } + t.Setenv("OVPN_PROXY_GEOSITE_PATH", site) + t.Setenv("OVPN_PROXY_GEOIP_PATH", ip) + argsPath := filepath.Join(dir, "args") + // Capture the path while it exists; verify its mode inside the fake command. + modeFlag := "-c '%a'" + if runtime.GOOS == "darwin" { + modeFlag = "-f '%Lp'" + } + script := "#!/bin/sh\nprintf '%s\\n' \"$@\" > \"$OVPN_TEST_CONFIG_ARGS\"\nfor arg do\ncase \"$arg\" in\n*:/etc/xray/config.json:ro) path=${arg%:/etc/xray/config.json:ro}; test -s \"$path\" || exit 90; mode=$(stat " + modeFlag + " \"$path\"); test \"$mode\" = 600 || exit 91;;\nesac\ndone\nexit \"$OVPN_TEST_CONFIG_EXIT\"\n" + if err := os.WriteFile(filepath.Join(dir, "docker"), []byte(script), 0o755); err != nil { + t.Fatal(err) + } + t.Setenv("PATH", dir+string(os.PathListSeparator)+os.Getenv("PATH")) + t.Setenv("OVPN_TEST_CONFIG_ARGS", argsPath) + t.Setenv("OVPN_TEST_CONFIG_EXIT", exitCode) + cmd := app.configCmd() + cmd.SetArgs([]string{"validate", "--server", proxy.Name}) + err := cmd.Execute() + if exitCode == "0" && err != nil { + t.Fatal(err) + } + if exitCode == "23" && (err == nil || !strings.Contains(err.Error(), "exit status 23")) { + t.Fatalf("expected docker failure, got %v", err) + } + raw, err := os.ReadFile(argsPath) + if err != nil { + t.Fatal(err) + } + args := strings.Split(strings.TrimSpace(string(raw)), "\n") + if !strings.Contains(string(raw), "--user\n"+fmt.Sprintf("%d:%d", os.Getuid(), os.Getgid())) { + t.Fatalf("missing CLI identity: %s", raw) + } + for _, path := range []string{site, ip} { + if !strings.Contains(string(raw), path+":/usr/local/share/xray/"+filepath.Base(path)+":ro") { + t.Fatalf("missing geodata mount: %s", raw) + } + info, err := os.Stat(path) + if err != nil || info.Mode().Perm() != 0o600 { + t.Fatalf("source permissions changed: %v", err) + } + } + var configPath string + for _, arg := range args { + if strings.HasSuffix(arg, ":/etc/xray/config.json:ro") { + configPath = strings.TrimSuffix(arg, ":/etc/xray/config.json:ro") + } + } + if configPath == "" { + t.Fatal("config mount missing") + } + if _, err := os.Stat(filepath.Dir(configPath)); !os.IsNotExist(err) { + t.Fatalf("temporary validation inputs remain after command: %v", err) + } + }) + } +} diff --git a/internal/cli/repo_root.go b/internal/cli/repo_root.go index 03c521d..21c12e5 100644 --- a/internal/cli/repo_root.go +++ b/internal/cli/repo_root.go @@ -63,6 +63,7 @@ func isRepoRoot(root string) bool { if strings.TrimSpace(root) == "" { return false } + // #nosec G304,G703 -- local repository discovery intentionally reads an operator-selected directory. mod, err := os.ReadFile(filepath.Join(root, "go.mod")) if err != nil { return false @@ -74,6 +75,7 @@ func isRepoRoot(root string) bool { filepath.Join("cmd", "ovpn-agent"), filepath.Join("cmd", "ovpn-telegram-bot"), } { + // #nosec G703 -- fixed repository markers under the operator-selected local root. info, err := os.Stat(filepath.Join(root, rel)) if err != nil || !info.IsDir() { return false diff --git a/internal/cli/runtime_env_helpers.go b/internal/cli/runtime_env_helpers.go index 196706c..a083073 100644 --- a/internal/cli/runtime_env_helpers.go +++ b/internal/cli/runtime_env_helpers.go @@ -20,7 +20,7 @@ func envWithFileOverride(key string) (string, bool) { fileKey := key + "_FILE" if filePath := strings.TrimSpace(os.Getenv(fileKey)); filePath != "" { cleanPath := filepath.Clean(filePath) - // #nosec G304 -- operator-controlled local path override for secret file loading. + // #nosec G304,G703 -- operator-controlled local path override for secret file loading. b, err := os.ReadFile(cleanPath) if err != nil { slog.Warn("failed to read *_FILE override; falling back", "key", fileKey, "path", cleanPath, "error", err) diff --git a/internal/defaults/images.go b/internal/defaults/images.go index af8d126..31d000a 100644 --- a/internal/defaults/images.go +++ b/internal/defaults/images.go @@ -5,15 +5,15 @@ import "strings" const ( DefaultXrayVersion = "26.7.28" DefaultXrayImageRepo = "ghcr.io/xtls/xray-core" - DefaultAgentImage = "alpine:3.23.4" - DefaultTelegramBotImage = "alpine:3.23.4" - DefaultWebImage = "nginx:1.29-alpine" - DefaultHAProxyImage = "haproxy:3.2.15-alpine3.23" - DefaultPrometheusImage = "prom/prometheus:v3.11.2" - DefaultAlertmanagerImage = "prom/alertmanager:v0.32.0" - DefaultGrafanaImage = "grafana/grafana:12.4.3" - DefaultNodeExporterImage = "prom/node-exporter:v1.11.1" - DefaultCAdvisorImage = "ghcr.io/google/cadvisor:0.56.2" + DefaultAgentImage = "alpine:3.24.2" + DefaultTelegramBotImage = "alpine:3.24.2" + DefaultWebImage = "nginx:1.30.5-alpine" + DefaultHAProxyImage = "haproxy:3.2.25-alpine3.24" + DefaultPrometheusImage = "prom/prometheus:v3.15.0" + DefaultAlertmanagerImage = "prom/alertmanager:v0.34.1" + DefaultGrafanaImage = "grafana/grafana:12.4.12" + DefaultNodeExporterImage = "prom/node-exporter:v1.12.1" + DefaultCAdvisorImage = "ghcr.io/google/cadvisor:0.60.6" ) func DefaultXrayImage(version string) string { diff --git a/internal/deploy/deploy.go b/internal/deploy/deploy.go index b5a7596..3e9ca16 100644 --- a/internal/deploy/deploy.go +++ b/internal/deploy/deploy.go @@ -66,12 +66,18 @@ func ValidateConfigWithDockerAndMounts(ctx context.Context, xrayImage string, co } // ghcr.io/xtls/xray-core images use /usr/local/bin/xray as ENTRYPOINT, so the command // passed to `docker run` must not include a second leading `xray` token. - args := []string{"run", "--rm", "-v", fmt.Sprintf("%s:/etc/xray/config.json:ro", configPath)} + // Local inputs belong to the CLI user and may intentionally be mode 0600 + // (geodata under umask 077 and temporary TLS keys). Use that identity for + // this disposable validation container without changing source permissions. + args := []string{"run", "--rm", "--user", fmt.Sprintf("%d:%d", os.Getuid(), os.Getgid()), "-v", fmt.Sprintf("%s:/etc/xray/config.json:ro", configPath)} args = append(args, extraMounts...) args = append(args, xrayImage, "run", "-test", "-config", "/etc/xray/config.json") cmd := exec.CommandContext(ctx, "docker", args...) out, err := cmd.CombinedOutput() if err != nil { + if isXrayGeodataPermissionError(string(out)) { + return fmt.Errorf("xray config validation failed: %w: %s; hint: check read permissions for the local geodata files mounted into the validation container", err, strings.TrimSpace(string(out))) + } if isLikelyXrayGeositeResourceError(string(out)) { return fmt.Errorf("xray config validation failed: %w: %s; hint: set OVPN_SECURITY_PROFILE=off to bypass BT/tracker geosite rules when this image lacks geosite resources", err, strings.TrimSpace(string(out))) } @@ -187,6 +193,12 @@ func isLikelyXrayGeositeResourceError(errText string) bool { (strings.Contains(text, "no such file") || strings.Contains(text, "failed") || strings.Contains(text, "not found")) } +func isXrayGeodataPermissionError(errText string) bool { + text := strings.ToLower(errText) + return strings.Contains(text, "permission denied") && + (strings.Contains(text, "geosite.dat") || strings.Contains(text, "geoip.dat")) +} + // buildDeployApplyCommand renders the script that swaps the validated bundle into the live runtime directory, preserving existing secret files. func buildDeployApplyCommand() string { // When ovpn-agent is running, truncating /opt/ovpn/agent/ovpn-agent in-place can fail with @@ -284,6 +296,9 @@ func DeployRemote(ctx context.Context, runner Runner, cfg ssh.Config) error { xrayCtx, cancelXray := ssh.TimeoutCtx(ctx, deployXrayValidateTimeout) defer cancelXray() if _, err := runner.Exec(xrayCtx, cfg, withRemoteTimeout(deployXrayValidateTimeout, xrayTestCmd)); err != nil { + if isXrayGeodataPermissionError(err.Error()) { + return fmt.Errorf("validate xray config in container on %s: %w; hint: check geodata read permissions for the Xray runtime user", cfg.Host, err) + } if isLikelyXrayVersionTagError(err.Error()) { return fmt.Errorf("validate xray config in container on %s: %w; hint: use xray version without 'v' prefix (example: 26.7.28)", cfg.Host, err) } diff --git a/internal/deploy/deploy_test.go b/internal/deploy/deploy_test.go index cf168d4..e45ecd1 100644 --- a/internal/deploy/deploy_test.go +++ b/internal/deploy/deploy_test.go @@ -169,7 +169,7 @@ func TestRenderBundleWithOverride(t *testing.T) { TelegramClientsRUPDFSource: tmpClientsRUPDF, RenderedOverride: override, XrayImage: "ghcr.io/xtls/xray-core:26.3.27", - AgentImage: "alpine:3.23.4", + AgentImage: "alpine:3.24.2", }) if err != nil { t.Fatalf("render bundle: %v", err) @@ -241,7 +241,7 @@ func TestRenderBundleWithOverride(t *testing.T) { if _, err := os.Stat(filepath.Join(bundle.Dir, "logs")); err != nil { t.Fatalf("expected logs dir in bundle: %v", err) } - if !strings.Contains(string(gotEnv), "PROMETHEUS_IMAGE=prom/prometheus:v3.11.2") { + if !strings.Contains(string(gotEnv), "PROMETHEUS_IMAGE=prom/prometheus:v3.15.0") { t.Fatalf("missing prometheus image in env: %q", string(gotEnv)) } for _, p := range []string{ @@ -478,7 +478,7 @@ func TestRenderBundleIncludesTLSSelfSNIWebSidecar(t *testing.T) { } env := string(envRaw) for _, want := range []string{ - "OVPN_WEB_IMAGE=nginx:1.29-alpine", + "OVPN_WEB_IMAGE=nginx:1.30.5-alpine", "OVPN_TLS_SELFSNI_CERT_DIR=/opt/ovpn/certs", "OVPN_CAMOUFLAGE_SITE_DIR=/opt/ovpn/camouflage-site", } { @@ -580,11 +580,11 @@ func TestRenderBundleAppliesMonitoringAndTelegramDefaults(t *testing.T) { } env := string(envRaw) for _, want := range []string{ - "PROMETHEUS_IMAGE=prom/prometheus:v3.11.2", - "ALERTMANAGER_IMAGE=prom/alertmanager:v0.32.0", - "GRAFANA_IMAGE=grafana/grafana:12.4.3", + "PROMETHEUS_IMAGE=prom/prometheus:v3.15.0", + "ALERTMANAGER_IMAGE=prom/alertmanager:v0.34.1", + "GRAFANA_IMAGE=grafana/grafana:12.4.12", "OVPN_AGENT_HOST_PORT=19000", - "OVPN_TELEGRAM_BOT_IMAGE=alpine:3.23.4", + "OVPN_TELEGRAM_BOT_IMAGE=alpine:3.24.2", "OVPN_TELEGRAM_BOT_HOST_PORT=19001", "OVPN_TELEGRAM_CLIENTS_PDF_PATH=/opt/ovpn-telegram-bot/assets/clients.pdf", "OVPN_TELEGRAM_CLIENTS_RU_PDF_PATH=/opt/ovpn-telegram-bot/assets/clients-ru.pdf", @@ -673,7 +673,7 @@ func TestRenderBundleProxyIncludesHAProxyAndGeodata(t *testing.T) { if err != nil { t.Fatalf("read env: %v", err) } - if !strings.Contains(string(gotEnv), "HAPROXY_IMAGE=haproxy:3.2.15-alpine3.23") { + if !strings.Contains(string(gotEnv), "HAPROXY_IMAGE=haproxy:3.2.25-alpine3.24") { t.Fatalf("expected haproxy image in env, got:\n%s", string(gotEnv)) } if !strings.Contains(string(gotEnv), "OVPN_TELEGRAM_HAPROXY_URL=http://haproxy:8404/metrics") { diff --git a/internal/deploy/validation_docker_test.go b/internal/deploy/validation_docker_test.go new file mode 100644 index 0000000..a2b53e9 --- /dev/null +++ b/internal/deploy/validation_docker_test.go @@ -0,0 +1,82 @@ +package deploy + +import ( + "context" + "fmt" + "os" + "os/exec" + "path/filepath" + "runtime" + "strings" + "testing" + "time" + + "github.com/xtls/xray-core/common/geodata" + "google.golang.org/protobuf/proto" + + "ovpn/internal/defaults" +) + +// This regression needs a Docker daemon; ordinary unit tests remain offline. +func TestValidateConfigWithDockerPrivateGeodata(t *testing.T) { + if os.Getenv("OVPN_TEST_DOCKER") != "1" { + t.Skip("set OVPN_TEST_DOCKER=1 to run the Docker regression") + } + ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute) + defer cancel() + image := os.Getenv("OVPN_TEST_XRAY_IMAGE") + if image == "" { + image = defaults.DefaultXrayImage("") + } + dir := t.TempDir() + config := filepath.Join(dir, "config.json") + site := filepath.Join(dir, "geosite.dat") + ip := filepath.Join(dir, "geoip.dat") + data, err := proto.Marshal(&geodata.GeoSiteList{Entry: []*geodata.GeoSite{{Code: "CATEGORY-PUBLIC-TRACKER", Domain: []*geodata.Domain{{Type: geodata.Domain_Full, Value: "tracker.example.test"}}}}}) + if err != nil { + t.Fatal(err) + } + ipData, err := proto.Marshal(&geodata.GeoIPList{Entry: []*geodata.GeoIP{{Code: "PRIVATE", Cidr: []*geodata.CIDR{{Ip: []byte{10, 0, 0, 0}, Prefix: 8}}}}}) + if err != nil { + t.Fatal(err) + } + for path, content := range map[string][]byte{ + config: []byte(`{"log":{"loglevel":"none"},"outbounds":[{"tag":"blocked","protocol":"blackhole"}],"routing":{"rules":[{"type":"field","domain":["geosite:category-public-tracker"],"outboundTag":"blocked"},{"type":"field","ip":["geoip:private"],"outboundTag":"blocked"}]}}`), + site: data, ip: ipData, + } { + if err := os.WriteFile(path, content, 0o600); err != nil { + t.Fatal(err) + } + } + mounts := []string{"-v", fmt.Sprintf("%s:/usr/local/share/xray/geosite.dat:ro", site), "-v", fmt.Sprintf("%s:/usr/local/share/xray/geoip.dat:ro", ip)} + // Keep the config readable for the original command to isolate the geodata failure. + if err := os.Chmod(config, 0o644); err != nil { + t.Fatal(err) + } + args := []string{"run", "--rm", "-v", config + ":/etc/xray/config.json:ro"} + args = append(args, mounts...) + args = append(args, image, "run", "-test", "-config", "/etc/xray/config.json") + out, err := exec.CommandContext(ctx, "docker", args...).CombinedOutput() + // Docker Desktop may remap bind-mount ownership and allow the original + // command. Linux CI must reproduce the permission failure before the fix. + if err == nil && runtime.GOOS != "linux" { + t.Log("Docker Desktop allows the original bind mounts; Linux CI checks the failure") + } else if err == nil || !strings.Contains(string(out), "geosite.dat") || !strings.Contains(string(out), "permission denied") { + t.Fatalf("expected original geosite permission failure: %v: %s", err, out) + } + if err := os.Chmod(config, 0o600); err != nil { + t.Fatal(err) + } + if err := ValidateConfigWithDockerAndMounts(ctx, image, config, mounts); err != nil { + t.Fatal(err) + } + for _, path := range []string{config, site, ip} { + info, err := os.Stat(path) + if err != nil { + t.Fatal(err) + } + if info.Mode().Perm() != 0o600 { + t.Fatalf("validation changed source permissions: %s: %o", filepath.Base(path), info.Mode().Perm()) + } + } +} diff --git a/internal/deploy/validation_test.go b/internal/deploy/validation_test.go new file mode 100644 index 0000000..a798aae --- /dev/null +++ b/internal/deploy/validation_test.go @@ -0,0 +1,87 @@ +package deploy + +import ( + "context" + "fmt" + "os" + "path/filepath" + "strings" + "testing" + + "ovpn/internal/ssh" +) + +func fakeValidationDocker(t *testing.T, output string) string { + t.Helper() + dir := t.TempDir() + argsFile := filepath.Join(dir, "args") + script := "#!/bin/sh\nprintf '%s\\n' \"$@\" > \"$OVPN_TEST_DOCKER_ARGS\"\nprintf '%s\\n' \"$OVPN_TEST_DOCKER_OUTPUT\"\nexit \"$OVPN_TEST_DOCKER_EXIT\"\n" + if err := os.WriteFile(filepath.Join(dir, "docker"), []byte(script), 0o755); err != nil { + t.Fatal(err) + } + t.Setenv("PATH", dir) + t.Setenv("OVPN_TEST_DOCKER_ARGS", argsFile) + t.Setenv("OVPN_TEST_DOCKER_OUTPUT", output) + t.Setenv("OVPN_TEST_DOCKER_EXIT", "0") + return argsFile +} + +func TestValidateConfigWithDockerUsesLocalIdentity(t *testing.T) { + argsFile := fakeValidationDocker(t, "") + mounts := []string{"-v", "/cache with spaces/geosite.dat:/usr/local/share/xray/geosite.dat:ro", "-v", "/cache/geoip.dat:/usr/local/share/xray/geoip.dat:ro", "-v", "/private/certs:/etc/xray/certs:ro"} + if err := ValidateConfigWithDockerAndMounts(context.Background(), "test/xray:1", "/private/config.json", mounts); err != nil { + t.Fatal(err) + } + raw, err := os.ReadFile(argsFile) + if err != nil { + t.Fatal(err) + } + want := []string{"run", "--rm", "--user", fmt.Sprintf("%d:%d", os.Getuid(), os.Getgid()), "-v", "/private/config.json:/etc/xray/config.json:ro"} + want = append(want, mounts...) + want = append(want, "test/xray:1", "run", "-test", "-config", "/etc/xray/config.json") + if string(raw) != strings.Join(want, "\n")+"\n" { + t.Fatalf("unexpected docker arguments: %s", raw) + } +} + +func TestValidateConfigWithDockerErrorHints(t *testing.T) { + for _, tc := range []struct{ name, output, hint string }{ + {"geosite permissions", "failed to open geosite.dat: permission denied", "check read permissions"}, + {"geoip permissions", "open geoip.dat: permission denied", "check read permissions"}, + {"missing geosite", "failed to open geosite.dat: no such file", "OVPN_SECURITY_PROFILE=off"}, + {"other error", "invalid config", ""}, + } { + t.Run(tc.name, func(t *testing.T) { + fakeValidationDocker(t, tc.output) + t.Setenv("OVPN_TEST_DOCKER_EXIT", "23") + err := ValidateConfigWithDocker(context.Background(), "test/xray:1", "/config.json") + if err == nil || !strings.Contains(err.Error(), tc.output) { + t.Fatalf("expected original error, got %v", err) + } + if tc.hint != "" && !strings.Contains(err.Error(), tc.hint) { + t.Fatalf("expected hint %q, got %v", tc.hint, err) + } + if tc.hint == "" && strings.Contains(err.Error(), "; hint:") { + t.Fatalf("unexpected hint: %v", err) + } + if strings.Contains(tc.name, "permissions") && strings.Contains(err.Error(), "OVPN_SECURITY_PROFILE=off") { + t.Fatalf("permission failure must not suggest disabling security: %v", err) + } + }) + } +} + +func TestDeployRemoteGeodataPermissionHint(t *testing.T) { + r := &failingRunner{failOn: "run -test -config /etc/xray/config.json", err: fmt.Errorf("open geosite.dat: permission denied")} + err := DeployRemote(context.Background(), r, ssh.Config{Host: "example-host"}) + if err == nil || !strings.Contains(err.Error(), "check geodata read permissions") || strings.Contains(err.Error(), "OVPN_SECURITY_PROFILE=off") { + t.Fatalf("unexpected permission hint: %v", err) + } +} + +func TestValidateConfigWithDockerRequiresImage(t *testing.T) { + err := ValidateConfigWithDocker(context.Background(), "", "/config.json") + if err == nil || err.Error() != "xray image is required" { + t.Fatalf("expected missing image error, got %v", err) + } +}