Compare commits

...
Author SHA1 Message Date
ginuerzh 0793f4730f Merge pull request #909 from iBug/pkg
chore: Create /etc/gost directory with package manager
2026-10-07 02:31:08 +00:00
ginuerzh 11220b15bb chore(gost): bump github.com/go-gost/x to v0.19.5 2026-10-03 23:38:53 +08:00
ginuerzh d7656ef3de tests/e2e: concurrent UDP endpoints must share a reverse tunnel intact
Covers gost#911 at the wire level: four UDP source sockets send through one
RUDP reverse tunnel bound on a relay server, and every datagram must come
back byte-identical to its own endpoint.

A frame torn by an interleaved writer shows up twice — as a corrupted
payload, and as the torn frame leaving half a header in the stream, which the
far end reads as "unexpected EOF" and answers by rebuilding the whole tunnel.
So the suite asserts both: no endpoint sees a corrupted datagram, and no
endpoint sees a receive gap long enough to be a teardown and rebind.

Verified against a binary built before the fix (4 endpoints, 35% loss, 1.4s
gaps, suite fails) and after it (0% loss, no corruption, 0.2s gaps, passes).
2026-10-03 16:27:05 +08:00
iBug 7a220c6cd7 chore: Create /etc/gost directory with package manager 2026-09-24 10:32:53 +08:00
iBug e9fab95872 Add back SystemCallFilter=@chown (#907)
Otherwise lumberjack will fail to rotate log files

Ref: https://github.com/natefinch/lumberjack/pull/43/changes#diff-7a80b3353ee34c4b79751e1e47e5010af54e82f393aa112770a4d1a731e70ba2R480
2026-09-22 22:06:55 +08:00
ginuerzh 97bb57230c docs: document DEB/RPM installation
Covers the amd64v3 vs amd64 choice — both packages declare Architecture:
amd64, so the file name is the only way to tell them apart — and the fact
that the packaged systemd service stays inactive until /etc/gost/gost.yml
is created.
2026-09-21 21:23:33 +08:00
ginuerzh bb45b12809 fix(release): use DynamicUser for gost.service
systemd-analyze verify warns "Special user nobody configured, this is not
safe!": nobody is a shared account, so any other service running as it can
read and write /run/gost. DynamicUser= gives the unit its own transient
identity, named gost, without adding a static user to the package.

Verified under a real systemd: the unit starts, binds :80 through the
ambient capabilities, runs as the dynamic user, and is skipped rather than
failed when /etc/gost/gost.yml is absent.
2026-09-21 21:15:33 +08:00
ginuerzh c4e789e2df fix(release): pin capability bounding set in gost.service
CapabilityBoundingSet= was dropped when CAP_NET_ADMIN was added for TUN,
which leaves the bounding set unrestricted. Pin it to the capabilities the
unit actually needs. Restore ~@privileged as well: TUN uses ioctl(TUNSETIFF)
and netlink, neither of which is in that group.
2026-09-21 21:03:43 +08:00
iBug 2a6ba4df85 Add DEB and RPM packages for easier installation (#906)
* Add systemd service file

* Add DEB and RPM packages for amd64, amd64v3 and arm64 architectures

* Remove GOMEMLIMIT from systemd service

* Fix packaged systemd service defaults
2026-09-21 21:01:26 +08:00
ginuerzh ece7770ad7 chore(gost): bump github.com/go-gost/x to v0.17.2 2026-09-13 21:12:53 +08:00
ginuerzh eb66804ec8 chore(gost): bump github.com/go-gost/x to v0.17.1 2026-09-12 12:12:22 +08:00
ginuerzh a20999046b chore(gost): bump github.com/go-gost/x to v0.17.0 2026-09-12 10:16:16 +08:00
Igor Kuzmenkov fe0982b2e4 fix: update go-m1cpu for macOS arm64 (#903) 2026-09-12 10:06:30 +08:00
ginuerzh 36cc266257 chore: anchor gost gitignore rule to repo root
The bare 'gost' pattern also matched the cmd/gost/ directory, forcing
-f to stage tracked files under it.
2026-09-07 17:46:21 +08:00
ginuerzh 074af1b175 program: shut down plugin subprocesses on stop 2026-09-07 17:42:44 +08:00
ginuerzh ee61f28688 test(e2e): add QUIC cipherKey invalid-datagram regression
Add an e2e suite covering go-gost/x#125: a malformed UDP datagram sent
to a cipherKey-enabled QUIC listener must not close the shared transport.
Uses the canonical http-over-quic chaining pattern and verifies the
forward still works after the invalid datagram.

Also bumps go.mod/go.sum (plugin v0.6.1, otel, x/net, etc.).
2026-09-07 14:53:36 +08:00
ginuerzh 27ac16439e bump to v3.3.1, x v0.16.1 2026-08-31 22:54:26 +08:00
ginuerzh d778c031eb test(e2e): transparent whitelist bypass + sniffing (gost#899)
Reproduce the regression where a domain-only whitelist bypass on a red
(transparent) proxy rejected every connection: the pre-sniffing bypass
check ran against the bare destination IP, which is never in a domain
whitelist, before SNI sniffing could match. A privileged container
redirects outbound TCP to the red service; asserts an allowlisted SNI
is forwarded (hello-gost) and a non-allowlisted SNI is rejected.
2026-08-31 22:35:03 +08:00
ginuerzh cb76f63754 e2e: add PROXY protocol test (gost#677), bump core/x deps
ProxyProtoSuite starts gost with metadata.proxyProtocol set and asserts
the HAProxy PROXY header is prepended on outbound connections (v1/v2),
backed by a raw-TCP capture script and configs. Add echo754_repro.py
from the hot-reload EADDRINUSE investigation (gost#754).

Bump github.com/go-gost/core to v0.6.1 and x to v0.16.0.
2026-08-29 09:26:33 +08:00
ginuerzh 76467efc59 test(e2e): regression for Gost-Target destination-policy bypass
Adds TestGostTargetPolicyBypass: a service-level bypass blacklists the
echo server; verifies the control request is 403, a checksum-valid
Gost-Target header naming an allowed authority cannot reach the blocked
backend, and a malformed header is fail-closed. Verified to catch the bug
by reverting the fix (exploit returned the backend response).
2026-08-27 20:16:34 +08:00
ginuerzh 35f7cd912c test(e2e): cover origin-form HTTP proxy request (#679)
Send a raw "GET / HTTP/1.1" + Host header through the http proxy,
mirroring nginx proxy_pass. Dumps container logs on failure.
2026-08-27 00:02:33 +08:00
ginuerzhandClaude 5e40415ac9 e2e: demux testcontainers exec stream, fix mtls proxy flags, add rtcp filter test
- Add ExecOutput helper to demultiplex the raw Docker exec stream so test
  output assertions see clean stdout+stderr instead of framed bytes.
- Wait for container readiness via exposed port or "listening on" log line
  when no ports are exposed.
- Use curl --proxy-* TLS options for the mTLS HTTPS-proxy test.
- Relax the http_cache first-request assertion (shared backend counter).
- Add gost#898 rtcp forwarder filter.host e2e test (multi-service one tunnel).
- Bump x to v0.15.7.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-26 22:27:52 +08:00
ginuerzh ecf14b8459 bump x to v0.15.6 2026-08-25 22:18:31 +08:00
ginuerzh 1f14e72e3c bump x to v0.15.5 2026-08-21 21:52:06 +08:00
ginuerzh aa1565c8b2 bump x to v0.15.4 2026-08-18 23:44:32 +08:00
ginuerzh f7b27a2c8b bump x to v0.15.3 2026-08-17 23:09:43 +08:00
ginuerzh c7d793c619 e2e: add cmd probe recovery test (gost#837)
Verifies dead->alive->dead node transition: a node excluded by a failing cmd
probe resumes carrying traffic once the probe flips healthy, without restart.
2026-08-01 23:23:28 +08:00
ginuerzh f25148f2e0 bump x to v0.15.2 (SSH authorized_keys trailing-newline fix, gost#699) 2026-08-01 23:22:22 +08:00
ginuerzh 960f081e53 e2e: add UDP forward QUIC sniffing test
Add TestUDPForwardQUICSniffing: sends a real captured QUIC v1 Initial packet
through the UDP forward handler (with sniffing enabled) and verifies the
datagram is echoed back. Covers the QUIC ClientHello/ServerHello sniff path
in x/handler/redirect/udp and x/handler/forward/local.
2026-07-30 23:24:35 +08:00
ginuerzh 55bb214a94 chore: bump x to v0.15.1, add e2e tests for chainGroup and sniffing
- Bump github.com/go-gost/x v0.15.0 → v0.15.1
- ChainGroupSuite: matcher routing by host, probe dead-chain pre-marking
- SniffingSuite: SOCKS5+HTTPS w/o SNI, debug-only log on empty SNI
- Add RunHTTPSEchoContainer and https_echo.py test helper
2026-07-29 23:06:15 +08:00
ginuerzh 555dacd244 chore: bump x to v0.15.0 (hopGroup, failCodes fix)
Add e2e regression test for http.failCodes selector bug —
FIFO selector + failCodes=429 on first node proves
node-429 is excluded after first 429 response and all
subsequent requests go to node-good.
2026-07-27 22:13:34 +08:00
ginuerzh f63b212434 chore: bump x to v0.14.2, tls-dissector to v0.3.1, quic-go to v0.60.0, webtransport-go to v0.11.1 2026-07-26 23:03:38 +08:00
ginuerzh 64017bf195 chore: bump x to v0.14.1 2026-07-26 20:17:23 +08:00
ginuerzh 5a1a799848 tests/e2e: add HTTP response cache e2e tests 2026-07-26 18:14:03 +08:00
ginuerzh 5204d6285e chore: bump deps (core v0.6.0, x v0.14.0) 2026-07-26 18:12:57 +08:00
ginuerzh 81108ee0a4 chore: bump deps (core v0.5.5, x v0.13.18) 2026-07-25 18:52:45 +08:00
ginuerzh 122dde8385 chore: go get -u all
- x v0.13.15 → v0.13.16
- tls-dissector v0.2.0 → v0.3.0
- golang.org/x/* bump (crypto, net, sys, text, mod, sync, term, tools)
- oTel v1.41.0 → v1.43.0
- grpc v1.79.3 → v1.82.1
- gonum v0.16.0 → v0.17.0
2026-07-24 23:15:01 +08:00
ginuerzh 9ccb007589 chore: bump deps
- x v0.13.14 → v0.13.15 (MetadataFeature, BodyJSON matcher)
- relay v0.6.2 → v0.7.0
- tls-dissector v0.2.0 → v0.3.0
- add tidwall gjson/match/pretty/sjson (indirect, via x)
2026-07-24 22:34:52 +08:00
ginuerzh 5a01aa4fd2 chore: bump x to v0.13.14 (forwarder probe passthrough fix) 2026-07-16 23:34:22 +08:00
ginuerzh 4cdc5588ff chore: bump core to v0.5.4 (node liveness probe types) 2026-07-16 21:34:33 +08:00
ginuerzh 89f9ea124a chore: bump x to v0.13.13 (Tor SOCKS5 resolve, cmd probe) 2026-07-16 21:24:41 +08:00
ginuerzh e9cf3b4d3f e2e: add cmd probe failover test with true/false command nodes 2026-07-16 20:53:30 +08:00
ginuerzh bef00dffef e2e: add node liveness probe tests
TCP probe + FailFilter: dead node pre-marked, all requests succeed.
LowestLatency strategy: two probed nodes, selector picks fastest.
2026-07-16 20:53:30 +08:00
ginuerzh 1c6fadcb1e test(e2e): add routing matcher module e2e test suite
Verify node-level routing matchers via a two-proxy relay: a forward
proxy whose only chain node carries matcher Host(`tcp-echo`) is only
eligible for a matching Host, so the request is relayed upstream to the
echo server; a non-matching Host is excluded (no eligible node) and never
reaches the echo server. Mirrors the resolver/ingress e2e pattern.
2026-07-16 20:53:30 +08:00
ginuerzh 20f1e4a0b9 test(e2e): add resolver module e2e test suite
Verifies that a configured resolver drives the proxy's outbound DNS
resolution. A gost HTTP proxy uses a resolver whose only nameserver is a
test responder answering echo.test with the real echo server IP and
NXDOMAIN for everything else. A request to echo.test is resolved by the
custom resolver and reaches the echo server; an unmapped host fails to
resolve. Adds a parametrized DNS responder script and container helper.
2026-07-16 20:53:30 +08:00
ginuerzh 1fd69ac704 test(e2e): add ingress module e2e test suite
Verifies hostname→endpoint routing at the reverse-proxy tunnel
entrypoint. A public gost runs a tunnel handler with an ingress
mapping example.local→tunnel-UUID and an HTTP entrypoint; an internal
client binds a reverse tunnel forwarding to the echo server. A request
with a mapped Host is routed through the tunnel to the echo server,
while an unmapped Host matches no ingress rule and is rejected.
2026-07-16 20:53:30 +08:00
ginuerzh 9355607ba7 test(e2e): add hosts module e2e test suite
Verify the static hosts mapping (HostMapper) overrides DNS: a mapped
hostname resolves to the configured IP and reaches the echo server,
while an unmapped hostname fails to resolve.
2026-07-16 20:53:30 +08:00
ginuerzh 6746f556b7 test(e2e): add auth module e2e test suite
Verify HTTP proxy authentication for both inline single-user auth and a
named auther with multiple users. Covers valid credentials, wrong
password, missing credentials, and any-user-in-auther acceptance.
2026-07-16 20:53:30 +08:00
ginuerzh 3119cb43f2 test(e2e): add admission module e2e test suite
Verify service-level admission control gating by client source IP, in
both whitelist and blacklist modes. Contrasts a loopback client (curl
inside the gost container) against an external client (curl inside the
echo container) to exercise both admit and deny paths.
2026-07-16 20:53:30 +08:00
ginuerzh c7ea19ec66 test(e2e): add bypass module e2e test suite
Verify both blacklist and whitelist bypass modes: a matching destination
skips the dead chain node and connects directly to the echo server, while a
non-matching destination is forced through the dead node and never reaches it.
2026-07-16 20:53:30 +08:00
ginuerzh b60c87e47f chore: ignore .claude/ session config 2026-07-16 20:53:30 +08:00
ginuerzh 632282436b chore: ignore build artifacts, codegraph index, python cache
.build/, .codegraph/ (generated multi-hundred-MB db), and __pycache__/
are local artifacts that should not be tracked.
2026-07-16 20:53:30 +08:00
ginuerzh fa815bf4a0 test(e2e): add TLS listener rejectUnknownSNI test suite
Covers rejectUnknownSNI with and without a serverNames allow list,
verifying allowed/unknown/empty SNI handshakes via openssl s_client.
2026-07-16 20:53:30 +08:00
ginuerzhandClaude a8bd4a5f16 test(e2e): consolidate selector tests, add round-robin/fifo/backup failover
Merge parallel_selector_test.go into selector_test.go and add e2e coverage
for the round-robin + fail filter, fifo sticky fallback, and backup filter
strategies. Each test drives requests through a hop with one dead node and
asserts the selector marks and skips it so traffic converges on the live node.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-16 20:53:30 +08:00
iBug 94dcc9c045 chore(release): Make files in tarball owned by root (#889) 2026-07-16 20:53:02 +08:00
ginuerzh 7643e16cca chore: bump go-gost/x to v0.13.12 2026-07-11 22:11:08 +08:00
ginuerzh 64b71bd8a6 test(e2e): add utls dialer regression suite (#888)
Adds an e2e suite (tests/e2e/utls_test.go + testdata/utls/*)
that exercises the utls dialer in a forward-proxy chain:

  curl -> client gost (http proxy :8080)
       -> chain node (http connector + utls dialer)
         -> server gost (http over TLS listener :8443)
           -> tcp-echo

Two cases:

- TestUTLSInsecure: regression for go-gost/gost#887. A utls
  dialer with `secure: false` must still complete the handshake
  (InsecureSkipVerify must be honoured). The old unsafe cast read
  garbage for InsecureSkipVerify and the handshake failed.
- TestUTLSSecureWithCA: exercises the converter's RootCAs /
  ServerName path with a CA-signed server cert.

Uses the deterministic `Chrome` fingerprint (not `randomized`, which
randomises the ClientHello and is flaky against a standard Go TLS
server).

Bumps github.com/go-gost/x to v0.13.11, which fixes the
second half of #887: the curve-preference enum divergence between
crypto/tls and utls (Go 1.24+ appends PQC hybrid curves that
utls does not define).

Related: go-gost/gost#887, go-gost/x#111, go-gost/x#112
2026-07-11 15:46:39 +08:00
ginuerzh ccf2989bc0 gost: bump x to v0.13.9, relay to v0.6.2; add PHT e2e tests
- go.mod: x v0.13.8 → v0.13.9, relay v0.6.1 → v0.6.2
- tests/e2e/pht_test.go: 3 test cases (basic tunnel, PHTs, heartbeat)
- tests/e2e/testdata/pht/: client/server and client_phts/server_phts YAML configs
2026-07-06 22:10:15 +08:00
ginuerzh d54a896a81 update go.mod 2026-07-05 21:22:15 +08:00
ginuerzh 6b7dbf08fa chore: bump go-gost/x to v0.13.8, plugin to v0.5.0 2026-07-05 21:19:32 +08:00
ginuerzh dd56308b2e test(mtls): add e2e tests for mTLS client cert identity forwarding
Tests verify: mTLS proxy works with valid client cert, mTLS rejects
connections without client cert, HTTP auth plugin receives client_cn,
client_san, client_cert_fingerprint via PeerCert context propagation.
2026-07-04 19:00:44 +08:00
ginuerzh eb9dbb1807 chore: bump core to v0.5.3 and x to v0.13.7 2026-07-03 20:20:26 +08:00
ginuerzh 02dc900686 chore: bump go-gost/x to v0.13.5 2026-07-02 21:09:46 +08:00
ginuerzh bb912edecc chore: bump go-gost/x to v0.13.3 2026-06-30 20:59:00 +08:00
ginuerzh ffb61cc19f chore: bump go-gost/core to v0.5.2, go-gost/x to v0.13.2 2026-06-30 11:05:02 +08:00
ginuerzh a864cbcfe5 chore: bump go-gost/x to v0.13.1 2026-06-28 20:06:03 +08:00
98 changed files with 5176 additions and 273 deletions
+13 -1
View File
@@ -32,10 +32,22 @@ _testmain.go
*.bak
cmd/gost/gost
gost
/gost
snap
*.pem
/*.yaml
*.txt
dist/
# Build artifacts
.build/
# Codegraph index (generated, multi-hundred-MB db)
.codegraph/
# Python cache
__pycache__/
# Claude session config
.claude/
+55 -4
View File
@@ -1,3 +1,5 @@
version: 2
# This is an example .goreleaser.yml file with some sensible defaults.
# Make sure to check the documentation at https://goreleaser.com
before:
@@ -7,7 +9,8 @@ before:
# you may remove this if you don't need go generate
# - go generate ./...
builds:
- env:
- id: gost
env:
- CGO_ENABLED=0
main: ./cmd/gost
targets:
@@ -38,6 +41,16 @@ builds:
- android_arm64
ldflags:
- "-s -w -X 'main.version={{ .Tag }}'"
- id: gost-packages
env:
- CGO_ENABLED=0
main: ./cmd/gost
targets:
- linux_amd64
- linux_amd64_v3
- linux_arm64
ldflags:
- "-s -w -X 'main.version={{ .Tag }}'"
upx:
- # UPX compression. Disabled by default (see #863): upx --best/--lzma/--brute
@@ -46,15 +59,53 @@ upx:
enabled: false
archives:
- format: tar.gz
- ids:
- gost
formats:
- tar.gz
builds_info:
group: root
owner: root
# use zip for windows archives
format_overrides:
- goos: windows
format: zip
formats:
- zip
nfpms:
- id: packages
package_name: gost
ids:
- gost-packages
vendor: go-gost
homepage: https://gost.run/
maintainer: go-gost contributors
description: GO Simple Tunnel
license: MIT
formats:
- deb
- rpm
bindir: /usr/bin
contents:
- src: gost.service
dst: /usr/lib/systemd/system/gost.service
- src: README.md
dst: /usr/share/doc/gost/README.md
- src: README_en.md
dst: /usr/share/doc/gost/README_en.md
- src: LICENSE
dst: /usr/share/doc/gost/LICENSE
- src: gost.yml
dst: /usr/share/doc/gost/examples/gost.yml
- dst: /etc/gost
type: dir
file_info:
mode: 0755
checksum:
name_template: 'checksums.txt'
snapshot:
name_template: "{{ incpatch .Version }}-next"
version_template: "{{ incpatch .Version }}-next"
changelog:
sort: asc
filters:
+16
View File
@@ -54,6 +54,22 @@ GOST作为隧道有三种主要使用方式。
[https://github.com/go-gost/gost/releases](https://github.com/go-gost/gost/releases)
### 系统包(DEB/RPM)
从 [Releases](https://github.com/go-gost/gost/releases) 下载对应架构的安装包(`amd64`、`amd64v3`、`arm64`):
```bash
# Debian/Ubuntu
sudo apt install ./gost_<version>_linux_amd64.deb
# RHEL/Fedora
sudo dnf install ./gost_<version>_linux_amd64.rpm
```
`amd64v3` 包需要支持 AVX2 的 CPU(x86-64-v3),否则请使用 `amd64`。
安装包附带 systemd 服务,但不会自动启用:先创建 `/etc/gost/gost.yml`(示例见 `/usr/share/doc/gost/examples/gost.yml`),再执行 `sudo systemctl enable --now gost`。未提供配置文件时该服务会被跳过,而不是报错。
### 安装脚本
```bash
+16
View File
@@ -54,6 +54,22 @@ Use tunnel and intranet penetration to expose local services behind NAT or firew
[https://github.com/go-gost/gost/releases](https://github.com/go-gost/gost/releases)
### Packages (DEB/RPM)
Download the package for your architecture from the [releases page](https://github.com/go-gost/gost/releases) (`amd64`, `amd64v3`, `arm64`):
```bash
# Debian/Ubuntu
sudo apt install ./gost_<version>_linux_amd64.deb
# RHEL/Fedora
sudo dnf install ./gost_<version>_linux_amd64.rpm
```
The `amd64v3` package requires an AVX2-capable CPU (x86-64-v3); use `amd64` otherwise.
The packages ship a systemd service, but it is not enabled automatically: create `/etc/gost/gost.yml` first (see `/usr/share/doc/gost/examples/gost.yml`), then run `sudo systemctl enable --now gost`. Without a config file the service is skipped rather than failing.
### install script
```bash
+3
View File
@@ -21,6 +21,7 @@ import (
"github.com/go-gost/x/config/parsing/parser"
xmetrics "github.com/go-gost/x/metrics"
metrics "github.com/go-gost/x/metrics/service"
xplugin "github.com/go-gost/x/plugin"
"github.com/go-gost/x/registry"
"github.com/judwhite/go-svc"
)
@@ -191,6 +192,8 @@ func (p *program) Stop() error {
logger.Default().Debug("service @profiling shutdown")
}
xplugin.Shutdown()
return nil
}
+1 -1
View File
@@ -1,5 +1,5 @@
package main
var (
version = "3.3.0"
version = "3.3.1"
)
+60 -59
View File
@@ -3,8 +3,8 @@ module github.com/go-gost/gost
go 1.26.3
require (
github.com/go-gost/core v0.5.1
github.com/go-gost/x v0.13.0
github.com/go-gost/core v0.6.1
github.com/go-gost/x v0.19.5
github.com/judwhite/go-svc v1.2.1
github.com/moby/moby/client v0.4.0
github.com/stretchr/testify v1.11.1
@@ -17,15 +17,14 @@ require (
github.com/Microsoft/go-winio v0.6.2 // indirect
github.com/alecthomas/units v0.0.0-20211218093645-b94a6e3cc137 // indirect
github.com/alessio/shellescape v1.4.1 // indirect
github.com/andybalholm/brotli v1.0.6 // indirect
github.com/asaskevich/govalidator v0.0.0-20210307081110-f21760c49a8d // indirect
github.com/andybalholm/brotli v1.2.2 // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/bytedance/sonic v1.11.6 // indirect
github.com/bytedance/sonic/loader v0.1.1 // indirect
github.com/bytedance/gopkg v0.1.3 // indirect
github.com/bytedance/sonic v1.15.0 // indirect
github.com/bytedance/sonic/loader v0.5.0 // indirect
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/cloudwego/base64x v0.1.4 // indirect
github.com/cloudwego/iasm v0.2.0 // indirect
github.com/cloudwego/base64x v0.1.6 // indirect
github.com/containerd/errdefs v1.0.0 // indirect
github.com/containerd/errdefs/pkg v0.3.0 // indirect
github.com/containerd/log v0.1.0 // indirect
@@ -42,25 +41,27 @@ require (
github.com/ebitengine/purego v0.10.0 // indirect
github.com/felixge/httpsnoop v1.0.4 // indirect
github.com/fsnotify/fsnotify v1.7.0 // indirect
github.com/gabriel-vasile/mimetype v1.4.3 // indirect
github.com/gabriel-vasile/mimetype v1.4.12 // indirect
github.com/gin-contrib/cors v1.7.2 // indirect
github.com/gin-contrib/sse v0.1.0 // indirect
github.com/gin-gonic/gin v1.10.1 // indirect
github.com/go-gost/go-shadowsocks2 v0.1.3 // indirect
github.com/gin-contrib/sse v1.1.0 // indirect
github.com/gin-gonic/gin v1.12.0 // indirect
github.com/go-gost/go-shadowsocks2 v0.1.4 // indirect
github.com/go-gost/gosocks4 v0.1.0 // indirect
github.com/go-gost/gosocks5 v0.5.0 // indirect
github.com/go-gost/plugin v0.4.0 // indirect
github.com/go-gost/relay v0.6.1 // indirect
github.com/go-gost/tls-dissector v0.2.0 // indirect
github.com/go-gost/plugin v0.8.1 // indirect
github.com/go-gost/quic-dissector v0.1.0 // indirect
github.com/go-gost/relay v0.7.0 // indirect
github.com/go-gost/tls-dissector v0.3.1 // indirect
github.com/go-logr/logr v1.4.3 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/go-ole/go-ole v1.2.6 // indirect
github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/go-playground/validator/v10 v10.20.0 // indirect
github.com/go-playground/validator/v10 v10.30.1 // indirect
github.com/go-redis/redis/v8 v8.11.5 // indirect
github.com/gobwas/glob v0.2.3 // indirect
github.com/goccy/go-json v0.10.2 // indirect
github.com/goccy/go-json v0.10.5 // indirect
github.com/goccy/go-yaml v1.19.2 // indirect
github.com/godbus/dbus/v5 v5.1.0 // indirect
github.com/golang/snappy v0.0.4 // indirect
github.com/google/btree v1.1.3 // indirect
@@ -68,57 +69,56 @@ require (
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/gorilla/websocket v1.5.3 // indirect
github.com/gravitational/trace v1.1.16-0.20220114165159-14a9a7dd6aaf // indirect
github.com/hashicorp/hcl v1.0.0 // indirect
github.com/jonboulle/clockwork v0.2.2 // indirect
github.com/hashicorp/yamux v0.1.1 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/klauspost/compress v1.18.5 // indirect
github.com/klauspost/cpuid/v2 v2.2.7 // indirect
github.com/klauspost/reedsolomon v1.11.8 // indirect
github.com/klauspost/compress v1.19.0 // indirect
github.com/klauspost/cpuid/v2 v2.4.0 // indirect
github.com/klauspost/reedsolomon v1.12.0 // indirect
github.com/leodido/go-urn v1.4.0 // indirect
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect
github.com/magiconair/properties v1.8.10 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-isatty v0.0.22 // indirect
github.com/miekg/dns v1.1.61 // indirect
github.com/mitchellh/go-homedir v1.1.0 // indirect
github.com/mitchellh/mapstructure v1.5.0 // indirect
github.com/moby/docker-image-spec v1.3.1 // indirect
github.com/moby/go-archive v0.2.0 // indirect
github.com/moby/go-archive v0.3.0 // indirect
github.com/moby/moby/api v1.54.1 // indirect
github.com/moby/patternmatcher v0.6.1 // indirect
github.com/moby/sys/sequential v0.6.0 // indirect
github.com/moby/sys/user v0.4.0 // indirect
github.com/moby/sys/sequential v0.7.0 // indirect
github.com/moby/sys/user v0.4.1 // indirect
github.com/moby/sys/userns v0.1.0 // indirect
github.com/moby/term v0.5.2 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/opencontainers/go-digest v1.0.0 // indirect
github.com/opencontainers/image-spec v1.1.1 // indirect
github.com/patrickmn/go-cache v2.1.0+incompatible // indirect
github.com/pelletier/go-toml/v2 v2.2.2 // indirect
github.com/pion/dtls/v3 v3.1.1 // indirect
github.com/pelletier/go-toml/v2 v2.4.2 // indirect
github.com/pion/dtls/v3 v3.1.4 // indirect
github.com/pion/logging v0.2.4 // indirect
github.com/pion/transport/v4 v4.0.1 // indirect
github.com/pires/go-proxyproto v0.8.1 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect
github.com/prometheus/client_golang v1.19.1 // indirect
github.com/prometheus/client_model v0.6.0 // indirect
github.com/prometheus/common v0.48.0 // indirect
github.com/prometheus/procfs v0.12.0 // indirect
github.com/prometheus/client_golang v1.21.1 // indirect
github.com/prometheus/client_model v0.6.2 // indirect
github.com/prometheus/common v0.62.0 // indirect
github.com/prometheus/procfs v0.15.1 // indirect
github.com/quic-go/qpack v0.6.0 // indirect
github.com/quic-go/quic-go v0.59.1 // indirect
github.com/quic-go/webtransport-go v0.10.0 // indirect
github.com/quic-go/quic-go v0.60.0 // indirect
github.com/quic-go/webtransport-go v0.11.1 // indirect
github.com/refraction-networking/utls v1.8.2 // indirect
github.com/riobard/go-bloom v0.0.0-20200614022211-cdc8013cb5b3 // indirect
github.com/rs/xid v1.3.0 // indirect
github.com/sagikazarmark/locafero v0.4.0 // indirect
github.com/sagikazarmark/slog-shim v0.1.0 // indirect
github.com/shadowsocks/go-shadowsocks2 v0.1.6-0.20241020092332-e1fe9ea73740 // indirect
github.com/shirou/gopsutil/v3 v3.24.5 // indirect
github.com/shirou/gopsutil/v4 v4.26.3 // indirect
github.com/shoenig/go-m1cpu v0.1.6 // indirect
github.com/shoenig/go-m1cpu v0.2.1 // indirect
github.com/sirupsen/logrus v1.9.4 // indirect
github.com/songgao/water v0.0.0-20200317203138-2b4b6d7c09d8 // indirect
github.com/sourcegraph/conc v0.3.0 // indirect
@@ -127,46 +127,47 @@ require (
github.com/spf13/pflag v1.0.5 // indirect
github.com/spf13/viper v1.19.0 // indirect
github.com/subosito/gotenv v1.6.0 // indirect
github.com/templexxx/cpu v0.1.1 // indirect
github.com/templexxx/xorsimd v0.4.3 // indirect
github.com/tidwall/gjson v1.19.0 // indirect
github.com/tidwall/match v1.1.1 // indirect
github.com/tidwall/pretty v1.2.0 // indirect
github.com/tidwall/sjson v1.2.5 // indirect
github.com/tjfoc/gmsm v1.4.1 // indirect
github.com/tklauser/go-sysconf v0.3.16 // indirect
github.com/tklauser/numcpus v0.11.0 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/ugorji/go/codec v1.2.12 // indirect
github.com/ugorji/go/codec v1.3.1 // indirect
github.com/vishvananda/netlink v1.1.1-0.20211118161826-650dca95af54 // indirect
github.com/vishvananda/netns v0.0.4 // indirect
github.com/vulcand/predicate v1.2.0 // indirect
github.com/xjasonlyu/tun2socks/v2 v2.6.0 // indirect
github.com/xtaci/kcp-go/v5 v5.6.5 // indirect
github.com/xtaci/smux v1.5.31 // indirect
github.com/xtaci/kcp-go/v5 v5.6.72 // indirect
github.com/xtaci/smux v1.5.57 // indirect
github.com/xtaci/tcpraw v1.2.25 // indirect
github.com/yl2chen/cidranger v1.0.2 // indirect
github.com/yusufpapurcu/wmi v1.2.4 // indirect
github.com/zalando/go-keyring v0.2.4 // indirect
github.com/zeebo/blake3 v0.2.4 // indirect
go.mongodb.org/mongo-driver/v2 v2.7.0 // indirect
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0 // indirect
go.opentelemetry.io/otel v1.41.0 // indirect
go.opentelemetry.io/otel/metric v1.41.0 // indirect
go.opentelemetry.io/otel/trace v1.41.0 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 // indirect
go.opentelemetry.io/otel v1.44.0 // indirect
go.opentelemetry.io/otel/metric v1.44.0 // indirect
go.opentelemetry.io/otel/trace v1.44.0 // indirect
go.uber.org/multierr v1.11.0 // indirect
golang.org/x/arch v0.8.0 // indirect
golang.org/x/crypto v0.50.0 // indirect
golang.org/x/arch v0.22.0 // indirect
golang.org/x/crypto v0.55.0 // indirect
golang.org/x/exp v0.0.0-20241210194714-1829a127f884 // indirect
golang.org/x/mod v0.34.0 // indirect
golang.org/x/net v0.53.0 // indirect
golang.org/x/sync v0.20.0 // indirect
golang.org/x/sys v0.43.0 // indirect
golang.org/x/term v0.42.0 // indirect
golang.org/x/text v0.36.0 // indirect
golang.org/x/time v0.12.0 // indirect
golang.org/x/tools v0.43.0 // indirect
golang.org/x/mod v0.38.0 // indirect
golang.org/x/net v0.58.0 // indirect
golang.org/x/sync v0.22.0 // indirect
golang.org/x/sys v0.47.0 // indirect
golang.org/x/text v0.41.0 // indirect
golang.org/x/time v0.14.0 // indirect
golang.org/x/tools v0.48.0 // indirect
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 // indirect
golang.zx2c4.com/wireguard v0.0.0-20250521234502-f333402bd9cb // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217 // indirect
google.golang.org/grpc v1.79.3 // indirect
google.golang.org/protobuf v1.36.10 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20260723215102-3fe39f3c1018 // indirect
google.golang.org/grpc v1.83.2 // indirect
google.golang.org/protobuf v1.36.11 // indirect
gopkg.in/ini.v1 v1.67.0 // indirect
gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
+140 -146
View File
@@ -12,26 +12,24 @@ github.com/alecthomas/units v0.0.0-20211218093645-b94a6e3cc137 h1:s6gZFSlWYmbqAu
github.com/alecthomas/units v0.0.0-20211218093645-b94a6e3cc137/go.mod h1:OMCwj8VM1Kc9e19TLln2VL61YJF0x1XFtfdL4JdbSyE=
github.com/alessio/shellescape v1.4.1 h1:V7yhSDDn8LP4lc4jS8pFkt0zCnzVJlG5JXy9BVKJUX0=
github.com/alessio/shellescape v1.4.1/go.mod h1:PZAiSCk0LJaZkiCSkPv8qIobYglO3FPpyFjDCtHLS30=
github.com/andybalholm/brotli v1.0.6 h1:Yf9fFpf49Zrxb9NlQaluyE92/+X7UVHlhMNJN2sxfOI=
github.com/andybalholm/brotli v1.0.6/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=
github.com/asaskevich/govalidator v0.0.0-20210307081110-f21760c49a8d h1:Byv0BzEl3/e6D5CLfI0j/7hiIEtvGVFPCZ7Ei2oq8iQ=
github.com/asaskevich/govalidator v0.0.0-20210307081110-f21760c49a8d/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw=
github.com/andybalholm/brotli v1.2.2 h1:HzTuoo2ErYQqf5qvcJInB8uvqSVxRttzkFexPWtnceM=
github.com/andybalholm/brotli v1.2.2/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY=
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/bytedance/sonic v1.11.6 h1:oUp34TzMlL+OY1OUWxHqsdkgC/Zfc85zGqw9siXjrc0=
github.com/bytedance/sonic v1.11.6/go.mod h1:LysEHSvpvDySVdC2f87zGWf6CIKJcAvqab1ZaiQtds4=
github.com/bytedance/sonic/loader v0.1.1 h1:c+e5Pt1k/cy5wMveRDyk2X4B9hF4g7an8N3zCYjJFNM=
github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
github.com/bytedance/gopkg v0.1.3 h1:TPBSwH8RsouGCBcMBktLt1AymVo2TVsBVCY4b6TnZ/M=
github.com/bytedance/gopkg v0.1.3/go.mod h1:576VvJ+eJgyCzdjS+c4+77QF3p7ubbtiKARP3TxducM=
github.com/bytedance/sonic v1.15.0 h1:/PXeWFaR5ElNcVE84U0dOHjiMHQOwNIx3K4ymzh/uSE=
github.com/bytedance/sonic v1.15.0/go.mod h1:tFkWrPz0/CUCLEF4ri4UkHekCIcdnkqXw9VduqpJh0k=
github.com/bytedance/sonic/loader v0.5.0 h1:gXH3KVnatgY7loH5/TkeVyXPfESoqSBSBEiDd5VjlgE=
github.com/bytedance/sonic/loader v0.5.0/go.mod h1:AR4NYCk5DdzZizZ5djGqQ92eEhCCcdf5x77udYiSJRo=
github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8=
github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
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/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/0Y=
github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w=
github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg=
github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY=
github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M=
github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU=
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI=
github.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M=
@@ -66,7 +64,6 @@ github.com/dunglas/httpsfv v1.1.0/go.mod h1:zID2mqw9mFsnt7YC3vYQ9/cjq30q41W+1AnD
github.com/ebitengine/purego v0.10.0 h1:QIw4xfpWT6GWTzaW5XEKy3HXoqrJGx1ijYHzTF0/ISU=
github.com/ebitengine/purego v0.10.0/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
@@ -75,30 +72,32 @@ github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHk
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA=
github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM=
github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0=
github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk=
github.com/gabriel-vasile/mimetype v1.4.12 h1:e9hWvmLYvtp846tLHam2o++qitpguFiYCKbn0w9jyqw=
github.com/gabriel-vasile/mimetype v1.4.12/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s=
github.com/gin-contrib/cors v1.7.2 h1:oLDHxdg8W/XDoN/8zamqk/Drgt4oVZDvaV0YmvVICQw=
github.com/gin-contrib/cors v1.7.2/go.mod h1:SUJVARKgQ40dmrzgXEVxj2m7Ig1v1qIboQkPDTQ9t2E=
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
github.com/gin-gonic/gin v1.10.1 h1:T0ujvqyCSqRopADpgPgiTT63DUQVSfojyME59Ei63pQ=
github.com/gin-gonic/gin v1.10.1/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y=
github.com/go-gost/core v0.5.1 h1:HbIn3naEOC661Z4SwzaSYUzffWSRbU0P6gkxiHmuG0I=
github.com/go-gost/core v0.5.1/go.mod h1:WGI43jOka7FAsSAwi/fSMaqxdR+E339ycb4NBGlFr6A=
github.com/go-gost/go-shadowsocks2 v0.1.3 h1:6CUZLp+mTWXnKP2aK8/Z9ZP+ERMX9gSbywmPu4kGX/A=
github.com/go-gost/go-shadowsocks2 v0.1.3/go.mod h1:866zFNNI3He6Wef1M/IvAjTal74WhcfKfBgRpTlkKys=
github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w=
github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM=
github.com/gin-gonic/gin v1.12.0 h1:b3YAbrZtnf8N//yjKeU2+MQsh2mY5htkZidOM7O0wG8=
github.com/gin-gonic/gin v1.12.0/go.mod h1:VxccKfsSllpKshkBWgVgRniFFAzFb9csfngsqANjnLc=
github.com/go-gost/core v0.6.1 h1:mBBvZpxIbrspuRsksj7YLHEiBGEc+sQBV3wDs4rX/AE=
github.com/go-gost/core v0.6.1/go.mod h1:WGI43jOka7FAsSAwi/fSMaqxdR+E339ycb4NBGlFr6A=
github.com/go-gost/go-shadowsocks2 v0.1.4 h1:n2Po4TDKdLp1PsvhSiWFB/6S4e/YKZfsKJkA0PUa168=
github.com/go-gost/go-shadowsocks2 v0.1.4/go.mod h1:866zFNNI3He6Wef1M/IvAjTal74WhcfKfBgRpTlkKys=
github.com/go-gost/gosocks4 v0.1.0 h1:eAzev6qw4fzkFQKC9uCHLVNnnPdHyqCggbnfNN80Pmk=
github.com/go-gost/gosocks4 v0.1.0/go.mod h1:hzVjwijJuZR1pp3GqpTj+AKcSGrx68RlWTrQMFMYBP0=
github.com/go-gost/gosocks5 v0.5.0 h1:YE37l1MJwde8diIQdynStqogMotG5enoTdborhA5yic=
github.com/go-gost/gosocks5 v0.5.0/go.mod h1:1G6I7HP7VFVxveGkoK8mnprnJqSqJjdcASKsdUn4Pp4=
github.com/go-gost/plugin v0.4.0 h1:M7MR5PL7QAFCrdWfcXVX+5iLNTVIZlhl8FfdV/K8dZY=
github.com/go-gost/plugin v0.4.0/go.mod h1:oN23l+yGDCIP9G3KnDl/I/0zVGOobZUDCB2Z5yYYXts=
github.com/go-gost/relay v0.6.1 h1:7SqnHFbY8x/DzvjpK03a5zcVH9+TbJAcnW/RT6s1ecc=
github.com/go-gost/relay v0.6.1/go.mod h1:Dku0f5sfjOClrZFiDmQUrYYJ4uof7rnkCUBfsl0PSAI=
github.com/go-gost/tls-dissector v0.2.0 h1:9tE6WOzzpurATTBWn60DU4R8gibpGNY8/qVcc1SicVg=
github.com/go-gost/tls-dissector v0.2.0/go.mod h1:/9QfdewqmHdaE362Hv5nDaSWLx3pCmtD870d6GaquXs=
github.com/go-gost/x v0.13.0 h1:iQOdO7o9GHIgNRtQTeoi/1WiTZw2C+/kC/y5JsN8WjE=
github.com/go-gost/x v0.13.0/go.mod h1:P9zH+y/4+bNN2ZcoQZJ0tD59RXdvuRhioXDNfSCVsxU=
github.com/go-gost/plugin v0.8.1 h1:mZpLbzRZosHocaiZ4dYYqX8waLiavy/rsEWwqx5aluo=
github.com/go-gost/plugin v0.8.1/go.mod h1:48pqi8/zS5OhEEoFETYZCqu2sR59DX3QxG5SjGmPWcU=
github.com/go-gost/quic-dissector v0.1.0 h1:zOaw2XjKxMf6vVC1z4BHDKGHbCs4zrYU1Rxaz5d0TQU=
github.com/go-gost/quic-dissector v0.1.0/go.mod h1:ar+I40Izq9ZT2k/aNnLFGEMvdeSOBLLKxhF/i3FSnQQ=
github.com/go-gost/relay v0.7.0 h1:J8e3Sba6DtBJQotXY5j5EaZNWwtv6Z9CoCFQsnrHNcE=
github.com/go-gost/relay v0.7.0/go.mod h1:Dku0f5sfjOClrZFiDmQUrYYJ4uof7rnkCUBfsl0PSAI=
github.com/go-gost/tls-dissector v0.3.1 h1:gvOteWog5pjY/HCpc8l+gngmSi8Q6zl5rRrfK8gwRKA=
github.com/go-gost/tls-dissector v0.3.1/go.mod h1:vGfog053fIm93iXBtvmVzMQqEJo5YwbbNaPNVDjbiOc=
github.com/go-gost/x v0.19.5 h1:brbvQ6Pkq2pMr7k+IQvKB8dDdkt1D2IhURaVZZ5sicw=
github.com/go-gost/x v0.19.5/go.mod h1:YknNANia9Jzp5/TgPzGN6tcVir0WbapexBOird+4s0U=
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
@@ -112,14 +111,16 @@ github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/o
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
github.com/go-playground/validator/v10 v10.20.0 h1:K9ISHbSaI0lyB2eWMPJo+kOS/FBExVwjEviJTixqxL8=
github.com/go-playground/validator/v10 v10.20.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM=
github.com/go-playground/validator/v10 v10.30.1 h1:f3zDSN/zOma+w6+1Wswgd9fLkdwy06ntQJp0BBvFG0w=
github.com/go-playground/validator/v10 v10.30.1/go.mod h1:oSuBIQzuJxL//3MelwSLD5hc2Tu889bF0Idm9Dg26cM=
github.com/go-redis/redis/v8 v8.11.5 h1:AcZZR7igkdvfVmQTPnu9WE37LRrO/YrBH5zWyjDC0oI=
github.com/go-redis/redis/v8 v8.11.5/go.mod h1:gREzHqY1hg6oD9ngVRbLStwAWKhA0FEgq8Jd4h5lpwo=
github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y=
github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8=
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
github.com/goccy/go-yaml v1.19.2 h1:PmFC1S6h8ljIz6gMRBopkjP1TVT7xuwrButHID66PoM=
github.com/goccy/go-yaml v1.19.2/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk=
github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
@@ -155,36 +156,34 @@ 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=
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/gravitational/trace v1.1.16-0.20220114165159-14a9a7dd6aaf h1:C1GPyPJrOlJlIrcaBBiBpDsqZena2Ks8spa5xZqr1XQ=
github.com/gravitational/trace v1.1.16-0.20220114165159-14a9a7dd6aaf/go.mod h1:zXqxTI6jXDdKnlf8s+nT+3c8LrwUEy3yNpO4XJL90lA=
github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
github.com/jonboulle/clockwork v0.2.2 h1:UOGuzwb1PwsrDAObMuhUnj0p5ULPj8V/xJ7Kx9qUBdQ=
github.com/jonboulle/clockwork v0.2.2/go.mod h1:Pkfl5aHPm1nk2H9h0bjmnJD/BcgbGXUBGnn1kMkgxc8=
github.com/hashicorp/yamux v0.1.1 h1:yrQxtgseBDrq9Y652vSRDvsKCJKOUD+GzTS4Y0Y8pvE=
github.com/hashicorp/yamux v0.1.1/go.mod h1:CtWFDAQgb7dxtzFs4tWbplKIe2jSi3+5vKbgIO0SLnQ=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/judwhite/go-svc v1.2.1 h1:a7fsJzYUa33sfDJRF2N/WXhA+LonCEEY8BJb1tuS5tA=
github.com/judwhite/go-svc v1.2.1/go.mod h1:mo/P2JNX8C07ywpP9YtO2gnBgnUiFTHqtsZekJrUuTk=
github.com/klauspost/compress v1.18.5 h1:/h1gH5Ce+VWNLSWqPzOVn6XBO+vJbCNGvjoaGBFW2IE=
github.com/klauspost/compress v1.18.5/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ=
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
github.com/klauspost/cpuid/v2 v2.2.7 h1:ZWSB3igEs+d0qvnxR/ZBzXVmxkgt8DdzP6m9pfuVLDM=
github.com/klauspost/cpuid/v2 v2.2.7/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
github.com/klauspost/reedsolomon v1.11.8 h1:s8RpUW5TK4hjr+djiOpbZJB4ksx+TdYbRH7vHQpwPOY=
github.com/klauspost/reedsolomon v1.11.8/go.mod h1:4bXRN+cVzMdml6ti7qLouuYi32KHJ5MGv0Qd8a47h6A=
github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M=
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.4.0 h1:S6Hrbc7+ywsr0r+RLapfGBHfyefhCTwEh3A0tV913Dw=
github.com/klauspost/cpuid/v2 v2.4.0/go.mod h1:19jmZ9mjzoF//ddRSUsv0zfBTJWh3QJh9FNxZTMrGxU=
github.com/klauspost/reedsolomon v1.12.0 h1:I5FEp3xSwVCcEh3F5A7dofEfhXdF/bWhQWPH+XwBFno=
github.com/klauspost/reedsolomon v1.12.0/go.mod h1:EPLZJeh4l27pUGC3aXOjheaoh1I9yut7xTURiW3LQ9Y=
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/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ81pIr0yLvtUWk2if982qA3F3QD6H4=
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I=
github.com/magiconair/properties v1.8.10 h1:s31yESBquKXCV9a/ScB3ESkOjUYYv+X0rg8SYxI99mE=
github.com/magiconair/properties v1.8.10/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
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/miekg/dns v1.1.61 h1:nLxbwF3XxhwVSm8g9Dghm9MHPaUZuqhPiGL+675ZmEs=
github.com/miekg/dns v1.1.61/go.mod h1:mnAarhS3nWaW+NVP2wTkYVIZyHNJ098SJZUki3eykwQ=
github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
@@ -193,18 +192,18 @@ github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyua
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0=
github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo=
github.com/moby/go-archive v0.2.0 h1:zg5QDUM2mi0JIM9fdQZWC7U8+2ZfixfTYoHL7rWUcP8=
github.com/moby/go-archive v0.2.0/go.mod h1:mNeivT14o8xU+5q1YnNrkQVpK+dnNe/K6fHqnTg4qPU=
github.com/moby/go-archive v0.3.0 h1:nos4BtzzUIqB406BgQnWGMI4qib9BZ8XUHU+ucv/n1c=
github.com/moby/go-archive v0.3.0/go.mod h1:Npdv43fFqlhZW7Xo8fbm3ZMYFvAGNviUPqX21VERbcE=
github.com/moby/moby/api v1.54.1 h1:TqVzuJkOLsgLDDwNLmYqACUuTehOHRGKiPhvH8V3Nn4=
github.com/moby/moby/api v1.54.1/go.mod h1:+RQ6wluLwtYaTd1WnPLykIDPekkuyD/ROWQClE83pzs=
github.com/moby/moby/client v0.4.0 h1:S+2XegzHQrrvTCvF6s5HFzcrywWQmuVnhOXe2kiWjIw=
github.com/moby/moby/client v0.4.0/go.mod h1:QWPbvWchQbxBNdaLSpoKpCdf5E+WxFAgNHogCWDoa7g=
github.com/moby/patternmatcher v0.6.1 h1:qlhtafmr6kgMIJjKJMDmMWq7WLkKIo23hsrpR3x084U=
github.com/moby/patternmatcher v0.6.1/go.mod h1:hDPoyOpDY7OrrMDLaYoY3hf52gNCR/YOUYxkhApJIxc=
github.com/moby/sys/sequential v0.6.0 h1:qrx7XFUd/5DxtqcoH1h438hF5TmOvzC/lspjy7zgvCU=
github.com/moby/sys/sequential v0.6.0/go.mod h1:uyv8EUTrca5PnDsdMGXhZe6CCe8U/UiTWd+lL+7b/Ko=
github.com/moby/sys/user v0.4.0 h1:jhcMKit7SA80hivmFJcbB1vqmw//wU61Zdui2eQXuMs=
github.com/moby/sys/user v0.4.0/go.mod h1:bG+tYYYJgaMtRKgEmuueC0hJEAZWwtIbZTB+85uoHjs=
github.com/moby/sys/sequential v0.7.0 h1:ASQNGNROJSuOO6LL6bPHbKvuZu6NU8P4ldPWk31zj/8=
github.com/moby/sys/sequential v0.7.0/go.mod h1:NfSTAp6V3fw4tmkD62PEcOKeZKquXT8VKCkf7aVR79o=
github.com/moby/sys/user v0.4.1 h1:RgjRlaDKi/Xmyrz4t8lyzXT6v2ooFeO/7xtchmhVWE0=
github.com/moby/sys/user v0.4.1/go.mod h1:E9QsW5WRe1kUAf7kW8hXKwu1uhsZEAdPLYHYSDudF4Y=
github.com/moby/sys/userns v0.1.0 h1:tVLXkFOxVu9A64/yh59slHVv9ahO9UIev4JZusOLG/g=
github.com/moby/sys/userns v0.1.0/go.mod h1:IHUYgu/kao6N8YZlp9Cf444ySSvCmDlmzUcYfDHOl28=
github.com/moby/term v0.5.2 h1:6qk3FJAFDs6i/q3W/pQ97SX192qKfZgGjCQqfCJkgzQ=
@@ -214,6 +213,8 @@ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE=
github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU=
github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE=
@@ -226,10 +227,10 @@ github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJw
github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M=
github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc=
github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ=
github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM=
github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs=
github.com/pion/dtls/v3 v3.1.1 h1:wSLMam9Kf7DL1A74hnqRvEb9OT+aXPAsQ5VS+BdXOJ0=
github.com/pion/dtls/v3 v3.1.1/go.mod h1:7FGvVYpHsUV6+aywaFpG7aE4Vz8nBOx74odPRFue6cI=
github.com/pelletier/go-toml/v2 v2.4.2 h1:M2fKKbmyvI+hGId/D0W64qDBMVhJnNR10O5gIbMc//Q=
github.com/pelletier/go-toml/v2 v2.4.2/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
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/transport/v4 v4.0.1 h1:sdROELU6BZ63Ab7FrOLn13M6YdJLY20wldXW2Cu2k8o=
@@ -243,21 +244,23 @@ github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRI
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 h1:o4JXh1EVt9k/+g42oCprj/FisM4qX9L3sZB3upGN2ZU=
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
github.com/prometheus/client_golang v1.19.1 h1:wZWJDwK+NameRJuPGDhlnFgx8e8HN3XHQeLaYJFJBOE=
github.com/prometheus/client_golang v1.19.1/go.mod h1:mP78NwGzrVks5S2H6ab8+ZZGJLZUq1hoULYBAYBw1Ho=
github.com/prometheus/client_golang v1.21.1 h1:DOvXXTqVzvkIewV/CDPFdejpMCGeMcbGCQ8YOmu+Ibk=
github.com/prometheus/client_golang v1.21.1/go.mod h1:U9NM32ykUErtVBxdvD3zfi+EuFkkaBvMb09mIfe0Zgg=
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/client_model v0.6.0 h1:k1v3CzpSRUTrKMppY35TLwPvxHqBu0bYgxZzqGIgaos=
github.com/prometheus/client_model v0.6.0/go.mod h1:NTQHnmxFpouOD0DpvP4XujX3CdOAGQPoaGhyTchlyt8=
github.com/prometheus/common v0.48.0 h1:QO8U2CdOzSn1BBsmXJXduaaW+dY/5QLjfB8svtSzKKE=
github.com/prometheus/common v0.48.0/go.mod h1:0/KsvlIEfPQCQ5I2iNSAWKPZziNCvRs5EC6ILDTlAPc=
github.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k6Bo=
github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo=
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.62.0 h1:xasJaQlnWAeyHdUBeGjXmutelfJHWMRr+Fg4QszZ2Io=
github.com/prometheus/common v0.62.0/go.mod h1:vyBcEuLSvWos9B1+CyL7JZ2up+uFzXhkqml0W5zIY1I=
github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc=
github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk=
github.com/quic-go/go-ossfuzz-seeds v0.1.0 h1:APacT+iIaNF6fd8AGEiN3bT/Jtkd2jz4v4TzM7MFjy0=
github.com/quic-go/go-ossfuzz-seeds v0.1.0/go.mod h1:3IOHRbJIc+L6YKMwfDtJAM9Vj9k0YY4muhuyUYk5tbk=
github.com/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8=
github.com/quic-go/qpack v0.6.0/go.mod h1:lUpLKChi8njB4ty2bFLX2x4gzDqXwUpaO1DP9qMDZII=
github.com/quic-go/quic-go v0.59.1 h1:0Gmua0HW1Tv7ANR7hUYwRyD0MG5OJfgvYSZasGZzBic=
github.com/quic-go/quic-go v0.59.1/go.mod h1:upnsH4Ju1YkqpLXC305eW3yDZ4NfnNbmQRCMWS58IKU=
github.com/quic-go/webtransport-go v0.10.0 h1:LqXXPOXuETY5Xe8ITdGisBzTYmUOy5eSj+9n4hLTjHI=
github.com/quic-go/webtransport-go v0.10.0/go.mod h1:LeGIXr5BQKE3UsynwVBeQrU1TPrbh73MGoC6jd+V7ow=
github.com/quic-go/quic-go v0.60.0 h1:xcQioE8OM66UQLeUMHltK1CCcOu3JbVB4JAQdDQSB+0=
github.com/quic-go/quic-go v0.60.0/go.mod h1:wpKpjmPpftl30sL6pFh7REVpjbcCVy4zt2vDyK1TuJk=
github.com/quic-go/webtransport-go v0.11.1 h1:rrFQMO+7/52ZDJ04fsrjIaWqn6q1z1MYo9iVFq6JtbA=
github.com/quic-go/webtransport-go v0.11.1/go.mod h1:SHgEzUFVyj+9WUSuGB1P6Zd351Pww2leWV3SwlTovkA=
github.com/refraction-networking/utls v1.8.2 h1:j4Q1gJj0xngdeH+Ox/qND11aEfhpgoEvV+S9iJ2IdQo=
github.com/refraction-networking/utls v1.8.2/go.mod h1:jkSOEkLqn+S/jtpEHPOsVv/4V4EVnelwbMQl4vCWXAM=
github.com/riobard/go-bloom v0.0.0-20200614022211-cdc8013cb5b3 h1:f/FNXud6gA3MNr8meMVVGxhp+QBTqY91tM8HjEuMjGg=
@@ -270,17 +273,14 @@ github.com/sagikazarmark/locafero v0.4.0 h1:HApY1R9zGo4DBgr7dqsTH/JJxLTTsOt7u6ke
github.com/sagikazarmark/locafero v0.4.0/go.mod h1:Pe1W6UlPYUk/+wc/6KFhbORCfqzgYEpgQ3O5fPuL3H4=
github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE=
github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ=
github.com/shadowsocks/go-shadowsocks2 v0.1.6-0.20241020092332-e1fe9ea73740 h1:XdDrN8rtxdgW3TLn7pAuobI9PhPMbf6Geu9nvFzXn2E=
github.com/shadowsocks/go-shadowsocks2 v0.1.6-0.20241020092332-e1fe9ea73740/go.mod h1:Oqfn/ykzqjeX00+7IuPyR7wGYgOzld0Tni6djgElacI=
github.com/shirou/gopsutil/v3 v3.24.5 h1:i0t8kL+kQTvpAYToeuiVk3TgDeKOFioZO3Ztz/iZ9pI=
github.com/shirou/gopsutil/v3 v3.24.5/go.mod h1:bsoOS1aStSs9ErQ1WWfxllSeS1K5D+U30r2NfcubMVk=
github.com/shirou/gopsutil/v4 v4.26.3 h1:2ESdQt90yU3oXF/CdOlRCJxrP+Am1aBYubTMTfxJ1qc=
github.com/shirou/gopsutil/v4 v4.26.3/go.mod h1:LZ6ewCSkBqUpvSOf+LsTGnRinC6iaNUNMGBtDkJBaLQ=
github.com/shoenig/go-m1cpu v0.1.6 h1:nxdKQNcEB6vzgA2E2bvzKIYRuNj7XNJ4S/aRSwKzFtM=
github.com/shoenig/go-m1cpu v0.1.6/go.mod h1:1JJMcUBvfNwpq05QDQVAnx3gUHr9IYF7GNg9SUEw2VQ=
github.com/shoenig/test v0.6.4 h1:kVTaSd7WLz5WZ2IaoM0RSzRsUD+m8wRR+5qvntpn4LU=
github.com/shoenig/test v0.6.4/go.mod h1:byHiCGXqrVaflBLAMq/srcZIHynQPQgeyvkvXnjqq0k=
github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
github.com/shoenig/go-m1cpu v0.2.1 h1:yqRB4fvOge2+FyRXFkXqsyMoqPazv14Yyy+iyccT2E4=
github.com/shoenig/go-m1cpu v0.2.1/go.mod h1:KkDOw6m3ZJQAPHbrzkZki4hnx+pDRR1Lo+ldA56wD5w=
github.com/shoenig/test v1.7.0 h1:eWcHtTXa6QLnBvm0jgEabMRN/uJ4DMV3M8xUGgRkZmk=
github.com/shoenig/test v1.7.0/go.mod h1:UxJ6u/x2v/TNs/LoLxBNJRV9DiwBBKYxXSyczsBHFoI=
github.com/sirupsen/logrus v1.9.4 h1:TsZE7l11zFCLZnZ+teH4Umoq5BhEIfIzfRDZ1Uzql2w=
github.com/sirupsen/logrus v1.9.4/go.mod h1:ftWc9WdOfJ0a92nsE2jF5u5ZwH8Bv2zdeOC42RjbV2g=
github.com/songgao/water v0.0.0-20200317203138-2b4b6d7c09d8 h1:TG/diQgUe0pntT/2D9tmUCz4VNwm9MfrtPr0SU2qSX8=
@@ -301,26 +301,27 @@ github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpE
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/objx v0.5.3 h1:jmXUvGomnU1o3W/V5h2VEradbpJDwGrzugQQvL0POH4=
github.com/stretchr/objx v0.5.3/go.mod h1:rDQraq+vQZU7Fde9LOZLr8Tax6zZvy4kuNKF+QYS+U0=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
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/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
github.com/templexxx/cpu v0.1.1 h1:isxHaxBXpYFWnk2DReuKkigaZyrjs2+9ypIdGP4h+HI=
github.com/templexxx/cpu v0.1.1/go.mod h1:w7Tb+7qgcAlIyX4NhLuDKt78AHA5SzPmq0Wj6HiEnnk=
github.com/templexxx/xorsimd v0.4.3 h1:9AQTFHd7Bhk3dIT7Al2XeBX5DWOvsUPZCuhyAtNbHjU=
github.com/templexxx/xorsimd v0.4.3/go.mod h1:oZQcD6RFDisW2Am58dSAGwwL6rHjbzrlu25VDqfWkQg=
github.com/testcontainers/testcontainers-go v0.42.0 h1:He3IhTzTZOygSXLJPMX7n44XtK+qhjat1nI9cneBbUY=
github.com/testcontainers/testcontainers-go v0.42.0/go.mod h1:vZjdY1YmUA1qEForxOIOazfsrdyORJAbhi0bp8plN30=
github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
github.com/tidwall/gjson v1.19.0 h1:xwxm7n691Uf3u5OFjzngavjGTh55KX5q/9w9xHW88JU=
github.com/tidwall/gjson v1.19.0/go.mod h1:V37/opeE/JbLUOfH0QTXiNez2l0RUjYUhpT4szFQAfc=
github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs=
github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY=
github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28=
github.com/tjfoc/gmsm v1.4.1 h1:aMe1GlZb+0bLjn+cKTPEvvn9oUEBlJitaZiiBwsbgho=
github.com/tjfoc/gmsm v1.4.1/go.mod h1:j4INPkHWMrhJb38G+J6W4Tw0AbuN8Thu3PbdVYhVcTE=
github.com/tklauser/go-sysconf v0.3.16 h1:frioLaCQSsF5Cy1jgRBrzr6t502KIIwQ0MArYICU0nA=
@@ -329,25 +330,25 @@ github.com/tklauser/numcpus v0.11.0 h1:nSTwhKH5e1dMNsCdVBukSZrURJRoHbSEQjdEbY+9R
github.com/tklauser/numcpus v0.11.0/go.mod h1:z+LwcLq54uWZTX0u/bGobaV34u6V7KNlTZejzM6/3MQ=
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE=
github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
github.com/ugorji/go/codec v1.3.1 h1:waO7eEiFDwidsBN6agj1vJQ4AG7lh2yqXyOXqhgQuyY=
github.com/ugorji/go/codec v1.3.1/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4=
github.com/vishvananda/netlink v1.1.1-0.20211118161826-650dca95af54 h1:8mhqcHPqTMhSPoslhGYihEgSfc77+7La1P6kiB6+9So=
github.com/vishvananda/netlink v1.1.1-0.20211118161826-650dca95af54/go.mod h1:twkDnbuQxJYemMlGd4JFIcuhgX83tXhKS2B/PRMpOho=
github.com/vishvananda/netns v0.0.0-20200728191858-db3c7e526aae/go.mod h1:DD4vA1DwXk04H54A1oHXtwZmA0grkVMdPxx/VGLCah0=
github.com/vishvananda/netns v0.0.4 h1:Oeaw1EM2JMxD51g9uhtC0D7erkIjgmj8+JZc26m1YX8=
github.com/vishvananda/netns v0.0.4/go.mod h1:SpkAiCQRtJ6TvvxPnOSyH3BMl6unz3xZlaprSwhNNJM=
github.com/vulcand/predicate v1.2.0 h1:uFsW1gcnnR7R+QTID+FVcs0sSYlIGntoGOTb3rQJt50=
github.com/vulcand/predicate v1.2.0/go.mod h1:VipoNYXny6c8N381zGUWkjuuNHiRbeAZhE7Qm9c+2GA=
github.com/xjasonlyu/tun2socks/v2 v2.6.0 h1:gI9saJT3XgH4e6v9jBuHRLwK7l3aN9YFWec/SsDTDx4=
github.com/xjasonlyu/tun2socks/v2 v2.6.0/go.mod h1:35AwqxIxnMkfBfT0UJ1Lku7PZm2ZiZJ8sxHyp0gt1yw=
github.com/xtaci/kcp-go/v5 v5.6.5 h1:oxGZNobj3OddrLzwdJYnR/waNgwrL98u02u0DWNHE3k=
github.com/xtaci/kcp-go/v5 v5.6.5/go.mod h1:Qy3Zf2tWTdFdEs0E8JvhrX+39r5UDZoYac8anvud7/Q=
github.com/xtaci/kcp-go/v5 v5.6.72 h1:FLaQPalgpufJYQRk0OK+gErEhXGLUPjv6FSRPrFR8Lk=
github.com/xtaci/kcp-go/v5 v5.6.72/go.mod h1:9O3D8WR+cyyUjGiTILYfg17vn72otWuXK2AFfqIe6CM=
github.com/xtaci/lossyconn v0.0.0-20190602105132-8df528c0c9ae h1:J0GxkO96kL4WF+AIT3M4mfUVinOCPgf2uUWYFUzN0sM=
github.com/xtaci/lossyconn v0.0.0-20190602105132-8df528c0c9ae/go.mod h1:gXtu8J62kEgmN++bm9BVICuT/e8yiLI2KFobd/TRFsE=
github.com/xtaci/smux v1.5.31 h1:3ha7sHtH46h85Iv7MfQogxasuRt1KPRhoFB3S4rmHgU=
github.com/xtaci/smux v1.5.31/go.mod h1:OMlQbT5vcgl2gb49mFkYo6SMf+zP3rcjcwQz7ZU7IGY=
github.com/xtaci/smux v1.5.57 h1:N72VbGoSYxgcm6mPOYX0QzEZNVD3UI/JlVvAtXF+WrY=
github.com/xtaci/smux v1.5.57/go.mod h1:IGQ9QYrBphmb/4aTnLEcJby0TNr3NV+OslIOMrX825Q=
github.com/xtaci/tcpraw v1.2.25 h1:VDlqo0op17JeXBM6e2G9ocCNLOJcw9mZbobMbJjo0vk=
github.com/xtaci/tcpraw v1.2.25/go.mod h1:dKyZ2V75s0cZ7cbgJYdxPvms7af0joIeOyx1GgJQbLk=
github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
github.com/yl2chen/cidranger v1.0.2 h1:lbOWZVCG1tCRX4u24kuM1Tb4nHqWkDxwLdoS+SevawU=
github.com/yl2chen/cidranger v1.0.2/go.mod h1:9U1yz7WPYDwf0vpNWFaeRh0bjwz5RVgRy/9UEQfHl0g=
github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0=
@@ -360,34 +361,34 @@ github.com/zeebo/blake3 v0.2.4 h1:KYQPkhpRtcqh0ssGYcKLG1JYvddkEA8QwCM/yBqhaZI=
github.com/zeebo/blake3 v0.2.4/go.mod h1:7eeQ6d2iXWRGF6npfaxl2CU+xy2Fjo2gxeyZGCRUjcE=
github.com/zeebo/pcg v1.0.1 h1:lyqfGeWiv4ahac6ttHs+I5hwtH/+1mrhlCtVNQM2kHo=
github.com/zeebo/pcg v1.0.1/go.mod h1:09F0S9iiKrwn9rlI5yjLkmrug154/YRW6KnnXVDM/l4=
go.mongodb.org/mongo-driver/v2 v2.7.0 h1:RO+zqavD2/GCL3cxOMyZhx6R9Irzr8/6gsoqx5tcY/c=
go.mongodb.org/mongo-driver/v2 v2.7.0/go.mod h1:yOI9kBsufol30iFsl1slpdq1I0eHPzybRWdyYUs8K/0=
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/contrib/instrumentation/net/http/otelhttp v0.60.0 h1:sbiXRNDSWJOTobXh5HyQKjq6wUC5tNybqjIqDpAY4CU=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0/go.mod h1:69uWxva0WgAA/4bu2Yy70SLDBwZXuQ6PbBpbsa5iZrQ=
go.opentelemetry.io/otel v1.41.0 h1:YlEwVsGAlCvczDILpUXpIpPSL/VPugt7zHThEMLce1c=
go.opentelemetry.io/otel v1.41.0/go.mod h1:Yt4UwgEKeT05QbLwbyHXEwhnjxNO6D8L5PQP51/46dE=
go.opentelemetry.io/otel/metric v1.41.0 h1:rFnDcs4gRzBcsO9tS8LCpgR0dxg4aaxWlJxCno7JlTQ=
go.opentelemetry.io/otel/metric v1.41.0/go.mod h1:xPvCwd9pU0VN8tPZYzDZV/BMj9CM9vs00GuBjeKhJps=
go.opentelemetry.io/otel/sdk v1.39.0 h1:nMLYcjVsvdui1B/4FRkwjzoRVsMK8uL/cj0OyhKzt18=
go.opentelemetry.io/otel/sdk v1.39.0/go.mod h1:vDojkC4/jsTJsE+kh+LXYQlbL8CgrEcwmt1ENZszdJE=
go.opentelemetry.io/otel/sdk/metric v1.39.0 h1:cXMVVFVgsIf2YL6QkRF4Urbr/aMInf+2WKg+sEJTtB8=
go.opentelemetry.io/otel/sdk/metric v1.39.0/go.mod h1:xq9HEVH7qeX69/JnwEfp6fVq5wosJsY1mt4lLfYdVew=
go.opentelemetry.io/otel/trace v1.41.0 h1:Vbk2co6bhj8L59ZJ6/xFTskY+tGAbOnCtQGVVa9TIN0=
go.opentelemetry.io/otel/trace v1.41.0/go.mod h1:U1NU4ULCoxeDKc09yCWdWe+3QoyweJcISEVa1RBzOis=
go.uber.org/mock v0.5.2 h1:LbtPTcP8A5k9WPXj54PPPbjcI4Y6lhyOZXn+VS7wNko=
go.uber.org/mock v0.5.2/go.mod h1:wLlUxC2vVTPTaE3UD51E0BGOAElKrILxhVSDYQLld5o=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 h1:F7Jx+6hwnZ41NSFTO5q4LYDtJRXBf2PD0rNBkeB/lus=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0/go.mod h1:UHB22Z8QsdRDrnAtX4PntOl36ajSxcdUMt1sF7Y6E7Q=
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/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y=
go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU=
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
golang.org/x/arch v0.8.0 h1:3wRIsP3pM4yUptoR96otTUOXI367OS0+c9eeRi9doIc=
golang.org/x/arch v0.8.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys=
golang.org/x/arch v0.22.0 h1:c/Zle32i5ttqRXjdLyyHZESLD/bB90DCU1g9l/0YBDI=
golang.org/x/arch v0.22.0/go.mod h1:dNHoOeKiyja7GTvF9NJS1l3Z2yntpQNzgrjh1cU103A=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20201012173705-84dcc777aaee/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20201016220609-9e8e0b390897/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.50.0 h1:zO47/JPrL6vsNkINmLoo/PH1gcxpls50DNogFvB5ZGI=
golang.org/x/crypto v0.50.0/go.mod h1:3muZ7vA7PBCE6xgPX7nkzzjiUq87kRItoJQM1Yo8S+Q=
golang.org/x/crypto v0.55.0 h1:+KWHjbgOaAQ66dh/YlkZKHlz9ZUlq61AFirAR9ntP8M=
golang.org/x/crypto v0.55.0/go.mod h1:uq0V9dE/fzQuJtbnL+2EhWOE63vo164FY8xqEnV9xis=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20241210194714-1829a127f884 h1:Y/Mj/94zIQQGHVSv1tTtQBDaQaJe62U9bkDZKKyhPCU=
golang.org/x/exp v0.0.0-20241210194714-1829a127f884/go.mod h1:qj5a5QZpwLU2NLQudwIN5koi3beDhSAlJwa67PuM98c=
@@ -396,8 +397,8 @@ golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvx
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
golang.org/x/mod v0.34.0 h1:xIHgNUUnW6sYkcM5Jleh05DvLOtwc6RitGHbDk4akRI=
golang.org/x/mod v0.34.0/go.mod h1:ykgH52iCZe79kzLLMhyCUzhMci+nQj+0XkbXpNYtVjY=
golang.org/x/mod v0.38.0 h1:MECBjubtXD7yj4HrhIUcywNaGeNVUdfVnxmPajOk4yk=
golang.org/x/mod v0.38.0/go.mod h1:V6Xz0pq8TQ3dGqVQ1FVHuelZpAL0uNhSkk9ogYP3c40=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
@@ -405,75 +406,70 @@ golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20201010224723-4f7140c49acb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.53.0 h1:d+qAbo5L0orcWAr0a9JweQpjXF19LMXJE8Ey7hwOdUA=
golang.org/x/net v0.53.0/go.mod h1:JvMuJH7rrdiCfbeHoo3fCQU24Lf5JJwT9W3sJFulfgs=
golang.org/x/net v0.58.0 h1:ynWG7rqYi4ccpTEuPZ2QGWHktVEM9DMCj9yzDE0Q7To=
golang.org/x/net v0.58.0/go.mod h1:YwCddHnFlT7eLQqVprV19OnhLGtc5xOKgE0RyqgfWAU=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
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.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200217220822-9197077df867/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200728102440-3e129f6d46b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI=
golang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/term v0.42.0 h1:UiKe+zDFmJobeJ5ggPwOshJIVt6/Ft0rcfrXZDLWAWY=
golang.org/x/term v0.42.0/go.mod h1:Dq/D+snpsbazcBG5+F9Q1n2rXV8Ma+71xEjTRufARgY=
golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs=
golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/term v0.45.0 h1:NwWyBmoJCbfTHpxrWoZ9C6/VxOf7ic219I8xZZFdrf0=
golang.org/x/term v0.45.0/go.mod h1:9aqxs0blBcrm/n0L9QW0aRVD+ktan8ssZromtqJC43w=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.36.0 h1:JfKh3XmcRPqZPKevfXVpI1wXPTqbkE5f7JA92a55Yxg=
golang.org/x/text v0.36.0/go.mod h1:NIdBknypM8iqVmPiuco0Dh6P5Jcdk8lJL0CUebqK164=
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/text v0.41.0 h1:vz/seA0lnX87Othu2f/0L24RcgrXD9/YFTSuGjj3rH8=
golang.org/x/text v0.41.0/go.mod h1:jvf1O8ajNzZqhSrQBPbutR/EB83Cc0CFrezNQIwbb5M=
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.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.43.0 h1:12BdW9CeB3Z+J/I/wj34VMl8X+fEXBxVR90JeMX5E7s=
golang.org/x/tools v0.43.0/go.mod h1:uHkMso649BX2cZK6+RpuIPXS3ho2hZo4FVwfoy1vIk0=
golang.org/x/tools v0.48.0 h1:3+hClM1aLL5mjMKm5ovokw9epgRXPuu2tILgismM6RE=
golang.org/x/tools v0.48.0/go.mod h1:08xX0orndb/F7jJxGDicx061tyd5pcMto75YMAXr6lk=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
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=
gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4=
gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217 h1:gRkg/vSppuSQoDjxyiGfN4Upv/h/DQmIR10ZU8dh4Ww=
google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260723215102-3fe39f3c1018 h1:yXIvV9x4Vu2wUs2cCW8puVLHAjZkuipNK1MnTCZ0Jo0=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260723215102-3fe39f3c1018/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
google.golang.org/grpc v1.79.3 h1:sybAEdRIEtvcD68Gx7dmnwjZKlyfuc61Dyo9pGXXkKE=
google.golang.org/grpc v1.79.3/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ=
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 v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE=
google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
google.golang.org/protobuf v1.36.11/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=
@@ -493,7 +489,5 @@ gvisor.dev/gvisor v0.0.0-20250523182742-eede7a881b20 h1:0DxLu8hxI1OGp1qVRPqNd+2k
gvisor.dev/gvisor v0.0.0-20250523182742-eede7a881b20/go.mod h1:3r5CMtNQMKIvBlrmM9xWUNamjKBYPOWyXOjmg5Kts3g=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50=
pgregory.net/rapid v1.2.0 h1:keKAYRcjm+e1F0oAuU5F5+YPAWcyxNNRK2wud503Gnk=
pgregory.net/rapid v1.2.0/go.mod h1:PY5XlDGj0+V1FCq0o192FdRhpKHGTRIWBgqjDBTrq04=
rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=
+46
View File
@@ -0,0 +1,46 @@
[Unit]
Description=GO Simple Tunnel
Documentation=https://gost.run/
After=network-online.target nss-lookup.target
ConditionPathExists=/etc/gost/gost.yml
[Service]
Type=simple
WorkingDirectory=/run/gost
ExecStart=/usr/bin/gost -C /etc/gost/gost.yml
ExecReload=/bin/kill -SIGHUP $MAINPID
DynamicUser=yes
ConfigurationDirectory=gost
RuntimeDirectory=gost
LogsDirectory=gost
TimeoutStopSec=5s
Restart=on-failure
RestartSec=1s
AmbientCapabilities=CAP_NET_ADMIN CAP_NET_BIND_SERVICE
CapabilityBoundingSet=CAP_NET_ADMIN CAP_NET_BIND_SERVICE
PrivateTmp=yes
PrivateDevices=no
ProtectSystem=full
ProtectHostname=yes
ProtectHome=yes
ProtectKernelTunables=yes
ProtectKernelLogs=yes
ProtectKernelModules=yes
ProtectControlGroups=yes
ProtectClock=yes
MemoryDenyWriteExecute=yes
NoNewPrivileges=yes
SystemCallFilter=~@privileged @mount @debug @cpu-emulation @obsolete
SystemCallFilter=@chown
SystemCallErrorNumber=EPERM
SystemCallArchitectures=native
RestrictNamespaces=yes
RestrictSUIDSGID=yes
LockPersonality=yes
[Install]
WantedBy=multi-user.target
+1 -1
View File
@@ -1,4 +1,4 @@
FROM alpine:3.22
# add tools needed by e2e containers and health checks
RUN apk add --no-cache iptables curl netcat-openbsd python3
RUN apk add --no-cache iptables curl netcat-openbsd python3 openssl
+1 -1
View File
@@ -35,7 +35,7 @@ tests/e2e/
│ └── udp_echo.py # UDP echo server (reflects payloads)
├── testdata/ # config files or data files for running cases
├── shadowsocks_test.go # Shadowsocks protocol tests
└── parallel_selector_test.go # Parallel node selector tests
└── selector_test.go # Node selector tests (round-robin, fifo, backup, parallel)
```
### How it works
+111
View File
@@ -0,0 +1,111 @@
package e2e
import (
"context"
"fmt"
"io"
"testing"
"github.com/stretchr/testify/suite"
"github.com/testcontainers/testcontainers-go"
)
// AdmissionSuite verifies service-level admission control, which gates
// connections by the client's source address. It contrasts a loopback client
// (curl run inside the gost container, source 127.0.0.1) against an external
// client (curl run inside the echo container, source = its container IP).
type AdmissionSuite struct {
suite.Suite
ctx context.Context
echoC testcontainers.Container
echoIP string
}
func (s *AdmissionSuite) SetupSuite() {
s.ctx = context.Background()
echoC, err := RunEchoContainer(s.ctx, SharedNetworkName)
s.Require().NoError(err)
s.echoC = echoC
echoIP, err := echoC.ContainerIP(s.ctx)
s.Require().NoError(err)
s.echoIP = echoIP
}
func (s *AdmissionSuite) TearDownSuite() {
if s.echoC != nil {
s.echoC.Terminate(s.ctx)
}
}
// curlIn runs curl inside the given container, proxying through the gost proxy
// at proxyAddr to the echo server, and returns the response body. An admitted
// request returns "hello-gost"; a denied connection returns an empty body.
func (s *AdmissionSuite) curlIn(c testcontainers.Container, proxyAddr string) string {
cmd := []string{
"curl", "-s",
"-x", fmt.Sprintf("http://%s", proxyAddr),
fmt.Sprintf("http://%s:5678", s.echoIP),
}
_, out, err := c.Exec(s.ctx, cmd)
s.Require().NoError(err)
body, err := io.ReadAll(out)
s.Require().NoError(err)
return string(body)
}
// TestWhitelistAdmitLoopback verifies that a whitelisted source (127.0.0.1,
// a loopback client inside the gost container) is admitted and reaches the
// echo server.
func (s *AdmissionSuite) TestWhitelistAdmitLoopback() {
gostC, err := RunGostContainerWithOptions(s.ctx, SharedNetworkName,
"testdata/admission/whitelist.yaml", []string{"gost-proxy"}, []string{"8080/tcp"})
s.Require().NoError(err)
defer gostC.Terminate(s.ctx)
body := s.curlIn(gostC, "127.0.0.1:8080")
s.Require().Contains(body, "hello-gost")
}
// TestWhitelistDenyExternal verifies that a non-whitelisted source (an external
// container, not 127.0.0.1) is denied, so the request never reaches the echo
// server.
func (s *AdmissionSuite) TestWhitelistDenyExternal() {
gostC, err := RunGostContainerWithOptions(s.ctx, SharedNetworkName,
"testdata/admission/whitelist.yaml", []string{"gost-proxy"}, []string{"8080/tcp"})
s.Require().NoError(err)
defer gostC.Terminate(s.ctx)
body := s.curlIn(s.echoC, "gost-proxy:8080")
s.Require().NotContains(body, "hello-gost")
}
// TestBlacklistDenyLoopback verifies that a blacklisted source (127.0.0.1,
// a loopback client inside the gost container) is denied.
func (s *AdmissionSuite) TestBlacklistDenyLoopback() {
gostC, err := RunGostContainerWithOptions(s.ctx, SharedNetworkName,
"testdata/admission/blacklist.yaml", []string{"gost-proxy"}, []string{"8080/tcp"})
s.Require().NoError(err)
defer gostC.Terminate(s.ctx)
body := s.curlIn(gostC, "127.0.0.1:8080")
s.Require().NotContains(body, "hello-gost")
}
// TestBlacklistAdmitExternal verifies that a source outside the blacklist (an
// external container, not 127.0.0.1) is admitted and reaches the echo server.
func (s *AdmissionSuite) TestBlacklistAdmitExternal() {
gostC, err := RunGostContainerWithOptions(s.ctx, SharedNetworkName,
"testdata/admission/blacklist.yaml", []string{"gost-proxy"}, []string{"8080/tcp"})
s.Require().NoError(err)
defer gostC.Terminate(s.ctx)
body := s.curlIn(s.echoC, "gost-proxy:8080")
s.Require().Contains(body, "hello-gost")
}
func TestAdmissionSuite(t *testing.T) {
suite.Run(t, new(AdmissionSuite))
}
+123
View File
@@ -0,0 +1,123 @@
package e2e
import (
"context"
"fmt"
"io"
"testing"
"github.com/stretchr/testify/suite"
"github.com/testcontainers/testcontainers-go"
)
// AuthSuite verifies proxy authentication on the HTTP handler, using both
// inline single-user auth and a named auther with multiple users. Requests
// carry proxy credentials via curl's -x http://user:pass@host form; a rejected
// request gets a 407 and never reaches the echo server.
type AuthSuite struct {
suite.Suite
ctx context.Context
echoC testcontainers.Container
echoIP string
}
func (s *AuthSuite) SetupSuite() {
s.ctx = context.Background()
echoC, err := RunEchoContainer(s.ctx, SharedNetworkName)
s.Require().NoError(err)
s.echoC = echoC
echoIP, err := echoC.ContainerIP(s.ctx)
s.Require().NoError(err)
s.echoIP = echoIP
}
func (s *AuthSuite) TearDownSuite() {
if s.echoC != nil {
s.echoC.Terminate(s.ctx)
}
}
// proxyRequest sends one request through the gost proxy using the given
// userinfo ("user:pass", or "" for no credentials) and returns the response
// body. An authenticated request returns "hello-gost"; a rejected one returns
// an empty body.
func (s *AuthSuite) proxyRequest(gostC testcontainers.Container, userinfo string) string {
proxy := "http://127.0.0.1:8080"
if userinfo != "" {
proxy = fmt.Sprintf("http://%s@127.0.0.1:8080", userinfo)
}
cmd := []string{
"curl", "-s",
"-x", proxy,
fmt.Sprintf("http://%s:5678", s.echoIP),
}
_, out, err := gostC.Exec(s.ctx, cmd)
s.Require().NoError(err)
body, err := io.ReadAll(out)
s.Require().NoError(err)
return string(body)
}
func (s *AuthSuite) runGost(cfg string) testcontainers.Container {
gostC, err := RunGostContainerWithPorts(s.ctx, SharedNetworkName, cfg, "8080/tcp")
s.Require().NoError(err)
return gostC
}
// TestInlineAuthValid verifies that correct inline credentials are accepted.
func (s *AuthSuite) TestInlineAuthValid() {
gostC := s.runGost("testdata/auth/inline.yaml")
defer gostC.Terminate(s.ctx)
s.Require().Contains(s.proxyRequest(gostC, "user:pass"), "hello-gost")
}
// TestInlineAuthWrongPassword verifies that a wrong password is rejected.
func (s *AuthSuite) TestInlineAuthWrongPassword() {
gostC := s.runGost("testdata/auth/inline.yaml")
defer gostC.Terminate(s.ctx)
s.Require().NotContains(s.proxyRequest(gostC, "user:wrong"), "hello-gost")
}
// TestInlineAuthMissing verifies that a request without credentials is rejected.
func (s *AuthSuite) TestInlineAuthMissing() {
gostC := s.runGost("testdata/auth/inline.yaml")
defer gostC.Terminate(s.ctx)
s.Require().NotContains(s.proxyRequest(gostC, ""), "hello-gost")
}
// TestNamedAutherFirstUser verifies that the first user in a named auther is
// accepted.
func (s *AuthSuite) TestNamedAutherFirstUser() {
gostC := s.runGost("testdata/auth/auther.yaml")
defer gostC.Terminate(s.ctx)
s.Require().Contains(s.proxyRequest(gostC, "alice:secret"), "hello-gost")
}
// TestNamedAutherSecondUser verifies that any user in a named auther, not just
// the first, is accepted.
func (s *AuthSuite) TestNamedAutherSecondUser() {
gostC := s.runGost("testdata/auth/auther.yaml")
defer gostC.Terminate(s.ctx)
s.Require().Contains(s.proxyRequest(gostC, "bob:hunter2"), "hello-gost")
}
// TestNamedAutherInvalid verifies that credentials not in the named auther are
// rejected.
func (s *AuthSuite) TestNamedAutherInvalid() {
gostC := s.runGost("testdata/auth/auther.yaml")
defer gostC.Terminate(s.ctx)
s.Require().NotContains(s.proxyRequest(gostC, "alice:wrong"), "hello-gost")
}
func TestAuthSuite(t *testing.T) {
suite.Run(t, new(AuthSuite))
}
+116
View File
@@ -0,0 +1,116 @@
package e2e
import (
"context"
"fmt"
"io"
"os"
"testing"
"github.com/stretchr/testify/suite"
"github.com/testcontainers/testcontainers-go"
)
type BypassSuite struct {
suite.Suite
ctx context.Context
echoC testcontainers.Container
echoIP string
}
func (s *BypassSuite) SetupSuite() {
s.ctx = context.Background()
echoC, err := RunEchoContainer(s.ctx, SharedNetworkName)
s.Require().NoError(err)
s.echoC = echoC
echoIP, err := echoC.ContainerIP(s.ctx)
s.Require().NoError(err)
s.echoIP = echoIP
}
func (s *BypassSuite) TearDownSuite() {
if s.echoC != nil {
s.echoC.Terminate(s.ctx)
}
}
// proxyRequest sends one request through the gost proxy and returns the
// response body. A bypassed (direct) request that reaches the echo server
// returns "hello-gost"; a request forced through the dead chain node returns
// a 503 and no echo body.
func (s *BypassSuite) proxyRequest(gostC testcontainers.Container, port, target string) string {
cmd := []string{
"curl", "-s",
"-x", fmt.Sprintf("http://127.0.0.1:%s", port),
target,
}
_, out, err := gostC.Exec(s.ctx, cmd)
s.Require().NoError(err)
body, err := io.ReadAll(out)
s.Require().NoError(err)
return string(body)
}
// TestBlacklistBypassDirect verifies that a destination matching a blacklist
// bypass rule skips the (dead) chain node and connects directly to the echo
// server, so the request succeeds despite the dead node.
func (s *BypassSuite) TestBlacklistBypassDirect() {
cfg, err := RenderConfig("testdata/bypass/blacklist.yaml", ConfigData{ServerAddr: s.echoIP})
s.Require().NoError(err)
defer os.Remove(cfg)
gostC, err := RunGostContainerWithPorts(s.ctx, SharedNetworkName, cfg, "8080/tcp")
s.Require().NoError(err)
defer gostC.Terminate(s.ctx)
body := s.proxyRequest(gostC, "8080", fmt.Sprintf("http://%s:5678", s.echoIP))
s.Require().Contains(body, "hello-gost")
}
// TestBlacklistBypassViaChain verifies that a destination NOT matching the
// blacklist bypass is routed through the chain node. With the node dead the
// request never reaches the echo server, confirming the prior success was due
// to the bypass.
func (s *BypassSuite) TestBlacklistBypassViaChain() {
cfg, err := RenderConfig("testdata/bypass/blacklist.yaml", ConfigData{ServerAddr: s.echoIP})
s.Require().NoError(err)
defer os.Remove(cfg)
gostC, err := RunGostContainerWithPorts(s.ctx, SharedNetworkName, cfg, "8080/tcp")
s.Require().NoError(err)
defer gostC.Terminate(s.ctx)
body := s.proxyRequest(gostC, "8080", "http://10.0.0.1:5678")
s.Require().NotContains(body, "hello-gost")
}
// TestWhitelistBypassDirect verifies that a destination outside the whitelist
// rule is bypassed (connects directly) and reaches the echo server even though
// the chain node is dead.
func (s *BypassSuite) TestWhitelistBypassDirect() {
gostC, err := RunGostContainerWithPorts(s.ctx, SharedNetworkName, "testdata/bypass/whitelist.yaml", "8080/tcp")
s.Require().NoError(err)
defer gostC.Terminate(s.ctx)
body := s.proxyRequest(gostC, "8080", fmt.Sprintf("http://%s:5678", s.echoIP))
s.Require().Contains(body, "hello-gost")
}
// TestWhitelistBypassViaChain verifies that a destination matching the
// whitelist rule is forced through the chain node; with the node dead the
// request never reaches the echo server.
func (s *BypassSuite) TestWhitelistBypassViaChain() {
gostC, err := RunGostContainerWithPorts(s.ctx, SharedNetworkName, "testdata/bypass/whitelist.yaml", "8080/tcp")
s.Require().NoError(err)
defer gostC.Terminate(s.ctx)
body := s.proxyRequest(gostC, "8080", "http://10.0.0.1:5678")
s.Require().NotContains(body, "hello-gost")
}
func TestBypassSuite(t *testing.T) {
suite.Run(t, new(BypassSuite))
}
+106
View File
@@ -0,0 +1,106 @@
package e2e
import (
"context"
"fmt"
"io"
"strings"
"testing"
"github.com/stretchr/testify/suite"
"github.com/testcontainers/testcontainers-go"
)
type ChainGroupSuite struct {
suite.Suite
ctx context.Context
echoC testcontainers.Container
echoIP string
}
func (s *ChainGroupSuite) SetupSuite() {
s.ctx = context.Background()
echoC, err := RunEchoContainer(s.ctx, SharedNetworkName)
s.Require().NoError(err)
s.echoC = echoC
echoIP, err := echoC.ContainerIP(s.ctx)
s.Require().NoError(err)
s.echoIP = echoIP
}
func (s *ChainGroupSuite) TearDownSuite() {
if s.echoC != nil {
s.echoC.Terminate(s.ctx)
}
}
// proxyRequest sends a request through the gost proxy, using the given
// target hostname (not IP) so that Host() matchers at the chain-group level
// can match against it.
func (s *ChainGroupSuite) proxyRequestHost(gostC testcontainers.Container, proxyPort, targetHost string) (int, string) {
cmd := []string{
"curl", "-s",
"-x", fmt.Sprintf("http://127.0.0.1:%s", proxyPort),
fmt.Sprintf("http://%s:5678", targetHost),
}
code, out, err := gostC.Exec(s.ctx, cmd)
s.Require().NoError(err)
body, err := io.ReadAll(out)
s.Require().NoError(err)
return code, string(body)
}
// proxyRequestIP sends a request via IP (like existing tests do).
func (s *ChainGroupSuite) proxyRequestIP(gostC testcontainers.Container, port string) (int, string) {
return s.proxyRequestHost(gostC, port, s.echoIP)
}
// TestMatcherRoutesByHost verifies that a chainGroup with per-entry Host() matchers
// routes traffic to the chain whose matcher matches the target hostname. The
// non-matching chain has a dead relay, so if the matcher fails to filter, requests
// would fail.
func (s *ChainGroupSuite) TestMatcherRoutesByHost() {
gostC, err := RunGostContainerWithPorts(s.ctx, SharedNetworkName, "testdata/chaingroup/matcher.yaml", "8080/tcp")
s.Require().NoError(err)
defer gostC.Terminate(s.ctx)
// Requests with hostname "tcp-echo" (the echo container's network alias)
// must match Host("tcp-echo") → chain-target → live relay → echo server.
for range 10 {
code, body := s.proxyRequestHost(gostC, "8080", "tcp-echo")
s.Require().Equal(0, code, "Host('tcp-echo') must match the target chain")
s.Require().Contains(body, "hello-gost")
}
}
// TestProbeMarksDeadChain verifies that a TCP probe detects a dead chain entry
// and marks it before real traffic, so the FailFilter excludes it. With the dead
// entry pre-marked, every request succeeds.
func (s *ChainGroupSuite) TestProbeMarksDeadChain() {
gostC, err := RunGostContainerWithPorts(s.ctx, SharedNetworkName, "testdata/chaingroup/probe.yaml", "8080/tcp")
s.Require().NoError(err)
defer gostC.Terminate(s.ctx)
// The probe fires immediately at startup; by the time we send requests
// the dead chain entry is already marked. All requests converge to the
// live chain.
failures := 0
for range 10 {
code, body := s.proxyRequestIP(gostC, "8080")
if code != 0 || !strings.Contains(body, "hello-gost") {
failures++
}
}
// At most one failure is acceptable (the first request might race with
// the probe's first Mark). Every request after must succeed.
s.Require().LessOrEqual(failures, 0,
"probe must pre-mark the dead chain entry; all requests must succeed")
}
func TestChainGroupSuite(t *testing.T) {
suite.Run(t, new(ChainGroupSuite))
}
+143
View File
@@ -0,0 +1,143 @@
package e2e
import (
"context"
"encoding/base64"
"fmt"
"io"
"strings"
"testing"
"time"
"github.com/moby/moby/client"
"github.com/stretchr/testify/suite"
"github.com/testcontainers/testcontainers-go"
"github.com/testcontainers/testcontainers-go/wait"
)
type FailCodesSuite struct {
suite.Suite
ctx context.Context
echoC testcontainers.Container
echoIP string
statusC testcontainers.Container
}
func (s *FailCodesSuite) SetupSuite() {
s.ctx = context.Background()
echoC, err := RunEchoContainer(s.ctx, SharedNetworkName)
s.Require().NoError(err)
s.echoC = echoC
echoIP, err := echoC.ContainerIP(s.ctx)
s.Require().NoError(err)
s.echoIP = echoIP
statusC, err := RunStatusBackendContainer(s.ctx, SharedNetworkName, 429)
s.Require().NoError(err)
s.statusC = statusC
}
func (s *FailCodesSuite) TearDownSuite() {
if s.echoC != nil {
s.echoC.Terminate(s.ctx)
}
if s.statusC != nil {
s.statusC.Terminate(s.ctx)
}
}
func (s *FailCodesSuite) sendRawHTTP(gostC testcontainers.Container, host, port string) string {
req := fmt.Sprintf(
"GET / HTTP/1.1\r\nHost: %s\r\nConnection: close\r\n\r\n",
s.echoIP,
)
encoded := base64.StdEncoding.EncodeToString([]byte(req))
cmd := []string{"sh", "-c",
fmt.Sprintf("echo %s | base64 -d | nc -w 5 %s %s", encoded, host, port)}
_, out, _ := gostC.Exec(s.ctx, cmd)
b, _ := io.ReadAll(out)
return string(b)
}
// TestFailCodesConvergence: reverse proxy (tcp handler + sniffing) with FIFO.
// node-429 is first. After the first 429 response, failCodes marks it (Count=1).
// FailFilter (maxFails=1) excludes it. All subsequent requests go to node-good.
func (s *FailCodesSuite) TestFailCodesConvergence() {
gostC, err := RunGostContainerWithFiles(s.ctx, SharedNetworkName,
"testdata/failcodes/failcodes.yaml",
nil,
"8080/tcp",
)
s.Require().NoError(err)
defer func() {
DumpLogs(s.T(), s.ctx, "gost-failcodes", gostC)
gostC.Terminate(s.ctx)
}()
time.Sleep(500 * time.Millisecond)
const totalRequests = 20
var successCount, failCount int
for range totalRequests {
body := s.sendRawHTTP(gostC, "127.0.0.1", "8080")
if strings.Contains(body, "hello-gost") {
successCount++
s.T().Logf(" ✓ 200 (node-good)")
} else if strings.Contains(body, "status-429") {
failCount++
s.T().Logf(" ✗ 429 (node-429)")
} else {
s.T().Logf(" ? %s", strings.TrimSpace(body)[:80])
}
}
s.T().Logf("Results: %d successes, %d failures out of %d requests",
successCount, failCount, totalRequests)
// After the first 429 marks node-429, FailFilter excludes it for 10s.
// At most 1-2 requests may fail before the marker is set.
s.Require().LessOrEqual(failCount, 2,
"failCodes: node-429 must be excluded after first 429 response. "+
"Got %d failures out of %d requests", failCount, totalRequests)
}
func TestFailCodesSuite(t *testing.T) {
suite.Run(t, new(FailCodesSuite))
}
func RunStatusBackendContainer(ctx context.Context, networkName string, statusCode int) (testcontainers.Container, error) {
req := testcontainers.ContainerRequest{
FromDockerfile: testcontainers.FromDockerfile{
Context: ".",
Dockerfile: "Dockerfile",
Repo: "gost-e2e",
Tag: "latest",
KeepImage: true,
BuildOptionsModifier: func(opts *client.ImageBuildOptions) {
opts.NetworkMode = "host"
},
},
Networks: []string{networkName},
NetworkAliases: map[string][]string{
networkName: {"status-backend"},
},
Files: []testcontainers.ContainerFile{
{HostFilePath: "scripts/http_status_backend.py", ContainerFilePath: "/scripts/http_status_backend.py", FileMode: 0644},
},
ExposedPorts: []string{"5680/tcp"},
Cmd: []string{
"python3", "/scripts/http_status_backend.py",
fmt.Sprintf("%d", statusCode), "5680",
},
WaitingFor: wait.ForExposedPort(),
}
return testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{
ContainerRequest: req,
Started: true,
})
}
+27
View File
@@ -286,6 +286,33 @@ func (s *ForwardSuite) TestTCPForwardMultiNodeProtocol() {
"HTTP request should route to the http-protocol node via protocol filtering")
}
// TestUDPForwardQUICSniffing verifies that the UDP forward handler with
// sniffing enabled correctly detects QUIC traffic and forwards the
// datagram. A pre-built QUIC Initial packet is sent through the proxy
// to the UDP echo server; the test passes when the echoed datagram
// comes back with the same length.
func (s *ForwardSuite) TestUDPForwardQUICSniffing() {
gostC, err := RunGostContainerWithFiles(s.ctx, SharedNetworkName,
"testdata/forward/server_udp_sniffing_quic.yaml",
[]testcontainers.ContainerFile{
{HostFilePath: "scripts/udp_quic_forward_test.py", ContainerFilePath: "/scripts/udp_quic_forward_test.py", FileMode: 0644},
},
"9000/udp")
s.Require().NoError(err)
defer gostC.Terminate(s.ctx)
code, out, err := gostC.Exec(s.ctx, []string{
"python3", "/scripts/udp_quic_forward_test.py",
"127.0.0.1", "9000",
})
output, _ := io.ReadAll(out)
s.T().Logf("udp quic forward output:\n%s", string(output))
if code != 0 {
DumpLogs(s.T(), s.ctx, "udp-quic-forward logs", gostC)
}
s.Require().Equal(0, code, "udp quic forward test script should exit 0")
}
func TestForwardSuite(t *testing.T) {
suite.Run(t, new(ForwardSuite))
}
+85
View File
@@ -0,0 +1,85 @@
package e2e
import (
"context"
"fmt"
"io"
"os"
"testing"
"github.com/stretchr/testify/suite"
"github.com/testcontainers/testcontainers-go"
)
// HostsSuite verifies the static hosts mapping (HostMapper), which overrides
// DNS for matched hostnames. A mapped hostname resolves to the configured IP
// and reaches the echo server; an unmapped hostname fails to resolve.
type HostsSuite struct {
suite.Suite
ctx context.Context
echoC testcontainers.Container
echoIP string
gostC testcontainers.Container
}
func (s *HostsSuite) SetupSuite() {
s.ctx = context.Background()
echoC, err := RunEchoContainer(s.ctx, SharedNetworkName)
s.Require().NoError(err)
s.echoC = echoC
echoIP, err := echoC.ContainerIP(s.ctx)
s.Require().NoError(err)
s.echoIP = echoIP
cfg, err := RenderConfig("testdata/hosts/hosts.yaml", ConfigData{ServerAddr: s.echoIP})
s.Require().NoError(err)
defer os.Remove(cfg)
gostC, err := RunGostContainerWithPorts(s.ctx, SharedNetworkName, cfg, "8080/tcp")
s.Require().NoError(err)
s.gostC = gostC
}
func (s *HostsSuite) TearDownSuite() {
if s.gostC != nil {
s.gostC.Terminate(s.ctx)
}
if s.echoC != nil {
s.echoC.Terminate(s.ctx)
}
}
// proxyRequest sends a request to the given hostname through the gost proxy and
// returns the response body. A resolvable host that reaches the echo server
// returns "hello-gost"; an unresolvable host returns an empty body.
func (s *HostsSuite) proxyRequest(host string) string {
cmd := []string{
"curl", "-s",
"-x", "http://127.0.0.1:8080",
fmt.Sprintf("http://%s:5678", host),
}
_, out, err := s.gostC.Exec(s.ctx, cmd)
s.Require().NoError(err)
body, err := io.ReadAll(out)
s.Require().NoError(err)
return string(body)
}
// TestMappedHostname verifies that a hostname in the hosts mapping resolves to
// the configured IP and reaches the echo server.
func (s *HostsSuite) TestMappedHostname() {
s.Require().Contains(s.proxyRequest("echo.internal"), "hello-gost")
}
// TestUnmappedHostname verifies that a hostname absent from the mapping fails to
// resolve, so the request never reaches the echo server.
func (s *HostsSuite) TestUnmappedHostname() {
s.Require().NotContains(s.proxyRequest("nomap.internal"), "hello-gost")
}
func TestHostsSuite(t *testing.T) {
suite.Run(t, new(HostsSuite))
}
+257
View File
@@ -0,0 +1,257 @@
package e2e
import (
"context"
"io"
"strings"
"testing"
"time"
"github.com/moby/moby/client"
"github.com/stretchr/testify/suite"
"github.com/testcontainers/testcontainers-go"
"github.com/testcontainers/testcontainers-go/wait"
)
type HTTPCacheSuite struct {
suite.Suite
ctx context.Context
beC testcontainers.Container
staleC testcontainers.Container
}
func (s *HTTPCacheSuite) SetupSuite() {
s.ctx = context.Background()
s.T().Log("start http cache backend container...")
beC, err := RunCacheBackendContainer(s.ctx, SharedNetworkName)
s.Require().NoError(err)
s.beC = beC
s.T().Log("start http cache serve-stale backend container...")
staleC, err := RunServeStaleBackendContainer(s.ctx, SharedNetworkName)
s.Require().NoError(err)
s.staleC = staleC
}
func (s *HTTPCacheSuite) TearDownSuite() {
if s.beC != nil {
s.beC.Terminate(s.ctx)
}
if s.staleC != nil {
s.staleC.Terminate(s.ctx)
}
}
// TestHTTPCacheHit verifies that the HTTP response cache returns the cached
// response on repeat requests. The test backend returns a monotonically
// increasing counter. With cache enabled, every request returns the same
// counter value (the cached response from the first request).
func (s *HTTPCacheSuite) TestHTTPCacheHit() {
gostC, err := RunGostContainerWithPorts(s.ctx, SharedNetworkName,
"testdata/http_cache/server_cache.yaml", "8080/tcp")
s.Require().NoError(err)
defer gostC.Terminate(s.ctx)
cmd := []string{"curl", "-v", "-s", "http://127.0.0.1:8080/"}
// First request — cache miss, backend returns "cache-test-N"
code, out, err := ExecOutput(s.ctx, gostC, cmd)
s.Require().NoError(err)
body1, _ := io.ReadAll(out)
if code != 0 || !strings.Contains(string(body1), "cache-test-") {
DumpLogs(s.T(), s.ctx, "cache-proxy logs (first req)", gostC)
}
s.Require().Equal(0, code, "first request should succeed")
// The backend counter is shared across subtests (a fresh counter backend is
// not started per test), so the first request here is not necessarily #1.
// Caching is proven by body2==body1 and body3==body1 below.
s.Require().Contains(string(body1), "cache-test-",
"first request should get a backend response")
// Second request — cache hit, same response
code, out, err = ExecOutput(s.ctx, gostC, cmd)
s.Require().NoError(err)
body2, _ := io.ReadAll(out)
if code != 0 {
DumpLogs(s.T(), s.ctx, "cache-proxy logs (second req)", gostC)
}
s.Require().Equal(0, code, "second request should succeed")
s.Require().Equal(string(body1), string(body2),
"cached response should equal first response")
// Third request — cache hit, same
code, out, err = ExecOutput(s.ctx, gostC, cmd)
s.Require().NoError(err)
body3, _ := io.ReadAll(out)
if code != 0 {
DumpLogs(s.T(), s.ctx, "cache-proxy logs (third req)", gostC)
}
s.Require().Equal(0, code, "third request should succeed")
s.Require().Equal(string(body1), string(body3),
"cached response should be consistent")
}
// TestHTTPCacheDisabled verifies that without a named cache, every request
// reaches the backend and gets a fresh response (monotonically increasing
// counter). This is the control test: it proves the counting backend works
// and that the cache test actually validates caching, not incidental
// idempotency of the backend.
func (s *HTTPCacheSuite) TestHTTPCacheDisabled() {
gostC, err := RunGostContainerWithPorts(s.ctx, SharedNetworkName,
"testdata/http_cache/server_nocache.yaml", "8080/tcp")
s.Require().NoError(err)
defer gostC.Terminate(s.ctx)
cmd := []string{"curl", "-v", "-s", "http://127.0.0.1:8080/"}
code, out, err := ExecOutput(s.ctx, gostC, cmd)
s.Require().NoError(err)
body1, _ := io.ReadAll(out)
if code != 0 || !strings.Contains(string(body1), "cache-test-") {
DumpLogs(s.T(), s.ctx, "no-cache proxy logs (first req)", gostC)
}
s.Require().Equal(0, code)
code, out, err = ExecOutput(s.ctx, gostC, cmd)
s.Require().NoError(err)
body2, _ := io.ReadAll(out)
if code != 0 {
DumpLogs(s.T(), s.ctx, "no-cache proxy logs (second req)", gostC)
}
s.Require().Equal(0, code)
// Without a named cache, each request hits the backend directly.
// The backend increments a counter per request, so consecutive
// requests return different bodies.
s.Require().NotEqual(string(body1), string(body2),
"without cache, consecutive requests should return different responses")
}
// TestHTTPCacheServeStale verifies that a stale (expired) cached response is
// served when the upstream fetch fails and cache.serveStale is enabled.
//
// The serve-stale backend serves one HTTP request, then shuts down its HTTP
// server and switches to accepting TCP connections and immediately closing
// them. This makes the upstream dial succeed but the HTTP response read fail,
// which triggers the serve-stale path.
//
// Steps:
// 1. First request: cache miss, backend responds → cache stores the response
// with TTL 3s. The backend then switches to connection-dropping mode.
// 2. Wait 4s for the cache entry to expire.
// 3. Second request: cache hit (stale), dial succeeds (TCP acceptor accepts),
// but no HTTP response arrives → http.ReadResponse fails → serveStale
// returns the cached "cache-test-1".
func (s *HTTPCacheSuite) TestHTTPCacheServeStale() {
gostC, err := RunGostContainerWithPorts(s.ctx, SharedNetworkName,
"testdata/http_cache/server_cache_servestale.yaml", "8080/tcp")
s.Require().NoError(err)
defer gostC.Terminate(s.ctx)
cmd := []string{"curl", "-v", "-s", "http://127.0.0.1:8080/"}
// First request — cache miss, backend returns "cache-test-1"
code, out, err := ExecOutput(s.ctx, gostC, cmd)
s.Require().NoError(err)
body1, _ := io.ReadAll(out)
if code != 0 || !strings.Contains(string(body1), "cache-test-") {
DumpLogs(s.T(), s.ctx, "serve-stale logs (first req)", gostC)
}
s.Require().Equal(0, code, "first request should succeed")
s.Require().Contains(string(body1), "cache-test-1",
"first request should get backend response #1")
// At this point the backend has shut down its HTTP server and switched to
// connection-dropping mode. Wait for cache TTL (3s) to expire.
time.Sleep(4 * time.Second)
// Second request — stale cache hit, upstream connects but yields no
// response → serve-stale returns the expired cached response.
code, out, err = ExecOutput(s.ctx, gostC, cmd)
s.Require().NoError(err)
body2, _ := io.ReadAll(out)
if code != 0 || !strings.Contains(string(body2), "cache-test-") {
DumpLogs(s.T(), s.ctx, "serve-stale logs (second req)", gostC)
}
s.Require().Equal(0, code, "serve-stale should return the cached response")
s.Require().Equal(string(body1), string(body2),
"serve-stale should return the expired cached response")
}
func TestHTTPCacheSuite(t *testing.T) {
suite.Run(t, new(HTTPCacheSuite))
}
// RunCacheBackendContainer starts a container running the cache test HTTP server
// that returns a monotonically increasing counter in each response body.
func RunCacheBackendContainer(ctx context.Context, networkName string) (testcontainers.Container, error) {
req := cacheBackendContainerRequest(ctx, networkName)
return testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{
ContainerRequest: req,
Started: true,
})
}
func cacheBackendContainerRequest(_ context.Context, networkName string) testcontainers.ContainerRequest {
return testcontainers.ContainerRequest{
FromDockerfile: testcontainers.FromDockerfile{
Context: ".",
Dockerfile: "Dockerfile",
Repo: "gost-e2e",
Tag: "latest",
KeepImage: true,
BuildOptionsModifier: func(opts *client.ImageBuildOptions) {
opts.NetworkMode = "host"
},
},
Networks: []string{networkName},
NetworkAliases: map[string][]string{
networkName: {"cache-backend"},
},
Files: []testcontainers.ContainerFile{
{HostFilePath: "scripts/http_cache_backend.py", ContainerFilePath: "/scripts/http_cache_backend.py", FileMode: 0644},
},
ExposedPorts: []string{"5677/tcp"},
Cmd: []string{"python3", "/scripts/http_cache_backend.py"},
WaitingFor: wait.ForExposedPort(),
}
}
// RunServeStaleBackendContainer starts a container running the serve-stale test
// HTTP server. It serves one request with a counter body, then replaces itself
// with a raw TCP socket that accepts connections and immediately closes them.
// This makes upstream dials succeed but HTTP response reads fail, which
// triggers the serve-stale path in the sniffer.
func RunServeStaleBackendContainer(ctx context.Context, networkName string) (testcontainers.Container, error) {
req := serveStaleBackendContainerRequest(ctx, networkName)
return testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{
ContainerRequest: req,
Started: true,
})
}
func serveStaleBackendContainerRequest(_ context.Context, networkName string) testcontainers.ContainerRequest {
return testcontainers.ContainerRequest{
FromDockerfile: testcontainers.FromDockerfile{
Context: ".",
Dockerfile: "Dockerfile",
Repo: "gost-e2e",
Tag: "latest",
KeepImage: true,
BuildOptionsModifier: func(opts *client.ImageBuildOptions) {
opts.NetworkMode = "host"
},
},
Networks: []string{networkName},
NetworkAliases: map[string][]string{
networkName: {"cache-servestale"},
},
Files: []testcontainers.ContainerFile{
{HostFilePath: "scripts/http_cache_servestale_backend.py", ContainerFilePath: "/scripts/http_cache_servestale_backend.py", FileMode: 0644},
},
ExposedPorts: []string{"5676/tcp"},
Cmd: []string{"python3", "/scripts/http_cache_servestale_backend.py"},
WaitingFor: wait.ForExposedPort(),
}
}
+108
View File
@@ -0,0 +1,108 @@
package e2e
import (
"context"
"encoding/base64"
"encoding/binary"
"fmt"
"hash/crc32"
"io"
"os"
"testing"
"github.com/stretchr/testify/suite"
"github.com/testcontainers/testcontainers-go"
)
// HTTPPolicyBypassSuite reproduces the destination-policy bypass where a valid
// Gost-Target header replaces req.Host (evaluated by the bypass) but, before the
// fix, left req.URL.Host (dialed by the transport) as the absolute-URL host.
// The echo server stands in for the protected backend.
type HTTPPolicyBypassSuite struct {
suite.Suite
ctx context.Context
echoC testcontainers.Container
echoIP string
}
func (s *HTTPPolicyBypassSuite) SetupSuite() {
s.ctx = context.Background()
echoC, err := RunEchoContainer(s.ctx, SharedNetworkName)
s.Require().NoError(err)
s.echoC = echoC
echoIP, err := echoC.ContainerIP(s.ctx)
s.Require().NoError(err)
s.echoIP = echoIP
}
func (s *HTTPPolicyBypassSuite) TearDownSuite() {
if s.echoC != nil {
s.echoC.Terminate(s.ctx)
}
}
// sendRaw sends a raw HTTP request to the proxy inside the gost container.
func (s *HTTPPolicyBypassSuite) sendRaw(gostC testcontainers.Container, data string) string {
encoded := base64.StdEncoding.EncodeToString([]byte(data))
cmd := []string{"sh", "-c",
fmt.Sprintf("echo %s | base64 -d | nc -w 5 127.0.0.1 8080", encoded)}
_, out, _ := gostC.Exec(s.ctx, cmd)
b, _ := io.ReadAll(out)
return string(b)
}
// encodeTarget encodes a host:port in the GOST v2 Gost-Target format:
// raw-URL-base64( big-endian-CRC32(name) + raw-URL-base64(name) ).
func encodeTarget(name string) string {
v := []byte(name)
b := make([]byte, 4)
binary.BigEndian.PutUint32(b, crc32.ChecksumIEEE(v))
inner := base64.RawURLEncoding.EncodeToString(v)
return base64.RawURLEncoding.EncodeToString(append(b, []byte(inner)...))
}
// TestGostTargetPolicyBypass asserts that a Gost-Target header naming an
// allowed authority cannot route a request to a blocked absolute-URL authority.
func (s *HTTPPolicyBypassSuite) TestGostTargetPolicyBypass() {
cfg, err := RenderConfig("testdata/http/server_policy_bypass.yaml", ConfigData{ServerAddr: s.echoIP})
s.Require().NoError(err)
defer os.Remove(cfg)
gostC, err := RunGostContainerWithPorts(s.ctx, SharedNetworkName, cfg, "8080/tcp")
s.Require().NoError(err)
defer gostC.Terminate(s.ctx)
target := fmt.Sprintf("http://%s:5678/", s.echoIP)
marker := "hello-gost"
// Control: absolute-form request to the blocked address, no target header.
// The bypass must deny it before it reaches the echo server.
control := fmt.Sprintf("GET %s HTTP/1.1\r\nHost: %s:5678\r\nConnection: close\r\n\r\n",
target, s.echoIP)
out := s.sendRaw(gostC, control)
s.Require().Contains(out, "403", "control request to blocked address should be denied")
s.Require().NotContains(out, marker, "control request must not reach the backend")
// Exploit: same request plus a valid Gost-Target header naming an allowed
// authority. Before the fix the transport dials the URL host (echo server)
// while the policy checks the header host, leaking the backend response.
exploit := fmt.Sprintf("GET %s HTTP/1.1\r\nHost: %s:5678\r\nGost-Target: %s\r\nConnection: close\r\n\r\n",
target, s.echoIP, encodeTarget("allowed.example.com:80"))
out = s.sendRaw(gostC, exploit)
s.Require().NotContains(out, marker,
"Gost-Target header must not bypass the destination policy to reach the blocked backend")
// Fail-closed control: a malformed target header is ignored, so the request
// is still evaluated (and denied) against the absolute-URL authority.
malformed := fmt.Sprintf("GET %s HTTP/1.1\r\nHost: %s:5678\r\nGost-Target: %s\r\nConnection: close\r\n\r\n",
target, s.echoIP, "not-a-valid-target")
out = s.sendRaw(gostC, malformed)
s.Require().Contains(out, "403", "malformed target header must be ignored and the request denied")
s.Require().NotContains(out, marker, "malformed target header must not reach the backend")
}
func TestHTTPPolicyBypassSuite(t *testing.T) {
suite.Run(t, new(HTTPPolicyBypassSuite))
}
+22
View File
@@ -600,6 +600,28 @@ func (s *HTTPSuite) TestHTTPUDPRelay() {
s.Require().Equal(0, code, "udp relay test script should exit 0")
}
// TestHTTPProxyOriginForm verifies the nginx proxy_pass scenario
// (go-gost/gost#679): an upstream client sends an origin-form request
// ("GET / HTTP/1.1" with only a Host header) instead of the proxy-form
// absolute URL. The proxy must resolve the target from the Host header.
func (s *HTTPSuite) TestHTTPProxyOriginForm() {
gostC, err := RunGostContainerWithPorts(s.ctx, SharedNetworkName,
"testdata/http/server.yaml", "8080/tcp")
s.Require().NoError(err)
defer gostC.Terminate(s.ctx)
// Origin-form request exactly as nginx proxy_pass emits it, with the
// Host header pointing at the echo server via its network alias
// ("tcp-echo", registered on SharedNetworkName).
req := "GET / HTTP/1.1\r\nHost: tcp-echo:5678\r\nConnection: close\r\n\r\n"
out := s.sendRaw(gostC, "127.0.0.1", "8080", req)
if !strings.Contains(out, "200") {
DumpLogs(s.T(), s.ctx, "http-proxy-origin-form logs", gostC)
}
s.Assert().Contains(out, "200 OK", "origin-form request should be proxied by Host header")
s.Assert().Contains(out, "hello-gost", "echo server response body should come through")
}
func TestHTTPSuite(t *testing.T) {
suite.Run(t, new(HTTPSuite))
}
+90
View File
@@ -0,0 +1,90 @@
package e2e
import (
"context"
"fmt"
"io"
"testing"
"github.com/stretchr/testify/suite"
"github.com/testcontainers/testcontainers-go"
)
// IngressSuite verifies hostname→endpoint routing at the reverse-proxy tunnel
// entrypoint. A public gost runs a tunnel handler with an ingress mapping
// example.local→tunnel-UUID and an HTTP entrypoint on :8420. An internal client
// binds a reverse tunnel (tunnel.id=UUID) that forwards to the echo server.
//
// A request to the entrypoint with Host:example.local is routed via the ingress
// table to the tunnel and reaches the echo server ("hello-gost"); a request
// with an unmapped Host matches no ingress rule and is rejected with no route.
type IngressSuite struct {
suite.Suite
ctx context.Context
echoC testcontainers.Container
serverC testcontainers.Container
clientC testcontainers.Container
}
func (s *IngressSuite) SetupSuite() {
s.ctx = context.Background()
echoC, err := RunEchoContainer(s.ctx, SharedNetworkName)
s.Require().NoError(err)
s.echoC = echoC
serverC, err := RunGostContainerWithOptions(s.ctx, SharedNetworkName,
"testdata/ingress/server.yaml", []string{"gost-server"}, []string{"8420/tcp"})
s.Require().NoError(err)
s.serverC = serverC
clientC, err := RunGostContainerWithOptions(s.ctx, SharedNetworkName,
"testdata/ingress/client.yaml", []string{"gost-client"}, []string{"8423/tcp"})
s.Require().NoError(err)
s.clientC = clientC
}
func (s *IngressSuite) TearDownSuite() {
for _, c := range []testcontainers.Container{s.clientC, s.serverC, s.echoC} {
if c != nil {
c.Terminate(s.ctx)
}
}
}
// request sends an HTTP GET to the server entrypoint with the given Host
// header. When waitTunnel is true it retries until the reverse tunnel is bound
// (the mapped-host happy path); otherwise it makes a single attempt (the
// unmapped-host case, which should never reach the echo server). Returns the
// response body.
func (s *IngressSuite) request(host string, waitTunnel bool) string {
loop := "for i in $(seq 1 30); do "
if !waitTunnel {
loop = "for i in 1; do "
}
cmd := []string{
"sh", "-c",
fmt.Sprintf("%sbody=$(curl -s -H 'Host: %s' http://gost-server:8420/); echo \"$body\" | grep -q hello-gost && { echo \"$body\"; exit 0; }; sleep 1; done; echo \"$body\"", loop, host),
}
_, out, err := s.echoC.Exec(s.ctx, cmd)
s.Require().NoError(err)
body, err := io.ReadAll(out)
s.Require().NoError(err)
return string(body)
}
// TestMappedHostname verifies that a Host matching an ingress rule is routed
// through the tunnel to the echo server.
func (s *IngressSuite) TestMappedHostname() {
s.Require().Contains(s.request("example.local", true), "hello-gost")
}
// TestUnmappedHostname verifies that a Host with no ingress rule is rejected
// (no route to host) and never reaches the echo server.
func (s *IngressSuite) TestUnmappedHostname() {
s.Require().NotContains(s.request("nomapped.local", false), "hello-gost")
}
func TestIngressSuite(t *testing.T) {
suite.Run(t, new(IngressSuite))
}
+292
View File
@@ -0,0 +1,292 @@
package e2e
import (
"context"
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"crypto/x509"
"crypto/x509/pkix"
"encoding/pem"
"fmt"
"io"
"math/big"
"net"
"os"
"strings"
"testing"
"time"
"github.com/moby/moby/client"
"github.com/stretchr/testify/suite"
"github.com/testcontainers/testcontainers-go"
"github.com/testcontainers/testcontainers-go/wait"
)
type MTLSSuite struct {
suite.Suite
ctx context.Context
echoC testcontainers.Container
echoIP string
pluginC testcontainers.Container
certDir string
}
func (s *MTLSSuite) SetupSuite() {
s.ctx = context.Background()
certDir, err := os.MkdirTemp("", "gost-mtls-certs-*")
s.Require().NoError(err)
s.certDir = certDir
s.generateCerts()
pluginC, err := runAuthPluginContainer(s.ctx, SharedNetworkName)
s.Require().NoError(err)
s.pluginC = pluginC
echoC, err := RunEchoContainer(s.ctx, SharedNetworkName)
s.Require().NoError(err)
s.echoC = echoC
echoIP, err := echoC.ContainerIP(s.ctx)
s.Require().NoError(err)
s.echoIP = echoIP
}
func (s *MTLSSuite) TearDownSuite() {
if s.pluginC != nil {
s.pluginC.Terminate(s.ctx)
}
if s.echoC != nil {
s.echoC.Terminate(s.ctx)
}
os.RemoveAll(s.certDir)
}
// generateCerts creates a self-signed CA, a server cert signed by the CA,
// and a client cert signed by the CA, writing PEM files to s.certDir.
func (s *MTLSSuite) generateCerts() {
// CA
caKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
s.Require().NoError(err)
caTmpl := &x509.Certificate{
SerialNumber: big.NewInt(1),
Subject: pkix.Name{CommonName: "Test CA"},
NotBefore: time.Now(),
NotAfter: time.Now().Add(24 * time.Hour),
KeyUsage: x509.KeyUsageCertSign | x509.KeyUsageDigitalSignature,
BasicConstraintsValid: true,
IsCA: true,
}
caDER, err := x509.CreateCertificate(rand.Reader, caTmpl, caTmpl, &caKey.PublicKey, caKey)
s.Require().NoError(err)
s.writeCertPEM(s.certDir+"/ca.pem", "CERTIFICATE", caDER)
s.writeKeyPEM(s.certDir+"/ca-key.pem", "EC PRIVATE KEY", caKey)
// Server
serverKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
s.Require().NoError(err)
serverTmpl := &x509.Certificate{
SerialNumber: big.NewInt(2),
Subject: pkix.Name{CommonName: "localhost"},
NotBefore: time.Now(),
NotAfter: time.Now().Add(24 * time.Hour),
KeyUsage: x509.KeyUsageDigitalSignature,
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
DNSNames: []string{"localhost"},
IPAddresses: []net.IP{net.ParseIP("127.0.0.1")},
}
serverDER, err := x509.CreateCertificate(rand.Reader, serverTmpl, caTmpl, &serverKey.PublicKey, caKey)
s.Require().NoError(err)
s.writeCertPEM(s.certDir+"/server.pem", "CERTIFICATE", serverDER)
s.writeKeyPEM(s.certDir+"/server-key.pem", "EC PRIVATE KEY", serverKey)
// Client
clientKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
s.Require().NoError(err)
clientTmpl := &x509.Certificate{
SerialNumber: big.NewInt(3),
Subject: pkix.Name{CommonName: "test-client"},
NotBefore: time.Now(),
NotAfter: time.Now().Add(24 * time.Hour),
KeyUsage: x509.KeyUsageDigitalSignature,
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth},
EmailAddresses: []string{"test@example.com"},
}
clientDER, err := x509.CreateCertificate(rand.Reader, clientTmpl, caTmpl, &clientKey.PublicKey, caKey)
s.Require().NoError(err)
s.writeCertPEM(s.certDir+"/client.pem", "CERTIFICATE", clientDER)
s.writeKeyPEM(s.certDir+"/client-key.pem", "EC PRIVATE KEY", clientKey)
}
// TestMTLSProxy verifies HTTP forward proxy works over an mTLS listener
// with a valid client certificate.
func (s *MTLSSuite) TestMTLSProxy() {
rendered, err := RenderConfig("testdata/mtls/server.yaml",
ConfigData{ServerAddr: "auth-plugin"})
s.Require().NoError(err)
defer os.Remove(rendered)
gostC, err := RunGostContainerWithFiles(s.ctx, SharedNetworkName, rendered,
[]testcontainers.ContainerFile{
{HostFilePath: s.certDir + "/ca.pem", ContainerFilePath: "/certs/ca.pem", FileMode: 0644},
{HostFilePath: s.certDir + "/server.pem", ContainerFilePath: "/certs/server.pem", FileMode: 0644},
{HostFilePath: s.certDir + "/server-key.pem", ContainerFilePath: "/certs/server-key.pem", FileMode: 0644},
{HostFilePath: s.certDir + "/client.pem", ContainerFilePath: "/certs/client.pem", FileMode: 0644},
{HostFilePath: s.certDir + "/client-key.pem", ContainerFilePath: "/certs/client-key.pem", FileMode: 0644},
},
"8443/tcp")
s.Require().NoError(err)
defer gostC.Terminate(s.ctx)
// Verify proxy works with valid client cert. curl in HTTPS-proxy mode
// requires --proxy-* TLS options: --cacert/--cert verify the origin (not
// the proxy) and are ignored for the proxy TLS connection.
cmd := []string{"curl", "-v", "-s", "--connect-timeout", "5",
"--proxy-cacert", "/certs/ca.pem",
"--proxy-cert", "/certs/client.pem",
"--proxy-key", "/certs/client-key.pem",
"-x", "https://127.0.0.1:8443",
fmt.Sprintf("http://%s:5678", s.echoIP)}
code, out, err := ExecOutput(s.ctx, gostC, cmd)
body, err2 := io.ReadAll(out)
if err != nil || err2 != nil || code != 0 || !strings.Contains(string(body), "hello-gost") {
DumpLogs(s.T(), s.ctx, "mtls gost logs", gostC)
DumpLogs(s.T(), s.ctx, "mtls auth-plugin logs", s.pluginC)
}
s.Require().NoError(err)
s.Require().NoError(err2)
s.Require().Equal(0, code)
s.Require().Contains(string(body), "hello-gost")
}
// TestMTLSWithoutClientCert verifies that an mTLS listener rejects
// connections from clients that do not present a certificate.
func (s *MTLSSuite) TestMTLSWithoutClientCert() {
rendered, err := RenderConfig("testdata/mtls/server.yaml",
ConfigData{ServerAddr: "auth-plugin"})
s.Require().NoError(err)
defer os.Remove(rendered)
gostC, err := RunGostContainerWithFiles(s.ctx, SharedNetworkName, rendered,
[]testcontainers.ContainerFile{
{HostFilePath: s.certDir + "/ca.pem", ContainerFilePath: "/certs/ca.pem", FileMode: 0644},
{HostFilePath: s.certDir + "/server.pem", ContainerFilePath: "/certs/server.pem", FileMode: 0644},
{HostFilePath: s.certDir + "/server-key.pem", ContainerFilePath: "/certs/server-key.pem", FileMode: 0644},
{HostFilePath: s.certDir + "/client.pem", ContainerFilePath: "/certs/client.pem", FileMode: 0644},
{HostFilePath: s.certDir + "/client-key.pem", ContainerFilePath: "/certs/client-key.pem", FileMode: 0644},
},
"8443/tcp")
s.Require().NoError(err)
defer gostC.Terminate(s.ctx)
// curl without client cert should fail TLS handshake. Verify the proxy
// cert against the CA, but present no client cert so the server rejects.
cmd := []string{"curl", "-v", "-s", "--connect-timeout", "5",
"--proxy-cacert", "/certs/ca.pem",
"-x", "https://127.0.0.1:8443",
fmt.Sprintf("http://%s:5678", s.echoIP)}
code, _, err := ExecOutput(s.ctx, gostC, cmd)
if err == nil && code == 0 {
DumpLogs(s.T(), s.ctx, "mtls gost logs", gostC)
}
// Expect failure (non-zero exit code from curl)
s.Require().NotEqual(0, code, "curl without client cert should fail with non-zero exit code")
}
// TestMTLSAuthPluginLogs verifies that the HTTP auth plugin receives
// the mTLS client certificate identity (client_cn, client_san,
// client_cert_fingerprint) when a request passes through the mTLS listener.
func (s *MTLSSuite) TestMTLSAuthPluginLogs() {
rendered, err := RenderConfig("testdata/mtls/server.yaml",
ConfigData{ServerAddr: "auth-plugin"})
s.Require().NoError(err)
defer os.Remove(rendered)
gostC, err := RunGostContainerWithFiles(s.ctx, SharedNetworkName, rendered,
[]testcontainers.ContainerFile{
{HostFilePath: s.certDir + "/ca.pem", ContainerFilePath: "/certs/ca.pem", FileMode: 0644},
{HostFilePath: s.certDir + "/server.pem", ContainerFilePath: "/certs/server.pem", FileMode: 0644},
{HostFilePath: s.certDir + "/server-key.pem", ContainerFilePath: "/certs/server-key.pem", FileMode: 0644},
{HostFilePath: s.certDir + "/client.pem", ContainerFilePath: "/certs/client.pem", FileMode: 0644},
{HostFilePath: s.certDir + "/client-key.pem", ContainerFilePath: "/certs/client-key.pem", FileMode: 0644},
},
"8443/tcp")
s.Require().NoError(err)
defer gostC.Terminate(s.ctx)
// Send a request with valid client cert through the mTLS proxy.
cmd := []string{"curl", "-v", "-s", "--connect-timeout", "5",
"--proxy-cacert", "/certs/ca.pem",
"--proxy-cert", "/certs/client.pem",
"--proxy-key", "/certs/client-key.pem",
"-x", "https://127.0.0.1:8443",
fmt.Sprintf("http://%s:5678", s.echoIP)}
code, out, err := ExecOutput(s.ctx, gostC, cmd)
body, _ := io.ReadAll(out)
s.Require().NoError(err)
s.Require().Equal(0, code)
s.Require().Contains(string(body), "hello-gost")
// Read auth plugin logs and verify PeerCert fields are present.
logs, err := s.pluginC.Logs(s.ctx)
s.Require().NoError(err)
logData, _ := io.ReadAll(logs)
logStr := string(logData)
s.T().Logf("auth plugin logs:\n%s", logStr)
s.Require().Contains(logStr, "clientCn", "auth plugin should receive client_cn")
s.Require().Contains(logStr, "test-client", "client_cn should be 'test-client'")
s.Require().Contains(logStr, "clientSan", "auth plugin should receive client_san")
s.Require().Contains(logStr, "clientCertFingerprint", "auth plugin should receive client_cert_fingerprint")
}
func TestMTLSSuite(t *testing.T) {
suite.Run(t, new(MTLSSuite))
}
// --- helpers ---
func runAuthPluginContainer(ctx context.Context, networkName string) (testcontainers.Container, error) {
req := testcontainers.ContainerRequest{
FromDockerfile: testcontainers.FromDockerfile{
Context: ".",
Dockerfile: "Dockerfile",
Repo: "gost-e2e",
Tag: "latest",
KeepImage: true,
BuildOptionsModifier: func(opts *client.ImageBuildOptions) {
opts.NetworkMode = "host"
},
},
Networks: []string{networkName},
NetworkAliases: map[string][]string{
networkName: {"auth-plugin"},
},
Files: []testcontainers.ContainerFile{
{HostFilePath: "scripts/auth_plugin.py", ContainerFilePath: "/scripts/auth_plugin.py", FileMode: 0644},
},
ExposedPorts: []string{"9000/tcp"},
Cmd: []string{"python3", "/scripts/auth_plugin.py", "9000"},
WaitingFor: wait.ForExposedPort(),
}
return testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{
ContainerRequest: req,
Started: true,
})
}
// writeCertPEM writes a DER-encoded certificate to a PEM file.
func (s *MTLSSuite) writeCertPEM(path, blockType string, der []byte) {
err := os.WriteFile(path, pem.EncodeToMemory(&pem.Block{Type: blockType, Bytes: der}), 0644)
s.Require().NoError(err)
}
// writeKeyPEM marshals an ECDSA private key and writes it to a PEM file.
func (s *MTLSSuite) writeKeyPEM(path, blockType string, key *ecdsa.PrivateKey) {
b, err := x509.MarshalECPrivateKey(key)
s.Require().NoError(err)
err = os.WriteFile(path, pem.EncodeToMemory(&pem.Block{Type: blockType, Bytes: b}), 0600)
s.Require().NoError(err)
}
-57
View File
@@ -1,57 +0,0 @@
package e2e
import (
"context"
"fmt"
"io"
"testing"
"github.com/stretchr/testify/suite"
"github.com/testcontainers/testcontainers-go"
)
type ParallelSelectorSuite struct {
suite.Suite
ctx context.Context
echoC testcontainers.Container
echoIP string
}
func (s *ParallelSelectorSuite) SetupSuite() {
s.ctx = context.Background()
echoC, err := RunEchoContainer(s.ctx, SharedNetworkName)
s.Require().NoError(err)
s.echoC = echoC
echoIP, err := echoC.ContainerIP(s.ctx)
s.Require().NoError(err)
s.echoIP = echoIP
}
func (s *ParallelSelectorSuite) TearDownSuite() {
if s.echoC != nil {
s.echoC.Terminate(s.ctx)
}
}
func (s *ParallelSelectorSuite) TestParallelSelector() {
gostC, err := RunGostContainerWithPorts(s.ctx, SharedNetworkName, "testdata/parallel_selector/server.yaml", "8080/tcp")
s.Require().NoError(err)
defer gostC.Terminate(s.ctx)
// Test the proxy by running curl inside the gost container
cmd := []string{"curl", "-v", "-s", "-x", "http://127.0.0.1:8080", fmt.Sprintf("http://%s:5678", s.echoIP)}
code, out, err := gostC.Exec(s.ctx, cmd)
s.Require().NoError(err)
body, err := io.ReadAll(out)
s.Require().NoError(err)
s.Require().Equal(0, code)
s.Require().Contains(string(body), "hello-gost")
}
func TestParallelSelectorSuite(t *testing.T) {
suite.Run(t, new(ParallelSelectorSuite))
}
+157
View File
@@ -0,0 +1,157 @@
package e2e
import (
"context"
"fmt"
"io"
"os"
"strings"
"testing"
"github.com/stretchr/testify/suite"
"github.com/testcontainers/testcontainers-go"
)
type PHTSuite struct {
suite.Suite
ctx context.Context
echoC testcontainers.Container
echoIP string
}
func (s *PHTSuite) SetupSuite() {
s.ctx = context.Background()
s.T().Logf("start tcp echo container...")
echoC, err := RunEchoContainer(s.ctx, SharedNetworkName)
s.Require().NoError(err)
s.echoC = echoC
echoIP, err := echoC.ContainerIP(s.ctx)
s.Require().NoError(err)
s.echoIP = echoIP
}
func (s *PHTSuite) TearDownSuite() {
if s.echoC != nil {
s.echoC.Terminate(s.ctx)
}
}
// TestPHTTunnel verifies a basic PHT tunnel: a client HTTP proxy chains
// through a PHT dialer to reach a PHT server, which forwards the request
// to the echo server.
func (s *PHTSuite) TestPHTTunnel() {
serverC, err := RunGostContainerWithOptions(s.ctx, SharedNetworkName,
"testdata/pht/server.yaml",
[]string{"pht-server"}, []string{"8443/tcp"})
s.Require().NoError(err)
defer serverC.Terminate(s.ctx)
rendered, err := RenderConfig("testdata/pht/client_connector.yaml",
ConfigData{ServerAddr: "pht-server:8443"})
s.Require().NoError(err)
defer os.Remove(rendered)
clientC, err := RunGostContainerWithPorts(s.ctx, SharedNetworkName,
rendered, "8080/tcp")
s.Require().NoError(err)
defer clientC.Terminate(s.ctx)
cmd := []string{"curl", "-v", "-s", "-x", "http://127.0.0.1:8080",
fmt.Sprintf("http://%s:5678", s.echoIP)}
code, out, err := clientC.Exec(s.ctx, cmd)
s.Require().NoError(err)
body, err := io.ReadAll(out)
s.Require().NoError(err)
if code != 0 || !strings.Contains(string(body), "hello-gost") {
DumpLogs(s.T(), s.ctx, "pht client logs", clientC)
DumpLogs(s.T(), s.ctx, "pht server logs", serverC)
}
s.Require().Equal(0, code)
s.Require().Contains(string(body), "hello-gost")
}
// TestPHTSTunnel verifies PHT over TLS. The PHTs listener auto-generates a
// self-signed cert; the PHTs dialer defaults to InsecureSkipVerify so the
// handshake succeeds without explicit cert configuration.
func (s *PHTSuite) TestPHTSTunnel() {
serverC, err := RunGostContainerWithOptions(s.ctx, SharedNetworkName,
"testdata/pht/server_phts.yaml",
[]string{"phts-server"}, []string{"8443/tcp"})
s.Require().NoError(err)
defer serverC.Terminate(s.ctx)
rendered, err := RenderConfig("testdata/pht/client_connector_phts.yaml",
ConfigData{ServerAddr: "phts-server:8443"})
s.Require().NoError(err)
defer os.Remove(rendered)
clientC, err := RunGostContainerWithPorts(s.ctx, SharedNetworkName,
rendered, "8080/tcp")
s.Require().NoError(err)
defer clientC.Terminate(s.ctx)
cmd := []string{"curl", "-v", "-s", "-x", "http://127.0.0.1:8080",
fmt.Sprintf("http://%s:5678", s.echoIP)}
code, out, err := clientC.Exec(s.ctx, cmd)
s.Require().NoError(err)
body, err := io.ReadAll(out)
s.Require().NoError(err)
if code != 0 || !strings.Contains(string(body), "hello-gost") {
DumpLogs(s.T(), s.ctx, "phts client logs", clientC)
DumpLogs(s.T(), s.ctx, "phts server logs", serverC)
}
s.Require().Equal(0, code)
s.Require().Contains(string(body), "hello-gost")
}
// TestPHTHeartbeat verifies that the PHT tunnel stays alive across idle
// periods. Sends a request, waits >readTimeout (default 10s), and sends
// another request to confirm the heartbeat kept the connection open.
func (s *PHTSuite) TestPHTHeartbeat() {
serverC, err := RunGostContainerWithOptions(s.ctx, SharedNetworkName,
"testdata/pht/server.yaml",
[]string{"pht-server"}, []string{"8443/tcp"})
s.Require().NoError(err)
defer serverC.Terminate(s.ctx)
rendered, err := RenderConfig("testdata/pht/client_connector.yaml",
ConfigData{ServerAddr: "pht-server:8443"})
s.Require().NoError(err)
defer os.Remove(rendered)
clientC, err := RunGostContainerWithPorts(s.ctx, SharedNetworkName,
rendered, "8080/tcp")
s.Require().NoError(err)
defer clientC.Terminate(s.ctx)
// First request: establish the tunnel.
cmd := []string{"curl", "-v", "-s", "-x", "http://127.0.0.1:8080",
fmt.Sprintf("http://%s:5678", s.echoIP)}
code, out, err := clientC.Exec(s.ctx, cmd)
s.Require().NoError(err)
body, _ := io.ReadAll(out)
s.Require().Equal(0, code)
s.Require().Contains(string(body), "hello-gost")
// Wait longer than readTimeout to trigger heartbeat.
// The PHT server's default read timeout is 10s. We wait 12s.
cmd = []string{"sh", "-c",
fmt.Sprintf("sleep 12 && curl -s -x http://127.0.0.1:8080 http://%s:5678", s.echoIP)}
code, out, err = clientC.Exec(s.ctx, cmd)
s.Require().NoError(err)
body, _ = io.ReadAll(out)
if code != 0 || !strings.Contains(string(body), "hello-gost") {
DumpLogs(s.T(), s.ctx, "pht heartbeat client logs", clientC)
DumpLogs(s.T(), s.ctx, "pht heartbeat server logs", serverC)
}
s.Require().Equal(0, code)
s.Require().Contains(string(body), "hello-gost")
}
func TestPHTSuite(t *testing.T) {
suite.Run(t, new(PHTSuite))
}
+149
View File
@@ -0,0 +1,149 @@
package e2e
import (
"context"
"fmt"
"io"
"testing"
"time"
"github.com/stretchr/testify/suite"
"github.com/testcontainers/testcontainers-go"
)
type ProbeSuite struct {
suite.Suite
ctx context.Context
echoC testcontainers.Container
echoIP string
}
func (s *ProbeSuite) SetupSuite() {
s.ctx = context.Background()
echoC, err := RunEchoContainer(s.ctx, SharedNetworkName)
s.Require().NoError(err)
s.echoC = echoC
echoIP, err := echoC.ContainerIP(s.ctx)
s.Require().NoError(err)
s.echoIP = echoIP
}
func (s *ProbeSuite) TearDownSuite() {
if s.echoC != nil {
s.echoC.Terminate(s.ctx)
}
}
func (s *ProbeSuite) proxyRequest(gostC testcontainers.Container, port string) (int, string) {
cmd := []string{
"curl", "-s",
"-x", fmt.Sprintf("http://127.0.0.1:%s", port),
fmt.Sprintf("http://%s:5678", s.echoIP),
}
code, out, err := gostC.Exec(s.ctx, cmd)
s.Require().NoError(err)
body, err := io.ReadAll(out)
s.Require().NoError(err)
return code, string(body)
}
// TestTCPProbeFailover verifies that the TCP probe detects a dead node and
// marks it before any real traffic, so the FailFilter excludes it. With the
// dead node pre-marked, every request succeeds immediately.
func (s *ProbeSuite) TestTCPProbeFailover() {
gostC, err := RunGostContainerWithPorts(s.ctx, SharedNetworkName, "testdata/probe/tcp.yaml", "8080/tcp")
s.Require().NoError(err)
defer gostC.Terminate(s.ctx)
// The probe fires immediately at startup, so the dead node is already
// marked by the time we send requests.
for range 10 {
code, body := s.proxyRequest(gostC, "8080")
s.Require().Equal(0, code, "every request must succeed; dead node pre-marked by probe")
s.Require().Contains(body, "hello-gost")
}
}
// TestLowestLatencyProbe verifies that the lowestlatency strategy works with
// probed nodes. Both nodes are live; the strategy selects the one with the
// lowest measured latency.
func (s *ProbeSuite) TestLowestLatencyProbe() {
gostC, err := RunGostContainerWithPorts(s.ctx, SharedNetworkName, "testdata/probe/lowestlatency.yaml", "8080/tcp")
s.Require().NoError(err)
defer gostC.Terminate(s.ctx)
// Give the initial probe a moment to fire so both nodes show healthy.
time.Sleep(200 * time.Millisecond)
for range 10 {
code, body := s.proxyRequest(gostC, "8080")
s.Require().Equal(0, code, "all requests must succeed with lowestlatency strategy")
s.Require().Contains(body, "hello-gost")
}
}
// TestCmdProbeFailover verifies that the cmd probe detects a dead node via
// shell exit code and marks it before real traffic, so FailFilter excludes it.
func (s *ProbeSuite) TestCmdProbeFailover() {
gostC, err := RunGostContainerWithPorts(s.ctx, SharedNetworkName, "testdata/probe/cmd.yaml", "8080/tcp")
s.Require().NoError(err)
defer gostC.Terminate(s.ctx)
// The probe fires at startup; the dead node is already marked.
for range 10 {
code, body := s.proxyRequest(gostC, "8080")
s.Require().Equal(0, code, "all requests must succeed; dead cmd node pre-marked")
s.Require().Contains(body, "hello-gost")
}
}
// TestProbeRecovery verifies the dead→alive→dead transition that issue #837
// describes: a node that fails is excluded, and once it recovers (probe flips
// healthy) it resumes carrying traffic. Both relays stay up; only the cmd
// probe flag files in the container flip, so no restart is needed.
//
// Phase 1: node-a marked dead, node-b healthy → every request goes to node-b.
// Phase 2: recover node-a, kill node-b → every request must still succeed,
// which is only possible if node-a's probe recovered it (otherwise both nodes
// would be excluded and the request would fail).
func (s *ProbeSuite) TestProbeRecovery() {
gostC, err := RunGostContainerWithPorts(s.ctx, SharedNetworkName, "testdata/probe/recovery.yaml", "8080/tcp")
s.Require().NoError(err)
defer gostC.Terminate(s.ctx)
// The node probes fire immediately at startup, so let both settle healthy
// before we start flipping flag files.
time.Sleep(1 * time.Second)
// Phase 1: knock node-a down, leave node-b up.
_, _, err = gostC.Exec(s.ctx, []string{"touch", "/tmp/a_down"})
s.Require().NoError(err)
time.Sleep(2500 * time.Millisecond) // > probe interval
for range 10 {
code, body := s.proxyRequest(gostC, "8080")
s.Require().Equal(0, code, "phase1: dead node-a must be excluded, all traffic via node-b")
s.Require().Contains(body, "hello-gost")
}
// Phase 2: recover node-a, knock node-b down. If node-a's probe did not
// revive it, the request would fail (no healthy candidate).
_, _, err = gostC.Exec(s.ctx, []string{"rm", "-f", "/tmp/a_down", "/tmp/b_down"})
s.Require().NoError(err)
_, _, err = gostC.Exec(s.ctx, []string{"touch", "/tmp/b_down"})
s.Require().NoError(err)
time.Sleep(2500 * time.Millisecond)
for range 10 {
code, body := s.proxyRequest(gostC, "8080")
s.Require().Equal(0, code, "phase2: recovered node-a must resume carrying traffic")
s.Require().Contains(body, "hello-gost")
}
}
func TestProbeSuite(t *testing.T) {
suite.Run(t, new(ProbeSuite))
}
+127
View File
@@ -0,0 +1,127 @@
package e2e
import (
"context"
"fmt"
"io"
"strings"
"testing"
"github.com/moby/moby/client"
"github.com/stretchr/testify/suite"
"github.com/testcontainers/testcontainers-go"
"github.com/testcontainers/testcontainers-go/wait"
)
// ProxyProtoSuite verifies go-gost/gost#677: a forward/rtcp handler with
// metadata.proxyProtocol set prepends a HAProxy PROXY protocol header on the
// outbound connection before forwarding. The backend (proxy-capture) reads the
// first bytes and reflects the header line back so the test can assert it.
//
// This exercises the exact send path the issue asks for (forward/remote's
// proxyproto.WrapClientConn). A single gost instance is used so the header's
// source address is the real connecting client — in a full reverse tunnel the
// source would be the tunnel peer (relay CONNECT propagates only the
// destination); that limitation is out of scope here.
type ProxyProtoSuite struct {
suite.Suite
ctx context.Context
backendC testcontainers.Container
gostV1C testcontainers.Container
gostV2C testcontainers.Container
}
func (s *ProxyProtoSuite) SetupSuite() {
s.ctx = context.Background()
backendC, err := s.runBackend()
s.Require().NoError(err)
s.backendC = backendC
gostV1C, err := RunGostContainerWithOptions(s.ctx, SharedNetworkName,
"testdata/proxyproto/v1.yaml", []string{"gost-v1"}, []string{"8080/tcp"})
s.Require().NoError(err)
s.gostV1C = gostV1C
gostV2C, err := RunGostContainerWithOptions(s.ctx, SharedNetworkName,
"testdata/proxyproto/v2.yaml", []string{"gost-v2"}, []string{"8080/tcp"})
s.Require().NoError(err)
s.gostV2C = gostV2C
}
func (s *ProxyProtoSuite) TearDownSuite() {
for _, c := range []testcontainers.Container{s.gostV1C, s.gostV2C, s.backendC} {
if c != nil {
c.Terminate(s.ctx)
}
}
}
// runBackend starts the raw-TCP PROXY-header-capturing backend, aliased
// "proxy-capture" on the shared network, listening on 5678. It is also used as
// the client host (it has python3) to connect through gost.
func (s *ProxyProtoSuite) runBackend() (testcontainers.Container, error) {
req := testcontainers.ContainerRequest{
FromDockerfile: testcontainers.FromDockerfile{
Context: ".",
Dockerfile: "Dockerfile",
Repo: "gost-e2e",
Tag: "latest",
KeepImage: true,
BuildOptionsModifier: func(opts *client.ImageBuildOptions) {
opts.NetworkMode = "host"
},
},
Networks: []string{SharedNetworkName},
NetworkAliases: map[string][]string{
SharedNetworkName: {"proxy-capture"},
},
Files: []testcontainers.ContainerFile{
{HostFilePath: "scripts/proxy_capture.py", ContainerFilePath: "/scripts/proxy_capture.py", FileMode: 0644},
},
ExposedPorts: []string{"5678/tcp"},
Cmd: []string{"python3", "/scripts/proxy_capture.py"},
WaitingFor: wait.ForExposedPort(),
}
return testcontainers.GenericContainer(s.ctx, testcontainers.GenericContainerRequest{
ContainerRequest: req,
Started: true,
})
}
// request connects from the backend container to gost on :8080, sends a payload,
// and returns the reflected response (which carries the PROXY header line).
func (s *ProxyProtoSuite) request(gostHost string) string {
cmd := []string{
"sh", "-c",
fmt.Sprintf("python3 -c \"import socket,sys; s=socket.socket(); s.settimeout(5); s.connect(('%s',8080)); s.sendall(b'hello-gost'); sys.stdout.write(s.recv(4096).decode())\"", gostHost),
}
_, out, err := s.backendC.Exec(s.ctx, cmd)
s.Require().NoError(err)
body, err := io.ReadAll(out)
s.Require().NoError(err)
return string(body)
}
// TestV1HeaderSent asserts gost prepends a text PROXY protocol v1 header.
func (s *ProxyProtoSuite) TestV1HeaderSent() {
body := s.request("gost-v1")
if !strings.Contains(body, "PROXY-RECEIVED") || !strings.Contains(body, "PROXY TCP") {
DumpLogs(s.T(), s.ctx, "gost-v1 logs", s.gostV1C)
}
s.Require().Contains(body, "PROXY-RECEIVED")
s.Require().Contains(body, "PROXY TCP")
}
// TestV2HeaderSent asserts gost prepends a binary PROXY protocol v2 header.
func (s *ProxyProtoSuite) TestV2HeaderSent() {
body := s.request("gost-v2")
if !strings.Contains(body, "PROXY-V2-RECEIVED") {
DumpLogs(s.T(), s.ctx, "gost-v2 logs", s.gostV2C)
}
s.Require().Contains(body, "PROXY-V2-RECEIVED")
}
func TestProxyProtoSuite(t *testing.T) {
suite.Run(t, new(ProxyProtoSuite))
}
+135
View File
@@ -0,0 +1,135 @@
package e2e
import (
"context"
"fmt"
"io"
"os"
"strings"
"testing"
"github.com/stretchr/testify/suite"
"github.com/testcontainers/testcontainers-go"
)
// QUICSuite covers the QUIC listener/dialer pair (listener type: quic, dialer
// type: quic) with the cipherKey obfuscation enabled. It uses the canonical
// GOST chaining pattern: a client container exposes a plain HTTP proxy that
// tunnels through a quic chain to a quic server (HTTP over QUIC).
type QUICSuite struct {
suite.Suite
ctx context.Context
echoC testcontainers.Container
echoIP string
}
func (s *QUICSuite) SetupSuite() {
s.ctx = context.Background()
s.T().Logf("start tcp echo container...")
echoC, err := RunEchoContainer(s.ctx, SharedNetworkName)
s.Require().NoError(err)
s.echoC = echoC
echoIP, err := echoC.ContainerIP(s.ctx)
s.Require().NoError(err)
s.echoIP = echoIP
}
func (s *QUICSuite) TearDownSuite() {
if s.echoC != nil {
s.echoC.Terminate(s.ctx)
}
}
// startChain brings up a quic server (alias "quic-server", cipherKey enabled,
// http handler) and a client container that chains to it through a quic
// dialer. The client exposes port 8080 for curl.
func (s *QUICSuite) startChain() (testcontainers.Container, testcontainers.Container) {
s.T().Helper()
serverC, err := RunGostContainerWithOptions(s.ctx, SharedNetworkName,
"testdata/quic/server.yaml", []string{"quic-server"}, []string{"18843/udp"})
s.Require().NoError(err)
rendered, err := RenderConfig("testdata/quic/client.yaml", ConfigData{ServerAddr: "quic-server:18843"})
s.Require().NoError(err)
s.T().Cleanup(func() { os.Remove(rendered) })
clientC, err := RunGostContainerWithPorts(s.ctx, SharedNetworkName, rendered, "8080/tcp")
s.Require().NoError(err)
return serverC, clientC
}
// curlEcho runs curl through the client's local http proxy and returns the
// exit code plus the captured body.
func (s *QUICSuite) curlEcho(clientC testcontainers.Container) (int, string) {
s.T().Helper()
cmd := []string{"curl", "-s", "--max-time", "20", "-x", "http://127.0.0.1:8080",
fmt.Sprintf("http://%s:5678/", s.echoIP)}
code, out, _ := clientC.Exec(s.ctx, cmd)
body, _ := io.ReadAll(out)
return code, string(body)
}
// sendInvalidDatagram sends a single malformed (non-encrypted) UDP datagram to
// the quic server's listener, reproducing the issue's repro packet.
func (s *QUICSuite) sendInvalidDatagram(clientC testcontainers.Container) {
s.T().Helper()
cmd := []string{"python3", "-c",
"import socket; s=socket.socket(socket.AF_INET, socket.SOCK_DGRAM); s.sendto(b\"x\", (\"quic-server\", 18843))"}
code, out, err := clientC.Exec(s.ctx, cmd)
s.Require().NoError(err)
if code != 0 {
b, _ := io.ReadAll(out)
s.T().Fatalf("send invalid datagram failed (%d): %s", code, string(b))
}
}
// TestQUICForwardCipherKeySurvivesInvalidDatagram verifies that a malformed
// UDP datagram does not close the shared QUIC transport (go-gost/x#125).
//
// With the bug, cipherConn.ReadFrom returned the decrypt error straight to
// quic-go, which treats any packet-socket error as fatal and closes the shared
// transport: a single invalid datagram kills the listener and correct clients
// subsequently time out. With the fix the invalid datagram is discarded and
// the listener keeps serving.
//
// Sequence: baseline forward works -> malformed datagram -> forward still works.
func (s *QUICSuite) TestQUICForwardCipherKeySurvivesInvalidDatagram() {
serverC, clientC := s.startChain()
defer serverC.Terminate(s.ctx)
defer clientC.Terminate(s.ctx)
// Baseline: a valid QUIC forward must work before we poke the listener.
code, body := s.curlEcho(clientC)
if code != 0 || !strings.Contains(body, "hello-gost") {
s.dump("quic-baseline logs", clientC, serverC)
}
s.Require().Equal(0, code, "baseline QUIC forward should work")
s.Require().Contains(body, "hello-gost")
// Send one malformed UDP datagram to the quic listener's cipher socket.
s.sendInvalidDatagram(clientC)
// The listener must still be alive: a fresh QUIC forward must succeed.
code, body = s.curlEcho(clientC)
if code != 0 || !strings.Contains(body, "hello-gost") {
s.dump("quic-after-invalid logs", clientC, serverC)
}
s.Require().Equal(0, code,
"QUIC forward should still work after an invalid datagram (issue #125)")
s.Require().Contains(body, "hello-gost",
"listener must not close on an invalid datagram (issue #125)")
}
func (s *QUICSuite) dump(label string, cs ...testcontainers.Container) {
for _, c := range cs {
DumpLogs(s.T(), s.ctx, fmt.Sprintf("%s: %s", label, c.GetContainerID()), c)
}
}
func TestQUICSuite(t *testing.T) {
suite.Run(t, new(QUICSuite))
}
+139
View File
@@ -0,0 +1,139 @@
package e2e
import (
"context"
"fmt"
"io"
"strings"
"testing"
"github.com/moby/moby/client"
"github.com/stretchr/testify/suite"
"github.com/testcontainers/testcontainers-go"
"github.com/testcontainers/testcontainers-go/wait"
)
// RedSniffingWhitelistSuite reproduces go-gost/gost#899: whitelist bypass +
// sniffing on a transparent (red) proxy.
//
// Inside a single privileged container, iptables redirects all outbound TCP to
// the gost red service, so the handler sees the original destination IP via
// SO_ORIGINAL_DST. The whitelist contains only a domain (the docker network
// alias "https-echo"); a bare destination IP is never in it. On a broken build
// (go-gost/x@fe394a8, x >= v0.13.12) the pre-sniffing bypass check on the
// destination IP rejects every connection before SNI sniffing runs, making the
// domain whitelist dead. The fix skips that check in whitelist mode and lets
// the sniffer match the SNI host. This suite also asserts the security
// property the skip must not weaken: a non-whitelisted SNI is still rejected.
type RedSniffingWhitelistSuite struct {
suite.Suite
ctx context.Context
httpsEchoC testcontainers.Container
gostC testcontainers.Container
}
// redirectRules are installed in the gost container before gost starts:
// loopback traffic (incl. Docker's embedded DNS) and gost's own marked sockets
// are exempt; every other TCP packet is redirected to the red service.
const redirectRules = `
iptables -t nat -A OUTPUT -m addrtype --dst-type LOCAL -j RETURN
iptables -t nat -A OUTPUT -m mark --mark 114514 -j RETURN
iptables -t nat -A OUTPUT -p tcp -j REDIRECT --to-ports 12345
`
func (s *RedSniffingWhitelistSuite) SetupSuite() {
s.ctx = context.Background()
echoC, err := RunHTTPSEchoContainer(s.ctx, SharedNetworkName)
s.Require().NoError(err)
s.httpsEchoC = echoC
req := testcontainers.ContainerRequest{
FromDockerfile: testcontainers.FromDockerfile{
Context: ".",
Dockerfile: "Dockerfile",
Repo: "gost-e2e",
Tag: "latest",
KeepImage: true,
BuildOptionsModifier: func(opts *client.ImageBuildOptions) {
opts.NetworkMode = "host"
},
},
Networks: []string{SharedNetworkName},
CapAdd: []string{"NET_ADMIN"},
Files: []testcontainers.ContainerFile{
{HostFilePath: GostBinPath, ContainerFilePath: "/bin/gost", FileMode: 0755},
{HostFilePath: "testdata/red_sniffing/whitelist.yaml", ContainerFilePath: "/config.yaml", FileMode: 0644},
},
Cmd: []string{"sh", "-c", redirectRules + "\nexec /bin/gost -C /config.yaml"},
WaitingFor: wait.ForLog("listening on"),
}
gostC, err := testcontainers.GenericContainer(s.ctx, testcontainers.GenericContainerRequest{
ContainerRequest: req,
Started: true,
})
s.Require().NoError(err)
s.gostC = gostC
}
func (s *RedSniffingWhitelistSuite) TearDownSuite() {
if s.gostC != nil {
s.gostC.Terminate(s.ctx)
}
if s.httpsEchoC != nil {
s.httpsEchoC.Terminate(s.ctx)
}
}
// TestWhitelistedDomainViaSNI is the gost#899 golden path: curl to a domain in
// the whitelist should be forwarded because the sniffer matches its SNI. The
// kernel redirects the outbound TCP to the red service, which must not reject
// the bare destination IP before sniffing. Retries tolerate startup timing.
func (s *RedSniffingWhitelistSuite) TestWhitelistedDomainViaSNI() {
s.T().Helper()
cmd := []string{"sh", "-c", `
for i in $(seq 1 30); do
body=$(curl -ks --max-time 5 https://https-echo:8443/ 2>/dev/null)
echo "$body" | grep -q hello-gost && { echo "$body"; exit 0; }
sleep 1
done
echo "$body"`}
_, out, err := s.gostC.Exec(s.ctx, cmd)
s.Require().NoError(err)
body, _ := io.ReadAll(out)
if !strings.Contains(string(body), "hello-gost") {
DumpLogs(s.T(), s.ctx, "gost-red logs", s.gostC)
}
s.Require().Contains(string(body), "hello-gost",
"whitelisted domain must be forwarded via SNI, not rejected on the bare IP")
}
// TestNonWhitelistedHostRejected verifies that skipping the pre-sniffing check
// in whitelist mode does not weaken the whitelist: a connection whose sniffed
// SNI is not allowlisted must still be rejected. curl connects to the same
// echo IP but with a different SNI, so only the SNI can drive the decision.
func (s *RedSniffingWhitelistSuite) TestNonWhitelistedHostRejected() {
s.T().Helper()
ip, err := s.httpsEchoC.ContainerIP(s.ctx)
s.Require().NoError(err)
cmd := []string{"sh", "-c", fmt.Sprintf(
"curl -ks --resolve other.test:8443:%s --max-time 5 https://other.test:8443/ 2>&1", ip)}
code, _, err := s.gostC.Exec(s.ctx, cmd)
s.Require().NoError(err)
s.Require().NotZero(code,
"curl to a non-whitelisted SNI should fail; got exit %d", code)
logs, err := s.gostC.Logs(s.ctx)
s.Require().NoError(err)
defer logs.Close()
logBody, _ := io.ReadAll(logs)
s.Require().Contains(string(logBody), "bypass:",
"gost should log a bypass decision for the non-whitelisted SNI")
}
func TestRedSniffingWhitelistSuite(t *testing.T) {
suite.Run(t, new(RedSniffingWhitelistSuite))
}
+91
View File
@@ -0,0 +1,91 @@
package e2e
import (
"context"
"fmt"
"io"
"testing"
"github.com/stretchr/testify/suite"
"github.com/testcontainers/testcontainers-go"
)
// ResolverSuite verifies that a configured resolver drives the proxy's
// outbound DNS resolution. A gost HTTP proxy uses a resolver whose only
// nameserver is a test responder: it answers echo.test with the real echo
// server IP and NXDOMAIN for everything else. A request to echo.test is
// resolved by the custom resolver and reaches the echo server; a request to an
// unmapped host fails to resolve.
type ResolverSuite struct {
suite.Suite
ctx context.Context
echoC testcontainers.Container
echoIP string
dnsC testcontainers.Container
proxyC testcontainers.Container
}
func (s *ResolverSuite) SetupSuite() {
s.ctx = context.Background()
echoC, err := RunEchoContainer(s.ctx, SharedNetworkName)
s.Require().NoError(err)
s.echoC = echoC
echoIP, err := echoC.ContainerIP(s.ctx)
s.Require().NoError(err)
s.echoIP = echoIP
dnsC, err := RunResolverResponderContainer(s.ctx, SharedNetworkName, s.echoIP)
s.Require().NoError(err)
s.dnsC = dnsC
proxyC, err := RunGostContainerWithOptions(s.ctx, SharedNetworkName,
"testdata/resolver/server.yaml", []string{"gost-proxy"}, []string{"8080/tcp"})
s.Require().NoError(err)
s.proxyC = proxyC
}
func (s *ResolverSuite) TearDownSuite() {
for _, c := range []testcontainers.Container{s.proxyC, s.dnsC, s.echoC} {
if c != nil {
c.Terminate(s.ctx)
}
}
}
// proxyRequest sends an HTTP GET to the given host through the gost proxy. When
// waitResolve is true it retries until the resolver/responder is ready (the
// resolved-host happy path); otherwise it makes a single attempt. Returns the
// response body.
func (s *ResolverSuite) proxyRequest(host string, waitResolve bool) string {
loop := "for i in $(seq 1 30); do "
if !waitResolve {
loop = "for i in 1; do "
}
cmd := []string{
"sh", "-c",
fmt.Sprintf("%sbody=$(curl -s -x http://gost-proxy:8080 http://%s:5678/); echo \"$body\" | grep -q hello-gost && { echo \"$body\"; exit 0; }; sleep 1; done; echo \"$body\"", loop, host),
}
_, out, err := s.echoC.Exec(s.ctx, cmd)
s.Require().NoError(err)
body, err := io.ReadAll(out)
s.Require().NoError(err)
return string(body)
}
// TestResolvedHostname verifies that the custom resolver resolves echo.test to
// the echo server IP, so the proxied request reaches the echo server.
func (s *ResolverSuite) TestResolvedHostname() {
s.Require().Contains(s.proxyRequest("echo.test", true), "hello-gost")
}
// TestUnresolvedHostname verifies that a host absent from the resolver gets
// NXDOMAIN and never reaches the echo server.
func (s *ResolverSuite) TestUnresolvedHostname() {
s.Require().NotContains(s.proxyRequest("nomapped.test", false), "hello-gost")
}
func TestResolverSuite(t *testing.T) {
suite.Run(t, new(ResolverSuite))
}
+81
View File
@@ -0,0 +1,81 @@
package e2e
import (
"context"
"io"
"testing"
"github.com/stretchr/testify/suite"
"github.com/testcontainers/testcontainers-go"
)
// RoutingSuite verifies node-level routing matchers. A forward proxy's only
// chain node relays to an upstream proxy, but the node carries a matcher
// Host(`tcp-echo`) so it is only eligible for requests whose Host matches. A
// request to tcp-echo:5678 is matched, relayed upstream, and reaches the echo
// server ("hello-gost"); a request to a non-matching host is excluded (no
// eligible node) and never reaches the echo server.
type RoutingSuite struct {
suite.Suite
ctx context.Context
echoC testcontainers.Container
proxyC testcontainers.Container
upstreamC testcontainers.Container
}
func (s *RoutingSuite) SetupSuite() {
s.ctx = context.Background()
echoC, err := RunEchoContainer(s.ctx, SharedNetworkName)
s.Require().NoError(err)
s.echoC = echoC
proxyC, err := RunGostContainerWithOptions(s.ctx, SharedNetworkName,
"testdata/routing/server.yaml", []string{"gost-proxy"}, []string{"8080/tcp"})
s.Require().NoError(err)
s.proxyC = proxyC
upstreamC, err := RunGostContainerWithOptions(s.ctx, SharedNetworkName,
"testdata/routing/upstream.yaml", []string{"gost-upstream"}, []string{"8081/tcp"})
s.Require().NoError(err)
s.upstreamC = upstreamC
}
func (s *RoutingSuite) TearDownSuite() {
for _, c := range []testcontainers.Container{s.proxyC, s.upstreamC, s.echoC} {
if c != nil {
c.Terminate(s.ctx)
}
}
}
// proxyRequest sends an HTTP GET to target through the matcher-gated proxy and
// returns the response body.
func (s *RoutingSuite) proxyRequest(target string) string {
cmd := []string{
"curl", "-s",
"-x", "http://gost-proxy:8080",
target,
}
_, out, err := s.echoC.Exec(s.ctx, cmd)
s.Require().NoError(err)
body, err := io.ReadAll(out)
s.Require().NoError(err)
return string(body)
}
// TestMatchedHost verifies that a request whose Host matches the node matcher is
// relayed upstream and reaches the echo server.
func (s *RoutingSuite) TestMatchedHost() {
s.Require().Contains(s.proxyRequest("http://tcp-echo:5678/"), "hello-gost")
}
// TestUnmatchedHost verifies that a request whose Host does not match the node
// matcher is excluded (no eligible node) and never reaches the echo server.
func (s *RoutingSuite) TestUnmatchedHost() {
s.Require().NotContains(s.proxyRequest("http://other.local:5678/"), "hello-gost")
}
func TestRoutingSuite(t *testing.T) {
suite.Run(t, new(RoutingSuite))
}
+85
View File
@@ -0,0 +1,85 @@
package e2e
import (
"context"
"fmt"
"io"
"strings"
"testing"
"github.com/stretchr/testify/suite"
"github.com/testcontainers/testcontainers-go"
)
// RTCPFilterSuite reproduces go-gost/gost#898: multiple TCP services sharing
// ONE tunnel ID, routed at the client (rtcp) side by forwarder.nodes[].filter.host.
//
// The server ingress maps two hostnames to the same tunnel endpoint. The client
// rtcp service has two forwarder nodes, both filtered by host, with NO fallback
// node. When the hostname is correctly propagated through the tunnel, a request
// for example.local is routed to the example node (tcp-echo:5678). When the
// hostname is lost (the regression), neither node matches and the connection
// fails with "node not available".
type RTCPFilterSuite struct {
suite.Suite
ctx context.Context
echoC testcontainers.Container
serverC testcontainers.Container
clientC testcontainers.Container
}
func (s *RTCPFilterSuite) SetupSuite() {
s.ctx = context.Background()
echoC, err := RunEchoContainer(s.ctx, SharedNetworkName)
s.Require().NoError(err)
s.echoC = echoC
serverC, err := RunGostContainerWithOptions(s.ctx, SharedNetworkName,
"testdata/rtcpfilter/server.yaml", []string{"gost-server"}, []string{"8420/tcp"})
s.Require().NoError(err)
s.serverC = serverC
clientC, err := RunGostContainerWithOptions(s.ctx, SharedNetworkName,
"testdata/rtcpfilter/client.yaml", []string{"gost-client"}, []string{"8423/tcp"})
s.Require().NoError(err)
s.clientC = clientC
}
func (s *RTCPFilterSuite) TearDownSuite() {
for _, c := range []testcontainers.Container{s.clientC, s.serverC, s.echoC} {
if c != nil {
c.Terminate(s.ctx)
}
}
}
// request sends an HTTP GET to the server entrypoint with the given Host
// header, retrying until the reverse tunnel is bound. Returns the response body.
func (s *RTCPFilterSuite) request(host string) string {
cmd := []string{
"sh", "-c",
fmt.Sprintf("for i in $(seq 1 30); do body=$(curl -s -H 'Host: %s' http://gost-server:8420/); echo \"$body\" | grep -q hello-gost && { echo \"$body\"; exit 0; }; sleep 1; done; echo \"$body\"", host),
}
_, out, err := s.echoC.Exec(s.ctx, cmd)
s.Require().NoError(err)
body, err := io.ReadAll(out)
s.Require().NoError(err)
return string(body)
}
// TestMappedHostnameFiltered verifies that a Host matching a filter.host node is
// routed through the tunnel to that node's backend. This is the gost#898 failure:
// with the regression, the hostname is dropped and no node matches.
func (s *RTCPFilterSuite) TestMappedHostnameFiltered() {
body := s.request("example.local")
if !strings.Contains(body, "hello-gost") {
DumpLogs(s.T(), s.ctx, "rtcp client logs", s.clientC)
DumpLogs(s.T(), s.ctx, "tunnel server logs", s.serverC)
}
s.Require().Contains(body, "hello-gost")
}
func TestRTCPFilterSuite(t *testing.T) {
suite.Run(t, new(RTCPFilterSuite))
}
+147
View File
@@ -0,0 +1,147 @@
package e2e
import (
"context"
"io"
"strconv"
"strings"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/suite"
"github.com/testcontainers/testcontainers-go"
)
// RUDPBindSuite reproduces go-gost/gost#911: concurrent UDP source sockets
// sharing ONE reverse UDP tunnel over a relay server.
//
// Topology (all three gost processes plus a UDP echo server):
//
// UDP source sockets (N) -> client udp service -> relay server
// -> reverse-bound UDP port -> host rudp listener (ONE shared stream)
// -> udp-echo
//
// A datagram frame is written to that shared stream as three separate Write
// calls (RSV/FRAG, SOCKS5 address, payload). Before the fix, with N endpoints
// writing concurrently, frames interleaved between those calls: replies came
// back corrupted, and a torn frame left half a header in the stream, which the
// far end read as "unexpected EOF" and answered by tearing down and rebinding
// the whole reverse tunnel every second.
//
// The host is the side that binds the tunnel, so its log is where a reset
// shows up as "unexpected EOF, retrying in 1s" followed by a rebind.
type RUDPBindSuite struct {
suite.Suite
ctx context.Context
udpC testcontainers.Container
serverC testcontainers.Container
hostC testcontainers.Container
clientC testcontainers.Container
}
func (s *RUDPBindSuite) SetupSuite() {
s.ctx = context.Background()
udpC, err := RunUDPEchoContainer(s.ctx, SharedNetworkName)
s.Require().NoError(err)
s.udpC = udpC
serverC, err := RunGostContainerWithOptions(s.ctx, SharedNetworkName,
"testdata/rudpbind/server.yaml", []string{"gost-server"}, []string{"8430/tcp"})
s.Require().NoError(err)
s.serverC = serverC
// The host's rudp listener opens no local port, so it exposes a dummy TCP
// port (service-1) purely for the readiness wait.
hostC, err := RunGostContainerWithOptions(s.ctx, SharedNetworkName,
"testdata/rudpbind/host.yaml", []string{"gost-host"}, []string{"8431/tcp"})
s.Require().NoError(err)
s.hostC = hostC
clientC, err := RunGostContainerWithFiles(s.ctx, SharedNetworkName,
"testdata/rudpbind/client.yaml",
[]testcontainers.ContainerFile{
{HostFilePath: "scripts/udp_rudp_concurrent.py", ContainerFilePath: "/scripts/udp_rudp_concurrent.py", FileMode: 0644},
})
s.Require().NoError(err)
s.clientC = clientC
// The reverse tunnel binds remotely on first traffic, not at startup.
up := assert.Eventually(s.T(), func() bool {
out := s.sendConcurrent(1, 1)
if !strings.Contains(out, "PASS") {
s.T().Logf("waiting for tunnel, sender said:\n%s", out)
}
return out != "" && strings.Contains(out, "PASS")
}, 30*time.Second, time.Second, "reverse UDP tunnel never came up")
if !up {
DumpLogs(s.T(), s.ctx, "host logs", s.hostC)
DumpLogs(s.T(), s.ctx, "server logs", s.serverC)
DumpLogs(s.T(), s.ctx, "client logs", s.clientC)
s.FailNow("reverse UDP tunnel never came up")
}
}
func (s *RUDPBindSuite) TearDownSuite() {
for _, c := range []testcontainers.Container{s.clientC, s.hostC, s.serverC, s.udpC} {
if c != nil {
c.Terminate(s.ctx)
}
}
}
// sendConcurrent runs the concurrent sender inside the client container and
// returns its combined stdout+stderr.
func (s *RUDPBindSuite) sendConcurrent(endpoints, seconds int) string {
cmd := []string{
"python3", "/scripts/udp_rudp_concurrent.py",
// The client service listens in this same container, so loopback is the
// correct target — no network alias needed for the sender.
"127.0.0.1", "8432",
strconv.Itoa(endpoints), strconv.Itoa(seconds),
}
_, out, err := s.clientC.Exec(s.ctx, cmd)
if err != nil {
return ""
}
var sb strings.Builder
buf := make([]byte, 4096)
for {
n, err := out.Read(buf)
sb.Write(buf[:n])
if err != nil {
break
}
}
return sb.String()
}
// TestConcurrentEndpointsShareTunnel is the regression assertion: several UDP
// source sockets sharing one reverse tunnel must each get their own datagrams
// back unchanged, and the tunnel must not be rebuilt underneath them.
func (s *RUDPBindSuite) TestConcurrentEndpointsShareTunnel() {
out := s.sendConcurrent(4, 8)
s.T().Logf("concurrent send:\n%s", out)
if strings.Contains(out, "FAIL") {
DumpLogs(s.T(), s.ctx, "host logs", s.hostC)
DumpLogs(s.T(), s.ctx, "server logs", s.serverC)
}
s.Require().Contains(out, "PASS", "concurrent endpoints over one reverse tunnel failed:\n%s", out)
// Belt and braces: the host log is where the reset appears directly.
hostLogs, err := s.hostC.Logs(s.ctx)
if err == nil {
body, readErr := io.ReadAll(hostLogs)
hostLogs.Close()
if readErr == nil {
s.Require().NotContains(string(body), "unexpected EOF",
"reverse tunnel was torn down while endpoints were sending")
}
}
}
func TestRUDPBindSuite(t *testing.T) {
suite.Run(t, new(RUDPBindSuite))
}
+27
View File
@@ -0,0 +1,27 @@
import http.server
import json
import sys
class AuthHandler(http.server.BaseHTTPRequestHandler):
def do_POST(self):
length = int(self.headers.get('Content-Length', 0))
body = self.rfile.read(length)
data = json.loads(body)
print(f"AUTH_REQUEST: {json.dumps(data)}", flush=True)
self.send_response(200)
self.send_header("Content-Type", "application/json")
self.end_headers()
self.wfile.write(
json.dumps(
{"ok": True, "id": data.get("username", "anonymous")}
).encode()
)
def log_message(self, format, *args):
pass # silence default logging, we use our own AUTH_REQUEST line
if __name__ == "__main__":
port = int(sys.argv[1]) if len(sys.argv) > 1 else 9000
http.server.HTTPServer(("0.0.0.0", port), AuthHandler).serve_forever()
@@ -0,0 +1,74 @@
"""DNS responder for resolver e2e tests.
Returns the target IP (argv[1]) for an A query of "echo.test" and
NXDOMAIN for every other query. This lets a gost proxy configured with
this server as its resolver resolve a made-up hostname to a reachable
echo server, proving the resolver drives outbound dialing.
Listens on UDP port argv[2] (default 5353).
"""
import socketserver
import struct
import socket
import sys
TARGET_IP = sys.argv[1] if len(sys.argv) > 1 else "10.0.0.1"
PORT = int(sys.argv[2]) if len(sys.argv) > 2 else 5353
MATCH_NAME = "echo.test"
def decode_name(data, offset):
labels = []
while True:
length = data[offset]
if length == 0:
offset += 1
break
if length & 0xC0:
offset += 2
break
offset += 1
labels.append(data[offset:offset + length].decode())
offset += length
return '.'.join(labels), offset
class DNSResponder(socketserver.DatagramRequestHandler):
def handle(self):
data = self.rfile.read(512)
if len(data) < 12:
return
tid = struct.unpack(">H", data[:2])[0]
qdcount = struct.unpack(">H", data[4:6])[0]
if qdcount == 0:
return
qname, pos = decode_name(data, 12)
qtype = struct.unpack(">H", data[pos:pos + 2])[0]
qclass = struct.unpack(">H", data[pos + 2:pos + 4])[0]
if qname == MATCH_NAME and qtype == 1:
rcode, ancount = 0, 1
rtype, ttl, rdata = 1, 300, socket.inet_aton(TARGET_IP)
else:
rcode, ancount = 3, 0 # NXDOMAIN
rtype, ttl, rdata = 0, 0, b""
flags = 0x8000 | 0x0400 | rcode # QR=1, AA=1, rcode
header = struct.pack(">HHHHHH", tid, flags, qdcount, ancount, 0, 0)
question = data[12:pos + 4]
answer = b""
if ancount:
answer = (
struct.pack(">HH", 0xC00C, rtype) # NAME pointer + TYPE
+ struct.pack(">HI", qclass, ttl) # CLASS + TTL
+ struct.pack(">H", len(rdata)) + rdata # RDLENGTH + RDATA
)
self.wfile.write(header + question + answer)
if __name__ == "__main__":
socketserver.ThreadingUDPServer.allow_reuse_address = True
with socketserver.ThreadingUDPServer(("0.0.0.0", PORT), DNSResponder) as srv:
srv.serve_forever()
+22
View File
@@ -0,0 +1,22 @@
import socket, threading
s = socket.socket()
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
s.bind(("127.0.0.1", 15738))
s.listen(50)
def loop(c):
try:
while True:
d = c.recv(65536)
if not d:
break
c.sendall(d)
except OSError:
pass
finally:
c.close()
while True:
c, _ = s.accept()
threading.Thread(target=loop, args=(c,), daemon=True).start()
+20
View File
@@ -0,0 +1,20 @@
from http.server import BaseHTTPRequestHandler, HTTPServer
_counter = 0
class Handler(BaseHTTPRequestHandler):
def do_GET(self):
global _counter
_counter += 1
body = b"cache-test-%d" % _counter
self.send_response(200)
self.send_header("Content-Length", str(len(body)))
self.end_headers()
self.wfile.write(body)
def log_message(self, format, *args):
return
HTTPServer(("0.0.0.0", 5677), Handler).serve_forever()
@@ -0,0 +1,46 @@
from http.server import BaseHTTPRequestHandler, HTTPServer
import socket
import threading
_counter = 0
_http_server = None
class Handler(BaseHTTPRequestHandler):
def do_GET(self):
global _counter
_counter += 1
body = b"cache-test-%d" % _counter
self.send_response(200)
self.send_header("Content-Length", str(len(body)))
self.send_header("Connection", "close")
self.end_headers()
self.wfile.write(body)
threading.Thread(target=stop_server, daemon=True).start()
def log_message(self, format, *args):
return
def stop_server():
global _http_server
if _http_server:
_http_server.shutdown()
_http_server.server_close()
def start_raw_acceptor():
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
sock.bind(("0.0.0.0", 5676))
sock.listen(5)
while True:
conn, addr = sock.accept()
conn.close()
_http_server = HTTPServer(("0.0.0.0", 5676), Handler)
_http_server.serve_forever()
_http_server.server_close()
start_raw_acceptor()
+35
View File
@@ -0,0 +1,35 @@
"""HTTP server that always returns a fixed status code."""
import sys
from http.server import BaseHTTPRequestHandler, HTTPServer
STATUS = int(sys.argv[1]) if len(sys.argv) > 1 else 429
PORT = int(sys.argv[2]) if len(sys.argv) > 2 else 5680
BODY = f"status-{STATUS}".encode()
class Handler(BaseHTTPRequestHandler):
def do_GET(self):
self.send_response(STATUS)
self.send_header("Content-Length", str(len(BODY)))
self.end_headers()
self.wfile.write(BODY)
def do_POST(self):
self.send_response(STATUS)
self.send_header("Content-Length", str(len(BODY)))
self.end_headers()
self.wfile.write(BODY)
# HTTP proxy CONNECT method
def do_CONNECT(self):
self.send_response(STATUS)
self.send_header("Content-Length", str(len(BODY)))
self.end_headers()
self.wfile.write(BODY)
def log_message(self, format, *args):
return
HTTPServer(("0.0.0.0", PORT), Handler).serve_forever()
+42
View File
@@ -0,0 +1,42 @@
#!/usr/bin/env python3
import os
import subprocess
import ssl
from http.server import BaseHTTPRequestHandler, HTTPServer
PORT = int(os.environ.get("PORT", "8443"))
cert_dir = "/tmp/certs"
os.makedirs(cert_dir, exist_ok=True)
cert_file = os.path.join(cert_dir, "cert.pem")
key_file = os.path.join(cert_dir, "key.pem")
if not os.path.exists(cert_file) or not os.path.exists(key_file):
subprocess.run(
[
"openssl", "req", "-x509", "-newkey", "rsa:2048",
"-keyout", key_file, "-out", cert_file,
"-days", "365", "-nodes",
"-subj", "/CN=localhost",
],
check=True,
)
class Handler(BaseHTTPRequestHandler):
def do_GET(self):
body = b"hello-gost"
self.send_response(200)
self.send_header("Content-Length", str(len(body)))
self.end_headers()
self.wfile.write(body)
def log_message(self, format, *args):
return
server = HTTPServer(("0.0.0.0", PORT), Handler)
ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
ctx.load_cert_chain(cert_file, key_file)
server.socket = ctx.wrap_socket(server.socket, server_side=True)
server.serve_forever()
+23
View File
@@ -0,0 +1,23 @@
# Raw-TCP backend for the PROXY-protocol e2e test (issue #677).
# Reads the first bytes of each connection and reflects whether a HAProxy
# PROXY protocol header was prepended by gost:
# - v1 ("PROXY TCP4 ...") -> "PROXY-RECEIVED: PROXY TCP4 ..."
# - v2 (12-byte signature) -> "PROXY-V2-RECEIVED"
# - neither -> "NO-PROXY"
import socket
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
s.bind(("0.0.0.0", 5678))
s.listen(5)
while True:
c, _ = s.accept()
d = c.recv(4096)
if d[:12] == b"\r\n\r\n\x00\r\nQUIT\n":
c.sendall(b"PROXY-V2-RECEIVED\n")
elif d[:6] == b"PROXY ":
c.sendall(b"PROXY-RECEIVED: " + d.split(b"\r\n", 1)[0] + b"\n")
else:
c.sendall(b"NO-PROXY\n")
c.close()
@@ -0,0 +1,35 @@
import socket
import sys
import base64
# QUIC v1 Initial packet (captured from real traffic, SNI=cloudflare-quic.com).
# Used to verify that the GOST proxy correctly sniffs and forwards QUIC traffic.
QUIC_PKT_B64 = "wQAAAAEMbPz5zifdvbrMMCfeAABE6onIhQqNpzy3/idh5rftIZ4dn6chk10EX5zjUSCc9HmGCZ95QWw9MTxkPZODKiEHadVyfmCt/2fjoHGmQUplPcmvWinaEXA6ysN/MREQ76J2DDH4YPZj1aSF2vGotyNf64/Qga6nfM3Nb+3iOKul7fwEjmi7kLjgHZ5ryTb+PHsddTg+07jW/hcFOgiQ9P/vreI+zJuEzcv2xV1oYAY7PnnM9aRrtAdik2V0WlAvTN9kFeV813+fnzQCw9ztyG9UGD6S7aUrUbCx3N3PFGc5yMmHsSGz5ZsO1wq8zGE3G9TSJTe6GwQpFwWz2J5gEgC7WB2ulfZYinZRC3+JtJ8lKm8dWt7tT8ymOYxLjpWh+JOHkkQHwCHvCoS8EugDZRsRNP+uME6SdRjzR/zAkawOz8jQL7oUWIYAEggxP3MmPw7uHVcYZHi8mhgvxIm8u7p5y5wqqweRhbRKrd1ji9Rq2wfEXrkumnfjkboP3OOqR/os4DFIaCNh1KHqsmBpbvTIQjrNQ11LZJSZ/I0pq6A1pSo9N6l38Ty4p8Ji6my1TRFmnCejxfSutmN/ozc7N9wD4ZVXcEMmqTsTrvjgHK4Frmqmmzr9SnMfbKdZ9BGdOFWXQrYqP1g6+Dg+nacCH2PdWjMyleSTMMvMlG6RrwLC/2eC1pDkRuhTVJS3h0ZOX+17jqHmKabzWv17qjzWx4dLu2z/fwWwaIJTO4sEvk05cquuyBYPUOfjbh7cQhIVOLBVJaXVIgro+C96wmoIlxfEMEZHDmriXU2SM6PKq8xw4a8AgnYBkGDofNOXMfdQj+byiwQS6pIOkii+haHqSSP6Y9r17jT3Ykytv+jeT4UanXjtjepBb5N/ApISY2Xs71irWRRZr9LXHXnJNFFdMWN367JIT3i865UMeXzndIu3iZdOWpSAApBnU2JMyjloRCA5Li9ABYgS2qjyo0SbleFj1Qp4CMNeuigkGR4oq/BSQmYMZL2KYJFAyX4BEev3YYkkJ12HlXgY54QL8jgxlJ+3e+qklM/lGajujPLfocswLUeWklfBZhoOJvPgIV5N9zGg1bxZLUe+brHrPpHnQZBAsksyM3YZra+PscMwQTopDIPBd1A52rSrmcvgQs6QR07Bh/ttS3tn0I4Xtb6rrkVgkC3zckIS2fB6WPHUOruDhQRPPehOfTehvTIFASt+6IR+jlGQ4/ZT1PkwAO+sLCBNEU1FBtLFWUpJsjSva49BjN5slRAI5SSCXeiwsvXuCfV8ZSmFZRvMR1BBuaFVVFrdK7PqMrtdJ90BInTE1J9vYUmWYvnZYI20xUm6mj2iourEkki4d8Vjtg2JSICnql/jKcIAxyJUDx1ZoFxvAbz34jTvx681lepy97n1jyGa7DlnpM/O6x+GHFaANQSj+2iIKjrQ+sM3YtSGMR2ClSg73f+pEdWUV5tyfiTB5nLI+VOpmt0AVCguau4ihLEmxADfQ1PXe9i1T0xvpceePc06T5pG37MYP5w6lLOJBajAFJuu6lavTtE7EyreoBnxkvzPciKupNwTxWdmWT8HVgQgaUUbvDCtTQr6nJnNMaGk2zbsXm8kiXfXS4lgf1sqw7HCphffMSH73JCabipof896LXjj2I8hj8wA8UMladryva0hv+Px0DmzsbO2/Wh+IhnbD8EoYH2gUehLQwQ="
def main():
host = sys.argv[1] if len(sys.argv) > 1 else "127.0.0.1"
port = int(sys.argv[2]) if len(sys.argv) > 2 else 9000
pkt = base64.b64decode(QUIC_PKT_B64)
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
sock.settimeout(10)
sock.sendto(pkt, (host, port))
data, addr = sock.recvfrom(4096)
if len(data) == len(pkt):
print(f"PASS: received {len(data)} bytes (echo matches)")
sys.exit(0)
elif len(data) > 0:
print(f"PARTIAL: sent {len(pkt)}, received {len(data)} bytes")
sys.exit(0)
else:
print("FAIL: no response")
sys.exit(1)
if __name__ == "__main__":
main()
+105
View File
@@ -0,0 +1,105 @@
"""Concurrent UDP sender for the gost#911 reverse-tunnel regression test.
Each socket is an independent source endpoint sharing ONE reverse tunnel
stream. Every datagram is a run of its own endpoint id, so a reply that does
not match the id byte exactly is a frame corrupted by an interleaved writer
rather than plain loss.
Usage: udp_rudp_concurrent.py <host> <port> <count> <duration_seconds>
"""
import socket
import sys
import threading
import time
def main():
host = sys.argv[1]
port = int(sys.argv[2])
count = int(sys.argv[3])
duration = float(sys.argv[4])
payload_len = 1200
end = time.monotonic() + duration
results = {}
lock = threading.Lock()
def endpoint(endpoint_id):
payload = bytes([endpoint_id]) * payload_len
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
sock.bind(("0.0.0.0", 0))
sock.settimeout(0.2)
sent = received = damaged = 0
largest_gap = 0.0
last = time.monotonic()
while time.monotonic() < end:
try:
sock.sendto(payload, (host, port))
sent += 1
except OSError:
pass
while True:
try:
data, _ = sock.recvfrom(65535)
except socket.timeout:
break
except OSError:
break
received += 1
if data != payload:
damaged += 1
now = time.monotonic()
largest_gap = max(largest_gap, now - last)
last = now
largest_gap = max(largest_gap, time.monotonic() - last)
with lock:
results[endpoint_id] = (sent, received, damaged, round(largest_gap, 3))
sock.close()
threads = [
threading.Thread(target=endpoint, args=(i + 1,)) for i in range(count)
]
for t in threads:
t.start()
for t in threads:
t.join()
failures = []
for endpoint_id in sorted(results):
sent, received, damaged, gap = results[endpoint_id]
loss_pct = 100.0 * (1.0 - received / sent) if sent else 100.0
print(
f"endpoint {endpoint_id}: sent={sent} received={received} "
f"damaged={damaged} loss={loss_pct:.1f}% max_gap={gap}"
)
# Unsent datagrams are outside our control (the UDP socket's own
# buffer); anything that comes BACK must be byte-identical.
if damaged:
failures.append(
f"endpoint {endpoint_id}: {damaged} datagram(s) came back "
f"corrupted — frames interleaved on the shared tunnel stream"
)
# A torn frame makes the far end read io.ErrUnexpectedEOF, which
# rebuilds the whole reverse tunnel. That shows up as a receive gap of
# seconds, not milliseconds.
if gap >= 1.0:
failures.append(
f"endpoint {endpoint_id}: {gap}s receive gap — the reverse "
f"tunnel was torn down and rebound under active traffic"
)
if failures:
for f in failures:
print(f"FAIL: {f}", file=sys.stderr)
sys.exit(1)
print(f"PASS: {count} concurrent endpoints, no corruption, no tunnel reset")
sys.exit(0)
if __name__ == "__main__":
main()
+114
View File
@@ -0,0 +1,114 @@
package e2e
import (
"context"
"fmt"
"io"
"strings"
"testing"
"github.com/stretchr/testify/suite"
"github.com/testcontainers/testcontainers-go"
)
type SelectorSuite struct {
suite.Suite
ctx context.Context
echoC testcontainers.Container
echoIP string
}
func (s *SelectorSuite) SetupSuite() {
s.ctx = context.Background()
echoC, err := RunEchoContainer(s.ctx, SharedNetworkName)
s.Require().NoError(err)
s.echoC = echoC
echoIP, err := echoC.ContainerIP(s.ctx)
s.Require().NoError(err)
s.echoIP = echoIP
}
func (s *SelectorSuite) TearDownSuite() {
if s.echoC != nil {
s.echoC.Terminate(s.ctx)
}
}
// proxyRequest sends one request through the gost proxy and returns the curl
// exit code and response body.
func (s *SelectorSuite) proxyRequest(gostC testcontainers.Container, port string) (int, string) {
cmd := []string{
"curl", "-s",
"-x", fmt.Sprintf("http://127.0.0.1:%s", port),
fmt.Sprintf("http://%s:5678", s.echoIP),
}
code, out, err := gostC.Exec(s.ctx, cmd)
s.Require().NoError(err)
body, err := io.ReadAll(out)
s.Require().NoError(err)
return code, string(body)
}
// runFailover starts a gost container from cfg and verifies that, despite a
// dead node in the hop, requests converge to the live node after the selector
// marks the dead node and skips it. At most the initial marking attempt may
// fail; every request after must reach the echo server.
func (s *SelectorSuite) runFailover(cfg, port string) {
gostC, err := RunGostContainerWithPorts(s.ctx, SharedNetworkName, cfg, port+"/tcp")
s.Require().NoError(err)
defer gostC.Terminate(s.ctx)
const n = 12
var failures int
lastOK := false
for range n {
code, body := s.proxyRequest(gostC, port)
ok := code == 0 && strings.Contains(body, "hello-gost")
if !ok {
failures++
}
lastOK = ok
}
s.Require().LessOrEqual(failures, 1,
"selector did not filter the dead node; too many requests failed")
s.Require().True(lastOK, "requests did not converge to the live node")
}
// TestRoundRobinFailover exercises the round-robin strategy combined with the
// fail filter: the dead node is tried once, marked, and then skipped.
func (s *SelectorSuite) TestRoundRobinFailover() {
s.runFailover("testdata/selector/roundrobin.yaml", "8080")
}
// TestFIFOFailover exercises the fifo (sticky) strategy: it always picks the
// first node until it fails, then falls through to the secondary node.
func (s *SelectorSuite) TestFIFOFailover() {
s.runFailover("testdata/selector/fifo.yaml", "8080")
}
// TestBackupFailover exercises the backup filter: with the primary node dead,
// the selector falls back to the backup node.
func (s *SelectorSuite) TestBackupFailover() {
s.runFailover("testdata/selector/backup.yaml", "8080")
}
// TestParallelSelector exercises the parallel strategy: it dials all nodes
// concurrently and uses the first that connects, so a dead node never blocks
// the request.
func (s *SelectorSuite) TestParallelSelector() {
gostC, err := RunGostContainerWithPorts(s.ctx, SharedNetworkName, "testdata/selector/parallel.yaml", "8080/tcp")
s.Require().NoError(err)
defer gostC.Terminate(s.ctx)
code, body := s.proxyRequest(gostC, "8080")
s.Require().Equal(0, code)
s.Require().Contains(body, "hello-gost")
}
func TestSelectorSuite(t *testing.T) {
suite.Run(t, new(SelectorSuite))
}
+115
View File
@@ -0,0 +1,115 @@
package e2e
import (
"context"
"io"
"strings"
"testing"
"github.com/stretchr/testify/suite"
"github.com/testcontainers/testcontainers-go"
)
// SniffingSuite covers protocol sniffing behavior.
type SniffingSuite struct {
suite.Suite
ctx context.Context
httpsEchoC testcontainers.Container
httpsIP string
}
func (s *SniffingSuite) SetupSuite() {
s.ctx = context.Background()
c, err := RunHTTPSEchoContainer(s.ctx, SharedNetworkName)
s.Require().NoError(err)
s.httpsEchoC = c
ip, err := c.ContainerIP(s.ctx)
s.Require().NoError(err)
s.httpsIP = ip
}
func (s *SniffingSuite) TearDownSuite() {
if s.httpsEchoC != nil {
s.httpsEchoC.Terminate(s.ctx)
}
}
// curlThroughSOCKS5 runs curl through the GOST SOCKS5 proxy on the given
// container address and port, connecting to the HTTPS echo server by IP.
// Connecting by IP makes curl omit the TLS SNI extension.
func (s *SniffingSuite) curlThroughSOCKS5(c testcontainers.Container, proxyPort string) string {
s.T().Helper()
// curl -k: don't verify the self-signed cert on the echo server
// curl -x socks5://...: route through GOST SOCKS5 proxy
// Using the container IP as the host makes curl skip SNI entirely.
cmd := []string{"curl", "-k", "-s",
"-x", "socks5://127.0.0.1:" + proxyPort,
"https://" + s.httpsIP + ":8443",
}
code, out, err := c.Exec(s.ctx, cmd)
s.Require().NoError(err)
s.Require().Zero(code, "curl should exit 0")
b, _ := io.ReadAll(out)
return string(b)
}
// TestSOCKS5NoSNI verifies that a SOCKS5 proxy with sniffing enabled can
// successfully forward TLS connections that lack an SNI extension — such as
// when a client connects to an HTTPS server by IP address.
func (s *SniffingSuite) TestSOCKS5NoSNI() {
gostC, err := RunGostContainer(s.ctx, SharedNetworkName,
"testdata/sniffing/no_sni.yaml",
)
s.Require().NoError(err)
defer func() {
DumpLogs(s.T(), s.ctx, "gost-no-sni", gostC)
gostC.Terminate(s.ctx)
}()
body := s.curlThroughSOCKS5(gostC, "8080")
s.Require().Contains(body, "hello-gost",
"HTTPS response should pass through when SNI is empty")
}
// TestSOCKS5NoSNI_Callback verifies the same behavior a second time, serving
// as a sanity check that state doesn't leak between connections.
func (s *SniffingSuite) TestSOCKS5NoSNI_Callback() {
s.TestSOCKS5NoSNI()
}
// TestSOCKS5NoSNI_LogsOnlyDebug verifies the proxy logs a debug message
// rather than an error when SNI is missing.
func (s *SniffingSuite) TestSOCKS5NoSNI_LogsOnlyDebug() {
gostC, err := RunGostContainer(s.ctx, SharedNetworkName,
"testdata/sniffing/no_sni.yaml",
)
s.Require().NoError(err)
defer func() {
// Do NOT dump logs before the grep — DumpLogs consumes the reader
gostC.Terminate(s.ctx)
}()
body := s.curlThroughSOCKS5(gostC, "8080")
s.Require().Contains(body, "hello-gost")
// Verify the debug log message exists and no error-level SNI message.
logs, err := gostC.Logs(s.ctx)
s.Require().NoError(err)
defer logs.Close()
logBody, _ := io.ReadAll(logs)
logStr := string(logBody)
s.Require().Contains(logStr, "no sni in clienthello",
"should log a debug message when SNI is empty")
s.Require().NotContains(logStr, "tls: sni is empty",
"must not error on empty SNI")
// The log level for "no sni" should be debug, not error.
s.Require().False(strings.Contains(logStr, `"level":"error"`),
"should not log any error-level message")
}
func TestSniffingSuite(t *testing.T) {
suite.Run(t, new(SniffingSuite))
}
+17
View File
@@ -0,0 +1,17 @@
# HTTP proxy gated by a blacklist admission rule: clients whose source address
# is 127.0.0.1 are denied; everything else is admitted. A loopback client (curl
# inside the gost container) is denied; an external client (a different
# container) is admitted and reaches the echo server.
services:
- name: proxy
addr: ":8080"
admission: admission-0
handler:
type: http
listener:
type: tcp
admissions:
- name: admission-0
matchers:
- 127.0.0.1
+18
View File
@@ -0,0 +1,18 @@
# HTTP proxy gated by a whitelist admission rule: only clients whose source
# address is 127.0.0.1 are admitted. A loopback client (curl inside the gost
# container) is admitted and reaches the echo server; an external client (a
# different container) is denied at accept time.
services:
- name: proxy
addr: ":8080"
admission: admission-0
handler:
type: http
listener:
type: tcp
admissions:
- name: admission-0
whitelist: true
matchers:
- 127.0.0.1
+18
View File
@@ -0,0 +1,18 @@
# HTTP proxy authenticated via a named auther holding multiple users. Any
# listed user/password pair is accepted; unlisted credentials are rejected.
services:
- name: proxy
addr: ":8080"
handler:
type: http
auther: auther-0
listener:
type: tcp
authers:
- name: auther-0
auths:
- username: alice
password: secret
- username: bob
password: hunter2
+13
View File
@@ -0,0 +1,13 @@
# HTTP proxy with inline single-user authentication. Clients must supply the
# proxy credentials user/pass; wrong or missing credentials get a 407 and never
# reach the echo server.
services:
- name: proxy
addr: ":8080"
handler:
type: http
auth:
username: user
password: pass
listener:
type: tcp
+29
View File
@@ -0,0 +1,29 @@
# Forward proxy whose only chain node is dead (127.0.0.1:18082). A blacklist
# bypass matching the destination skips that node and connects directly to the
# echo server; a non-matching destination is forced through the dead node.
services:
- name: proxy
addr: ":8080"
handler:
type: http
chain: my-chain
listener:
type: tcp
chains:
- name: my-chain
hops:
- name: hop-0
nodes:
- name: node-0
addr: 127.0.0.1:18082
bypass: bypass-0
connector:
type: http
dialer:
type: tcp
bypasses:
- name: bypass-0
matchers:
- "{{.ServerAddr}}"
+32
View File
@@ -0,0 +1,32 @@
# Forward proxy whose only chain node is dead (127.0.0.1:18082). A whitelist
# bypass inverts the logic: only destinations matching the rule are forced
# through the chain. The echo server's address is outside 10.0.0.0/8, so it is
# bypassed (connected directly); a 10.0.0.0/8 destination is forced through the
# dead node.
services:
- name: proxy
addr: ":8080"
handler:
type: http
chain: my-chain
listener:
type: tcp
chains:
- name: my-chain
hops:
- name: hop-0
nodes:
- name: node-0
addr: 127.0.0.1:18082
bypass: bypass-0
connector:
type: http
dialer:
type: tcp
bypasses:
- name: bypass-0
whitelist: true
matchers:
- "10.0.0.0/8"
+48
View File
@@ -0,0 +1,48 @@
services:
- name: proxy
addr: :8080
handler:
type: http
chainGroup:
chains:
- chain: chain-target
matcher:
rule: Host(`tcp-echo`)
- chain: chain-other
matcher:
rule: Host(`no-match`)
selector:
strategy: round
maxFails: 1
failTimeout: 10s
listener:
type: tcp
# Live relay: forwards to echo server.
- name: relay
addr: 127.0.0.1:18081
handler:
type: http
listener:
type: tcp
chains:
- name: chain-target
hops:
- name: hop-target
nodes:
- name: n-live
addr: 127.0.0.1:18081
connector:
type: http
# This chain has a dead relay — but the matcher Host("no-match") ensures it is
# never selected for requests targeting tcp-echo.
- name: chain-other
hops:
- name: hop-other
nodes:
- name: n-dead
addr: 127.0.0.1:18082
connector:
type: http
+52
View File
@@ -0,0 +1,52 @@
services:
- name: proxy
addr: :8080
handler:
type: http
chainGroup:
chains:
- chain: chain-live
probe:
type: tcp
addr: 127.0.0.1:18081
interval: 3s
- chain: chain-dead
probe:
type: tcp
addr: 127.0.0.1:18082
interval: 3s
selector:
strategy: round
maxFails: 1
failTimeout: 10s
listener:
type: tcp
# Live relay: forwards to echo server.
- name: relay
addr: 127.0.0.1:18081
handler:
type: http
listener:
type: tcp
chains:
- name: chain-live
hops:
- name: hop-live
nodes:
- name: n-live
addr: 127.0.0.1:18081
connector:
type: http
# Dead chain: relay never listens here. TCP probe marks the chain entry so
# FailFilter excludes it before real traffic reaches it.
- name: chain-dead
hops:
- name: hop-dead
nodes:
- name: n-dead
addr: 127.0.0.1:18082
connector:
type: http
+27
View File
@@ -0,0 +1,27 @@
services:
# Reverse proxy with sniffing: raw TCP listener inspects HTTP via the
# forwarder's sniffer. The hop has FIFO selector (always picks first node).
# node-429 is first in the list with failCodes=429 — it must be marked on
# the first 429 response and excluded by FailFilter (maxFails=1).
- name: reverse-proxy
addr: :8080
handler:
type: tcp
metadata:
sniffing: true
sniffing.timeout: 2s
listener:
type: tcp
forwarder:
selector:
strategy: fifo
maxFails: 1
nodes:
- name: node-429
addr: status-backend:5680
protocol: http
http:
failCodes: "429"
- name: node-good
addr: tcp-echo:5678
protocol: http
@@ -0,0 +1,15 @@
services:
- name: udp-forward-quic
addr: :9000
handler:
type: udp
metadata:
sniffing: true
sniffing.timeout: 5s
stateless: true
listener:
type: udp
forwarder:
nodes:
- name: echo
addr: udp-echo:5679
+18
View File
@@ -0,0 +1,18 @@
# HTTP proxy with a static hosts mapping. The made-up hostname echo.internal is
# mapped to the echo server's real IP, overriding DNS. A request to
# echo.internal resolves via the mapping and reaches the echo server; a request
# to an unmapped hostname fails to resolve and never connects.
services:
- name: proxy
addr: ":8080"
hosts: hosts-0
handler:
type: http
listener:
type: tcp
hosts:
- name: hosts-0
mappings:
- ip: {{.ServerAddr}}
hostname: echo.internal
+18
View File
@@ -0,0 +1,18 @@
# Service-level destination bypass (blacklist) on an HTTP forward proxy.
# Blocks the echo server's address ({{.ServerAddr}}). Used by
# TestHTTPProxyGostTargetPolicyBypass to verify that a Gost-Target header
# cannot separate the authority evaluated by the policy from the authority the
# transport dials.
bypasses:
- name: bypass-0
matchers:
- "{{.ServerAddr}}"
services:
- name: http-policy
addr: ":8080"
handler:
type: http
listener:
type: tcp
bypass: bypass-0
+22
View File
@@ -0,0 +1,22 @@
caches:
- name: http-cache
memory:
ttl: 10m
maxSize: 1000
services:
- name: cache-proxy
addr: :8080
cache: http-cache
handler:
type: tcp
metadata:
sniffing: true
sniffing.timeout: 2s
cache.ttl: 60s
listener:
type: tcp
forwarder:
nodes:
- name: target
addr: cache-backend:5677
@@ -0,0 +1,23 @@
caches:
- name: http-cache
memory:
ttl: 10m
maxSize: 1000
services:
- name: cache-proxy
addr: :8080
cache: http-cache
handler:
type: tcp
metadata:
sniffing: true
sniffing.timeout: 2s
cache.ttl: 3s
cache.serveStale: true
listener:
type: tcp
forwarder:
nodes:
- name: target
addr: cache-servestale:5676
+14
View File
@@ -0,0 +1,14 @@
services:
- name: cache-proxy
addr: :8080
handler:
type: tcp
metadata:
sniffing: true
sniffing.timeout: 2s
listener:
type: tcp
forwarder:
nodes:
- name: target
addr: cache-backend:5677
+37
View File
@@ -0,0 +1,37 @@
# Internal client behind NAT: binds a reverse tunnel (tunnel.id matches the
# server's ingress endpoint) and forwards tunneled connections to the echo
# server. The tunnel server is reached via chain-0's tunnel connector.
#
# service-1 is a dummy TCP listener on a fixed port; the rtcp reverse-tunnel
# listener binds remotely through the chain and opens no local port, so this
# gives the e2e harness a stable port to wait on.
services:
- name: service-0
addr: ":0"
handler:
type: rtcp
listener:
type: rtcp
chain: chain-0
forwarder:
nodes:
- addr: tcp-echo:5678
- name: service-1
addr: ":8423"
handler:
type: http
listener:
type: tcp
chains:
- name: chain-0
hops:
- nodes:
- addr: gost-server:8421
connector:
type: tunnel
metadata:
tunnel.id: 6be39003-f4fa-49e4-a6ef-1997684d160e
dialer:
type: tcp
+23
View File
@@ -0,0 +1,23 @@
# Public-facing gost: a tunnel handler with an ingress table that maps the
# hostname example.local to a tunnel endpoint, plus an HTTP entrypoint on :8420.
# External requests to the entrypoint are routed by their Host header through
# the ingress table to the matching tunnel.
services:
- name: service-0
addr: ":8421"
handler:
type: tunnel
metadata:
entrypoint: ":8420"
ingress: ingress-0
listener:
type: tcp
ingresses:
- name: ingress-0
rules:
- hostname: example.local
endpoint: 6be39003-f4fa-49e4-a6ef-1997684d160e
log:
level: debug
+17
View File
@@ -0,0 +1,17 @@
services:
- name: https-mtls
addr: :8443
handler:
type: http
auther: auther-0
listener:
type: tls
tls:
certFile: /certs/server.pem
keyFile: /certs/server-key.pem
caFile: /certs/ca.pem
authers:
- name: auther-0
plugin:
type: http
addr: http://{{.ServerAddr}}:9000/auth
+20
View File
@@ -0,0 +1,20 @@
services:
- name: pht-proxy
addr: :8080
handler:
type: http
chain: pht-chain
listener:
type: tcp
chains:
- name: pht-chain
hops:
- name: pht-hop
nodes:
- name: pht-node
addr: {{.ServerAddr}}
connector:
type: http
dialer:
type: pht
+20
View File
@@ -0,0 +1,20 @@
services:
- name: phts-proxy
addr: :8080
handler:
type: http
chain: phts-chain
listener:
type: tcp
chains:
- name: phts-chain
hops:
- name: phts-hop
nodes:
- name: phts-node
addr: {{.ServerAddr}}
connector:
type: http
dialer:
type: phts
+7
View File
@@ -0,0 +1,7 @@
services:
- name: pht-server
addr: :8443
handler:
type: http
listener:
type: pht
+7
View File
@@ -0,0 +1,7 @@
services:
- name: phts-server
addr: :8443
handler:
type: http
listener:
type: phts
+43
View File
@@ -0,0 +1,43 @@
services:
- name: proxy
addr: :8080
handler:
type: http
chain: my-chain
listener:
type: tcp
# Live relay: forwards to the echo server.
- name: relay
addr: 127.0.0.1:18081
handler:
type: http
listener:
type: tcp
chains:
- name: my-chain
hops:
- name: hop-1
selector:
strategy: round
maxFails: 1
failTimeout: 10s
nodes:
- name: node-live
addr: 127.0.0.1:18081
connector:
type: http
probe:
type: cmd
command: "true"
interval: 5s
# This node's probe always fails → pre-marked dead → FailFilter excludes it.
- name: node-dead
addr: 127.0.0.1:18082
connector:
type: http
probe:
type: cmd
command: "false"
interval: 5s
+49
View File
@@ -0,0 +1,49 @@
services:
- name: proxy
addr: :8080
handler:
type: http
chain: my-chain
listener:
type: tcp
# Two live relays — both forward to the echo server.
- name: relay-a
addr: 127.0.0.1:18081
handler:
type: http
listener:
type: tcp
- name: relay-b
addr: 127.0.0.1:18082
handler:
type: http
listener:
type: tcp
chains:
- name: my-chain
hops:
- name: hop-1
selector:
strategy: lowestlatency
maxFails: 1
failTimeout: 10s
nodes:
- name: node-a
addr: 127.0.0.1:18081
connector:
type: http
probe:
type: tcp
addr: 127.0.0.1:18081
interval: 5s
- name: node-b
addr: 127.0.0.1:18082
connector:
type: http
probe:
type: tcp
addr: 127.0.0.1:18082
interval: 5s
+52
View File
@@ -0,0 +1,52 @@
services:
- name: proxy
addr: :8080
handler:
type: http
chain: my-chain
listener:
type: tcp
# Two live relays — both forward to the echo server. Relays never go down;
# liveness is driven solely by the cmd probes reading flag files in the
# container, so the test can flip node health without restarting anything.
- name: relay-a
addr: 127.0.0.1:18081
handler:
type: http
listener:
type: tcp
- name: relay-b
addr: 127.0.0.1:18082
handler:
type: http
listener:
type: tcp
chains:
- name: my-chain
hops:
- name: hop-1
selector:
strategy: lowestlatency
maxFails: 1
failTimeout: 10s
nodes:
- name: node-a
addr: 127.0.0.1:18081
connector:
type: http
probe:
# exits 1 while /tmp/a_down exists → node marked dead by probe.
type: cmd
command: "test -f /tmp/a_down && exit 1 || exit 0"
interval: 2s
- name: node-b
addr: 127.0.0.1:18082
connector:
type: http
probe:
type: cmd
command: "test -f /tmp/b_down && exit 1 || exit 0"
interval: 2s
+44
View File
@@ -0,0 +1,44 @@
services:
- name: proxy
addr: :8080
handler:
type: http
chain: my-chain
listener:
type: tcp
# Live relay: forwards to the echo server.
- name: relay
addr: 127.0.0.1:18081
handler:
type: http
listener:
type: tcp
chains:
- name: my-chain
hops:
- name: hop-1
selector:
strategy: round
maxFails: 1
failTimeout: 10s
nodes:
- name: node-live
addr: 127.0.0.1:18081
connector:
type: http
probe:
type: tcp
addr: 127.0.0.1:18081
interval: 5s
# dead node: probed, marked as failed by the probe goroutine,
# then excluded by FailFilter before any real traffic tries it.
- name: node-dead
addr: 127.0.0.1:18082
connector:
type: http
probe:
type: tcp
addr: 127.0.0.1:18082
interval: 5s
+19
View File
@@ -0,0 +1,19 @@
# go-gost/gost#677 — send-proxy (PROXY protocol v1) for the rtcp forward handler.
# gost accepts a TCP connection on :8080 and forwards it to the backend,
# prepending a "PROXY TCP4 ..." header because metadata.proxyProtocol is 1.
services:
- name: service-0
addr: ":8080"
handler:
type: rtcp
metadata:
proxyProtocol: 1
listener:
type: rtcp
forwarder:
nodes:
- name: target-0
addr: proxy-capture:5678
log:
level: debug
+18
View File
@@ -0,0 +1,18 @@
# go-gost/gost#677 — send-proxy (PROXY protocol v2) for the rtcp forward handler.
# Same as v1.yaml but emits the binary v2 header (metadata.proxyProtocol: 2).
services:
- name: service-0
addr: ":8080"
handler:
type: rtcp
metadata:
proxyProtocol: 2
listener:
type: rtcp
forwarder:
nodes:
- name: target-0
addr: proxy-capture:5678
log:
level: debug
+22
View File
@@ -0,0 +1,22 @@
services:
- name: http-proxy
addr: :8080
handler:
type: http
chain: quic-chain
listener:
type: tcp
chains:
- name: quic-chain
hops:
- name: hop-0
nodes:
- name: node-0
addr: {{.ServerAddr}}
connector:
type: http
dialer:
type: quic
metadata:
cipherKey: 0123456789abcdef0123456789abcdef
+9
View File
@@ -0,0 +1,9 @@
services:
- name: quic-http
addr: :18843
handler:
type: http
listener:
type: quic
metadata:
cipherKey: 0123456789abcdef0123456789abcdef
+27
View File
@@ -0,0 +1,27 @@
# gost#899: whitelist bypass + sniffing on a transparent (red) proxy.
# The whitelist contains only a domain ("https-echo", the docker network alias
# of the HTTPS echo container). A bare destination IP is never in it, so on a
# broken build the pre-sniffing IP bypass check rejects every connection
# before SNI sniffing can match the host. The fix skips that check in
# whitelist mode and lets the sniffer drive the decision from the SNI.
log:
level: debug
services:
- name: transparent-egress
addr: ":12345"
bypass: allowlist
metadata:
so_mark: 114514
handler:
type: red
metadata:
sniffing: true
sniffing.timeout: 5s
sniffing.fallback: true
listener:
type: red
bypasses:
- name: allowlist
whitelist: true
matchers:
- https-echo
+19
View File
@@ -0,0 +1,19 @@
# HTTP proxy whose outbound dialing uses a custom resolver. The resolver's
# only nameserver is the test DNS responder, which answers echo.test with the
# real echo server IP and NXDOMAIN for everything else. A request to
# echo.test:5678 is therefore resolved by the custom resolver and reaches the
# echo server; a request to an unmapped host fails to resolve.
services:
- name: proxy
addr: ":8080"
resolver: resolver-0
handler:
type: http
listener:
type: tcp
resolvers:
- name: resolver-0
nameservers:
- addr: dns-responder:5353
timeout: 3s
+27
View File
@@ -0,0 +1,27 @@
# Forward proxy gated by a node-level routing matcher. The only chain node is a
# relay to an upstream proxy, but it is only eligible when the request Host
# matches Host(`tcp-echo`). A request to tcp-echo:5678 matches, is relayed
# upstream, and reaches the echo server ("hello-gost"); a request to any other
# host is excluded (no eligible node) and never reaches the echo server.
services:
- name: proxy
addr: ":8080"
handler:
type: http
chain: chain-0
listener:
type: tcp
chains:
- name: chain-0
hops:
- name: hop-0
nodes:
- name: node-0
addr: gost-upstream:8081
matcher:
rule: 'Host(`tcp-echo`)'
connector:
type: http
dialer:
type: tcp
+9
View File
@@ -0,0 +1,9 @@
# Upstream relay target for the routing matcher test: a plain forward proxy
# that reaches the echo server directly.
services:
- name: proxy
addr: ":8081"
handler:
type: http
listener:
type: tcp
+51
View File
@@ -0,0 +1,51 @@
# Internal client behind NAT: binds a reverse tunnel (tunnel.id matches the
# server's ingress endpoint) and forwards tunneled connections to per-hostname
# target services via filter.host node matching. There is deliberately NO
# fallback node — routing must come from the hostname.
#
# node example → tcp-echo:5678 (the real echo HTTP server)
# node other → tcp-echo:5679 (nothing listens here; must never be hit by
# example.local traffic)
services:
- name: service-0
addr: ":0"
handler:
type: rtcp
listener:
type: rtcp
chain: chain-0
forwarder:
nodes:
- name: example
addr: tcp-echo:5678
filter:
host: example.local
- name: other
addr: tcp-echo:5679
filter:
host: other.local
# Dummy TCP service on a fixed port: the rtcp reverse-tunnel listener binds
# remotely through the chain and opens no local port, so this gives the e2e
# harness a stable port to wait on.
- name: service-1
addr: ":8423"
handler:
type: http
listener:
type: tcp
chains:
- name: chain-0
hops:
- nodes:
- addr: gost-server:8421
connector:
type: tunnel
metadata:
tunnel.id: 6be39003-f4fa-49e4-a6ef-1997684d160e
dialer:
type: tcp
log:
level: debug
+25
View File
@@ -0,0 +1,25 @@
# Public-facing gost: tunnel handler with an ingress table mapping two
# hostnames to the SAME tunnel endpoint (multi-TCP-service sharing one tunnel
# ID — the gost#898 scenario). Requests to the HTTP entrypoint are routed by
# their Host header through the ingress table to the matching tunnel.
services:
- name: service-0
addr: ":8421"
handler:
type: tunnel
metadata:
entrypoint: ":8420"
ingress: ingress-0
listener:
type: tcp
ingresses:
- name: ingress-0
rules:
- hostname: example.local
endpoint: 6be39003-f4fa-49e4-a6ef-1997684d160e
- hostname: other.local
endpoint: 6be39003-f4fa-49e4-a6ef-1997684d160e
log:
level: debug
+34
View File
@@ -0,0 +1,34 @@
# Internal client behind NAT for gost#911: a UDP forward service that sends all
# traffic to the port the host bound through the reverse tunnel.
#
# The forwarder target is gost-server:8432 — that is where the reverse tunnel is
# actually bound (the host's rudp listener asked the relay server to bind it, so
# the port lives on the relay server, not on the host). Datagrams go there and
# are carried back to the internal host over the one shared tunnel stream.
services:
- name: service-0
addr: ":8432"
handler:
type: udp
listener:
type: udp
metadata:
keepalive: true
ttl: 5m
forwarder:
nodes:
- name: tunnel
addr: gost-server:8432
chains:
- name: chain-0
hops:
- nodes:
- addr: gost-server:8430
connector:
type: relay
dialer:
type: tcp
log:
level: debug
+44
View File
@@ -0,0 +1,44 @@
# Internal host behind NAT for gost#911: binds a reverse UDP tunnel (rudp)
# through the relay server and forwards it to the local UDP echo server.
#
# The rudp listener opens no local port — it binds the UDP port remotely through
# the chain. service-1 is a dummy TCP listener purely so the e2e harness has a
# stable port to wait on for container readiness.
services:
- name: service-0
addr: "0.0.0.0:8432"
handler:
type: rudp
listener:
type: rudp
chain: chain-0
metadata:
ttl: 5m
forwarder:
nodes:
- name: echo
addr: udp-echo:5679
# Dummy TCP service on a fixed port: the rudp reverse-tunnel listener binds
# remotely through the chain and opens no local port, so this gives the e2e
# harness a stable port to wait on. Nothing ever targets it — the harness
# only opens and immediately closes the connection.
- name: service-1
addr: ":8431"
handler:
type: tcp
listener:
type: tcp
chains:
- name: chain-0
hops:
- nodes:
- addr: gost-server:8430
connector:
type: relay
dialer:
type: tcp
log:
level: debug
+16
View File
@@ -0,0 +1,16 @@
# Public-facing relay server for gost#911: allows UDP BIND so an internal host
# can bind a reverse UDP tunnel through it. The host asks the server to bind
# the tunnel's UDP port, so that port lives here — on the relay server, not on
# the host behind NAT.
services:
- name: service-0
addr: ":8430"
handler:
type: relay
metadata:
bind: true
listener:
type: tcp
log:
level: debug
+37
View File
@@ -0,0 +1,37 @@
services:
- name: proxy
addr: :8080
handler:
type: http
chain: my-chain
listener:
type: tcp
# Live relay: forwards the request to the echo server (curl target).
- name: relay
addr: 127.0.0.1:18081
handler:
type: http
listener:
type: tcp
chains:
- name: my-chain
hops:
- name: hop-1
selector:
strategy: round
maxFails: 1
nodes:
# primary: dead, non-backup.
- name: node-1
addr: 127.0.0.1:18082
connector:
type: http
# backup: only used once all primary nodes are dead.
- name: node-2
addr: 127.0.0.1:18081
connector:
type: http
metadata:
backup: true
+34
View File
@@ -0,0 +1,34 @@
services:
- name: proxy
addr: :8080
handler:
type: http
chain: my-chain
listener:
type: tcp
# Live relay: forwards the request to the echo server (curl target).
- name: relay
addr: 127.0.0.1:18081
handler:
type: http
listener:
type: tcp
chains:
- name: my-chain
hops:
- name: hop-1
selector:
strategy: fifo
maxFails: 1
nodes:
# primary: dead, so fifo must fall through to the secondary node.
- name: node-1
addr: 127.0.0.1:18082
connector:
type: http
- name: node-2
addr: 127.0.0.1:18081
connector:
type: http
+34
View File
@@ -0,0 +1,34 @@
services:
- name: proxy
addr: :8080
handler:
type: http
chain: my-chain
listener:
type: tcp
# Live relay: forwards the request to the echo server (curl target).
- name: relay
addr: 127.0.0.1:18081
handler:
type: http
listener:
type: tcp
chains:
- name: my-chain
hops:
- name: hop-1
selector:
strategy: round
maxFails: 1
nodes:
- name: node-1
addr: 127.0.0.1:18081
connector:
type: http
# dead node: never listens; must be marked and skipped by the selector.
- name: node-2
addr: 127.0.0.1:18082
connector:
type: http
+12
View File
@@ -0,0 +1,12 @@
services:
- name: service-0
addr: ":8080"
handler:
type: socks5
metadata:
sniffing: true
listener:
type: tcp
log:
level: debug
+11
View File
@@ -0,0 +1,11 @@
services:
- name: tls-reject
addr: :8443
handler:
type: http
listener:
type: tls
tls:
rejectUnknownSNI: true
serverNames:
- example.com
+9
View File
@@ -0,0 +1,9 @@
services:
- name: tls-reject
addr: :8443
handler:
type: http
listener:
type: tls
tls:
rejectUnknownSNI: true
+24
View File
@@ -0,0 +1,24 @@
services:
- name: http-proxy
addr: :8080
handler:
type: http
chain: utls-chain
listener:
type: tcp
chains:
- name: utls-chain
hops:
- name: hop-0
nodes:
- name: node-0
addr: {{.ServerAddr}}
connector:
type: http
dialer:
type: utls
metadata:
fingerprint: Chrome
tls:
secure: false
+26
View File
@@ -0,0 +1,26 @@
services:
- name: http-proxy
addr: :8080
handler:
type: http
chain: utls-chain
listener:
type: tcp
chains:
- name: utls-chain
hops:
- name: hop-0
nodes:
- name: node-0
addr: {{.ServerAddr}}
connector:
type: http
dialer:
type: utls
metadata:
fingerprint: Chrome
tls:
secure: true
serverName: utls-server
caFile: /certs/ca.pem
+7
View File
@@ -0,0 +1,7 @@
services:
- name: https-server
addr: :8443
handler:
type: http
listener:
type: tls
+10
View File
@@ -0,0 +1,10 @@
services:
- name: https-server
addr: :8443
handler:
type: http
listener:
type: tls
tls:
certFile: /certs/server.pem
keyFile: /certs/server-key.pem
+93
View File
@@ -0,0 +1,93 @@
package e2e
import (
"context"
"io"
"os"
"testing"
"github.com/stretchr/testify/suite"
"github.com/testcontainers/testcontainers-go"
)
// TLSSuite covers TLS listener behavior, in particular the rejectUnknownSNI
// option which drops TLS handshakes with an unknown or empty SNI before any
// certificate is sent.
type TLSSuite struct {
suite.Suite
ctx context.Context
}
func (s *TLSSuite) SetupSuite() {
s.ctx = context.Background()
}
// startGost starts a gost container with the given TLS listener config. The
// listener always binds :8443; checks run openssl inside the container.
func (s *TLSSuite) startGost(yamlPath string) testcontainers.Container {
s.T().Helper()
rendered, err := RenderConfig(yamlPath, ConfigData{})
s.Require().NoError(err)
s.T().Cleanup(func() { os.Remove(rendered) })
c, err := RunGostContainerWithPorts(s.ctx, SharedNetworkName, rendered, "8443/tcp")
s.Require().NoError(err)
return c
}
// sniHandshake runs openssl s_client against the TLS listener and returns its
// exit code: 0 means the handshake completed (a certificate was exchanged),
// non-zero means the handshake was rejected. An empty sni sends no SNI
// extension.
func (s *TLSSuite) sniHandshake(c testcontainers.Container, sni string) int {
s.T().Helper()
args := []string{"openssl", "s_client", "-brief", "-connect", "127.0.0.1:8443"}
if sni == "" {
args = append(args, "-noservername")
} else {
args = append(args, "-servername", sni)
}
code, out, err := c.Exec(s.ctx, args)
if err != nil {
s.T().Logf("openssl exec error: %v", err)
return -1
}
if b, e := io.ReadAll(out); e == nil {
s.T().Logf("openssl output (sni=%q):\n%s", sni, string(b))
}
return code
}
// TestRejectWithAllowList covers rejectUnknownSNI with a populated serverNames
// list: the allowed SNI completes the handshake, while a wrong or missing SNI
// is rejected.
func (s *TLSSuite) TestRejectWithAllowList() {
c := s.startGost("testdata/tls/reject_sni.yaml")
defer c.Terminate(s.ctx)
s.Require().Zero(s.sniHandshake(c, "example.com"),
"handshake with allowed SNI should succeed")
s.Require().NotZero(s.sniHandshake(c, "wrong.com"),
"handshake with disallowed SNI should fail")
s.Require().NotZero(s.sniHandshake(c, ""),
"handshake without SNI should fail")
}
// TestRejectEmptyOnly covers rejectUnknownSNI with an empty serverNames list:
// only a missing SNI is rejected, any named SNI is allowed.
func (s *TLSSuite) TestRejectEmptyOnly() {
c := s.startGost("testdata/tls/reject_sni_empty.yaml")
defer c.Terminate(s.ctx)
s.Require().NotZero(s.sniHandshake(c, ""),
"handshake without SNI should fail")
s.Require().Zero(s.sniHandshake(c, "anything.test"),
"named SNI should be allowed when allow list is empty")
}
func TestTLSSuite(t *testing.T) {
suite.Run(t, new(TLSSuite))
}
+87 -3
View File
@@ -10,6 +10,7 @@ import (
"github.com/moby/moby/client"
"github.com/testcontainers/testcontainers-go"
tcexec "github.com/testcontainers/testcontainers-go/exec"
"github.com/testcontainers/testcontainers-go/wait"
)
@@ -23,6 +24,15 @@ type ConfigData struct {
ServerAddr string
}
// ExecOutput runs cmd in c and returns the demultiplexed output reader.
// testcontainers' Exec returns the raw Docker stream, which multiplexes stdout
// and stderr with 8-byte framing headers (type byte + 6 zero + length) that
// interleave with the real data. Demultiplexing produces a clean combined
// stdout+stderr stream that tests can assert against.
func ExecOutput(ctx context.Context, c testcontainers.Container, cmd []string) (int, io.Reader, error) {
return c.Exec(ctx, cmd, tcexec.Multiplexed())
}
func DumpLogs(t *testing.T, ctx context.Context, label string, c testcontainers.Container) {
logs, err := c.Logs(ctx)
if err != nil {
@@ -192,6 +202,72 @@ func RunTCPDNSResponderContainer(ctx context.Context, networkName string) (testc
})
}
// RunResolverResponderContainer starts a UDP DNS responder that answers A
// queries for "echo.test" with targetIP (an echo server's address) and returns
// NXDOMAIN for all other names. It is used to prove the resolver module drives
// outbound dialing: a gost proxy pointed at this responder resolves echo.test
// to a reachable address. The container is aliased "dns-responder".
func RunResolverResponderContainer(ctx context.Context, networkName, targetIP string) (testcontainers.Container, error) {
req := testcontainers.ContainerRequest{
FromDockerfile: testcontainers.FromDockerfile{
Context: ".",
Dockerfile: "Dockerfile",
Repo: "gost-e2e",
Tag: "latest",
KeepImage: true,
BuildOptionsModifier: func(opts *client.ImageBuildOptions) {
opts.NetworkMode = "host"
},
},
Networks: []string{networkName},
NetworkAliases: map[string][]string{
networkName: {"dns-responder"},
},
Files: []testcontainers.ContainerFile{
{HostFilePath: "scripts/dns_resolver_responder.py", ContainerFilePath: "/scripts/dns_server.py", FileMode: 0644},
},
ExposedPorts: []string{"5353/udp"},
Cmd: []string{"python3", "/scripts/dns_server.py", targetIP, "5353"},
WaitingFor: wait.ForExposedPort().SkipInternalCheck(),
}
return testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{
ContainerRequest: req,
Started: true,
})
}
// RunHTTPSEchoContainer starts an HTTPS echo server (self-signed cert) that
// responds with "hello-gost" on port 8443. The container is registered with
// the network alias "https-echo".
func RunHTTPSEchoContainer(ctx context.Context, networkName string) (testcontainers.Container, error) {
return testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{
ContainerRequest: testcontainers.ContainerRequest{
FromDockerfile: testcontainers.FromDockerfile{
Context: ".",
Dockerfile: "Dockerfile",
Repo: "gost-e2e",
Tag: "latest",
KeepImage: true,
BuildOptionsModifier: func(opts *client.ImageBuildOptions) {
opts.NetworkMode = "host"
},
},
Networks: []string{networkName},
NetworkAliases: map[string][]string{
networkName: {"https-echo"},
},
Files: []testcontainers.ContainerFile{
{HostFilePath: "scripts/https_echo.py", ContainerFilePath: "/scripts/https_echo.py", FileMode: 0644},
},
ExposedPorts: []string{"8443/tcp"},
Cmd: []string{"python3", "/scripts/https_echo.py"},
WaitingFor: wait.ForExposedPort(),
},
Started: true,
})
}
func RunGostContainer(ctx context.Context, networkName, yamlPath string) (testcontainers.Container, error) {
return runGostContainer(ctx, networkName, yamlPath, nil, nil, nil)
}
@@ -228,9 +304,7 @@ func runGostContainer(ctx context.Context, networkName, yamlPath string, aliases
},
},
ExposedPorts: exposedPorts,
// internal check for udp ports will be failed
WaitingFor: wait.ForExposedPort().SkipInternalCheck(),
Networks: []string{networkName},
Networks: []string{networkName},
NetworkAliases: map[string][]string{
networkName: aliases,
},
@@ -238,6 +312,16 @@ func runGostContainer(ctx context.Context, networkName, yamlPath string, aliases
Cmd: []string{"/bin/gost", "-C", "/config.yaml"},
}
// Wait for the gost process to be ready. With exposed ports we wait for the
// port to accept connections; without them (e.g. a socks5 proxy consumed
// from inside the container) we wait for a startup log line instead.
if len(exposedPorts) > 0 {
// internal check for udp ports will be failed
req.WaitingFor = wait.ForExposedPort().SkipInternalCheck()
} else {
req.WaitingFor = wait.ForLog("listening on")
}
return testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{
ContainerRequest: req,
Started: true,
+188
View File
@@ -0,0 +1,188 @@
package e2e
import (
"context"
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"crypto/x509"
"crypto/x509/pkix"
"encoding/pem"
"fmt"
"io"
"math/big"
"os"
"strings"
"testing"
"time"
"github.com/stretchr/testify/suite"
"github.com/testcontainers/testcontainers-go"
)
// UTLSSuite exercises the utls dialer in a forward-proxy chain.
//
// Topology:
//
// curl -> client gost (http proxy, :8080)
// -> chain node (http connector + utls dialer)
// -> server gost (http proxy over TLS listener, :8443)
// -> tcp-echo
type UTLSSuite struct {
suite.Suite
ctx context.Context
echoC testcontainers.Container
echoIP string
certDir string
}
func (s *UTLSSuite) SetupSuite() {
s.ctx = context.Background()
certDir, err := os.MkdirTemp("", "gost-utls-certs-*")
s.Require().NoError(err)
s.certDir = certDir
s.generateCerts()
echoC, err := RunEchoContainer(s.ctx, SharedNetworkName)
s.Require().NoError(err)
s.echoC = echoC
echoIP, err := echoC.ContainerIP(s.ctx)
s.Require().NoError(err)
s.echoIP = echoIP
}
func (s *UTLSSuite) TearDownSuite() {
if s.echoC != nil {
s.echoC.Terminate(s.ctx)
}
os.RemoveAll(s.certDir)
}
// generateCerts creates a self-signed CA and a server cert for the
// "utls-server" hostname, writing PEM files to s.certDir.
func (s *UTLSSuite) generateCerts() {
caKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
s.Require().NoError(err)
caTmpl := &x509.Certificate{
SerialNumber: big.NewInt(1),
Subject: pkix.Name{CommonName: "Test CA"},
NotBefore: time.Now(),
NotAfter: time.Now().Add(24 * time.Hour),
KeyUsage: x509.KeyUsageCertSign | x509.KeyUsageDigitalSignature,
BasicConstraintsValid: true,
IsCA: true,
}
caDER, err := x509.CreateCertificate(rand.Reader, caTmpl, caTmpl, &caKey.PublicKey, caKey)
s.Require().NoError(err)
s.writeCertPEM(s.certDir+"/ca.pem", "CERTIFICATE", caDER)
s.writeKeyPEM(s.certDir+"/ca-key.pem", "EC PRIVATE KEY", caKey)
serverKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
s.Require().NoError(err)
serverTmpl := &x509.Certificate{
SerialNumber: big.NewInt(2),
Subject: pkix.Name{CommonName: "utls-server"},
NotBefore: time.Now(),
NotAfter: time.Now().Add(24 * time.Hour),
KeyUsage: x509.KeyUsageDigitalSignature,
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
DNSNames: []string{"utls-server"},
}
serverDER, err := x509.CreateCertificate(rand.Reader, serverTmpl, caTmpl, &serverKey.PublicKey, caKey)
s.Require().NoError(err)
s.writeCertPEM(s.certDir+"/server.pem", "CERTIFICATE", serverDER)
s.writeKeyPEM(s.certDir+"/server-key.pem", "EC PRIVATE KEY", serverKey)
}
// TestUTLSInsecure is the regression test for go-gost/gost#887.
//
// A utls dialer with `secure: false` must still complete the handshake
// (InsecureSkipVerify must be honoured). The previous unsafe.Pointer cast
// read garbage for InsecureSkipVerify, so it came back false and the
// handshake failed with "at least one of ServerName, InsecureSkipVerify or
// InsecureServerNameToVerify must be specified".
func (s *UTLSSuite) TestUTLSInsecure() {
rendered, err := RenderConfig("testdata/utls/client_insecure.yaml",
ConfigData{ServerAddr: "utls-server:8443"})
s.Require().NoError(err)
defer os.Remove(rendered)
serverC, err := RunGostContainerWithOptions(s.ctx, SharedNetworkName,
"testdata/utls/server_auto.yaml", []string{"utls-server"}, []string{"8443/tcp"})
s.Require().NoError(err)
defer serverC.Terminate(s.ctx)
clientC, err := RunGostContainerWithPorts(s.ctx, SharedNetworkName, rendered, "8080/tcp")
s.Require().NoError(err)
defer clientC.Terminate(s.ctx)
s.requireProxyWorks(clientC, serverC)
}
// TestUTLSSecureWithCA verifies the converter's RootCAs/ServerName path:
// with `secure: true` and a CA file, the utls dialer must verify the
// server's CA-signed certificate and still reach the echo server.
func (s *UTLSSuite) TestUTLSSecureWithCA() {
rendered, err := RenderConfig("testdata/utls/client_secure.yaml",
ConfigData{ServerAddr: "utls-server:8443"})
s.Require().NoError(err)
defer os.Remove(rendered)
serverC, err := runGostContainer(s.ctx, SharedNetworkName,
"testdata/utls/server_ca.yaml",
[]string{"utls-server"}, []string{"8443/tcp"},
[]testcontainers.ContainerFile{
{HostFilePath: s.certDir + "/server.pem", ContainerFilePath: "/certs/server.pem", FileMode: 0644},
{HostFilePath: s.certDir + "/server-key.pem", ContainerFilePath: "/certs/server-key.pem", FileMode: 0644},
})
s.Require().NoError(err)
defer serverC.Terminate(s.ctx)
clientC, err := RunGostContainerWithFiles(s.ctx, SharedNetworkName, rendered,
[]testcontainers.ContainerFile{
{HostFilePath: s.certDir + "/ca.pem", ContainerFilePath: "/certs/ca.pem", FileMode: 0644},
},
"8080/tcp")
s.Require().NoError(err)
defer clientC.Terminate(s.ctx)
s.requireProxyWorks(clientC, serverC)
}
// requireProxyWorks drives curl through the client proxy and asserts the
// echo server's "hello-gost" response is returned.
func (s *UTLSSuite) requireProxyWorks(clientC, serverC testcontainers.Container) {
cmd := []string{"curl", "-v", "-s", "--connect-timeout", "5",
"-x", "http://127.0.0.1:8080",
fmt.Sprintf("http://%s:5678", s.echoIP)}
code, out, err := clientC.Exec(s.ctx, cmd)
body, err2 := io.ReadAll(out)
if err != nil || err2 != nil || code != 0 || !strings.Contains(string(body), "hello-gost") {
DumpLogs(s.T(), s.ctx, "utls client logs", clientC)
DumpLogs(s.T(), s.ctx, "utls server logs", serverC)
}
s.Require().NoError(err)
s.Require().NoError(err2)
s.Require().Equal(0, code)
s.Require().Contains(string(body), "hello-gost")
}
func TestUTLSSuite(t *testing.T) {
suite.Run(t, new(UTLSSuite))
}
// --- helpers (mirror mtls_test.go) ---
func (s *UTLSSuite) writeCertPEM(path, blockType string, der []byte) {
err := os.WriteFile(path, pem.EncodeToMemory(&pem.Block{Type: blockType, Bytes: der}), 0644)
s.Require().NoError(err)
}
func (s *UTLSSuite) writeKeyPEM(path, blockType string, key *ecdsa.PrivateKey) {
b, err := x509.MarshalECPrivateKey(key)
s.Require().NoError(err)
err = os.WriteFile(path, pem.EncodeToMemory(&pem.Block{Type: blockType, Bytes: b}), 0600)
s.Require().NoError(err)
}