Compare commits

...

311 Commits

Author SHA1 Message Date
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
ginuerzh b139ed5968 chore: bump go-gost/core to v0.5.1, go-gost/x to v0.13.0, go-gost/plugin to v0.4.0 2026-06-27 23:05:22 +08:00
ginuerzh 43724a5c40 test: add http2 handler e2e suite and tighten host-mapper + idle-timeout tests
Add tests/e2e/http2_test.go covering the http2 handler/listener/connector/dialer
end-to-end through the canonical gost-as-client pattern (8 subtests: forward,
auth, bypass, probeResist/metadata, h2 stream multiplexing). Include
testdata/http2/{server,server_auth,server_bypass,server_proberesist,client,
client_auth}.yaml. Note that h2/h2c listeners pair with the tunnel handler
and only http2 listener pairs with handler http2 (reads r/w from metadata).

Tighten TestDNSHostMapper by pointing the handler at an unreachable upstream
so unmapped names fail at the exchanger, confirming the host-mapper path was
the sole reason mapped names resolved. Fix http_idle_timeout.py to speak HTTP
through the CONNECT tunnel rather than a raw ping, matching the echo backend.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-06-27 20:15:31 +08:00
ginuerzh 0bb9109c71 feat: add comprehensive forward handler e2e tests
Add TCP/UDP forward handler e2e tests covering basic forwarding, alias
(forward/tcp), sniffing, raw pipe, idleTimeout, multi-node protocol
filtering, sniffing bypass (403), stateful UDP, and stateless UDP.

New files:
- forward_test.go: ForwardSuite with 9 test methods
- testdata/forward/: server configs (tcp, udp, stateless, sniffing,
  bypass, multi-node, idleTimeout)
- scripts/tcp_idle_timeout.py, udp_forward_test.py: Python harnesses
  for in-container idle timeout and UDP echo verification
2026-06-27 13:59:22 +08:00
ginuerzh a0427be086 feat: add comprehensive DNS handler e2e tests
Adds 22 DNS e2e subtests across 8 test methods covering: upstream
resolution (A, AAAA, multi-A), TCP mode, bypass rules, host mapper,
exchange failure (graceful error recovery), rate limiter wiring,
invalid query handling, and DNS over TLS (mode: tls).

Includes two authoritative DNS responders (UDP/TCP), a standalone
DNS query client, and a TLS DNS query script.
2026-06-27 00:21:37 +08:00
ginuerzh 590c6f48bd feat: add file handler e2e tests covering GET, PUT, index, auth, and 404
Adds 5 test methods (7 test cases) for the file handler:
GET existing file, GET nonexistent file, GET index.html, PUT upload,
PUT without permission, and auth (no-auth-401, with-auth-success).

Includes test data files and server YAML configs under
tests/e2e/testdata/file/.
2026-06-26 22:19:13 +08:00
ginuerzh 69b561f944 feat: add comprehensive HTTP e2e tests covering connector, TLS, probeResist, idleTimeout, and UDP relay
Adds 12 new HTTP handler test scenarios: HTTP connector (no-auth, auth, TLS
upstream), CONNECT tunnel (no-sniffing, bypass-403), probeResist (host, web,
file, knock), idleTimeout, and UDP relay over HTTP. Introduces
RunGostContainerWithFiles helper for mounting extra files into test containers.
2026-06-26 22:00:35 +08:00
ginuerzh 3c87a8b96e chore(deps): bump go-gost/x to v0.12.5 2026-06-26 20:21:42 +08:00
ginuerzh c9cfc440ad build(deps): bump go-gost/x from v0.12.3 to v0.12.4 2026-06-25 20:50:53 +08:00
ginuerzh c305f93703 chore(deps): bump go-gost/x to v0.12.3 2026-06-23 21:47:44 +08:00
ginuerzh f638d8081f build(deps): bump go-gost/x from v0.12.1 to v0.12.2
x v0.12.2 adds SO_REUSEPORT support for TCP listeners (#654).
2026-06-22 21:46:46 +08:00
ginuerzh 286c1615fd build(deps): bump go-gost/x from v0.12.0 to v0.12.1 2026-06-21 22:55:44 +08:00
ginuerzh b985e5138c fix: use atomic int for multi-worker exit code and clean up cancellation
- Replace bare int with atomic.Int32 to fix data race on ret
- Replace wg.Add/go/defer wg.Done pattern with wg.Go (Go 1.22+)
- Pass context by value instead of pointer
- Add ctx.Err() guard to suppress fatal on cancellation errors
- Add ProcessState nil check before accessing ExitCode
- Move cancel() after wg.Wait() to avoid race where a successful
  worker's deferred cancel kills siblings and triggers log.Fatal
2026-06-21 21:07:37 +08:00
ginuerzh b628475871 fix: enable metrics before loading services
Move xmetrics.Enable(true) from run() to Start(), before loader.Load(),
so that listener wrappers can observe the enabled state at Init time
rather than after wrapper decisions have already been made.
2026-06-21 20:35:19 +08:00
ginuerzh 43dd0a6958 feat: register stdio listener for SSH ProxyCommand support
Adds blank import of x/listener/stdio to register the stdio listener.

Refs go-gost/gost#433
2026-06-21 15:40:41 +08:00
ginuerzh 9d00ad8c8f build(deps): bump go-gost/x from v0.11.1 to v0.12.0 2026-06-20 22:15:57 +08:00
ginuerzh 1bebce0ed7 feat: register utls dialer for TLS ClientHello fingerprint simulation
Blank-import x/dialer/utls to enable the "utls" dialer type via
init() self-registration.

Refs go-gost/gost#31
2026-06-20 20:17:08 +08:00
ginuerzh 1178c4757f feat(cmd): add -R auto-reload flag for periodic config refresh
Introduce -R <duration> flag (e.g., -R 30s, -R 1m) that triggers
periodic config reload. Combined with -C URL support, this enables
centralized fleet management where nodes poll a remote config server.

The reload goroutine reuses the existing SIGHUP reloadConfig path:
parser.Parse() → loader.Load() → p.run(cfg). When -R is zero
(default) behavior is unchanged; SIGHUP still works independently.
2026-06-20 20:12:11 +08:00
ginuerzh 73069f50e3 feat: support multiple -C config files
Change -C flag from string to stringList, allowing:
  gost -C base.yml -C services.yml -C auth.yml

Closes go-gost/gost#150
2026-06-20 18:56:25 +08:00
ginuerzh 06ec0097f9 ci: harden docker/release/nightly GitHub Actions workflows
- buildx: adopt docker/metadata-action for image tags (replaces hand-rolled shell), add gha build cache, concurrency control, explicit checkout, and least-privilege permissions

- release: add explicit permissions and concurrency (no cancel-in-flight); drop dead UPX install step (disabled in .goreleaser.yaml, see #863)

- trigger-nightly: add concurrency, replace archived dev-drprasad/delete-older-releases with native gh, and harden the 24h commit check
2026-06-19 19:11:25 +08:00
ginuerzh 396b0977bd chore: bump x to v0.11.1 2026-06-19 09:29:14 +08:00
Yuan Tong 604dccc7b0
feat: register runix listener (#871) 2026-06-19 09:27:06 +08:00
ginuerzh 6d80e06ab3 chore: bump x to v0.11.0 2026-06-17 22:32:51 +08:00
ginuerzh e2447ce578 fix(e2e): use host network for Docker image builds in DinD environments
Docker-in-Docker containers cannot reach the internet via the default
bridge network, causing 'apk add' in the Dockerfile to hang indefinitely
and e2e tests to timeout after 10 minutes.

Add BuildOptionsModifier with NetworkMode=host to all FromDockerfile
definitions so the build step uses the host's network stack. This has
no negative impact on non-DinD environments where bridge networking
already works.

Also move GostBinPath flag and init() from main_test.go to utils.go
(non-test file) so the symbol is accessible across the package.
2026-06-06 20:34:24 +08:00
ginuerzh 811420e923 chore: bump x to include stateless UDP forwarding (#853) 2026-06-05 23:27:22 +08:00
ginuerzh 374b46dfe1 bump x to v0.10.10 2026-06-03 23:30:01 +08:00
ginuerzh 22edb92084 bump x to v0.10.10 2026-06-03 23:25:54 +08:00
ginuerzh 78a0a8c734 fix: disable UPX compression in Dockerfile to resolve #863 startup regression 2026-06-03 00:13:58 +08:00
ZHAO Jin-Xiang 41e1878ebc
feat: add nftables package to Dockerfile (#865)
nftables is a more modern iptables which supports both ipv4 and ipv6.
2026-06-02 23:56:53 +08:00
ginuerzh 5639c90e98 bump x to v0.10.9 2026-06-02 23:54:13 +08:00
ginuerzh 36abf9bcd9 bump x to v0.10.8 2026-06-02 20:07:48 +08:00
ginuerzh d584b7ac61 bump x to v0.10.7 2026-05-31 22:49:10 +08:00
ginuerzh 56e2a1c496 fix: disable UPX compression to resolve #863 startup regression
UPX --best/--lzma/--brute adds ~3s startup time on low-spec systems
(linux/arm) due to in-memory decompression on every invocation of gost -V.
Disabled by default; opt in via GORELEASER_UPX=true for release builds
that need it.
2026-05-31 22:25:57 +08:00
ginuerzh b0bea19275 bump x to v0.10.6 2026-05-31 22:13:42 +08:00
ginuerzh cd64f2edd3 bump x to v0.10.5 2026-05-31 19:50:49 +08:00
ginuerzh 5f04c84e32 bump core to v0.4.1 2026-05-31 19:40:28 +08:00
ginuerzh 600a64e611 bump x to v0.10.4 2026-05-31 17:47:52 +08:00
ginuerzh 3d6b16686b bump x to v0.10.3 2026-05-29 00:29:26 +08:00
ginuerzh 44684d40c3 add CLAUDE.md with build, CLI, and lifecycle documentation 2026-05-23 00:00:56 +08:00
ginuerzh 2be36abe75 bump x to v0.10.1, go-shadowsocks2 to v0.1.3; add gost binary to .gitignore 2026-05-22 23:21:30 +08:00
ginuerzh 8db62785fa Merge pull request #862: Add e2e test cases for shadowsocks
Add comprehensive e2e tests covering TCP and UDP shadowsocks
connections with various cipher modes (aes-128-gcm, aes-256-gcm,
chacha20, 2022-blake3-aes-128, 2022-blake3-aes-256, including
multi-PSK variants).
2026-05-22 23:09:42 +08:00
RMT 8740e6f258
Add e2e test cases for shadowsocks 2026-05-22 14:28:17 +08:00
ginuerzh d4c9ef5056 add masque connector, dialer, and handler registrations; update x to v0.10.0 2026-05-21 23:03:49 +08:00
RMT c06eb0d331 WIP 2026-04-26 22:18:07 +08:00
RMT c8b48dc248 WIP 2026-04-26 22:18:07 +08:00
azoway 45d94cf391 Update install.sh
在alpine服务器上运行安装脚本报错:
curl: (3) URL rejected: Malformed input to a URL function

排查后发现,当前版本部分会匹配出来2个版本的下载链接:
gost_3.2.6_linux_amd64.tar.gz
gost_3.2.6_linux_amd64v3.tar.gz

修改后默认取第一个兼容性更好的版本
2026-04-26 22:16:06 +08:00
nivesh bc96fe3918 streamline download/install process
(get binary only, eliminate unnecessary intermediate files)
2026-04-22 00:25:59 +08:00
nivesh 646d3f906c enhance portability (detect user-preferred bash, replace all greps with awk) 2026-04-22 00:25:59 +08:00
nivesh 4c69940f1e Fix #835 2026-04-22 00:25:59 +08:00
David Manouchehri 3d1f6fcbbb add masque connector, dialer, and handler 2026-04-22 00:23:53 +08:00
ginuerzh e388426ec6 go1.26 2026-04-21 23:59:31 +08:00
ginuerzh 340ba32ef0 v3.2.6 2025-11-22 22:47:02 +08:00
ginuerzh 96551d5fa5 metrics: fix server conn wrapper (#797) 2025-10-11 22:07:41 +08:00
ginuerzh 8d05a6ed93 add multiple entrypoints for tunnel 2025-10-09 22:33:31 +08:00
ginuerzh 0348a16aa9 fix panic for channel close (#779) 2025-09-20 10:06:15 +08:00
ginuerzh 50934e0978 fix sniffing for websocket 2025-09-04 21:32:34 +08:00
ginuerzh c2ed9c6f07 add service option for plugin 2025-08-29 23:38:29 +08:00
ginuerzh 3b9da4e260 go1.25 2025-08-21 22:53:28 +08:00
ginuerzh f0a67a1108 fix bugs 2025-08-21 22:24:42 +08:00
ginuerzh 59c9638ce6 add bypass and admission matcher 2025-08-13 21:27:36 +08:00
ginuerzh 49fa28882f fix issue #760 2025-08-10 19:01:10 +08:00
ginuerzh 08c617b54e v3.2.3 2025-08-09 21:01:25 +08:00
ginuerzh d03b0e2360 v3.2.2 2025-08-05 20:46:21 +08:00
ginuerzh 245d610baf without cancel context 2025-08-05 20:25:24 +08:00
ginuerzh f01f0c9215 fix ws listener 2025-08-05 00:19:51 +08:00
ginuerzh 9b6e9d9cae add context for conn 2025-08-04 19:34:54 +08:00
ginuerzh 1a0e2b06ac add proxyProtocol support for dialer 2025-08-03 15:38:57 +08:00
ginuerzh de8bb498db http2: added non-connect request support #749 2025-07-30 21:44:46 +08:00
ginuerzh 1f735d0649 v3.2.0 2025-07-29 09:15:55 +08:00
ginuerzh d173d167da fix tungo for windows and darwin 2025-07-28 21:21:33 +08:00
ginuerzh b593fdb952 add upx for binary compression 2025-07-26 16:43:49 +08:00
ginuerzh d448628fea add tun2socks (tungo) 2025-07-26 16:43:22 +08:00
ginuerzh 0bfc7f10cd add vtun handle and listener 2025-07-23 22:49:59 +08:00
ginuerzh a24b29c8c2 get real client IP from request 2025-07-15 20:21:52 +08:00
ginuerzh 87af3da1b6 add src field for logger and recorder 2025-07-02 21:49:03 +08:00
ginuerzh dc6fe595e0 read config from stdin #701 2025-06-28 18:20:28 +08:00
ginuerzh db4a0a9259 goreleaser: added linux/loong64 2025-06-28 18:13:45 +08:00
Meng Zhuo 62a097c5e4 enable riscv64 in install.sh 2025-06-27 23:36:31 +08:00
ginuerzh b51cecbbfd add new web APIs 2025-06-25 21:31:22 +08:00
ginuerzh 66a260abd1 fix metrics 2025-06-24 21:23:00 +08:00
ginuerzh 1d3603f9ed
Merge pull request #713 from ravitheg/FixSSHVulnerability
Fix ssh vulnerability
2025-05-30 15:21:37 +08:00
ravitheg 952c61c1a2
Update go.sum 2025-05-27 14:09:22 -07:00
ravitheg 6d61ab82db
Update go.mod 2025-05-27 14:08:56 -07:00
ginuerzh f61bb2fd72 revert to go1.23 2025-02-18 21:43:03 +08:00
ginuerzh 4c2835db04 go1.24 2025-02-18 21:16:47 +08:00
ginuerzh d662b1fd46 router handler: add cache for sd service 2025-02-09 16:19:40 +08:00
ginuerzh 015923fa5d add entrypoint for router handler 2025-02-08 23:11:18 +08:00
ginuerzh f66fce9d49 feat: add router handler & connector 2025-02-07 15:51:51 +08:00
ginuerzh 982a93eee4 add limiter.scope option for service 2025-01-20 23:27:04 +08:00
ginuerzh 758251d3f9 update go.mod 2025-01-10 21:50:18 +08:00
ginuerzh a1e48e164b update config parsing 2025-01-10 21:41:55 +08:00
ginuerzh 581dd74bbb add config hot reload 2025-01-08 23:16:55 +08:00
ginuerzh 3c7a984aa3 v3.0.0 2024-12-31 20:19:49 +08:00
ginuerzh 2a9b7e7d9b fix panic for stats wrapper 2024-12-27 20:43:42 +08:00
ginuerzh ab2b9714ee add traffic reset for stats observer 2024-12-24 20:13:40 +08:00
ginuerzh 6d38c24d88 handle tls for tunnel entrypoint 2024-12-23 21:35:58 +08:00
ginuerzh f376db2000 fix issue #625 2024-12-18 19:06:59 +08:00
ginuerzh 161d96f582 fix observer for http2 2024-12-16 20:49:41 +08:00
ginuerzh 128c0c3117 update go.sum 2024-12-11 22:31:42 +08:00
ginuerzh 66ce980c36 fix webtransport 2024-12-11 22:28:31 +08:00
ginuerzh 829148d3f8 fix ipv6 port for redirect 2024-12-11 22:05:30 +08:00
ginuerzh c45f148a88 add unix domain socket support for api & metrics services 2024-11-22 22:51:16 +08:00
ginuerzh fdc88af4ac fix http keepalive for forwarding 2024-11-17 16:17:31 +08:00
ginuerzh c41c5bf79a add routing rule for node 2024-11-15 20:30:35 +08:00
ginuerzh b3eeb338fb add custom http response header for reverse proxy 2024-11-07 11:08:38 +08:00
ginuerzh 72f185bbf6 add compression flag option for http request 2024-11-02 20:43:44 +08:00
ginuerzh a7599990a8 update deps 2024-11-01 18:19:23 +08:00
ginuerzh 17ca5600d3 update sample rate for websocket recording 2024-10-31 19:16:12 +08:00
ginuerzh 32ccfe483c fix udp port forwarding 2024-10-22 22:57:18 +08:00
ginuerzh 1d0eb06938 sniffing: fix race condition when sniffing websocket frames 2024-10-20 19:24:40 +08:00
ginuerzh 1fa5067d14 sniffing: full websocket frame recording 2024-10-19 20:41:29 +08:00
ginuerzh 7ddfef85a5 sniffing websocket frame 2024-10-19 19:37:21 +08:00
ginuerzh fa98777ab1 fix handler sniffing 2024-10-16 23:03:27 +08:00
ginuerzh e53486efe0 add more fields for log 2024-10-16 22:17:54 +08:00
ginuerzh 881ebfad5b add ClientAddr for websocket conn 2024-10-16 20:48:07 +08:00
ginuerzh d5bb691b38 tunnel: fix http switching protocols 2024-10-15 19:38:43 +08:00
ginuerzh 24c13b861a update tunnel entrypoint 2024-10-15 18:49:21 +08:00
ginuerzh d6daa8bf56 update sniffer for forward & tunnel handler 2024-10-14 23:10:58 +08:00
ginuerzh d18539ef66 recorder: add input/output traffic stats for handler recorder object 2024-10-11 20:39:29 +08:00
ginuerzh 48b0a690c5 update Dockerfile 2024-10-11 13:10:15 +08:00
ginuerzh a7037a27bf update Dockerfile 2024-10-11 11:28:06 +08:00
ginuerzh ac9960de6d update Dockerfile 2024-10-11 00:48:47 +08:00
ginuerzh 404e099907 update github actions 2024-10-11 00:05:00 +08:00
ginuerzh 94cd590142 add keepalive option for http handler 2024-10-10 22:52:04 +08:00
ginuerzh d3809ec634 add sniffer utility for handler traffic sniffing 2024-10-02 22:53:07 +08:00
ginuerzh efb4da6fa6 add traffic sniffing for handler 2024-09-27 20:54:24 +08:00
ginuerzh 2ec8584e96 add route field for reocrder 2024-09-24 20:25:10 +08:00
ginuerzh f05f5cf862 add tls handshake for recorder 2024-09-20 21:28:09 +08:00
ginuerzh bf3018b5b1 add http body for handler recorder 2024-09-16 19:46:24 +08:00
ginuerzh 073af6163e fix recorder clientIP 2024-09-15 18:38:55 +08:00
ginuerzh ef07551e01 add clientIP for handler recorder object 2024-09-15 18:30:02 +08:00
ginuerzh 8077c039f3 update handler recorder object 2024-09-15 12:14:01 +08:00
ginuerzh 33c8f3595a add recorder for handler 2024-09-14 23:24:17 +08:00
ginuerzh 87bff8cac6 add sniffing.fallback option for red handler 2024-09-04 22:46:03 +08:00
ginuerzh d52fcccf06 add scope parameter for traffic limiter 2024-08-31 09:42:03 +08:00
ginuerzh 18610d4608 fix timeout in router 2024-08-06 21:35:19 +08:00
ginuerzh c1de9e4e72 close udp connection when timeout 2024-08-06 18:35:26 +08:00
ginuerzh 1f7c080043 add riscv64 in buildx 2024-08-01 22:05:15 +08:00
ginuerzh b091f72900 fix issue #546 2024-08-01 20:54:21 +08:00
ginuerzh 8e50e9c085 fix ssu handler port exhaustion 2024-07-31 21:03:59 +08:00
ginuerzh 7a35d25c5c add http body rewrite for forward handler 2024-07-19 20:47:01 +08:00
ginuerzh 8e311e7891 add support for icmpv6 2024-07-15 20:42:30 +08:00
ginuerzh 17fc806c2d fix ipv6 for tproxy 2024-07-11 22:39:15 +08:00
ginuerzh b21fd4a22b fix port range parsing 2024-07-10 23:00:06 +08:00
ginuerzh 3e8a025c45 update go.sum 2024-07-08 22:54:09 +08:00
ginuerzh 0d7a6f8091 add port range support for service 2024-07-08 22:52:01 +08:00
ginuerzh 4a2a2a00de update go.sum 2024-07-04 23:12:01 +08:00
ginuerzh c71e128fec bump deps 2024-07-04 23:09:05 +08:00
ginuerzh 1400b5ff5d remove content-length header for http proxy response 2024-06-25 22:26:11 +08:00
ginuerzh cd4fad8c08 add dialTimeout option for service 2024-06-25 21:44:39 +08:00
ginuerzh 56a68ae06f fix netns for socks5 and relay handler 2024-06-24 21:42:37 +08:00
ginuerzh 5d7100f749 add linux network namespace support for listener and dialer 2024-06-21 23:41:27 +08:00
ginuerzh 12871a4ff3 update README.md 2024-06-18 20:59:46 +08:00
ginuerzh 6130bbc075 update go.mod 2024-06-13 23:29:34 +08:00
ginuerzh 85d4fcaf7d add observePeriod option for observer 2024-06-13 22:45:28 +08:00
ginuerzh 7904e571c1 fix #498 2024-06-11 21:58:48 +08:00
ginuerzh 8253c41de6 fix marker for selector 2024-06-07 22:43:48 +08:00
ginuerzh 2148d10def
Merge pull request #488 from cgroschupp/update-mod
remove unnecessary replace and update gost modules
2024-05-31 21:25:47 +08:00
Christian Groschupp 8846c69c05 remove unnecessary replace and update gost modules 2024-05-29 18:40:40 +02:00
ginuerzh 2161b97064 add p2p option for tun handler 2024-04-26 20:58:27 +08:00
ginuerzh e245526b33 update README.md 2024-04-25 22:56:57 +08:00
ginuerzh 60389894c0 update go.mod 2024-04-25 21:50:49 +08:00
dependabot[bot] 9ab142ac99 Bump golang.org/x/net from 0.19.0 to 0.23.0
Bumps [golang.org/x/net](https://github.com/golang/net) from 0.19.0 to 0.23.0.
- [Commits](https://github.com/golang/net/compare/v0.19.0...v0.23.0)

---
updated-dependencies:
- dependency-name: golang.org/x/net
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-04-24 23:19:10 +08:00
ginuerzh be1b0627d1
Merge pull request #451 from jalalsaberi/master
Add Chinese and English README Button
2024-04-24 23:18:33 +08:00
dependabot[bot] f4115b867d Bump github.com/quic-go/quic-go from 0.40.1 to 0.42.0
Bumps [github.com/quic-go/quic-go](https://github.com/quic-go/quic-go) from 0.40.1 to 0.42.0.
- [Release notes](https://github.com/quic-go/quic-go/releases)
- [Changelog](https://github.com/quic-go/quic-go/blob/master/Changelog.md)
- [Commits](https://github.com/quic-go/quic-go/compare/v0.40.1...v0.42.0)

---
updated-dependencies:
- dependency-name: github.com/quic-go/quic-go
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-04-24 23:11:03 +08:00
kLiHz a513417d7e fixup moving to Docker actions 2024-04-24 23:10:14 +08:00
kLiHz 58bd3d062b bump `setup-go` in workflow 2024-04-24 23:10:14 +08:00
kLiHz d2b79c13a6 move to Docker actions since it is archived 2024-04-24 23:10:14 +08:00
kLiHz fedd7a7b07 fixup bumping `ghaction-docker-buildx` 2024-04-24 23:10:14 +08:00
kLiHz 27f4b6abeb bump `ghaction-docker-buildx` in workflow 2024-04-24 23:10:14 +08:00
kLiHz d4aadbbc14 bump `delete-older-releases` in workflow 2024-04-24 23:10:14 +08:00
kLiHz 4508747635 fix deprecated `set-output` command in workflow 2024-04-24 23:10:14 +08:00
dependabot[bot] 50844821f8 Bump google.golang.org/protobuf from 1.31.0 to 1.33.0
Bumps google.golang.org/protobuf from 1.31.0 to 1.33.0.

---
updated-dependencies:
- dependency-name: google.golang.org/protobuf
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-04-24 23:09:36 +08:00
Jalal Saberi 29ec7c94ad
Add Chinese and English README Button 2024-04-10 19:09:02 +03:30
Jalal Saberi 53d6706b5d
Add Chinese and English README Button 2024-04-10 18:53:53 +03:30
ginuerzh 594418db53 added url path rewriting for forwarder node 2024-01-31 23:20:35 +08:00
ginuerzh 72cd708dfe fix tunnel connector selection 2024-01-28 18:57:34 +08:00
ginuerzh b576bb5645 fix tunnel weight 2024-01-28 18:34:44 +08:00
ginuerzh 8c529e3595 add weight for tunnel connector 2024-01-27 23:32:50 +08:00
ginuerzh b75e133719 fix hop http plugin 2024-01-27 21:33:24 +08:00
dependabot[bot] abfbf23842 Bump github.com/quic-go/quic-go from 0.40.0 to 0.40.1
Bumps [github.com/quic-go/quic-go](https://github.com/quic-go/quic-go) from 0.40.0 to 0.40.1.
- [Release notes](https://github.com/quic-go/quic-go/releases)
- [Changelog](https://github.com/quic-go/quic-go/blob/master/Changelog.md)
- [Commits](https://github.com/quic-go/quic-go/compare/v0.40.0...v0.40.1)

---
updated-dependencies:
- dependency-name: github.com/quic-go/quic-go
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-01-18 17:33:31 +08:00
ginuerzh 42d62550bf fix deadlock in websocket client conn 2024-01-12 23:50:27 +08:00
ginuerzh 69ba5864ad fix #390 2024-01-08 21:25:41 +08:00
ginuerzh fdd3eb61c3 add auth CLI support for api & metrics 2024-01-07 19:42:14 +08:00
ginuerzh f68bfdb149 add observer 2024-01-03 20:56:24 +08:00
ginuerzh 9e4bca84f1 fix tls handshake sniffing 2023-12-27 20:08:12 +08:00
ginuerzh 925ff89b78 fix issue #376 2023-12-25 22:11:03 +08:00
ginuerzh ca077d418a fix issue #372 2023-12-20 22:00:23 +08:00
ginuerzh 7264fca4f8 parsing config directly from cmd flag -C 2023-12-19 21:45:34 +08:00
ginuerzh bc37fac037 added logger group for service 2023-12-19 21:44:58 +08:00
ginuerzh dc48c4a3b2 fix auth for file handler 2023-12-16 14:33:52 +08:00
ginuerzh 310614663a check date for nightly trigger 2023-12-02 13:03:16 +08:00
ginuerzh 8f094d86b0 fix issue #338 2023-11-30 19:46:29 +08:00
ginuerzh 638c780acd update go.mod 2023-11-29 21:18:54 +08:00
ginuerzh 1f20f199c0 fix tun config 2023-11-20 20:43:27 +08:00
ginuerzh 8dc6146f79 fix go.mod 2023-11-19 20:03:23 +08:00
ginuerzh ed29b34c68 add logger component 2023-11-19 19:57:02 +08:00
ginuerzh b41aea03da add web api for router 2023-11-19 16:17:01 +08:00
ginuerzh 2c17e4417d add logger option for tun router 2023-11-19 14:41:10 +08:00
ginuerzh ea8ff5f77a add router component 2023-11-19 14:24:48 +08:00
ginuerzh 6762957c65 add traffic limiter for proxy handler 2023-11-18 18:30:04 +08:00
ginuerzh 4c934cd737 add more config options for kcp 2023-11-16 22:19:42 +08:00
ginuerzh 0d494f5ef2 fix #335 2023-11-16 20:39:42 +08:00
ginuerzh bf1bab7d84 fix race condition in relay udp conn 2023-11-14 22:37:33 +08:00
ginuerzh b8383b1076 add range port support for forwarder node 2023-11-14 19:43:57 +08:00
ginuerzh 345da28f01 update ingress interface 2023-11-13 20:42:45 +08:00
ginuerzh 907420d18b add webtransport tunnel 2023-11-10 21:30:20 +08:00
ginuerzh c1437794f8 add path option for hop 2023-11-09 20:36:21 +08:00
ginuerzh c2df5650fb add prefer and only options for resolver 2023-11-08 21:09:08 +08:00
ginuerzh 23506579e7 improve udp listener 2023-11-07 23:11:38 +08:00
ginuerzh 969db8691b update README 2023-11-03 17:48:56 +08:00
ginuerzh 3b0a2f1bae update sd 2023-11-02 22:34:11 +08:00
ginuerzh b2ed4ae9fd update sd 2023-11-02 20:54:59 +08:00
ginuerzh b2784011d0 add service discovery for tunnel 2023-10-31 23:00:32 +08:00
ginuerzh d4e00683c3 add tls options 2023-10-29 10:28:46 +08:00
ginuerzh 471513896d update nightly build schedule 2023-10-28 22:48:59 +08:00
ginuerzh 6967278600 add nightly build 2023-10-28 22:41:14 +08:00
ginuerzh 2898732c20 improve tunnel entrypoint 2023-10-27 22:15:53 +08:00
dependabot[bot] bb0c0450e8 Bump google.golang.org/grpc from 1.58.1 to 1.58.3
Bumps [google.golang.org/grpc](https://github.com/grpc/grpc-go) from 1.58.1 to 1.58.3.
- [Release notes](https://github.com/grpc/grpc-go/releases)
- [Commits](https://github.com/grpc/grpc-go/compare/v1.58.1...v1.58.3)

---
updated-dependencies:
- dependency-name: google.golang.org/grpc
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-10-27 22:11:52 +08:00
ginuerzh 6bb7dfea8f fix #315 2023-10-25 23:13:39 +08:00
ginuerzh e27d2bc593 set smux version to 1 2023-10-24 20:49:19 +08:00
ginuerzh 3144d61ecd generate random tunnel ID 2023-10-20 23:57:43 +08:00
ginuerzh e4b68d385a fix race condition 2023-10-19 23:50:27 +08:00
ginuerzh a91300d078 get real client ip for http forwarding 2023-10-18 21:23:38 +08:00
ginuerzh b8785eb71e fix race condition when forwarding http 2023-10-18 19:20:43 +08:00
ginuerzh 67fcd929be fix http traffic forwarding 2023-10-18 14:33:38 +08:00
ginuerzh 48c262bf71 fix websocket for forward handler 2023-10-17 23:26:04 +08:00
ginuerzh 05c35005e9 fix websocket for forward handler 2023-10-17 23:06:02 +08:00
ginuerzh f6112773a1 default to smux version 2 for tunnel 2023-10-17 22:16:28 +08:00
ginuerzh 61002b7c9c add mux config 2023-10-17 21:56:29 +08:00
ginuerzh f8a19e0829 fix http handler for tunnel 2023-10-16 23:56:36 +08:00
ginuerzh b1ace73943 update forward handler 2023-10-16 23:17:51 +08:00
ginuerzh 3d0e993498 fix compile error for serial 2023-10-15 23:57:49 +08:00
ginuerzh 9cbc875965 add tunnel handler and connector 2023-10-15 15:40:49 +08:00
dependabot[bot] 139b934fc6 Bump golang.org/x/net from 0.15.0 to 0.17.0
Bumps [golang.org/x/net](https://github.com/golang/net) from 0.15.0 to 0.17.0.
- [Commits](https://github.com/golang/net/compare/v0.15.0...v0.17.0)

---
updated-dependencies:
- dependency-name: golang.org/x/net
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-10-14 21:07:47 +08:00
ginuerzh c1d9228eee add auther for metrics 2023-10-09 21:30:43 +08:00
ginuerzh 2ea7dc250a fix #239 2023-10-08 22:05:39 +08:00
ginuerzh be33046c16 add proxy protocol support for relay tunnel and rtcp 2023-10-08 19:50:12 +08:00
ginuerzh d85b8d487b fix socks5 connector 2023-10-04 16:37:10 +08:00
ginuerzh ab8d77bb5e fix issue #280 2023-09-30 17:55:22 +08:00
ginuerzh 8640596064 go1.21 2023-09-28 21:31:17 +08:00
ginuerzh 8a699e1906 add reload and plugin support for hop 2023-09-28 21:05:44 +08:00
ginuerzh 0d41f5edc8 update go.sum 2023-09-21 21:55:23 +08:00
ginuerzh 87364d2494 add client addr for auth plugin 2023-09-21 20:01:25 +08:00
ginuerzh 6982b8af63 add HTTP based plugin 2023-09-20 22:58:48 +08:00
ginuerzh 0b1540b23a add client ID for plugin service 2023-09-19 22:39:42 +08:00
ginuerzh 1bbeeb7e66 add unix/serial dialer and connector 2023-09-18 23:18:27 +08:00
ginuerzh 5d4db6e377 add recorder for serial handler 2023-09-18 21:13:52 +08:00
ginuerzh 05d06a2718 add recorder for serial handler 2023-09-18 09:43:44 +08:00
ginuerzh 420b36b2f8 update README.md 2023-09-17 00:23:11 +08:00
ginuerzh 3883a4493a add support for unix domain socket 2023-09-16 23:15:49 +08:00
ginuerzh 355aaa7690 fix typos 2023-09-16 21:52:00 +08:00
ginuerzh c73c98974c add support for serial(COM) port redirector 2023-09-16 21:50:54 +08:00
Sam Sesh 98c9cc2821 update 2023-09-15 23:47:01 +08:00
127.0.0.1:443 b48d2c0dbc Update README.md 2023-09-15 23:47:01 +08:00
127.0.0.1:443 b7266f4660 Update README_en.md 2023-09-15 23:47:01 +08:00
Sam Sesh 6883c32157 set 777 per 2023-09-15 23:47:01 +08:00
Sam Sesh f1c5f7598c check root 2023-09-15 23:47:01 +08:00
Sam Sesh c31242efbb add install script 2023-09-15 23:47:01 +08:00
dependabot[bot] 6ee632cee2 Bump github.com/gin-gonic/gin from 1.9.0 to 1.9.1
Bumps [github.com/gin-gonic/gin](https://github.com/gin-gonic/gin) from 1.9.0 to 1.9.1.
- [Release notes](https://github.com/gin-gonic/gin/releases)
- [Changelog](https://github.com/gin-gonic/gin/blob/master/CHANGELOG.md)
- [Commits](https://github.com/gin-gonic/gin/compare/v1.9.0...v1.9.1)

---
updated-dependencies:
- dependency-name: github.com/gin-gonic/gin
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-09-15 23:44:45 +08:00
ginuerzh 8e2060582e add timeout for sniffing 2023-05-21 15:54:57 +08:00
dependabot[bot] 410b4ddf53 Bump github.com/gin-gonic/gin from 1.8.2 to 1.9.0
Bumps [github.com/gin-gonic/gin](https://github.com/gin-gonic/gin) from 1.8.2 to 1.9.0.
- [Release notes](https://github.com/gin-gonic/gin/releases)
- [Changelog](https://github.com/gin-gonic/gin/blob/master/CHANGELOG.md)
- [Commits](https://github.com/gin-gonic/gin/compare/v1.8.2...v1.9.0)

---
updated-dependencies:
- dependency-name: github.com/gin-gonic/gin
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-05-10 17:17:53 +08:00
ginuerzh 0b6474d836 increase transport buffer size 2023-05-01 14:06:37 +08:00
ginuerzh 4b6d6e7e7c add token for plugin 2023-04-20 19:07:55 +08:00
ginuerzh b2434dd05b add plugin system 2023-04-18 20:55:04 +08:00
ginuerzh 3d15dedab2 add async option for dns handler 2023-04-14 19:17:10 +08:00
ginuerzh 436864ca92 fix issue #206 2023-04-12 21:32:13 +08:00
ginuerzh 4847f9ee9f add support for windows service 2023-03-31 14:51:08 +08:00
ginuerzh a88b44d01d fix #174: load default CA from ca.pem 2023-03-21 20:25:13 +08:00
ginuerzh 45f1cc2efc fix #188 2023-03-20 18:31:47 +08:00
ginuerzh bfa1530ef6 fix grpc dialer 2023-03-08 00:01:36 +08:00
ginuerzh f02c9810f2 update go.mod 2023-02-26 13:54:17 +08:00
ginuerzh fda6214c12 fix #180: add default server name for tls client config 2023-02-26 13:48:42 +08:00
dependabot[bot] acb8c5bd53 bump golang.org/x/net from 0.5.0 to 0.7.0
Bumps [golang.org/x/net](https://github.com/golang/net) from 0.5.0 to 0.7.0.
- [Release notes](https://github.com/golang/net/releases)
- [Commits](https://github.com/golang/net/compare/v0.5.0...v0.7.0)

---
updated-dependencies:
- dependency-name: golang.org/x/net
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-02-26 13:43:20 +08:00
ginuerzh 3f53858c83 update version 2023-02-14 18:57:06 +08:00
ginuerzh a379565412 fix relay connect handler 2023-02-14 18:57:06 +08:00
dependabot[bot] 69089b790f Bump github.com/pion/dtls/v2 from 2.1.5 to 2.2.4
Bumps [github.com/pion/dtls/v2](https://github.com/pion/dtls) from 2.1.5 to 2.2.4.
- [Release notes](https://github.com/pion/dtls/releases)
- [Commits](https://github.com/pion/dtls/compare/v2.1.5...v2.2.4)

---
updated-dependencies:
- dependency-name: github.com/pion/dtls/v2
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-02-14 09:48:22 +08:00
ginuerzh 1c2d9b2a5b relay: fix routing for http traffic from entrypoint 2023-02-12 20:44:50 +08:00
ginuerzh 8d852b62cf relay: add direct routing for tunnel 2023-02-06 21:18:35 +08:00
ginuerzh a80dec0dc1 go1.20 2023-02-04 18:48:20 +08:00
ginuerzh 62ca8ff2de add tunnel option for relay 2023-02-02 23:28:04 +08:00
ginuerzh b8504f7f28 update go.mod 2023-02-02 20:00:12 +08:00
ginuerzh 95a2f0d649 add UDP support for reverse proxy tunnel 2023-02-02 19:20:22 +08:00
ginuerzh d810224e7b add .goreleaser.yaml 2023-01-31 21:24:54 +08:00
ginuerzh db36804bfa add goreleaser github action 2023-01-31 21:24:16 +08:00
ginuerzh ad8af1942d update .gitignore 2023-01-31 19:36:10 +08:00
ginuerzh 3d01f74f7c fix nil route panic 2023-01-31 18:08:50 +08:00
ginuerzh 6b05bcb0ab add auth support for forwarder node 2023-01-31 14:07:20 +08:00
ginuerzh 799c95bfb4 forward non-HTTP traffic based on host 2023-01-30 21:10:59 +08:00
ginuerzh de0a584175 fix http traffic forwarding 2023-01-30 14:40:40 +08:00
ginuerzh 381b1c9bf5 fix http traffic forwarding 2023-01-29 23:33:54 +08:00
ginuerzh ed7d72bfd8 update go.mod 2023-01-29 20:53:33 +08:00
ginuerzh 98aef6ba90 forwarder: add http node settings 2023-01-29 20:33:58 +08:00
ginuerzh dfde6efdeb relay: add private tunnel 2023-01-29 11:57:57 +08:00
ginuerzh 8858598cf0 update go.mod 2023-01-20 10:29:39 +08:00
ginuerzh c765789076 fix race condition in config updating 2023-01-20 10:28:32 +08:00
ginuerzh cf49a3ae46 add webapi for ingress 2023-01-15 17:46:24 +08:00
ginuerzh fa47726c3d add tunnel feature for relay 2023-01-14 13:18:45 +08:00
ginuerzh c351b849e6 case-insensitive metadata key 2023-01-11 22:28:43 +08:00
ginuerzh 44fca4e04c grpc: cancel stream when done 2022-12-30 19:38:51 +08:00
ginuerzh a795923980 fix dtls 2022-12-23 18:58:24 +08:00
117 changed files with 6653 additions and 1906 deletions

View File

@ -1,89 +0,0 @@
# ref: https://docs.docker.com/ci-cd/github-actions/
# https://blog.oddbit.com/post/2020-09-25-building-multi-architecture-im/
name: Docker
on:
push:
branches:
- master
tags:
- 'v*'
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Prepare
id: prepare
run: |
DOCKER_IMAGE=${{ secrets.DOCKER_IMAGE }}
VERSION=latest
# If this is git tag, use the tag name as a docker tag
if [[ $GITHUB_REF == refs/tags/* ]]; then
VERSION=${GITHUB_REF#refs/tags/v}
fi
TAGS="${DOCKER_IMAGE}:${VERSION}"
# If the VERSION looks like a version number, assume that
# this is the most recent version of the image and also
# tag it 'latest'.
if [[ $VERSION =~ ^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}$ ]]; then
TAGS="$TAGS,${DOCKER_IMAGE}:latest"
fi
# Set output parameters.
echo ::set-output name=tags::${TAGS}
echo ::set-output name=docker_image::${DOCKER_IMAGE}
echo ::set-output name=docker_platforms::linux/386,linux/amd64,linux/arm/v6,linux/arm/v7,linux/arm64/v8,linux/s390x
# https://github.com/crazy-max/ghaction-docker-buildx
- name: Set up Docker Buildx
id: buildx
uses: crazy-max/ghaction-docker-buildx@v1
with:
version: latest
- name: Environment
run: |
echo home=$HOME
echo git_ref=$GITHUB_REF
echo git_sha=$GITHUB_SHA
echo version=${{ steps.prepare.outputs.version }}
echo image=${{ steps.prepare.outputs.docker_image }}
echo platforms=${{ steps.prepare.outputs.docker_platforms }}
echo avail_platforms=${{ steps.buildx.outputs.platforms }}
# https://github.com/actions/checkout
- name: Checkout
uses: actions/checkout@v2
- name: Docker Buildx (no push)
run: |
docker buildx bake \
--set ${{ github.event.repository.name }}.platform=${{ steps.prepare.outputs.docker_platforms }} \
--set ${{ github.event.repository.name }}.output=type=image,push=false \
--set ${{ github.event.repository.name }}.tags="${{ steps.prepare.outputs.tags }}" \
--file docker-compose.yaml
- name: Docker Login
if: success()
env:
DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }}
run: |
echo "${DOCKER_PASSWORD}" | docker login --username "${{ secrets.DOCKER_USERNAME }}" --password-stdin
- name: Docker Buildx (push)
if: success()
run: |
docker buildx bake \
--set ${{ github.event.repository.name }}.platform=${{ steps.prepare.outputs.docker_platforms }} \
--set ${{ github.event.repository.name }}.output=type=image,push=true \
--set ${{ github.event.repository.name }}.tags="${{ steps.prepare.outputs.tags }}" \
--file docker-compose.yaml
- name: Clear
if: always()
run: |
rm -f ${HOME}/.docker/config.json

64
.github/workflows/buildx.yml vendored 100644
View File

@ -0,0 +1,64 @@
# ref: https://docs.docker.com/ci-cd/github-actions/
# https://blog.oddbit.com/post/2020-09-25-building-multi-architecture-im/
name: docker
on:
push:
branches:
- master
tags:
- 'v*'
permissions:
contents: read
# New pushes cancel an in-progress build for the same ref. Tags are isolated
# by ref, so re-pushing a release tag only de-dupes itself, never another tag.
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
# Auto-generates image tags, replacing the former hand-rolled shell:
# - master push -> :latest
# - clean release tag vX.Y.Z -> :X.Y.Z :X :X.Y :latest
# - nightly tag vX.Y.Z-nightly.<date> -> :X.Y.Z-nightly.<date> (no major/minor/latest)
- name: Docker meta
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ secrets.DOCKER_IMAGE }}
flavor: latest=false
tags: |
type=raw,value=latest,enable=${{ github.ref == format('refs/heads/{0}', github.event.repository.default_branch) || (startsWith(github.ref, 'refs/tags/v') && !contains(github.ref_name, '-')) }}
type=semver,pattern={{version}}
type=semver,pattern={{major}},enable=${{ !contains(github.ref_name, '-') }}
type=semver,pattern={{major}}.{{minor}},enable=${{ !contains(github.ref_name, '-') }}
- name: Set up Docker Buildx
id: buildx
uses: docker/setup-buildx-action@v3
- name: Login to DockerHub
if: github.event_name != 'pull_request'
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Buildx and push
uses: docker/build-push-action@v6
with:
platforms: linux/386,linux/amd64,linux/arm/v6,linux/arm/v7,linux/arm64/v8,linux/s390x,linux/riscv64
push: true
tags: ${{ steps.meta.outputs.tags }}
# Reuse build layers across runs via the GitHub Actions cache.
cache-from: type=gha
cache-to: type=gha,mode=max

40
.github/workflows/release.yml vendored 100644
View File

@ -0,0 +1,40 @@
name: goreleaser
on:
push:
# run only against tags
tags:
- 'v*'
permissions:
contents: write
# Never cancel an in-flight release; only de-dupe a re-pushed tag.
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: false
jobs:
goreleaser:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- run: git fetch --force --tags
- uses: actions/setup-go@v5
with:
go-version: '1.26'
cache: true
# UPX is disabled in .goreleaser.yaml (see #863); the install step was removed.
- uses: goreleaser/goreleaser-action@v6
with:
# either 'goreleaser' (default) or 'goreleaser-pro':
distribution: goreleaser
version: '~> v2'
args: release --clean
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
# Your GoReleaser Pro key, if you are using the 'goreleaser-pro'
# distribution:
# GORELEASER_KEY: ${{ secrets.GORELEASER_KEY }}

View File

@ -0,0 +1,71 @@
name: Trigger nightly build
on:
schedule:
# * is a special character in YAML, so you have to quote this string
- cron: '00 15 * * *'
workflow_dispatch:
# Only one nightly run at a time; a new run cancels a stale one.
concurrency:
group: ${{ github.workflow }}
cancel-in-progress: true
jobs:
check_date:
runs-on: ubuntu-latest
name: Check latest commit
outputs:
should_run: ${{ steps.should_run.outputs.should_run }}
steps:
- uses: actions/checkout@v4
- name: print latest_commit
run: echo ${{ github.sha }}
- id: should_run
name: Skip if no commit in the last 24 hours
if: ${{ github.event_name == 'schedule' }}
run: |
if [ "$(git rev-list --count --after='24 hours' ${{ github.sha }})" -eq 0 ]; then
echo "should_run=false" >> $GITHUB_OUTPUT
fi
trigger-nightly:
needs: check_date
if: ${{ needs.check_date.outputs.should_run != 'false' }}
name: Push tag for nightly build
runs-on: ubuntu-latest
steps:
-
name: 'Checkout'
uses: actions/checkout@v4
with:
token: ${{ secrets.NIGHTLY_BUILD_GH_TOKEN }}
fetch-depth: 0
-
name: 'Push new tag'
run: |
git config user.name "${GITHUB_ACTOR}"
git config user.email "${GITHUB_ACTOR}@users.noreply.github.com"
# A previous release was created using a lightweight tag
# git describe by default includes only annotated tags
# git describe --tags includes lightweight tags as well
DESCRIBE=`git tag -l --sort=-v:refname | grep -v nightly | head -n 1`
MAJOR_VERSION=`echo $DESCRIBE | awk '{split($0,a,"."); print a[1]}'`
MINOR_VERSION=`echo $DESCRIBE | awk '{split($0,a,"."); print a[2]}'`
PATCH_VERSION=`echo $DESCRIBE | awk '{split($0,a,"."); print a[3]}'`
PATCH_VERSION="$((${PATCH_VERSION} + 1))"
TAG="${MAJOR_VERSION}.${MINOR_VERSION}.${PATCH_VERSION}-nightly.$(date +'%Y%m%d')"
git tag -a $TAG -m "$TAG: nightly build"
git push origin $TAG
- name: 'Clean up nightly releases'
env:
GH_TOKEN: ${{ secrets.NIGHTLY_BUILD_GH_TOKEN }}
run: |
# Keep the 2 most recent nightly releases; delete the rest and their tags.
# Replaces the archived dev-drprasad/delete-older-releases action.
gh release list --json tagName,createdAt --limit 100 \
--jq '[.[] | select(.tagName | test("nightly"))] | sort_by(.createdAt) | reverse | .[2:] | .[].tagName' \
| while read -r tag; do
gh release delete "$tag" --yes --cleanup-tag || true
done

4
.gitignore vendored
View File

@ -32,8 +32,10 @@ _testmain.go
*.bak
cmd/gost/gost
gost
snap
*.pem
*.yaml
/*.yaml
*.txt
dist/

72
.goreleaser.yaml 100644
View File

@ -0,0 +1,72 @@
# This is an example .goreleaser.yml file with some sensible defaults.
# Make sure to check the documentation at https://goreleaser.com
before:
hooks:
# You may remove this if you don't use go modules.
- go mod tidy
# you may remove this if you don't need go generate
# - go generate ./...
builds:
- env:
- CGO_ENABLED=0
main: ./cmd/gost
targets:
- darwin_amd64
- darwin_arm64
- linux_386
- linux_amd64
- linux_amd64_v3
- linux_arm_5
- linux_arm_6
- linux_arm_7
- linux_arm64
- linux_mips_softfloat
- linux_mips_hardfloat
- linux_mipsle_softfloat
- linux_mipsle_hardfloat
- linux_mips64
- linux_mips64le
- linux_s390x
- linux_riscv64
- linux_loong64
- freebsd_386
- freebsd_amd64
- windows_386
- windows_amd64
- windows_amd64_v3
- windows_arm64
- android_arm64
ldflags:
- "-s -w -X 'main.version={{ .Tag }}'"
upx:
- # UPX compression. Disabled by default (see #863): upx --best/--lzma/--brute
# adds ~3s startup time on low-spec systems (linux/arm). Release builds can
# opt in via GORELEASER_UPX=true environment variable.
enabled: false
archives:
- format: tar.gz
# use zip for windows archives
format_overrides:
- goos: windows
format: zip
checksum:
name_template: 'checksums.txt'
snapshot:
name_template: "{{ incpatch .Version }}-next"
changelog:
sort: asc
filters:
exclude:
- '^docs:'
- '^test:'
release:
prerelease: auto
mode: keep-existing
# The lines beneath this are called `modelines`. See `:help modeline`
# Feel free to remove those if you don't want/use them.
# yaml-language-server: $schema=https://goreleaser.com/static/schema.json
# vim: set ts=2 sw=2 tw=0 fo=cnqoj

66
CLAUDE.md 100644
View File

@ -0,0 +1,66 @@
# CLAUDE.md — gost/
CLI binary entry point for GOST (GO Simple Tunnel). This module compiles the `gost` command.
## Build & Run
```bash
cd gost && go build ./cmd/gost/...
# Cross-compile
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags="-s -w" ./cmd/gost/...
# Build all platforms
make all
# Run
./gost -L "http://:8080" -F "socks5://:1080"
./gost -C gost.yml
```
## Structure
| File | Purpose |
|------|---------|
| `cmd/gost/main.go` | CLI entry point: flag parsing, multi-worker mode (`--` separator), program launch via `svc.Run` |
| `cmd/gost/program.go` | Service lifecycle (`Init`/`Start`/`Stop`), config loading, API/metrics/profiling servers, SIGHUP reload |
| `cmd/gost/register.go` | Blank imports for all built-in handlers, listeners, dialers, connectors — triggers `init()` registration |
| `cmd/gost/version.go` | Version string (`3.3.0`) |
## CLI Flags
| Flag | Usage |
|------|-------|
| `-L` | Inline service definition (repeatable) |
| `-F` | Inline chain node definition (repeatable) |
| `-C` | Path to config file (YAML/JSON) |
| `-D` / `-DD` | Debug / trace logging |
| `-api` | API service address (e.g. `:8080`) |
| `-metrics` | Prometheus metrics address |
| `-O` | Output merged config as yaml or json, then exit |
| `-V` | Print version and exit |
## Multi-Worker Mode
Arguments separated by ` -- ` spawn multiple worker processes via `exec.CommandContext`. Each worker runs as a child process with `_GOST_ID` set. Any worker's exit cancels all others. This is triggered in `init()` before flag parsing.
## Service Lifecycle
- `Init` → calls `parser.Init()` with all CLI/config inputs
- `Start``parser.Parse()``loader.Load()``p.run()` (starts services, API, metrics, profiling)
- `Stop` → cancels reload context, closes all services
- SIGHUP → `reloadConfig()` re-parses and re-runs without restarting the process
## Key Dependencies
- `github.com/go-gost/core` — interface definitions
- `github.com/go-gost/x` — all implementations, config, registry
- `github.com/judwhite/go-svc` — OS service framework (handles daemon/SIGHUP/SIGTERM)
## Registration Pattern
All components register via `init()` side-effects in their packages. `cmd/gost/register.go` triggers them with blank imports. When adding a new built-in handler/listener/dialer/connector, add a blank import here.
## Tests
Tests are in `tests/e2e/` — integration tests using the built binary. No unit tests. Run with `go test ./tests/e2e/...`.

View File

@ -1,28 +1,37 @@
FROM --platform=$BUILDPLATFORM golang:1.19-alpine as builder
FROM --platform=$BUILDPLATFORM tonistiigi/xx:1.6.1 AS xx
# Convert TARGETPLATFORM to GOARCH format
# https://github.com/tonistiigi/xx
COPY --from=tonistiigi/xx:golang / /
FROM --platform=$BUILDPLATFORM golang:1.26-alpine3.23 AS builder
# UPX compression disabled by default (see #863): upx --best adds ~3s startup
# time on low-spec systems (linux/arm). Builds can opt in by uncommenting the
# upx install line and adding upx --best to the build command below.
# RUN apk add --no-cache upx || echo "upx not found"
COPY --from=xx / /
ARG TARGETPLATFORM
RUN apk add --no-cache musl-dev git gcc
RUN xx-info env
ADD . /src
ENV CGO_ENABLED=0
WORKDIR /src
ENV XX_VERIFY_STATIC=1
ENV GO111MODULE=on
WORKDIR /app
RUN cd cmd/gost && go env && go build
COPY . .
FROM alpine:latest
RUN cd cmd/gost && \
xx-go build -ldflags "-s -w" && \
xx-verify gost
# add iptables for tun/tap
RUN apk add --no-cache iptables
FROM alpine:3.23
# add iptables/nftables for tun/tap
RUN apk add --no-cache iptables nftables
WORKDIR /bin/
COPY --from=builder /src/cmd/gost/gost .
COPY --from=builder /app/cmd/gost/gost .
ENTRYPOINT ["/bin/gost"]

View File

@ -2,7 +2,7 @@
### GO语言实现的安全隧道
[English README](README_en.md)
[![zh](https://img.shields.io/badge/Chinese%20README-green)](README.md) [![en](https://img.shields.io/badge/English%20README-gray)](README_en.md)
## 功能特性
@ -10,34 +10,61 @@
- [x] [多级转发链](https://gost.run/concepts/chain/)
- [x] [多协议支持](https://gost.run/tutorials/protocols/overview/)
- [x] [TCP/UDP端口转发](https://gost.run/tutorials/port-forwarding/)
- [x] [反向代理](https://gost.run/tutorials/reverse-proxy/)和[隧道](https://gost.run/tutorials/reverse-proxy-tunnel/)
- [x] [TCP/UDP透明代理](https://gost.run/tutorials/redirect/)
- [x] DNS[解析](https://gost.run/concepts/resolver/)和[代理](https://gost.run/tutorials/dns/)
- [x] [TUN/TAP设备](https://gost.run/tutorials/tuntap/)
- [x] [反向代理](https://gost.run/tutorials/reverse-proxy/)
- [x] [TUN/TAP设备](https://gost.run/tutorials/tuntap/)与[TUN2SOCKS](https://gost.run/tutorials/tungo/)
- [x] [负载均衡](https://gost.run/concepts/selector/)
- [x] [路由控制](https://gost.run/concepts/bypass/)
- [x] [限速限流](https://gost.run/concepts/limiter/)
- [x] [准入控制](https://gost.run/concepts/admission/)
- [x] [动态配置](https://gost.run/tutorials/api/config/)
- [x] [限速限流](https://gost.run/concepts/limiter/)
- [x] [插件系统](https://gost.run/concepts/plugin/)
- [x] [Prometheus监控指标](https://gost.run/tutorials/metrics/)
- [x] [动态配置](https://gost.run/tutorials/api/config/)
- [x] [Web API](https://gost.run/tutorials/api/overview/)
- [ ] Web UI
- [x] [GUI](https://github.com/go-gost/gostctl)/[WebUI](https://github.com/go-gost/gost-ui)
Wiki站点[https://gost.run](https://gost.run)
## 概览
Telegram讨论群[https://t.me/gogost](https://t.me/gogost)
![Overview](https://gost.run/images/overview.png)
Google讨论组[https://groups.google.com/d/forum/go-gost](https://groups.google.com/d/forum/go-gost)
GOST作为隧道有三种主要使用方式。
旧版入口:[v2.gost.run](https://v2.gost.run)
### 正向代理
作为代理服务访问网络,可以组合使用多种协议组成转发链进行转发。
![Proxy](https://gost.run/images/proxy.png)
### 端口转发
将一个服务的端口映射到另外一个服务的端口,同样可以组合使用多种协议组成转发链进行转发。
![Forward](https://gost.run/images/forward.png)
### 反向代理
利用隧道和内网穿透将内网服务暴露到公网访问。
![Reverse Proxy](https://gost.run/images/reverse-proxy.png)
## 下载安装
### 二进制文件
[https://github.com/go-gost/gost/releases](https://github.com/go-gost/gost/releases)
### 安装脚本
```bash
# 安装最新版本 [https://github.com/go-gost/gost/releases](https://github.com/go-gost/gost/releases)
bash <(curl -fsSL https://github.com/go-gost/gost/raw/master/install.sh) --install
```
```bash
# 选择要安装的版本
bash <(curl -fsSL https://github.com/go-gost/gost/raw/master/install.sh)
```
### 源码编译
```
@ -52,6 +79,28 @@ go build
docker run --rm gogost/gost -V
```
## 工具
### GUI
[go-gost/gostctl](https://github.com/go-gost/gostctl)
### WebUI
[go-gost/gost-ui](https://github.com/go-gost/gost-ui)
### Shadowsocks Android插件
[xausky/ShadowsocksGostPlugin](https://github.com/xausky/ShadowsocksGostPlugin)
[hamid-nazari/ShadowsocksGostPlugin](https://github.com/hamid-nazari/ShadowsocksGostPlugin)
## 帮助与支持
Wiki站点[https://gost.run](https://gost.run)
YouTube: [https://www.youtube.com/@gost-tunnel](https://www.youtube.com/@gost-tunnel)
Telegram[https://t.me/gogost](https://t.me/gogost)
Google讨论组[https://groups.google.com/d/forum/go-gost](https://groups.google.com/d/forum/go-gost)
旧版入口:[v2.gost.run](https://v2.gost.run)

View File

@ -2,40 +2,69 @@
### A simple security tunnel written in golang
[![en](https://img.shields.io/badge/English%20README-green)](README_en.md) [![zh](https://img.shields.io/badge/Chinese%20README-gray)](README.md)
## Features
- [x] [Listening on multiple ports](https://gost.run/en/getting-started/quick-start/)
- [x] [Multi-level forwarding chain](https://gost.run/en/concepts/chain/)
- [x] Rich protocol
- [x] [TCP/UDP port forwarding](https://gost.run/en/tutorials/port-forwarding/)
- [x] [Reverse Proxy](https://gost.run/en/tutorials/reverse-proxy/) and [Tunnel](https://gost.run/en/tutorials/reverse-proxy-tunnel/)
- [x] [TCP/UDP transparent proxy](https://gost.run/en/tutorials/redirect/)
- [x] DNS [resolver](https://gost.run/en/concepts/resolver/) and [proxy](https://gost.run/en/tutorials/dns/)
- [x] [TUN/TAP device](https://gost.run/en/tutorials/tuntap/)
- [x] [Reverse Proxy](https://gost.run/en/tutorials/reverse-proxy/)
- [x] [TUN/TAP device](https://gost.run/en/tutorials/tuntap/) and [TUN2SOCKS](https://gost.run/en/tutorials/tungo/)
- [x] [Load balancing](https://gost.run/en/concepts/selector/)
- [x] [Routing control](https://gost.run/en/concepts/bypass/)
- [x] [Bandwidth/Rate Limiter](https://gost.run/en/concepts/limiter/)
- [x] [Admission control](https://gost.run/en/concepts/limiter/)
- [x] [Dynamic configuration](https://gost.run/en/tutorials/api/config/)
- [x] [Bandwidth/Rate Limiter](https://gost.run/en/concepts/limiter/)
- [x] [Plugin System](https://gost.run/en/concepts/plugin/)
- [x] [Prometheus metrics](https://gost.run/en/tutorials/metrics/)
- [x] [Dynamic configuration](https://gost.run/en/tutorials/api/config/)
- [x] [Web API](https://gost.run/en/tutorials/api/overview/)
- [ ] Web UI
- [x] [GUI](https://github.com/go-gost/gostctl)/[WebUI](https://github.com/go-gost/gost-ui)
Wiki: [https://gost.run](https://gost.run/en/)
## Overview
Telegram: [https://t.me/gogost](https://t.me/gogost)
![Overview](https://gost.run/images/overview.png)
Google group: [https://groups.google.com/d/forum/go-gost](https://groups.google.com/d/forum/go-gost)
There are three main ways to use GOST as a tunnel.
Legacy version: [v2.gost.run](https://v2.gost.run/en/)
### Proxy
As a proxy service to access the network, multiple protocols can be used in combination to form a forwarding chain for traffic forwarding.
![Proxy](https://gost.run/images/proxy.png)
### Port Forwarding
Mapping the port of one service to the port of another service, you can also use a combination of multiple protocols to form a forwarding chain for traffic forwarding.
![Forward](https://gost.run/images/forward.png)
### Reverse Proxy
Use tunnel and intranet penetration to expose local services behind NAT or firewall to public network for access.
![Reverse Proxy](https://gost.run/images/reverse-proxy.png)
## Installation
### Binary files
[https://github.com/go-gost/gost/releases](https://github.com/go-gost/gost/releases)
### install script
```bash
# install latest from [https://github.com/go-gost/gost/releases](https://github.com/go-gost/gost/releases)
bash <(curl -fsSL https://github.com/go-gost/gost/raw/master/install.sh) --install
```
```bash
# select version for install
bash <(curl -fsSL https://github.com/go-gost/gost/raw/master/install.sh)
```
### From source
```
@ -50,6 +79,28 @@ go build
docker run --rm gogost/gost -V
```
## Tools
### GUI
[go-gost/gostctl](https://github.com/go-gost/gostctl)
### WebUI
[go-gost/gost-ui](https://github.com/go-gost/gost-ui)
### Shadowsocks Android
[xausky/ShadowsocksGostPlugin](https://github.com/xausky/ShadowsocksGostPlugin)
[hamid-nazari/ShadowsocksGostPlugin](https://github.com/hamid-nazari/ShadowsocksGostPlugin)
## Support
Wiki: [https://gost.run](https://gost.run/en/)
YouTube: [https://www.youtube.com/@gost-tunnel](https://www.youtube.com/@gost-tunnel)
Telegram: [https://t.me/gogost](https://t.me/gogost)
Google group: [https://groups.google.com/d/forum/go-gost](https://groups.google.com/d/forum/go-gost)
Legacy version: [v2.gost.run](https://v2.gost.run/en/)

View File

@ -1,619 +0,0 @@
package main
import (
"encoding/base64"
"errors"
"fmt"
"net/url"
"os"
"strconv"
"strings"
"time"
mdutil "github.com/go-gost/core/metadata/util"
"github.com/go-gost/x/config"
"github.com/go-gost/x/limiter/conn"
"github.com/go-gost/x/limiter/traffic"
mdx "github.com/go-gost/x/metadata"
"github.com/go-gost/x/registry"
)
var (
ErrInvalidCmd = errors.New("invalid cmd")
ErrInvalidNode = errors.New("invalid node")
)
type stringList []string
func (l *stringList) String() string {
return fmt.Sprintf("%s", *l)
}
func (l *stringList) Set(value string) error {
*l = append(*l, value)
return nil
}
func buildConfigFromCmd(services, nodes stringList) (*config.Config, error) {
namePrefix := ""
cfg := &config.Config{}
var chain *config.ChainConfig
if len(nodes) > 0 {
chain = &config.ChainConfig{
Name: fmt.Sprintf("%schain-0", namePrefix),
}
cfg.Chains = append(cfg.Chains, chain)
}
for i, node := range nodes {
url, err := normCmd(node)
if err != nil {
return nil, err
}
nodeConfig, err := buildNodeConfig(url)
if err != nil {
return nil, err
}
nodeConfig.Name = fmt.Sprintf("%snode-0", namePrefix)
var nodes []*config.NodeConfig
for _, host := range strings.Split(nodeConfig.Addr, ",") {
if host == "" {
continue
}
nodeCfg := &config.NodeConfig{}
*nodeCfg = *nodeConfig
nodeCfg.Name = fmt.Sprintf("%snode-%d", namePrefix, len(nodes))
nodeCfg.Addr = host
nodes = append(nodes, nodeCfg)
}
mc := nodeConfig.Connector.Metadata
md := mdx.NewMetadata(mc)
hopConfig := &config.HopConfig{
Name: fmt.Sprintf("%shop-%d", namePrefix, i),
Selector: parseSelector(mc),
Nodes: nodes,
}
if v := mdutil.GetString(md, "bypass"); v != "" {
bypassCfg := &config.BypassConfig{
Name: fmt.Sprintf("%sbypass-%d", namePrefix, len(cfg.Bypasses)),
}
if v[0] == '~' {
bypassCfg.Whitelist = true
v = v[1:]
}
for _, s := range strings.Split(v, ",") {
if s == "" {
continue
}
bypassCfg.Matchers = append(bypassCfg.Matchers, s)
}
hopConfig.Bypass = bypassCfg.Name
cfg.Bypasses = append(cfg.Bypasses, bypassCfg)
delete(mc, "bypass")
}
if v := mdutil.GetString(md, "resolver"); v != "" {
resolverCfg := &config.ResolverConfig{
Name: fmt.Sprintf("%sresolver-%d", namePrefix, len(cfg.Resolvers)),
}
for _, rs := range strings.Split(v, ",") {
if rs == "" {
continue
}
resolverCfg.Nameservers = append(
resolverCfg.Nameservers,
&config.NameserverConfig{
Addr: rs,
},
)
}
hopConfig.Resolver = resolverCfg.Name
cfg.Resolvers = append(cfg.Resolvers, resolverCfg)
delete(mc, "resolver")
}
if v := mdutil.GetString(md, "hosts"); v != "" {
hostsCfg := &config.HostsConfig{
Name: fmt.Sprintf("%shosts-%d", namePrefix, len(cfg.Hosts)),
}
for _, s := range strings.Split(v, ",") {
ss := strings.SplitN(s, ":", 2)
if len(ss) != 2 {
continue
}
hostsCfg.Mappings = append(
hostsCfg.Mappings,
&config.HostMappingConfig{
Hostname: ss[0],
IP: ss[1],
},
)
}
hopConfig.Hosts = hostsCfg.Name
cfg.Hosts = append(cfg.Hosts, hostsCfg)
delete(mc, "hosts")
}
if v := mdutil.GetString(md, "interface"); v != "" {
hopConfig.Interface = v
delete(mc, "interface")
}
if v := mdutil.GetInt(md, "so_mark"); v > 0 {
hopConfig.SockOpts = &config.SockOptsConfig{
Mark: v,
}
delete(mc, "so_mark")
}
chain.Hops = append(chain.Hops, hopConfig)
}
for i, svc := range services {
url, err := normCmd(svc)
if err != nil {
return nil, err
}
service, err := buildServiceConfig(url)
if err != nil {
return nil, err
}
service.Name = fmt.Sprintf("%sservice-%d", namePrefix, i)
if chain != nil {
if service.Listener.Type == "rtcp" || service.Listener.Type == "rudp" {
service.Listener.Chain = chain.Name
} else {
service.Handler.Chain = chain.Name
}
}
cfg.Services = append(cfg.Services, service)
mh := service.Handler.Metadata
md := mdx.NewMetadata(mh)
if v := mdutil.GetInt(md, "retries"); v > 0 {
service.Handler.Retries = v
delete(mh, "retries")
}
if v := mdutil.GetString(md, "admission"); v != "" {
admCfg := &config.AdmissionConfig{
Name: fmt.Sprintf("%sadmission-%d", namePrefix, len(cfg.Admissions)),
}
if v[0] == '~' {
admCfg.Whitelist = true
v = v[1:]
}
for _, s := range strings.Split(v, ",") {
if s == "" {
continue
}
admCfg.Matchers = append(admCfg.Matchers, s)
}
service.Admission = admCfg.Name
cfg.Admissions = append(cfg.Admissions, admCfg)
delete(mh, "admission")
}
if v := mdutil.GetString(md, "bypass"); v != "" {
bypassCfg := &config.BypassConfig{
Name: fmt.Sprintf("%sbypass-%d", namePrefix, len(cfg.Bypasses)),
}
if v[0] == '~' {
bypassCfg.Whitelist = true
v = v[1:]
}
for _, s := range strings.Split(v, ",") {
if s == "" {
continue
}
bypassCfg.Matchers = append(bypassCfg.Matchers, s)
}
service.Bypass = bypassCfg.Name
cfg.Bypasses = append(cfg.Bypasses, bypassCfg)
delete(mh, "bypass")
}
if v := mdutil.GetString(md, "resolver"); v != "" {
resolverCfg := &config.ResolverConfig{
Name: fmt.Sprintf("%sresolver-%d", namePrefix, len(cfg.Resolvers)),
}
for _, rs := range strings.Split(v, ",") {
if rs == "" {
continue
}
resolverCfg.Nameservers = append(
resolverCfg.Nameservers,
&config.NameserverConfig{
Addr: rs,
Prefer: mdutil.GetString(md, "prefer"),
},
)
}
service.Resolver = resolverCfg.Name
cfg.Resolvers = append(cfg.Resolvers, resolverCfg)
delete(mh, "resolver")
}
if v := mdutil.GetString(md, "hosts"); v != "" {
hostsCfg := &config.HostsConfig{
Name: fmt.Sprintf("%shosts-%d", namePrefix, len(cfg.Hosts)),
}
for _, s := range strings.Split(v, ",") {
ss := strings.SplitN(s, ":", 2)
if len(ss) != 2 {
continue
}
hostsCfg.Mappings = append(
hostsCfg.Mappings,
&config.HostMappingConfig{
Hostname: ss[0],
IP: ss[1],
},
)
}
service.Hosts = hostsCfg.Name
cfg.Hosts = append(cfg.Hosts, hostsCfg)
delete(mh, "hosts")
}
in := mdutil.GetString(md, "limiter.in")
out := mdutil.GetString(md, "limiter.out")
cin := mdutil.GetString(md, "limiter.conn.in")
cout := mdutil.GetString(md, "limiter.conn.out")
if in != "" || cin != "" || out != "" || cout != "" {
limiter := &config.LimiterConfig{
Name: fmt.Sprintf("%slimiter-%d", namePrefix, len(cfg.Limiters)),
}
if in != "" || out != "" {
limiter.Limits = append(limiter.Limits,
fmt.Sprintf("%s %s %s", traffic.GlobalLimitKey, in, out))
}
if cin != "" || cout != "" {
limiter.Limits = append(limiter.Limits,
fmt.Sprintf("%s %s %s", traffic.ConnLimitKey, cin, cout))
}
service.Limiter = limiter.Name
cfg.Limiters = append(cfg.Limiters, limiter)
delete(mh, "limiter.in")
delete(mh, "limiter.out")
delete(mh, "limiter.conn.in")
delete(mh, "limiter.conn.out")
}
if climit := mdutil.GetInt(md, "climiter"); climit > 0 {
limiter := &config.LimiterConfig{
Name: fmt.Sprintf("%sclimiter-%d", namePrefix, len(cfg.CLimiters)),
Limits: []string{fmt.Sprintf("%s %d", conn.GlobalLimitKey, climit)},
}
service.CLimiter = limiter.Name
cfg.CLimiters = append(cfg.CLimiters, limiter)
delete(mh, "climiter")
}
if rlimit := mdutil.GetFloat(md, "rlimiter"); rlimit > 0 {
limiter := &config.LimiterConfig{
Name: fmt.Sprintf("%srlimiter-%d", namePrefix, len(cfg.RLimiters)),
Limits: []string{fmt.Sprintf("%s %s", conn.GlobalLimitKey, strconv.FormatFloat(rlimit, 'f', -1, 64))},
}
service.RLimiter = limiter.Name
cfg.RLimiters = append(cfg.RLimiters, limiter)
delete(mh, "rlimiter")
}
}
return cfg, nil
}
func buildServiceConfig(url *url.URL) (*config.ServiceConfig, error) {
namePrefix := ""
if v := os.Getenv("_GOST_ID"); v != "" {
namePrefix = fmt.Sprintf("go-%s@", v)
}
var handler, listener string
schemes := strings.Split(url.Scheme, "+")
if len(schemes) == 1 {
handler = schemes[0]
listener = schemes[0]
}
if len(schemes) == 2 {
handler = schemes[0]
listener = schemes[1]
}
svc := &config.ServiceConfig{
Addr: url.Host,
}
if h := registry.HandlerRegistry().Get(handler); h == nil {
handler = "auto"
}
if ln := registry.ListenerRegistry().Get(listener); ln == nil {
listener = "tcp"
if handler == "ssu" {
listener = "udp"
}
}
// forward mode
if remotes := strings.Trim(url.EscapedPath(), "/"); remotes != "" {
svc.Forwarder = &config.ForwarderConfig{
// Targets: strings.Split(remotes, ","),
}
for i, addr := range strings.Split(remotes, ",") {
svc.Forwarder.Nodes = append(svc.Forwarder.Nodes,
&config.ForwardNodeConfig{
Name: fmt.Sprintf("%starget-%d", namePrefix, i),
Addr: addr,
})
}
if handler != "relay" {
if listener == "tcp" || listener == "udp" ||
listener == "rtcp" || listener == "rudp" ||
listener == "tun" || listener == "tap" ||
listener == "dns" {
handler = listener
} else {
handler = "forward"
}
}
}
var auth *config.AuthConfig
if url.User != nil {
auth = &config.AuthConfig{
Username: url.User.Username(),
}
auth.Password, _ = url.User.Password()
}
m := map[string]any{}
for k, v := range url.Query() {
if len(v) > 0 {
m[k] = v[0]
}
}
md := mdx.NewMetadata(m)
if sa := mdutil.GetString(md, "auth"); sa != "" {
au, err := parseAuthFromCmd(sa)
if err != nil {
return nil, err
}
auth = au
}
delete(m, "auth")
tlsConfig := &config.TLSConfig{
CertFile: mdutil.GetString(md, "certFile"),
KeyFile: mdutil.GetString(md, "keyFile"),
CAFile: mdutil.GetString(md, "caFile"),
}
if tlsConfig.CertFile == "" {
tlsConfig.CertFile = mdutil.GetString(md, "cert")
}
if tlsConfig.KeyFile == "" {
tlsConfig.KeyFile = mdutil.GetString(md, "key")
}
if tlsConfig.CAFile == "" {
tlsConfig.CAFile = mdutil.GetString(md, "ca")
}
delete(m, "certFile")
delete(m, "cert")
delete(m, "keyFile")
delete(m, "key")
delete(m, "caFile")
delete(m, "ca")
if tlsConfig.CertFile == "" {
tlsConfig = nil
}
if v := mdutil.GetString(md, "dns"); v != "" {
md.Set("dns", strings.Split(v, ","))
}
if svc.Forwarder != nil {
svc.Forwarder.Selector = parseSelector(m)
}
svc.Handler = &config.HandlerConfig{
Type: handler,
Auth: auth,
Metadata: m,
}
svc.Listener = &config.ListenerConfig{
Type: listener,
TLS: tlsConfig,
Metadata: m,
}
svc.Metadata = m
if svc.Listener.Type == "ssh" || svc.Listener.Type == "sshd" {
svc.Handler.Auth = nil
svc.Listener.Auth = auth
}
return svc, nil
}
func buildNodeConfig(url *url.URL) (*config.NodeConfig, error) {
var connector, dialer string
schemes := strings.Split(url.Scheme, "+")
if len(schemes) == 1 {
connector = schemes[0]
dialer = schemes[0]
}
if len(schemes) == 2 {
connector = schemes[0]
dialer = schemes[1]
}
node := &config.NodeConfig{
Addr: url.Host,
}
if c := registry.ConnectorRegistry().Get(connector); c == nil {
connector = "http"
}
if d := registry.DialerRegistry().Get(dialer); d == nil {
dialer = "tcp"
if connector == "ssu" {
dialer = "udp"
}
}
var auth *config.AuthConfig
if url.User != nil {
auth = &config.AuthConfig{
Username: url.User.Username(),
}
auth.Password, _ = url.User.Password()
}
m := map[string]any{}
for k, v := range url.Query() {
if len(v) > 0 {
m[k] = v[0]
}
}
md := mdx.NewMetadata(m)
if sauth := mdutil.GetString(md, "auth"); sauth != "" && auth == nil {
au, err := parseAuthFromCmd(sauth)
if err != nil {
return nil, err
}
auth = au
}
delete(m, "auth")
tlsConfig := &config.TLSConfig{
CertFile: mdutil.GetString(md, "certFile"),
KeyFile: mdutil.GetString(md, "keyFile"),
CAFile: mdutil.GetString(md, "caFile"),
Secure: mdutil.GetBool(md, "secure"),
ServerName: mdutil.GetString(md, "serverName"),
}
if tlsConfig.ServerName == "" {
tlsConfig.ServerName = url.Hostname()
}
if tlsConfig.CertFile == "" {
tlsConfig.CertFile = mdutil.GetString(md, "cert")
}
if tlsConfig.KeyFile == "" {
tlsConfig.KeyFile = mdutil.GetString(md, "key")
}
if tlsConfig.CAFile == "" {
tlsConfig.CAFile = mdutil.GetString(md, "ca")
}
delete(m, "certFile")
delete(m, "cert")
delete(m, "keyFile")
delete(m, "key")
delete(m, "caFile")
delete(m, "ca")
delete(m, "secure")
delete(m, "serverName")
if !tlsConfig.Secure && tlsConfig.CertFile == "" && tlsConfig.CAFile == "" {
tlsConfig = nil
}
node.Connector = &config.ConnectorConfig{
Type: connector,
Auth: auth,
Metadata: m,
}
node.Dialer = &config.DialerConfig{
Type: dialer,
TLS: tlsConfig,
Metadata: m,
}
if node.Dialer.Type == "ssh" || node.Dialer.Type == "sshd" {
node.Connector.Auth = nil
node.Dialer.Auth = auth
}
return node, nil
}
func normCmd(s string) (*url.URL, error) {
s = strings.TrimSpace(s)
if s == "" {
return nil, ErrInvalidCmd
}
if s[0] == ':' || !strings.Contains(s, "://") {
s = "auto://" + s
}
url, err := url.Parse(s)
if err != nil {
return nil, err
}
if url.Scheme == "https" {
url.Scheme = "http+tls"
}
return url, nil
}
func parseAuthFromCmd(sa string) (*config.AuthConfig, error) {
v, err := base64.StdEncoding.DecodeString(sa)
if err != nil {
return nil, err
}
cs := string(v)
n := strings.IndexByte(cs, ':')
if n < 0 {
return &config.AuthConfig{
Username: cs,
}, nil
}
return &config.AuthConfig{
Username: cs[:n],
Password: cs[n+1:],
}, nil
}
func parseSelector(m map[string]any) *config.SelectorConfig {
md := mdx.NewMetadata(m)
strategy := mdutil.GetString(md, "strategy")
maxFails := mdutil.GetInt(md, "maxFails")
if maxFails == 0 {
maxFails = mdutil.GetInt(md, "max_fails")
}
failTimeout := mdutil.GetDuration(md, "failTimeout")
if failTimeout == 0 {
failTimeout = mdutil.GetDuration(md, "fail_timeout")
}
if strategy == "" && maxFails <= 0 && failTimeout <= 0 {
return nil
}
if strategy == "" {
strategy = "round"
}
if maxFails <= 0 {
maxFails = 1
}
if failTimeout <= 0 {
failTimeout = 30 * time.Second
}
delete(m, "strategy")
delete(m, "maxFails")
delete(m, "max_fails")
delete(m, "failTimeout")
delete(m, "fail_timeout")
return &config.SelectorConfig{
Strategy: strategy,
MaxFails: maxFails,
FailTimeout: failTimeout,
}
}

View File

@ -1,196 +0,0 @@
package main
import (
"io"
"os"
"path/filepath"
"github.com/go-gost/core/logger"
"github.com/go-gost/core/service"
"github.com/go-gost/x/api"
"github.com/go-gost/x/config"
"github.com/go-gost/x/config/parsing"
xlogger "github.com/go-gost/x/logger"
metrics "github.com/go-gost/x/metrics/service"
"github.com/go-gost/x/registry"
"gopkg.in/natefinch/lumberjack.v2"
)
func buildService(cfg *config.Config) (services []service.Service) {
if cfg == nil {
return
}
for _, autherCfg := range cfg.Authers {
if auther := parsing.ParseAuther(autherCfg); auther != nil {
if err := registry.AutherRegistry().Register(autherCfg.Name, auther); err != nil {
log.Fatal(err)
}
}
}
for _, admissionCfg := range cfg.Admissions {
if adm := parsing.ParseAdmission(admissionCfg); adm != nil {
if err := registry.AdmissionRegistry().Register(admissionCfg.Name, adm); err != nil {
log.Fatal(err)
}
}
}
for _, bypassCfg := range cfg.Bypasses {
if bp := parsing.ParseBypass(bypassCfg); bp != nil {
if err := registry.BypassRegistry().Register(bypassCfg.Name, bp); err != nil {
log.Fatal(err)
}
}
}
for _, resolverCfg := range cfg.Resolvers {
r, err := parsing.ParseResolver(resolverCfg)
if err != nil {
log.Fatal(err)
}
if r != nil {
if err := registry.ResolverRegistry().Register(resolverCfg.Name, r); err != nil {
log.Fatal(err)
}
}
}
for _, hostsCfg := range cfg.Hosts {
if h := parsing.ParseHosts(hostsCfg); h != nil {
if err := registry.HostsRegistry().Register(hostsCfg.Name, h); err != nil {
log.Fatal(err)
}
}
}
for _, recorderCfg := range cfg.Recorders {
if h := parsing.ParseRecorder(recorderCfg); h != nil {
if err := registry.RecorderRegistry().Register(recorderCfg.Name, h); err != nil {
log.Fatal(err)
}
}
}
for _, limiterCfg := range cfg.Limiters {
if h := parsing.ParseTrafficLimiter(limiterCfg); h != nil {
if err := registry.TrafficLimiterRegistry().Register(limiterCfg.Name, h); err != nil {
log.Fatal(err)
}
}
}
for _, limiterCfg := range cfg.CLimiters {
if h := parsing.ParseConnLimiter(limiterCfg); h != nil {
if err := registry.ConnLimiterRegistry().Register(limiterCfg.Name, h); err != nil {
log.Fatal(err)
}
}
}
for _, limiterCfg := range cfg.RLimiters {
if h := parsing.ParseRateLimiter(limiterCfg); h != nil {
if err := registry.RateLimiterRegistry().Register(limiterCfg.Name, h); err != nil {
log.Fatal(err)
}
}
}
for _, hopCfg := range cfg.Hops {
hop, err := parsing.ParseHop(hopCfg)
if err != nil {
log.Fatal(err)
}
if hop != nil {
if err := registry.HopRegistry().Register(hopCfg.Name, hop); err != nil {
log.Fatal(err)
}
}
}
for _, chainCfg := range cfg.Chains {
c, err := parsing.ParseChain(chainCfg)
if err != nil {
log.Fatal(err)
}
if c != nil {
if err := registry.ChainRegistry().Register(chainCfg.Name, c); err != nil {
log.Fatal(err)
}
}
}
for _, svcCfg := range cfg.Services {
svc, err := parsing.ParseService(svcCfg)
if err != nil {
log.Fatal(err)
}
if svc != nil {
if err := registry.ServiceRegistry().Register(svcCfg.Name, svc); err != nil {
log.Fatal(err)
}
}
services = append(services, svc)
}
return
}
func logFromConfig(cfg *config.LogConfig) logger.Logger {
if cfg == nil {
cfg = &config.LogConfig{}
}
opts := []xlogger.LoggerOption{
xlogger.FormatLoggerOption(logger.LogFormat(cfg.Format)),
xlogger.LevelLoggerOption(logger.LogLevel(cfg.Level)),
}
var out io.Writer = os.Stderr
switch cfg.Output {
case "none", "null":
return xlogger.Nop()
case "stdout":
out = os.Stdout
case "stderr", "":
out = os.Stderr
default:
if cfg.Rotation != nil {
out = &lumberjack.Logger{
Filename: cfg.Output,
MaxSize: cfg.Rotation.MaxSize,
MaxAge: cfg.Rotation.MaxAge,
MaxBackups: cfg.Rotation.MaxBackups,
LocalTime: cfg.Rotation.LocalTime,
Compress: cfg.Rotation.Compress,
}
} else {
os.MkdirAll(filepath.Dir(cfg.Output), 0755)
f, err := os.OpenFile(cfg.Output, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666)
if err != nil {
log.Warn(err)
} else {
out = f
}
}
}
opts = append(opts, xlogger.OutputLoggerOption(out))
return xlogger.NewLogger(opts...)
}
func buildAPIService(cfg *config.APIConfig) (service.Service, error) {
auther := parsing.ParseAutherFromAuth(cfg.Auth)
if cfg.Auther != "" {
auther = registry.AutherRegistry().Get(cfg.Auther)
}
return api.NewService(
cfg.Addr,
api.PathPrefixOption(cfg.PathPrefix),
api.AccessLogOption(cfg.AccessLog),
api.AutherOption(auther),
)
}
func buildMetricsService(cfg *config.MetricsConfig) (service.Service, error) {
return metrics.NewService(
cfg.Addr,
metrics.PathOption(cfg.Path),
)
}

View File

@ -4,73 +4,88 @@ import (
"context"
"flag"
"fmt"
"net/http"
"log"
_ "net/http/pprof"
"os"
"os/exec"
"os/signal"
"runtime"
"strings"
"sync"
"syscall"
"sync/atomic"
"time"
"github.com/go-gost/core/logger"
"github.com/go-gost/x/config"
"github.com/go-gost/x/config/parsing"
xlogger "github.com/go-gost/x/logger"
xmetrics "github.com/go-gost/x/metrics"
"github.com/go-gost/x/registry"
"github.com/judwhite/go-svc"
)
var (
log logger.Logger
type stringList []string
cfgFile string
func (l *stringList) String() string {
return fmt.Sprintf("%s", *l)
}
func (l *stringList) Set(value string) error {
*l = append(*l, value)
return nil
}
var (
cfgFiles stringList
outputFormat string
services stringList
nodes stringList
debug bool
trace bool
apiAddr string
metricsAddr string
reload time.Duration
)
func init() {
log.SetFlags(log.LstdFlags | log.Lshortfile | log.Lmicroseconds)
args := strings.Join(os.Args[1:], " ")
if strings.Contains(args, " -- ") {
var (
wg sync.WaitGroup
ret int
ret atomic.Int32
)
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
wargsList := strings.Split(" "+args+" ", " -- ")
for wid, wargs := range strings.Split(" "+args+" ", " -- ") {
wg.Add(1)
go func(wid int, wargs string) {
defer wg.Done()
defer cancel()
worker(wid, strings.Split(wargs, " "), &ctx, &ret)
}(wid, strings.TrimSpace(wargs))
ctx, cancel := context.WithCancel(context.Background())
for wid, wargs := range wargsList {
wg.Go(func() {
worker(wid, strings.Split(strings.TrimSpace(wargs), " "), ctx, &ret)
})
}
wg.Wait()
cancel()
os.Exit(ret)
os.Exit(int(ret.Load()))
}
}
func worker(id int, args []string, ctx *context.Context, ret *int) {
cmd := exec.CommandContext(*ctx, os.Args[0], args...)
func worker(id int, args []string, ctx context.Context, ret *atomic.Int32) {
cmd := exec.CommandContext(ctx, os.Args[0], args...)
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
cmd.Env = append(os.Environ(), fmt.Sprintf("_GOST_ID=%d", id))
cmd.Run()
if cmd.ProcessState.Exited() {
*ret = cmd.ProcessState.ExitCode()
if err := cmd.Run(); err != nil {
// Context cancellation is expected when one worker exits early.
// Only log fatal on other errors.
if ctx.Err() == nil {
log.Fatal(err)
}
return
}
if cmd.ProcessState != nil && cmd.ProcessState.Exited() {
ret.Store(int32(cmd.ProcessState.ExitCode()))
}
}
@ -79,12 +94,14 @@ func init() {
flag.Var(&services, "L", "service list")
flag.Var(&nodes, "F", "chain node list")
flag.StringVar(&cfgFile, "C", "", "configuration file")
flag.Var(&cfgFiles, "C", "config file(s), URL(s), or inline JSON")
flag.BoolVar(&printVersion, "V", false, "print version")
flag.StringVar(&outputFormat, "O", "", "output format, one of yaml|json format")
flag.BoolVar(&debug, "D", false, "debug mode")
flag.BoolVar(&trace, "DD", false, "trace mode")
flag.StringVar(&apiAddr, "api", "", "api service address")
flag.StringVar(&metricsAddr, "metrics", "", "metrics service address")
flag.DurationVar(&reload, "R", 0, "auto reload period (e.g. 30s, 1m)")
flag.Parse()
if printVersion {
@ -92,193 +109,15 @@ func init() {
version, runtime.Version(), runtime.GOOS, runtime.GOARCH)
os.Exit(0)
}
log = xlogger.NewLogger()
logger.SetDefault(log)
}
func main() {
cfg := &config.Config{}
if cfgFile != "" {
if err := cfg.ReadFile(cfgFile); err != nil {
log.Fatal(err)
}
}
cmdCfg, err := buildConfigFromCmd(services, nodes)
if err != nil {
log.Fatal(err)
}
cfg = mergeConfig(cfg, cmdCfg)
if len(cfg.Services) == 0 && apiAddr == "" {
if err := cfg.Load(); err != nil {
log.Fatal(err)
}
}
if v := os.Getenv("GOST_PROFILING"); v != "" {
cfg.Profiling = &config.ProfilingConfig{
Addr: v,
}
}
if v := os.Getenv("GOST_METRICS"); v != "" {
cfg.Metrics = &config.MetricsConfig{
Addr: v,
}
}
if v := os.Getenv("GOST_LOGGER_LEVEL"); v != "" {
cfg.Log = &config.LogConfig{
Level: v,
}
}
if v := os.Getenv("GOST_API"); v != "" {
cfg.API = &config.APIConfig{
Addr: v,
}
}
if debug {
if cfg.Log == nil {
cfg.Log = &config.LogConfig{}
}
cfg.Log.Level = string(logger.DebugLevel)
}
if apiAddr != "" {
cfg.API = &config.APIConfig{
Addr: apiAddr,
}
}
if metricsAddr != "" {
cfg.Metrics = &config.MetricsConfig{
Addr: metricsAddr,
}
}
log = logFromConfig(cfg.Log)
log := xlogger.NewLogger()
logger.SetDefault(log)
if outputFormat != "" {
if err := cfg.Write(os.Stdout, outputFormat); err != nil {
log.Fatal(err)
}
os.Exit(0)
}
p := &program{}
if cfg.Profiling != nil {
go func() {
addr := cfg.Profiling.Addr
if addr == "" {
addr = ":6060"
}
log.Info("profiling server on ", addr)
log.Fatal(http.ListenAndServe(addr, nil))
}()
}
if cfg.API != nil {
s, err := buildAPIService(cfg.API)
if err != nil {
log.Fatal(err)
}
defer s.Close()
go func() {
log.Info("api service on ", s.Addr())
log.Fatal(s.Serve())
}()
}
if cfg.Metrics != nil {
xmetrics.Init(xmetrics.NewMetrics())
if cfg.Metrics.Addr != "" {
s, err := buildMetricsService(cfg.Metrics)
if err != nil {
log.Fatal(err)
}
go func() {
defer s.Close()
log.Info("metrics service on ", s.Addr())
log.Fatal(s.Serve())
}()
}
}
parsing.BuildDefaultTLSConfig(cfg.TLS)
services := buildService(cfg)
for _, svc := range services {
svc := svc
go func() {
svc.Serve()
// svc.Close()
}()
}
config.SetGlobal(cfg)
sigs := make(chan os.Signal, 1)
signal.Notify(sigs, os.Interrupt, syscall.SIGTERM, syscall.SIGINT, syscall.SIGHUP)
for sig := range sigs {
switch sig {
case syscall.SIGHUP:
return
default:
for name, srv := range registry.ServiceRegistry().GetAll() {
srv.Close()
log.Debugf("service %s shutdown", name)
}
return
}
if err := svc.Run(p); err != nil {
logger.Default().Fatal(err)
}
}
func mergeConfig(cfg1, cfg2 *config.Config) *config.Config {
if cfg1 == nil {
return cfg2
}
if cfg2 == nil {
return cfg1
}
cfg := &config.Config{
Services: append(cfg1.Services, cfg2.Services...),
Chains: append(cfg1.Chains, cfg2.Chains...),
Hops: append(cfg1.Hops, cfg2.Hops...),
Authers: append(cfg1.Authers, cfg2.Authers...),
Admissions: append(cfg1.Admissions, cfg2.Admissions...),
Bypasses: append(cfg1.Bypasses, cfg2.Bypasses...),
Resolvers: append(cfg1.Resolvers, cfg2.Resolvers...),
Hosts: append(cfg1.Hosts, cfg2.Hosts...),
Recorders: append(cfg1.Recorders, cfg2.Recorders...),
Limiters: append(cfg1.Limiters, cfg2.Limiters...),
CLimiters: append(cfg1.CLimiters, cfg2.CLimiters...),
RLimiters: append(cfg1.RLimiters, cfg2.RLimiters...),
TLS: cfg1.TLS,
Log: cfg1.Log,
API: cfg1.API,
Metrics: cfg1.Metrics,
Profiling: cfg1.Profiling,
}
if cfg2.TLS != nil {
cfg.TLS = cfg2.TLS
}
if cfg2.Log != nil {
cfg.Log = cfg2.Log
}
if cfg2.API != nil {
cfg.API = cfg2.API
}
if cfg2.Metrics != nil {
cfg.Metrics = cfg2.Metrics
}
if cfg2.Profiling != nil {
cfg.Profiling = cfg2.Profiling
}
return cfg
}

293
cmd/gost/program.go 100644
View File

@ -0,0 +1,293 @@
package main
import (
"context"
"errors"
"net/http"
"os"
"os/signal"
"strings"
"syscall"
"time"
"github.com/go-gost/core/auth"
"github.com/go-gost/core/logger"
"github.com/go-gost/core/service"
api_service "github.com/go-gost/x/api/service"
xauth "github.com/go-gost/x/auth"
"github.com/go-gost/x/config"
"github.com/go-gost/x/config/loader"
auth_parser "github.com/go-gost/x/config/parsing/auth"
"github.com/go-gost/x/config/parsing/parser"
xmetrics "github.com/go-gost/x/metrics"
metrics "github.com/go-gost/x/metrics/service"
"github.com/go-gost/x/registry"
"github.com/judwhite/go-svc"
)
type program struct {
srvApi service.Service
srvMetrics service.Service
srvProfiling *http.Server
cancel context.CancelFunc
}
func (p *program) Init(env svc.Environment) error {
parser.Init(parser.Args{
CfgFiles: cfgFiles,
Services: services,
Nodes: nodes,
Debug: debug,
Trace: trace,
ApiAddr: apiAddr,
MetricsAddr: metricsAddr,
})
return nil
}
func (p *program) Start() error {
cfg, err := parser.Parse()
if err != nil {
return err
}
if outputFormat != "" {
if err := cfg.Write(os.Stdout, outputFormat); err != nil {
return err
}
os.Exit(0)
}
config.Set(cfg)
// Enable metrics before loading services so that listener wrappers
// can observe the enabled state at Init time.
if cfg.Metrics != nil && cfg.Metrics.Addr != "" {
xmetrics.Enable(true)
}
if err := loader.Load(cfg); err != nil {
return err
}
if err := p.run(cfg); err != nil {
return err
}
ctx, cancel := context.WithCancel(context.Background())
p.cancel = cancel
go p.reload(ctx)
return nil
}
func (p *program) run(cfg *config.Config) error {
for _, svc := range registry.ServiceRegistry().GetAll() {
svc := svc
go func() {
svc.Serve()
}()
}
if p.srvApi != nil {
p.srvApi.Close()
p.srvApi = nil
}
if cfg.API != nil {
s, err := buildApiService(cfg.API)
if err != nil {
return err
}
p.srvApi = s
go func() {
defer s.Close()
log := logger.Default().WithFields(map[string]any{"kind": "service", "service": "@api"})
log.Info("listening on ", s.Addr())
if err := s.Serve(); !errors.Is(err, http.ErrServerClosed) {
log.Error(err)
}
}()
}
if p.srvMetrics != nil {
p.srvMetrics.Close()
p.srvMetrics = nil
}
if cfg.Metrics != nil && cfg.Metrics.Addr != "" {
s, err := buildMetricsService(cfg.Metrics)
if err != nil {
return err
}
p.srvMetrics = s
go func() {
defer s.Close()
log := logger.Default().WithFields(map[string]any{"kind": "service", "service": "@metrics"})
log.Info("listening on ", s.Addr())
if err := s.Serve(); !errors.Is(err, http.ErrServerClosed) {
log.Error(err)
}
}()
}
if p.srvProfiling != nil {
p.srvProfiling.Close()
p.srvProfiling = nil
}
if cfg.Profiling != nil {
addr := cfg.Profiling.Addr
if addr == "" {
addr = ":6060"
}
s := &http.Server{
Addr: addr,
}
p.srvProfiling = s
go func() {
defer s.Close()
log := logger.Default().WithFields(map[string]any{"kind": "service", "service": "@profiling"})
log.Info("listening on ", addr)
if err := s.ListenAndServe(); !errors.Is(err, http.ErrServerClosed) {
log.Error(err)
}
}()
}
return nil
}
func (p *program) Stop() error {
if p.cancel != nil {
p.cancel()
}
for name, srv := range registry.ServiceRegistry().GetAll() {
srv.Close()
logger.Default().Debugf("service %s shutdown", name)
}
if p.srvApi != nil {
p.srvApi.Close()
logger.Default().Debug("service @api shutdown")
}
if p.srvMetrics != nil {
p.srvMetrics.Close()
logger.Default().Debug("service @metrics shutdown")
}
if p.srvProfiling != nil {
p.srvProfiling.Close()
logger.Default().Debug("service @profiling shutdown")
}
return nil
}
func (p *program) reload(ctx context.Context) {
c := make(chan os.Signal, 1)
signal.Notify(c, syscall.SIGHUP)
var ticker <-chan time.Time
if reload > 0 {
t := time.NewTicker(reload)
defer t.Stop()
ticker = t.C
}
for {
select {
case <-c:
if err := p.reloadConfig(); err != nil {
logger.Default().Error(err)
} else {
logger.Default().Info("config reloaded")
}
case <-ticker:
if err := p.reloadConfig(); err != nil {
logger.Default().Errorf("auto reload: %v", err)
} else {
logger.Default().Debug("config auto reloaded")
}
case <-ctx.Done():
return
}
}
}
func (p *program) reloadConfig() error {
cfg, err := parser.Parse()
if err != nil {
return err
}
config.Set(cfg)
if err := loader.Load(cfg); err != nil {
return err
}
if err := p.run(cfg); err != nil {
return err
}
return nil
}
func buildApiService(cfg *config.APIConfig) (service.Service, error) {
var authers []auth.Authenticator
if auther := auth_parser.ParseAutherFromAuth(cfg.Auth); auther != nil {
authers = append(authers, auther)
}
if cfg.Auther != "" {
authers = append(authers, registry.AutherRegistry().Get(cfg.Auther))
}
var auther auth.Authenticator
if len(authers) > 0 {
auther = xauth.AuthenticatorGroup(authers...)
}
network := "tcp"
addr := cfg.Addr
if strings.HasPrefix(addr, "unix://") {
network = "unix"
addr = strings.TrimPrefix(addr, "unix://")
}
return api_service.NewService(
network, addr,
api_service.PathPrefixOption(cfg.PathPrefix),
api_service.AccessLogOption(cfg.AccessLog),
api_service.AutherOption(auther),
)
}
func buildMetricsService(cfg *config.MetricsConfig) (service.Service, error) {
auther := auth_parser.ParseAutherFromAuth(cfg.Auth)
if cfg.Auther != "" {
auther = registry.AutherRegistry().Get(cfg.Auther)
}
network := "tcp"
addr := cfg.Addr
if strings.HasPrefix(addr, "unix://") {
network = "unix"
addr = strings.TrimPrefix(addr, "unix://")
}
return metrics.NewService(
network, addr,
metrics.PathOption(cfg.Path),
metrics.AutherOption(auther),
)
}

View File

@ -6,13 +6,19 @@ import (
_ "github.com/go-gost/x/connector/forward"
_ "github.com/go-gost/x/connector/http"
_ "github.com/go-gost/x/connector/http2"
_ "github.com/go-gost/x/connector/masque"
_ "github.com/go-gost/x/connector/relay"
_ "github.com/go-gost/x/connector/router"
_ "github.com/go-gost/x/connector/serial"
_ "github.com/go-gost/x/connector/sni"
_ "github.com/go-gost/x/connector/socks/v4"
_ "github.com/go-gost/x/connector/socks/v5"
_ "github.com/go-gost/x/connector/ss"
_ "github.com/go-gost/x/connector/ss/udp"
_ "github.com/go-gost/x/connector/sshd"
_ "github.com/go-gost/x/connector/tcp"
_ "github.com/go-gost/x/connector/tunnel"
_ "github.com/go-gost/x/connector/unix"
// Register dialers
_ "github.com/go-gost/x/dialer/direct"
@ -22,32 +28,44 @@ import (
_ "github.com/go-gost/x/dialer/http2"
_ "github.com/go-gost/x/dialer/http2/h2"
_ "github.com/go-gost/x/dialer/http3"
_ "github.com/go-gost/x/dialer/http3/masque"
_ "github.com/go-gost/x/dialer/http3/wt"
_ "github.com/go-gost/x/dialer/icmp"
_ "github.com/go-gost/x/dialer/kcp"
_ "github.com/go-gost/x/dialer/mtcp"
_ "github.com/go-gost/x/dialer/mtls"
_ "github.com/go-gost/x/dialer/mws"
_ "github.com/go-gost/x/dialer/obfs/http"
_ "github.com/go-gost/x/dialer/obfs/tls"
_ "github.com/go-gost/x/dialer/pht"
_ "github.com/go-gost/x/dialer/quic"
_ "github.com/go-gost/x/dialer/serial"
_ "github.com/go-gost/x/dialer/ssh"
_ "github.com/go-gost/x/dialer/sshd"
_ "github.com/go-gost/x/dialer/tcp"
_ "github.com/go-gost/x/dialer/tls"
_ "github.com/go-gost/x/dialer/udp"
_ "github.com/go-gost/x/dialer/unix"
_ "github.com/go-gost/x/dialer/utls"
_ "github.com/go-gost/x/dialer/ws"
// Register handlers
_ "github.com/go-gost/x/handler/api"
_ "github.com/go-gost/x/handler/auto"
_ "github.com/go-gost/x/handler/dns"
_ "github.com/go-gost/x/handler/file"
_ "github.com/go-gost/x/handler/forward/local"
_ "github.com/go-gost/x/handler/forward/remote"
_ "github.com/go-gost/x/handler/http"
_ "github.com/go-gost/x/handler/http2"
_ "github.com/go-gost/x/handler/http3"
_ "github.com/go-gost/x/handler/masque"
_ "github.com/go-gost/x/handler/metrics"
_ "github.com/go-gost/x/handler/redirect/tcp"
_ "github.com/go-gost/x/handler/redirect/udp"
_ "github.com/go-gost/x/handler/relay"
_ "github.com/go-gost/x/handler/router"
_ "github.com/go-gost/x/handler/serial"
_ "github.com/go-gost/x/handler/sni"
_ "github.com/go-gost/x/handler/socks/v4"
_ "github.com/go-gost/x/handler/socks/v5"
@ -56,6 +74,9 @@ import (
_ "github.com/go-gost/x/handler/sshd"
_ "github.com/go-gost/x/handler/tap"
_ "github.com/go-gost/x/handler/tun"
_ "github.com/go-gost/x/handler/tungo"
_ "github.com/go-gost/x/handler/tunnel"
_ "github.com/go-gost/x/handler/unix"
// Register listeners
_ "github.com/go-gost/x/listener/dns"
@ -66,8 +87,10 @@ import (
_ "github.com/go-gost/x/listener/http2/h2"
_ "github.com/go-gost/x/listener/http3"
_ "github.com/go-gost/x/listener/http3/h3"
_ "github.com/go-gost/x/listener/http3/wt"
_ "github.com/go-gost/x/listener/icmp"
_ "github.com/go-gost/x/listener/kcp"
_ "github.com/go-gost/x/listener/mtcp"
_ "github.com/go-gost/x/listener/mtls"
_ "github.com/go-gost/x/listener/mws"
_ "github.com/go-gost/x/listener/obfs/http"
@ -78,12 +101,17 @@ import (
_ "github.com/go-gost/x/listener/redirect/udp"
_ "github.com/go-gost/x/listener/rtcp"
_ "github.com/go-gost/x/listener/rudp"
_ "github.com/go-gost/x/listener/runix"
_ "github.com/go-gost/x/listener/serial"
_ "github.com/go-gost/x/listener/stdio"
_ "github.com/go-gost/x/listener/ssh"
_ "github.com/go-gost/x/listener/sshd"
_ "github.com/go-gost/x/listener/tap"
_ "github.com/go-gost/x/listener/tcp"
_ "github.com/go-gost/x/listener/tls"
_ "github.com/go-gost/x/listener/tun"
_ "github.com/go-gost/x/listener/tungo"
_ "github.com/go-gost/x/listener/udp"
_ "github.com/go-gost/x/listener/unix"
_ "github.com/go-gost/x/listener/ws"
)

View File

@ -1,5 +1,5 @@
package main
const (
version = "3.0.0-rc.2"
var (
version = "3.3.0"
)

View File

@ -1,4 +0,0 @@
version: "3.4"
services:
gost:
build: .

226
go.mod
View File

@ -1,108 +1,176 @@
module github.com/go-gost/gost
go 1.18
replace github.com/templexxx/cpu v0.0.7 => github.com/templexxx/cpu v0.0.10-0.20211111114238-98168dcec14a
go 1.26.3
require (
github.com/go-gost/core v0.0.0-20221221101823-36ed0eae2dec
github.com/go-gost/x v0.0.0-20221222094430-fb29d5c80e89
gopkg.in/natefinch/lumberjack.v2 v2.0.0
github.com/go-gost/core v0.5.3
github.com/go-gost/x v0.13.12
github.com/judwhite/go-svc v1.2.1
github.com/moby/moby/client v0.4.0
github.com/stretchr/testify v1.11.1
github.com/testcontainers/testcontainers-go v0.42.0
)
require (
github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da // indirect
dario.cat/mergo v1.0.2 // indirect
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c // indirect
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/beorn7/perks v1.0.1 // indirect
github.com/cespare/xxhash/v2 v2.1.2 // 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.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
github.com/containerd/platforms v0.2.1 // indirect
github.com/coreos/go-iptables v0.5.0 // indirect
github.com/cpuguy83/dockercfg v0.3.2 // indirect
github.com/danieljoos/wincred v1.2.0 // indirect
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
github.com/fsnotify/fsnotify v1.5.4 // indirect
github.com/gin-contrib/cors v1.3.1 // indirect
github.com/gin-contrib/sse v0.1.0 // indirect
github.com/gin-gonic/gin v1.7.7 // indirect
github.com/go-gost/gosocks4 v0.0.1 // indirect
github.com/go-gost/gosocks5 v0.3.1-0.20211109033403-d894d75b7f09 // indirect
github.com/go-gost/relay v0.1.1-0.20211123134818-8ef7fd81ffd7 // indirect
github.com/go-gost/tls-dissector v0.0.2-0.20220408131628-aac992c27451 // indirect
github.com/go-playground/locales v0.14.0 // indirect
github.com/go-playground/universal-translator v0.18.0 // indirect
github.com/go-playground/validator/v10 v10.10.1 // indirect
github.com/distribution/reference v0.6.0 // indirect
github.com/docker/go-connections v0.7.0 // indirect
github.com/docker/go-units v0.5.0 // indirect
github.com/dunglas/httpsfv v1.1.0 // indirect
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.12 // indirect
github.com/gin-contrib/cors v1.7.2 // 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.3 // 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.5.0 // indirect
github.com/go-gost/relay v0.6.2 // indirect
github.com/go-gost/tls-dissector v0.2.0 // 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.30.1 // indirect
github.com/go-redis/redis/v8 v8.11.5 // indirect
github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0 // indirect
github.com/gobwas/glob v0.2.3 // indirect
github.com/golang/mock v1.6.0 // indirect
github.com/golang/protobuf v1.5.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
github.com/google/gopacket v1.1.19 // indirect
github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38 // indirect
github.com/gorilla/websocket v1.5.0 // indirect
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/json-iterator/go v1.1.12 // indirect
github.com/klauspost/cpuid v1.3.1 // indirect
github.com/klauspost/reedsolomon v1.9.9 // indirect
github.com/leodido/go-urn v1.2.1 // indirect
github.com/lucas-clemente/quic-go v0.30.0 // indirect
github.com/magiconair/properties v1.8.6 // indirect
github.com/marten-seemann/qpack v0.3.0 // indirect
github.com/marten-seemann/qtls-go1-18 v0.1.3 // indirect
github.com/marten-seemann/qtls-go1-19 v0.1.1 // indirect
github.com/mattn/go-isatty v0.0.14 // indirect
github.com/matttproud/golang_protobuf_extensions v1.0.1 // indirect
github.com/miekg/dns v1.1.50 // indirect
github.com/mitchellh/mapstructure v1.4.3 // indirect
github.com/mmcloughlin/avo v0.0.0-20200803215136-443f81d77104 // indirect
github.com/klauspost/compress v1.19.0 // indirect
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
github.com/klauspost/reedsolomon v1.11.8 // 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/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/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/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/onsi/ginkgo/v2 v2.2.0 // 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 v1.9.4 // indirect
github.com/pion/dtls/v2 v2.1.5 // indirect
github.com/pion/logging v0.2.2 // indirect
github.com/pion/transport v0.14.1 // indirect
github.com/pion/udp v0.1.1 // indirect
github.com/pires/go-proxyproto v0.6.2 // indirect
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
github.com/pion/dtls/v3 v3.1.1 // 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/prometheus/client_golang v1.12.1 // indirect
github.com/prometheus/client_model v0.2.0 // indirect
github.com/prometheus/common v0.32.1 // indirect
github.com/prometheus/procfs v0.7.3 // indirect
github.com/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/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/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/shadowsocks/go-shadowsocks2 v0.1.5 // indirect
github.com/shadowsocks/shadowsocks-go v0.0.0-20200409064450-3e585ff90601 // indirect
github.com/sirupsen/logrus v1.8.1 // 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/sirupsen/logrus v1.9.4 // indirect
github.com/songgao/water v0.0.0-20200317203138-2b4b6d7c09d8 // indirect
github.com/spf13/afero v1.8.2 // indirect
github.com/spf13/cast v1.4.1 // indirect
github.com/spf13/jwalterweatherman v1.1.0 // indirect
github.com/sourcegraph/conc v0.3.0 // indirect
github.com/spf13/afero v1.11.0 // indirect
github.com/spf13/cast v1.6.0 // indirect
github.com/spf13/pflag v1.0.5 // indirect
github.com/spf13/viper v1.10.1 // indirect
github.com/subosito/gotenv v1.2.0 // indirect
github.com/templexxx/cpu v0.0.7 // indirect
github.com/templexxx/xorsimd v0.4.1 // indirect
github.com/tjfoc/gmsm v1.3.2 // indirect
github.com/ugorji/go/codec v1.2.7 // indirect
github.com/vishvananda/netlink v1.1.0 // indirect
github.com/vishvananda/netns v0.0.0-20191106174202-0a2b9b5464df // indirect
github.com/xtaci/kcp-go/v5 v5.6.1 // indirect
github.com/xtaci/smux v1.5.16 // 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/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.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/tcpraw v1.2.25 // indirect
github.com/yl2chen/cidranger v1.0.2 // indirect
golang.org/x/crypto v0.3.0 // indirect
golang.org/x/exp v0.0.0-20220827204233-334a2380cb91 // indirect
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4 // indirect
golang.org/x/net v0.2.0 // indirect
golang.org/x/sys v0.2.0 // indirect
golang.org/x/text v0.4.0 // indirect
golang.org/x/time v0.2.0 // indirect
golang.org/x/tools v0.1.12 // indirect
golang.zx2c4.com/wintun v0.0.0-20211104114900-415007cec224 // indirect
golang.zx2c4.com/wireguard v0.0.0-20220703234212-c31a7b1ab478 // indirect
google.golang.org/genproto v0.0.0-20220126215142-9970aeb2e350 // indirect
google.golang.org/grpc v1.51.0 // indirect
google.golang.org/protobuf v1.28.1 // indirect
gopkg.in/ini.v1 v1.66.4 // indirect
gopkg.in/yaml.v2 v2.4.0 // 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.5.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.uber.org/multierr v1.11.0 // indirect
golang.org/x/arch v0.22.0 // indirect
golang.org/x/crypto v0.53.0 // indirect
golang.org/x/exp v0.0.0-20241210194714-1829a127f884 // indirect
golang.org/x/mod v0.36.0 // indirect
golang.org/x/net v0.56.0 // indirect
golang.org/x/sync v0.21.0 // indirect
golang.org/x/sys v0.46.0 // indirect
golang.org/x/term v0.44.0 // indirect
golang.org/x/text v0.38.0 // indirect
golang.org/x/time v0.12.0 // indirect
golang.org/x/tools v0.45.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.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
gvisor.dev/gvisor v0.0.0-20250523182742-eede7a881b20 // indirect
)

1027
go.sum

File diff suppressed because it is too large Load Diff

102
install.sh 100755
View File

@ -0,0 +1,102 @@
#!/usr/bin/env bash
# Check Root User
# If you want to run as another user, please modify $EUID to be owned by this user
if [[ "$EUID" -ne '0' ]]; then
echo "$(tput setaf 1)Error: You must run this script as root!$(tput sgr0)"
exit 1
fi
# Set the desired GitHub repository
repo="go-gost/gost"
base_url="https://api.github.com/repos/$repo/releases"
# Function to download and install gost
install_gost() {
version=$1
# Detect the operating system
if [[ "$(uname)" == "Linux" ]]; then
os="linux"
elif [[ "$(uname)" == "Darwin" ]]; then
os="darwin"
elif [[ "$(uname)" == "MINGW"* ]]; then
os="windows"
else
echo "Unsupported operating system."
exit 1
fi
# Detect the CPU architecture
arch=$(uname -m)
case $arch in
x86_64)
cpu_arch="amd64"
;;
armv5*)
cpu_arch="armv5"
;;
armv6*)
cpu_arch="armv6"
;;
armv7*)
cpu_arch="armv7"
;;
aarch64|arm64)
cpu_arch="arm64"
;;
i686)
cpu_arch="386"
;;
mips64*)
cpu_arch="mips64"
;;
mips*)
cpu_arch="mips"
;;
mipsel*)
cpu_arch="mipsle"
;;
riscv64)
cpu_arch="riscv64"
;;
*)
echo "Unsupported CPU architecture."
exit 1
;;
esac
get_download_url="$base_url/tags/$version"
download_url=$(curl -s "$get_download_url" | awk -F'"' -v re=".*${os}.*${cpu_arch}.*" '/"browser_download_url":/ && $4 ~ re { print $4 }' | head -n 1)
# Download and install the binary
install_path="/usr/local/bin"
echo "Downloading and installing gost version $version..."
curl -fsSL "$download_url" | tar -xzC "$install_path" gost
chmod +x "$install_path/gost"
# Remove binary from macOS quarantine when installing for first time
[[ "$os" == "darwin" ]] && { xattr -d com.apple.quarantine "$install_path/gost" 2>&-; }
echo "gost installation completed!"
}
# Retrieve available versions from GitHub API
versions=$(curl -s "$base_url" | awk -F'"' '/"tag_name":/ {print $4}')
# Check if --install option provided
if [[ "$1" == "--install" ]]; then
# Install the latest version automatically
latest_version=$(echo "$versions" | head -n 1)
install_gost $latest_version
else
# Display available versions to the user
echo "Available gost versions:"
select version in $versions; do
if [[ -n $version ]]; then
install_gost $version
break
else
echo "Invalid choice! Please select a valid option."
fi
done
fi

View File

@ -0,0 +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

View File

@ -0,0 +1,276 @@
# 执行计划HTTP Handler E2E 测试 + utils.go 重构
## 背景
从 PLAN.md 的 Step 0 和 Step 1A 开始实施。HTTP proxy 是 gost 最基础的协议,目前 e2e 测试仅覆盖 shadowsocks 和 parallel selector。本计划先重构 utils.go 提取公共 helper然后用 helper 写 HTTP handler 测试套件,最后同步重构 shadowsocks_test.go。
## 变更文件清单
| 文件 | 操作 | 说明 |
|------|------|------|
| `tests/e2e/utils.go` | 修改 | 新增 `RunTCPCase` / `RunUDPCase` |
| `tests/e2e/http_test.go` | 新建 | HTTP proxy 测试套件 |
| `tests/e2e/testdata/http/server.yaml` | 新建 | 无 auth 的 HTTP proxy 服务器 |
| `tests/e2e/testdata/http/server_auth.yaml` | 新建 | 带 auth 的 HTTP proxy 服务器 |
| `tests/e2e/shadowsocks_test.go` | 修改 | 重构为使用 `RunTCPCase` / `RunUDPCase` |
---
## Step 1: 修改 `utils.go` — 提取 RunTCPCase / RunUDPCase
`shadowsocks_test.go``runTCPCase``runUDPCase` 提取为包级函数。关键差异:参数需要传入 echo container IP 和 echo container 本身(用于 dump logs
### RunTCPCase 签名与逻辑
```go
// RunTCPCase runs a full TCP proxy test case:
// start server → render client config → start client → curl assertion → cleanup.
func RunTCPCase(t *testing.T, ctx context.Context, networkName, echoIP string,
name, serverConfig, clientConfig string)
```
逻辑(从 shadowsocks_test.go:132-160 提取,一字不差):
1. 生成 serverAlias = `name + "-server"`
2. `RunGostContainerWithOptions(ctx, networkName, serverConfig, [serverAlias], ["8388/tcp"])`
3. `defer serverC.Terminate(ctx)`
4. `RenderConfig(clientConfig, {ServerAddr: serverAlias + ":8388"})`
5. `defer os.Remove(rendered)`
6. `RunGostContainerWithPorts(ctx, networkName, rendered, "8080/tcp")`
7. `defer clientC.Terminate(ctx)`
8. `clientC.Exec(ctx, ["curl", "-v", "-s", "-x", "http://127.0.0.1:8080", "http://<echoIP>:5678"])`
9. 断言 exitCode==0 且 body 包含 `"hello-gost"`,失败时 DumpLogs
**问题:端口硬编码**。shadowsocks 用 8388 作为服务端口,但其他协议可能用不同端口。需要参数化。
### 修正设计 — 参数化端口
```go
type TCPOptions struct {
ServerPort string // 容器内服务端口,默认 "8388/tcp"
ClientPort string // 客户端代理端口,默认 "8080/tcp"
}
func RunTCPCase(t *testing.T, ctx context.Context, networkName, echoIP string,
name, serverConfig, clientConfig string, opts ...TCPOptions)
```
`opts` 为空时使用默认值 `{ServerPort: "8388/tcp", ClientPort: "8080/tcp"}`。这样 shadowsocks 测试无需任何改动,而 HTTP 测试可传 `TCPOptions{ServerPort: "8080/tcp", ClientPort: "8080/tcp"}`
等一下 — 仔细看 shadowsocks 的 server 容器暴露的是 `8388/tcp`HTTP proxy 场景下只有一个 gost 容器(不做 server→client 链路),直接暴露 proxy 端口。
**重新思考HTTP proxy 不需要 server+client 两容器模式。**
HTTP proxy 测试是最简单的模式 — 只需一个 gost 容器运行 HTTP proxy然后容器内 curl 通过它访问 echo server。这和 parallel_selector_test.go 的模式一致。但为了统一框架和后续协议SOCKS5、relay 等都需要 server+client 模式),我们应该:
- HTTP 的"无 auth"用例:**单容器模式**(同 parallel_selector
- HTTP 的"带 auth"用例:**也可以单容器**,只需在 curl 加 `--proxy-user`
所以 HTTP proxy 不需要 server/client 分离,但 RunTCPCase helper 仍然服务于 SOCKS5/relay 等需要链路的协议。
### 最终 RunTCPCase 设计
```go
func RunTCPCase(t *testing.T, ctx context.Context, networkName, echoIP, name string,
serverConfig, clientConfig string, serverPort, clientPort string)
```
- `serverPort`: 服务端容器暴露端口,如 `"8388/tcp"`
- `clientPort`: 客户端容器暴露端口,如 `"8080/tcp"`
- 渲染模板时用 `ServerAddr: serverAlias + ":" + strings.TrimSuffix(serverPort, "/tcp")`
### RunUDPCase 设计
```go
func RunUDPCase(t *testing.T, ctx context.Context, networkName, name string,
serverConfig, clientConfig string, serverPort, clientPort string)
```
逻辑从 shadowsocks_test.go:76-126 提取。
---
## Step 2: 新建 `testdata/http/` 配置文件
### `testdata/http/server.yaml` — 无 auth 的 HTTP proxy
```yaml
services:
- name: http-proxy
addr: :8080
handler:
type: http
listener:
type: tcp
```
单容器即可测试gost 启动后在 :8080 提供 HTTP proxy 服务。
### `testdata/http/server_auth.yaml` — 带 auth 的 HTTP proxy
```yaml
services:
- name: http-proxy-auth
addr: :8080
handler:
type: http
auther: auther-0
listener:
type: tcp
authers:
- name: auther-0
auths:
- username: user
password: pass
```
---
## Step 3: 新建 `http_test.go`
### 套件结构
```go
type HTTPSuite struct {
suite.Suite
ctx context.Context
echoC testcontainers.Container
echoIP string
}
func (s *HTTPSuite) SetupSuite() // 启动 TCP echo
func (s *HTTPSuite) TearDownSuite() // 关闭 echo
func (s *HTTPSuite) TestHTTPProxy() // 无 auth
func (s *HTTPSuite) TestHTTPProxyAuth() // 带 auth
func TestHTTPSuite(t *testing.T) {
suite.Run(t, new(HTTPSuite))
}
```
### TestHTTPProxy — 单容器模式(同 parallel_selector
```go
func (s *HTTPSuite) TestHTTPProxy() {
gostC, err := RunGostContainerWithPorts(s.ctx, SharedNetworkName,
"testdata/http/server.yaml", "8080/tcp")
s.Require().NoError(err)
defer gostC.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 := gostC.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, "http-proxy logs", gostC)
}
s.Require().Equal(0, code)
s.Require().Contains(string(body), "hello-gost")
}
```
### TestHTTPProxyAuth — 带 auth 验证
```go
func (s *HTTPSuite) TestHTTPProxyAuth() {
gostC, err := RunGostContainerWithPorts(s.ctx, SharedNetworkName,
"testdata/http/server_auth.yaml", "8080/tcp")
s.Require().NoError(err)
defer gostC.Terminate(s.ctx)
// 测试1无 auth 应失败407
s.T().Run("no-auth-should-fail", func(t *testing.T) {
cmd := []string{"curl", "-s", "-o", "/dev/null", "-w", "%{http_code}",
"-x", "http://127.0.0.1:8080", fmt.Sprintf("http://%s:5678", s.echoIP)}
code, out, _ := gostC.Exec(s.ctx, cmd)
body, _ := io.ReadAll(out)
// curl exit code 非 0 或 HTTP status 是 407
// 注意gost HTTP proxy 在无 auth 时返回 407curl 不会自动重试
// 所以这里验证返回码是 407 或 curl 返回非零
httpStatus := strings.TrimSpace(string(body))
s.Assert().True(code != 0 || httpStatus == "407",
"expected auth failure, got status: %s, exit code: %d", httpStatus, code)
})
// 测试2正确 auth 应成功
s.T().Run("with-auth-should-succeed", func(t *testing.T) {
cmd := []string{"curl", "-v", "-s", "-x",
"http://user:pass@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)
if code != 0 || !strings.Contains(string(body), "hello-gost") {
DumpLogs(s.T(), s.ctx, "http-proxy-auth logs", gostC)
}
s.Require().Equal(0, code)
s.Require().Contains(string(body), "hello-gost")
})
}
```
### 注意事项
- HTTP proxy 是**单容器模式**,不需要 server/client 分离(不像 shadowsocks 需要解密/加密链路)
- auth 测试分两步:先验证无 auth 被拒407再验证正确 auth 成功
- curl 使用 `http://user:pass@host:port` 格式传递 proxy auth
---
## Step 4: 重构 `shadowsocks_test.go`
`runTCPCase``runUDPCase` 方法替换为调用 `utils.go` 中的 `RunTCPCase` / `RunUDPCase`
**Before**:
```go
func (s *ShadowsocksSuite) TestShadowsocksTCP() {
s.runTCPCase("aes256gcm", "testdata/shadowsocks/tcp_server_aes256gcm.yaml", "testdata/shadowsocks/tcp_client_aes256gcm.yaml")
s.runTCPCase("chacha20", "testdata/shadowsocks/tcp_server_chacha20.yaml", "testdata/shadowsocks/tcp_client_chacha20.yaml")
}
```
**After**:
```go
func (s *ShadowsocksSuite) TestShadowsocksTCP() {
RunTCPCase(s.T(), s.ctx, SharedNetworkName, s.echoIP, "aes256gcm",
"testdata/shadowsocks/tcp_server_aes256gcm.yaml",
"testdata/shadowsocks/tcp_client_aes256gcm.yaml",
"8388/tcp", "8080/tcp")
RunTCPCase(s.T(), s.ctx, SharedNetworkName, s.echoIP, "chacha20",
"testdata/shadowsocks/tcp_server_chacha20.yaml",
"testdata/shadowsocks/tcp_client_chacha20.yaml",
"8388/tcp", "8080/tcp")
}
```
删除 `runTCPCase``runUDPCase` 方法。UDP 测试同理。
---
## 实施顺序
1. **`utils.go`** — 添加 `RunTCPCase` + `RunUDPCase`(不删除任何现有函数)
2. **`testdata/http/`** — 创建 `server.yaml` + `server_auth.yaml`
3. **`http_test.go`** — 创建 HTTP 套件
4. **编译验证** — `cd gost && go build ./... && go vet ./tests/e2e/...`
5. **`shadowsocks_test.go`** — 重构为使用新 helper
6. **最终编译验证** — `go build ./... && go vet ./tests/e2e/...`
7. **运行测试** — `go test ./tests/e2e/ -v -run TestHTTPSuite -timeout 5m`
8. **回归测试** — `go test ./tests/e2e/ -v -run TestShadowsocksSuite -timeout 5m`
---
## 验证
```bash
cd /config/workspace/go-gost/gost
go build ./...
go vet ./tests/e2e/...
go test ./tests/e2e/ -v -run TestHTTPSuite -timeout 5m
go test ./tests/e2e/ -v -run TestShadowsocksSuite -timeout 5m
```

183
tests/e2e/PLAN.md 100644
View File

@ -0,0 +1,183 @@
# Plan: Expand E2E Test Suite
## Context
The e2e test framework (`tests/e2e/`) currently covers only **2 protocols** (Shadowsocks, parallel selector) out of **~30+ registered protocols** in gost. This plan systematically adds test coverage for all major protocols, following the existing Docker-based testcontainers pattern exactly.
**Key insight**: gost auto-generates self-signed TLS certs when none are provided (`x/config/parsing/tls.go` — `BuildDefaultTLSConfig`), and clients with default config skip cert verification. This means TLS-based protocols (ws→wss, h2c→h2, quic, etc.) can be tested **without mounting cert files**.
---
## Step 0: Refactor `utils.go` — Add Reusable Test Helpers
**File**: `tests/e2e/utils.go`
Extract the repeated patterns from `shadowsocks_test.go` into reusable helpers:
1. **`RunTCPCase(t, ctx, networkName, echoIP, name, serverConfig, clientConfig string)`** — Full lifecycle: start server → render client config → start client → curl assertion → cleanup. Extracts the logic from `ShadowsocksSuite.runTCPCase`.
2. **`RunUDPCase(t, ctx, networkName, name, serverConfig, clientConfig string)`** — Same for UDP: start server → render client → start client → dial UDP → retry loop → assertion → cleanup. Extracts from `runUDPCase`.
These eliminate ~40 lines of boilerplate per suite. Each suite's test methods become one-liners:
```go
func (s *SOCKS5Suite) TestSOCKS5TCP() {
RunTCPCase(s.T(), s.ctx, SharedNetworkName, s.echoIP, "socks5-tcp",
"testdata/socks5/tcp_server.yaml", "testdata/socks5/tcp_client.yaml")
}
```
---
## Step 1: Tier 1 — Core Protocols (Plain TCP, No Special Infrastructure)
Each suite follows the exact pattern: suite struct → `SetupSuite` (echo containers) → `TearDownSuite` → test methods calling `RunTCPCase`/`RunUDPCase` → `suite.Run()` entry point.
### 1A. HTTP Proxy Suite
- **Files**: `http_test.go`, `testdata/http/` (2 configs)
- **Configs**: `server.yaml` (handler `http`, listener `tcp`), `server_auth.yaml` (+ basic auth)
- **Tests**: `TestHTTPProxy`, `TestHTTPProxyAuth` (curl with `--proxy-user`)
- **Note**: Simplest test — single gost container acts as HTTP proxy
### 1B. SOCKS5 Suite
- **Files**: `socks5_test.go`, `testdata/socks5/` (4 configs)
- **Configs**: `tcp_server.yaml` (handler `socks5`), `tcp_client.yaml` (connector `socks5`, dialer `tcp`), + auth variants
- **Tests**: `TestSOCKS5TCP`, `TestSOCKS5TCPAuth`
### 1C. SOCKS4 Suite
- **Files**: `socks4_test.go`, `testdata/socks4/` (4 configs)
- **Configs**: `tcp_server.yaml` (handler `socks4`), `tcp_client.yaml` (connector `socks4`), + socks4a variants
- **Tests**: `TestSOCKS4TCP`, `TestSOCKS4aTCP`
### 1D. HTTP2 Cleartext Suite
- **Files**: `http2_test.go`, `testdata/http2/` (2 configs)
- **Configs**: `tcp_server.yaml` (handler `http2`, listener `http2`), `tcp_client.yaml` (connector `http2`, dialer `http2`)
- **Tests**: `TestHTTP2TCP`
### 1E. Relay Suite
- **Files**: `relay_test.go`, `testdata/relay/` (4 configs)
- **Configs**: `tcp_server.yaml` (handler `relay`, listener `tcp`), `tcp_client.yaml` (connector `relay`, dialer `tcp`), + auth variants
- **Tests**: `TestRelayTCP`, `TestRelayTCPAuth`
---
## Step 2: Tier 2 — TLS/Transport Protocols
Same pattern but using TLS listeners/dialers. No explicit cert files needed — gost auto-generates them.
### 2A. TLS Suite
- **Files**: `tls_test.go`, `testdata/tls/` (4 configs)
- **Tests**: `TestTLSHTTPProxy` (handler `http` + listener `tls`), `TestTLSSOCKS5` (handler `socks5` + listener `tls`)
### 2B. WebSocket Suite
- **Files**: `ws_test.go`, `testdata/ws/` (4 configs)
- **Tests**: `TestWSSOCKS5` (dialer `ws`), `TestWSSSOCKS5` (dialer `wss`)
### 2C. gRPC Suite
- **Files**: `grpc_test.go`, `testdata/grpc/` (2 configs)
- **Tests**: `TestGRPCSOCKS5` (listener `grpc`, metadata `insecure: true`)
### 2D. HTTP/2 TLS (h2) Suite
- **Files**: `h2_test.go`, `testdata/h2/` (2 configs)
- **Tests**: `TestH2HTTP2Proxy` (listener `h2`, dialer `h2`)
### 2E. QUIC Suite
- **Files**: `quic_test.go`, `testdata/quic/` (2 configs)
- **Tests**: `TestQUICHTTP` (listener `quic`, handler `http` — uses UDP ports)
### 2F. KCP Suite
- **Files**: `kcp_test.go`, `testdata/kcp/` (2 configs)
- **Tests**: `TestKCPSOCKS5` (listener `kcp`, handler `socks5` — uses UDP)
### 2G. Multiplex Variants (mws/mwss/mtls/mtcp)
- **Files**: `mux_test.go`, `testdata/mux/` (6 configs)
- **Tests**: `TestMWSSOCKS5`, `TestMWSSSOCKS5`, `TestMtlSSOCKS5`, `TestMtcpSOCKS5`
### 2H. Obfuscation (ohttp/otls)
- **Files**: `obfs_test.go`, `testdata/obfs/` (4 configs)
- **Tests**: `TestOHTTP`, `TestOTLS`
### 2I. PHT (HTTP Pipe)
- **Files**: `pht_test.go`, `testdata/pht/` (4 configs)
- **Tests**: `TestPHT`, `TestPHTS`
---
## Step 3: Tier 3 — Special Infrastructure (Deferred)
These require Docker capabilities (`NET_ADMIN`, `NET_RAW`), SSH key generation, or iptables setup. Implemented after Tiers 1-2 are stable.
| Protocol | Challenge | Approach |
|----------|-----------|----------|
| SSH/sshd | Host key generation | Generate SSH key at test time, mount into container |
| HTTP/3/WebTransport | QUIC+UDP, TLS | Uses UDP like QUIC suite; auto certs work |
| TUN/TAP | `CAP_NET_ADMIN`, `/dev/net/tun` | Privileged container + routing setup |
| Tungo/MASQUE | Same as TUN | Same approach |
| ICMP | `CAP_NET_RAW`, raw sockets | Privileged container |
| Redirect (red/redu) | iptables rules | Privileged + iptables setup in container |
| DTLS | UDP + TLS | Auto certs; UDP port mapping |
---
## Files to Create/Modify
### New files (per suite):
- `tests/e2e/<protocol>_test.go` — Suite struct + test methods
- `tests/e2e/testdata/<protocol>/` — Server + client YAML configs
### Modified files:
- `tests/e2e/utils.go` — Add `RunTCPCase`, `RunUDPCase` helpers
- `tests/e2e/shadowsocks_test.go` — Refactor to use new helpers (optional, reduces duplication)
- `tests/e2e/README.md` — Document new suites
### Reference files (read-only):
- `tests/e2e/shadowsocks_test.go` — Pattern template for all suites
- `tests/e2e/testdata/shadowsocks/tcp_server_aes256gcm.yaml` — Server config template
- `tests/e2e/testdata/shadowsocks/tcp_client_aes256gcm.yaml` — Client config template
---
## Config Pattern Reference
**Server** (all protocols):
```yaml
services:
- name: <proto>-server
addr: :<port>
handler:
type: <handler-type>
listener:
type: <listener-type>
```
**Client** (all protocols):
```yaml
services:
- name: http-proxy
addr: :8080
handler:
type: http
chain: <proto>-chain
listener:
type: tcp
chains:
- name: <proto>-chain
hops:
- name: <proto>-hop
nodes:
- name: <proto>-node
addr: {{.ServerAddr}}
connector:
type: <connector-type>
dialer:
type: <dialer-type>
```
---
## Verification
After each suite is implemented:
1. `cd gost && go build ./...` — Ensure compilation
2. `go vet ./tests/e2e/` — Static analysis
3. `go test ./tests/e2e/ -v -run TestXxxSuite -timeout 5m` — Run individual suite
4. `go test ./tests/e2e/ -v -timeout 10m` — Run all suites together

View File

@ -0,0 +1,53 @@
# End-to-End Tests
Integration tests that spin up real gost instances inside Docker containers and verify protocol behavior over the network.
## Prerequisites
- [Docker](https://docs.docker.com/get-docker/) (running daemon)
- Go toolchain (for compiling the gost binary under test)
## Running
From the repository root:
```bash
# Run all e2e tests
go test ./tests/e2e/ -v -timeout 10m
# Run a specific test suite
go test ./tests/e2e/ -v -run TestShadowsocksSuite -timeout 5m
go test ./tests/e2e/ -v -run TestParallelSelectorSuite -timeout 5m
# Use a pre-built gost binary (skips compilation)
go test ./tests/e2e/ -v -gost-bin /path/to/gost
```
## Architecture
```
tests/e2e/
├── Dockerfile # Shared base image (Alpine + curl, python3, etc.)
├── main_test.go # TestMain: compiles gost, creates Docker network
├── utils.go # Helpers: container lifecycle, config rendering
├── scripts/
│ ├── tcp_echo.py # HTTP echo server (responds with "hello-gost")
│ └── 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
```
### How it works
1. **TestMain** (`main_test.go`) compiles the gost binary from `../../cmd/gost` (unless `-gost-bin` is provided) and creates a shared Docker network for all containers.
2. Each test suite starts echo server containers (TCP/UDP), then launches separate gost containers for server and client roles.
3. Client configs use Go template syntax (`{{.ServerAddr}}`) so the server address is injected at runtime.
4. Tests verify end-to-end connectivity by sending traffic through the gost proxy chain and checking that the echo server responds correctly.
## Tips
- Increase `-timeout` for CI or slow networks. Container image builds on first run take extra time.
- Use `-gost-bin` to avoid recompiling when iterating on tests locally.
- Add `-v` to see container log output on failure.
- RunGostContainer will wait for exposedPorts automatically, but it's not reliable for udp ports. So, you should check the readiness of udp ports inside cases.

View File

@ -0,0 +1,360 @@
package e2e
import (
"context"
"encoding/base64"
"fmt"
"io"
"strings"
"testing"
"time"
"github.com/stretchr/testify/suite"
"github.com/testcontainers/testcontainers-go"
)
type DNSSuite struct {
suite.Suite
ctx context.Context
}
func (s *DNSSuite) SetupSuite() {
s.ctx = context.Background()
}
// dnsQuery sends a DNS query via the Python client and retries on failure.
// expected can be an IP string, "empty" (expect zero answer records), or "" (no check).
func (s *DNSSuite) dnsQuery(gostC testcontainers.Container, mode, host, port, qname, qtype, expected string) {
args := []string{"python3", "/scripts/dns_query.py", mode, host, port, qname, qtype}
if expected != "" {
args = append(args, expected)
}
for i := range 5 {
code, out, err := gostC.Exec(s.ctx, args)
if err != nil {
s.T().Logf("query attempt %d exec error: %v", i+1, err)
time.Sleep(time.Second)
continue
}
body, err := io.ReadAll(out)
if err != nil {
s.T().Logf("query attempt %d read error: %v", i+1, err)
time.Sleep(time.Second)
continue
}
output := string(body)
s.T().Logf("query attempt %d: %s", i+1, strings.TrimSpace(output))
if code == 0 {
return
}
time.Sleep(time.Second)
}
s.T().Fatalf("DNS query %s %s %s %s failed after 5 retries", mode, qname, qtype, host)
}
// dnsQueryOnce sends a single DNS query with no retries. Returns exit code and output.
func (s *DNSSuite) dnsQueryOnce(gostC testcontainers.Container, mode, host, port, qname, qtype string) (int, string) {
args := []string{"python3", "/scripts/dns_query.py", mode, host, port, qname, qtype}
code, out, err := gostC.Exec(s.ctx, args)
if err != nil {
return 1, fmt.Sprintf("exec error: %v", err)
}
body, _ := io.ReadAll(out)
return code, string(body)
}
// dnsQueryWithDelay calls dnsQuery with a small wait before the first attempt.
func (s *DNSSuite) dnsQueryWithDelay(gostC testcontainers.Container, mode, host, port, qname, qtype, expected string) {
time.Sleep(500 * time.Millisecond)
s.dnsQuery(gostC, mode, host, port, qname, qtype, expected)
}
// sendRaw sends raw data via base64+nc and returns the response.
func (s *DNSSuite) sendRaw(gostC testcontainers.Container, host, port, data string) string {
encoded := base64.StdEncoding.EncodeToString([]byte(data))
cmd := []string{"sh", "-c",
fmt.Sprintf("echo %s | base64 -d | nc -w 3 -u %s %s", encoded, host, port)}
_, out, _ := gostC.Exec(s.ctx, cmd)
b, _ := io.ReadAll(out)
return string(b)
}
// startDNSResponder starts the UDP DNS responder and returns the container.
func (s *DNSSuite) startDNSResponder() testcontainers.Container {
dnsC, err := RunDNSResponderContainer(s.ctx, SharedNetworkName)
s.Require().NoError(err)
return dnsC
}
// startGostWithQueryScript starts a gost container with dns_query.py mounted.
func (s *DNSSuite) startGostWithQueryScript(yamlPath, exposedPort string) testcontainers.Container {
gostC, err := RunGostContainerWithFiles(s.ctx, SharedNetworkName,
yamlPath,
[]testcontainers.ContainerFile{
{HostFilePath: "scripts/dns_query.py", ContainerFilePath: "/scripts/dns_query.py", FileMode: 0644},
},
exposedPort)
s.Require().NoError(err)
return gostC
}
// ---------------------------------------------------------------------------
// Upstream resolution
// ---------------------------------------------------------------------------
func (s *DNSSuite) TestDNSUpstream() {
dnsC := s.startDNSResponder()
defer dnsC.Terminate(s.ctx)
gostC := s.startGostWithQueryScript("testdata/dns/server_upstream.yaml", "1053/udp")
defer gostC.Terminate(s.ctx)
s.T().Run("a-record", func(t *testing.T) {
s.dnsQueryWithDelay(gostC, "udp", "127.0.0.1", "1053", "test.example.com", "A", "10.0.0.1")
})
s.T().Run("aaaa-record", func(t *testing.T) {
s.dnsQuery(gostC, "udp", "127.0.0.1", "1053", "example.com", "AAAA", "::1")
})
s.T().Run("second-a-record", func(t *testing.T) {
s.dnsQuery(gostC, "udp", "127.0.0.1", "1053", "test2.example.com", "A", "10.0.0.2")
})
}
// ---------------------------------------------------------------------------
// TCP mode
// ---------------------------------------------------------------------------
func (s *DNSSuite) TestDNSTCP() {
s.T().Log("start TCP DNS responder container...")
dnsC, err := RunTCPDNSResponderContainer(s.ctx, SharedNetworkName)
s.Require().NoError(err)
defer dnsC.Terminate(s.ctx)
gostC := s.startGostWithQueryScript("testdata/dns/server_tcp.yaml", "1053/tcp")
defer gostC.Terminate(s.ctx)
s.dnsQueryWithDelay(gostC, "tcp", "127.0.0.1", "1053", "test.example.com", "A", "10.0.0.1")
s.dnsQuery(gostC, "tcp", "127.0.0.1", "1053", "test2.example.com", "A", "10.0.0.2")
}
// ---------------------------------------------------------------------------
// Bypass rules
// ---------------------------------------------------------------------------
func (s *DNSSuite) TestDNSBypass() {
dnsC := s.startDNSResponder()
defer dnsC.Terminate(s.ctx)
gostC := s.startGostWithQueryScript("testdata/dns/server_bypass.yaml", "1053/udp")
defer gostC.Terminate(s.ctx)
s.T().Run("blocked-domain-empty-answer", func(t *testing.T) {
s.dnsQueryWithDelay(gostC, "udp", "127.0.0.1", "1053",
"test.example.com", "A", "empty")
})
s.T().Run("non-blocked-domain", func(t *testing.T) {
s.dnsQuery(gostC, "udp", "127.0.0.1", "1053",
"test2.example.com", "A", "10.0.0.2")
})
}
// ---------------------------------------------------------------------------
// Host mapper
// ---------------------------------------------------------------------------
// TestDNSHostMapper verifies DNS resolution via the host mapper before
// reaching the upstream exchanger.
//
// Config: server_hosts.yaml maps mapped.example.com → 10.0.0.100 and
// points the handler at an unreachable upstream (udp://127.0.0.1:1).
// The handler checks the host mapper before the exchange path, so
// mapped names resolve without needing any upstream DNS. Unmapped
// names must fall through to the exchanger, which fails — confirming
// the host-mapper path was the only reason mapped names worked.
func (s *DNSSuite) TestDNSHostMapper() {
gostC, err := RunGostContainerWithFiles(s.ctx, SharedNetworkName,
"testdata/dns/server_hosts.yaml",
[]testcontainers.ContainerFile{
{HostFilePath: "scripts/dns_query.py", ContainerFilePath: "/scripts/dns_query.py", FileMode: 0644},
},
"1053/udp")
s.Require().NoError(err)
defer gostC.Terminate(s.ctx)
s.T().Run("mapped-a-record", func(t *testing.T) {
// mapped.example.com → host mapper returns 10.0.0.100
// without contacting any upstream.
s.dnsQueryWithDelay(gostC, "udp", "127.0.0.1", "1053",
"mapped.example.com", "A", "10.0.0.100")
})
s.T().Run("unmapped-domain", func(t *testing.T) {
// test.example.com is not in the hosts mapping. The handler
// will fall through to the exchanger path, which fails because
// no upstream is configured. Query should return empty/NXDOMAIN.
code, output := s.dnsQueryOnce(gostC, "udp", "127.0.0.1", "1053",
"test.example.com", "A")
s.T().Logf("unmapped query: code=%d, %s", code, strings.TrimSpace(output))
// Non-zero exit indicates no valid response — expected since
// there's no working upstream.
s.Assert().NotEqual(0, code,
"unmapped domain should fail with no upstream")
})
}
// ---------------------------------------------------------------------------
// Exchange failure (unreachable upstream)
// ---------------------------------------------------------------------------
// TestDNSExchangeFailure verifies graceful handling when the upstream DNS
// exchanger is unreachable.
//
// Config: server_exchange_failure.yaml points to udp://127.0.0.1:1 which
// is unreachable. The handler must not crash or leak goroutines when the
// upstream exchange fails.
func (s *DNSSuite) TestDNSExchangeFailure() {
gostC, err := RunGostContainerWithFiles(s.ctx, SharedNetworkName,
"testdata/dns/server_exchange_failure.yaml",
[]testcontainers.ContainerFile{
{HostFilePath: "scripts/dns_query.py", ContainerFilePath: "/scripts/dns_query.py", FileMode: 0644},
},
"1053/udp")
s.Require().NoError(err)
defer gostC.Terminate(s.ctx)
s.T().Run("unreachable-upstream", func(t *testing.T) {
// Send query once — the exchange will fail (timeout / ICMP
// unreachable). No response is expected.
code, output := s.dnsQueryOnce(gostC, "udp", "127.0.0.1", "1053",
"test.example.com", "A")
s.T().Logf("exchange failure result: code=%d, %s",
code, strings.TrimSpace(output))
// Non-zero exit is expected since the exchange fails.
s.Assert().NotEqual(0, code,
"exchange failure should return non-zero exit")
})
s.T().Run("container-alive-after-failure", func(t *testing.T) {
// Verify the gost container is still running after the
// failed exchange — proves no crash or hang.
aliveCode, _, aliveErr := gostC.Exec(s.ctx, []string{"true"})
s.Require().NoError(aliveErr,
"container exec should succeed after exchange failure")
s.Require().Equal(0, aliveCode,
"gost container should be alive after exchange failure")
})
}
// ---------------------------------------------------------------------------
// Rate limiter
// ---------------------------------------------------------------------------
// TestDNSRateLimiter verifies that rate limiting configuration is accepted
// and the handler processes queries through the rate limiter path without
// crashing. Uses a generous global limit (1000/s) so queries pass.
func (s *DNSSuite) TestDNSRateLimiter() {
dnsC := s.startDNSResponder()
defer dnsC.Terminate(s.ctx)
gostC, err := RunGostContainerWithFiles(s.ctx, SharedNetworkName,
"testdata/dns/server_rlimiter.yaml",
[]testcontainers.ContainerFile{
{HostFilePath: "scripts/dns_query.py", ContainerFilePath: "/scripts/dns_query.py", FileMode: 0644},
},
"1053/udp")
s.Require().NoError(err)
defer gostC.Terminate(s.ctx)
s.T().Run("query-with-limiter", func(t *testing.T) {
s.dnsQueryWithDelay(gostC, "udp", "127.0.0.1", "1053",
"test.example.com", "A", "10.0.0.1")
})
s.T().Run("second-query-with-limiter", func(t *testing.T) {
s.dnsQuery(gostC, "udp", "127.0.0.1", "1053",
"test2.example.com", "A", "10.0.0.2")
})
}
// ---------------------------------------------------------------------------
// Invalid query
// ---------------------------------------------------------------------------
// TestDNSInvalidQuery verifies the DNS listener gracefully handles malformed
// DNS messages. Sends garbage bytes and checks the gost container is
// unaffected (no crash, no hang).
func (s *DNSSuite) TestDNSInvalidQuery() {
gostC := s.startGostWithQueryScript("testdata/dns/server_upstream.yaml", "1053/udp")
defer gostC.Terminate(s.ctx)
s.T().Run("send-garbage-bytes", func(t *testing.T) {
// Send 5 bytes of garbage via UDP. The miekg/dns server
// will fail to parse them and discard the packet.
s.sendRaw(gostC, "127.0.0.1", "1053", "garbage!")
})
s.T().Run("container-alive-after-garbage", func(t *testing.T) {
// Verify the gost container is still alive after receiving
// invalid data.
aliveCode, _, aliveErr := gostC.Exec(s.ctx, []string{"true"})
s.Require().NoError(aliveErr,
"container exec should succeed after invalid query")
s.Require().Equal(0, aliveCode,
"gost container should be alive after invalid query")
})
}
// ---------------------------------------------------------------------------
// DNS over TLS
// ---------------------------------------------------------------------------
// TestDNSTLS verifies the DNS listener in TLS mode (DNS over TLS).
// Starts the UDP DNS responder (unencrypted), a gost DNS server with
// listener mode: tls, and queries through the TLS endpoint.
func (s *DNSSuite) TestDNSTLS() {
dnsC := s.startDNSResponder()
defer dnsC.Terminate(s.ctx)
gostC, err := RunGostContainerWithFiles(s.ctx, SharedNetworkName,
"testdata/dns/server_tls.yaml",
[]testcontainers.ContainerFile{
{HostFilePath: "scripts/dns_tls_query.py", ContainerFilePath: "/scripts/dns_tls_query.py", FileMode: 0644},
},
"1053/tcp")
s.Require().NoError(err)
defer gostC.Terminate(s.ctx)
// Query via TLS
args := []string{"python3", "/scripts/dns_tls_query.py",
"127.0.0.1", "1053", "test.example.com", "A", "10.0.0.1"}
for i := range 5 {
code, out, err := gostC.Exec(s.ctx, args)
if err != nil {
s.T().Logf("tls query attempt %d exec error: %v", i+1, err)
time.Sleep(time.Second)
continue
}
body, err := io.ReadAll(out)
if err != nil {
s.T().Logf("tls query attempt %d read error: %v", i+1, err)
time.Sleep(time.Second)
continue
}
output := string(body)
s.T().Logf("tls query attempt %d: %s", i+1, strings.TrimSpace(output))
if code == 0 {
return
}
time.Sleep(time.Second)
}
s.T().Fatal("TLS query failed after 5 retries")
}
func TestDNSSuite(t *testing.T) {
suite.Run(t, new(DNSSuite))
}

View File

@ -0,0 +1,194 @@
package e2e
import (
"context"
"io"
"strings"
"testing"
"github.com/stretchr/testify/suite"
"github.com/testcontainers/testcontainers-go"
)
type FileSuite struct {
suite.Suite
ctx context.Context
}
func (s *FileSuite) SetupSuite() {
s.ctx = context.Background()
}
func (s *FileSuite) TearDownSuite() {}
// TestFileGetExisting verifies that GET on an existing file returns
// the file content with HTTP 200. Covers the basic file serving path:
// file handler → http.FileServer → file read.
func (s *FileSuite) TestFileGetExisting() {
gostC, err := RunGostContainerWithFiles(s.ctx, SharedNetworkName,
"testdata/file/server.yaml",
[]testcontainers.ContainerFile{
{HostFilePath: "testdata/file/hello.txt", ContainerFilePath: "/srv/files/hello.txt", FileMode: 0644},
},
"8080/tcp")
s.Require().NoError(err)
defer gostC.Terminate(s.ctx)
cmd := []string{"curl", "-v", "-s", "http://127.0.0.1:8080/hello.txt"}
code, out, err := gostC.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-file") {
DumpLogs(s.T(), s.ctx, "file-server logs", gostC)
}
s.Require().Equal(0, code)
s.Require().Contains(string(body), "hello-gost-file")
}
// TestFileGetNotFound verifies that GET on a nonexistent file returns
// 404 Not Found.
func (s *FileSuite) TestFileGetNotFound() {
gostC, err := RunGostContainerWithFiles(s.ctx, SharedNetworkName,
"testdata/file/server.yaml",
[]testcontainers.ContainerFile{
{HostFilePath: "testdata/file/hello.txt", ContainerFilePath: "/srv/files/hello.txt", FileMode: 0644},
},
"8080/tcp")
s.Require().NoError(err)
defer gostC.Terminate(s.ctx)
cmd := []string{"curl", "-v", "-s", "-o", "/dev/null", "-w", "%{http_code}",
"http://127.0.0.1:8080/nonexistent.txt"}
_, out, err := gostC.Exec(s.ctx, cmd)
s.Require().NoError(err)
body, _ := io.ReadAll(out)
s.Assert().Contains(string(body), "404")
}
// TestFileGetIndexHtml verifies that GET / serves index.html when
// present in the served directory. Covers the default index document
// behavior of http.FileServer.
func (s *FileSuite) TestFileGetIndexHtml() {
gostC, err := RunGostContainerWithFiles(s.ctx, SharedNetworkName,
"testdata/file/server.yaml",
[]testcontainers.ContainerFile{
{HostFilePath: "testdata/file/index.html", ContainerFilePath: "/srv/files/index.html", FileMode: 0644},
},
"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 := gostC.Exec(s.ctx, cmd)
s.Require().NoError(err)
body, err := io.ReadAll(out)
s.Require().NoError(err)
if code != 0 || !strings.Contains(string(body), "gost file index") {
DumpLogs(s.T(), s.ctx, "file-server index logs", gostC)
}
s.Require().Equal(0, code)
s.Require().Contains(string(body), "gost file index")
}
// TestFilePutUpload verifies that PUT uploads a file when file.put is
// enabled. Uploads a file and then reads it back via GET to confirm
// the content was persisted.
func (s *FileSuite) TestFilePutUpload() {
gostC, err := RunGostContainerWithFiles(s.ctx, SharedNetworkName,
"testdata/file/server_put.yaml",
[]testcontainers.ContainerFile{
{HostFilePath: "testdata/file/.empty", ContainerFilePath: "/srv/files/.empty", FileMode: 0644},
},
"8080/tcp")
s.Require().NoError(err)
defer gostC.Terminate(s.ctx)
// Create source content inside the container.
_, _, err = gostC.Exec(s.ctx, []string{
"sh", "-c", "printf 'uploaded-content' > /tmp/upload_src.txt",
})
s.Require().NoError(err)
// Upload via PUT.
_, out, err := gostC.Exec(s.ctx, []string{
"curl", "-v", "-s", "-T", "/tmp/upload_src.txt",
"http://127.0.0.1:8080/uploaded.txt",
})
s.Require().NoError(err)
putBody, _ := io.ReadAll(out)
s.T().Logf("PUT response:\n%s", string(putBody))
// Read back the uploaded file via GET.
_, out2, err := gostC.Exec(s.ctx, []string{
"curl", "-v", "-s",
"http://127.0.0.1:8080/uploaded.txt",
})
s.Require().NoError(err)
body, _ := io.ReadAll(out2)
s.Assert().Contains(string(body), "uploaded-content")
}
// TestFilePutNoPermission verifies that PUT returns 405 Method Not
// Allowed when file.put is not enabled (default: false).
func (s *FileSuite) TestFilePutNoPermission() {
gostC, err := RunGostContainerWithFiles(s.ctx, SharedNetworkName,
"testdata/file/server.yaml",
[]testcontainers.ContainerFile{
{HostFilePath: "testdata/file/.empty", ContainerFilePath: "/srv/files/.empty", FileMode: 0644},
},
"8080/tcp")
s.Require().NoError(err)
defer gostC.Terminate(s.ctx)
cmd := []string{"curl", "-v", "-s", "-o", "/dev/null", "-w", "%{http_code}",
"-T", "/dev/null", "http://127.0.0.1:8080/test.txt"}
_, out, err := gostC.Exec(s.ctx, cmd)
s.Require().NoError(err)
body, _ := io.ReadAll(out)
s.Assert().Contains(string(body), "405")
}
// TestFileAuth verifies authentication on the file handler.
// Without credentials the handler returns 401 Unauthorized,
// with valid credentials it serves the file.
func (s *FileSuite) TestFileAuth() {
gostC, err := RunGostContainerWithFiles(s.ctx, SharedNetworkName,
"testdata/file/server_auth.yaml",
[]testcontainers.ContainerFile{
{HostFilePath: "testdata/file/hello.txt", ContainerFilePath: "/srv/files/hello.txt", FileMode: 0644},
},
"8080/tcp")
s.Require().NoError(err)
defer gostC.Terminate(s.ctx)
s.T().Run("no-auth-401", func(t *testing.T) {
cmd := []string{"curl", "-v", "-s", "-o", "/dev/null", "-w", "%{http_code}",
"http://127.0.0.1:8080/hello.txt"}
_, out, _ := gostC.Exec(s.ctx, cmd)
body, _ := io.ReadAll(out)
s.Assert().Contains(string(body), "401")
})
s.T().Run("with-auth-success", func(t *testing.T) {
cmd := []string{"curl", "-v", "-s", "-u", "user:pass",
"http://127.0.0.1:8080/hello.txt"}
code, out, err := gostC.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-file") {
DumpLogs(s.T(), s.ctx, "file-server auth logs", gostC)
}
s.Require().Equal(0, code)
s.Require().Contains(string(body), "hello-gost-file")
})
}
func TestFileSuite(t *testing.T) {
suite.Run(t, new(FileSuite))
}

View File

@ -0,0 +1,291 @@
package e2e
import (
"context"
"encoding/base64"
"fmt"
"io"
"strings"
"testing"
"github.com/stretchr/testify/suite"
"github.com/testcontainers/testcontainers-go"
)
type ForwardSuite struct {
suite.Suite
ctx context.Context
echoC testcontainers.Container
echoIP string
udpC testcontainers.Container
}
func (s *ForwardSuite) 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
s.T().Logf("start udp echo container...")
udpC, err := RunUDPEchoContainer(s.ctx, SharedNetworkName)
s.Require().NoError(err)
s.udpC = udpC
}
// sendRaw sends a raw HTTP request via netcat and returns the response.
// Uses base64 to avoid shell quoting issues with CRLF bytes.
func (s *ForwardSuite) sendRaw(gostC testcontainers.Container, host, port, data string) string {
encoded := base64.StdEncoding.EncodeToString([]byte(data))
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)
}
func (s *ForwardSuite) TearDownSuite() {
if s.echoC != nil {
s.echoC.Terminate(s.ctx)
}
if s.udpC != nil {
s.udpC.Terminate(s.ctx)
}
}
// TestTCPForward verifies basic TCP forward handler (handler type: tcp).
// The forward handler pipes raw TCP connections to the configured forwarder
// node (tcp-echo:5678). curl connects directly to the handler port and sends
// an HTTP request, expecting the echo server's "hello-gost" response.
func (s *ForwardSuite) TestTCPForward() {
gostC, err := RunGostContainerWithPorts(s.ctx, SharedNetworkName,
"testdata/forward/server.yaml", "8000/tcp")
s.Require().NoError(err)
defer gostC.Terminate(s.ctx)
// curl directly to the handler port (not via -x proxy flag).
// The TCP forward handler pipes our connection to tcp-echo:5678.
cmd := []string{"curl", "-v", "-s", "http://127.0.0.1:8000/"}
code, out, err := gostC.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, "tcp-forward logs", gostC)
}
s.Require().Equal(0, code)
s.Require().Contains(string(body), "hello-gost")
}
// TestForwardAlias verifies that the "forward" handler type (alias for "tcp")
// works identically to the "tcp" handler type.
func (s *ForwardSuite) TestForwardAlias() {
gostC, err := RunGostContainerWithPorts(s.ctx, SharedNetworkName,
"testdata/forward/server.yaml", "8000/tcp")
s.Require().NoError(err)
defer gostC.Terminate(s.ctx)
cmd := []string{"curl", "-v", "-s", "http://127.0.0.1:8000/"}
code, out, err := gostC.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, "forward-alias logs", gostC)
}
s.Require().Equal(0, code)
s.Require().Contains(string(body), "hello-gost")
}
// TestTCPForwardSniffing verifies TCP forward handler with sniffing enabled.
// When sniffing is enabled and the connection starts with HTTP data, the handler
// detects the protocol via sniffing.Sniff and delegates to the HTTP sniffer
// for protocol-aware forwarding. The result is the same as raw forwarding:
// "hello-gost" from the echo server.
func (s *ForwardSuite) TestTCPForwardSniffing() {
gostC, err := RunGostContainerWithPorts(s.ctx, SharedNetworkName,
"testdata/forward/server_sniffing.yaml", "8000/tcp")
s.Require().NoError(err)
defer gostC.Terminate(s.ctx)
// curl directly — sniffing detects HTTP and handles via sniffer.
cmd := []string{"curl", "-v", "-s", "http://127.0.0.1:8000/"}
code, out, err := gostC.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, "tcp-forward-sniffing logs", gostC)
}
s.Require().Equal(0, code)
s.Require().Contains(string(body), "hello-gost")
}
// TestTCPForwardRaw verifies raw TCP forwarding by sending an HTTP request
// via netcat through the forward handler. This tests the handleRawForwarding
// code path (no sniffing), proving that raw bytes are piped through correctly.
func (s *ForwardSuite) TestTCPForwardRaw() {
gostC, err := RunGostContainerWithPorts(s.ctx, SharedNetworkName,
"testdata/forward/server.yaml", "8000/tcp")
s.Require().NoError(err)
defer gostC.Terminate(s.ctx)
// Send raw HTTP request via nc (not curl) to exercise the raw pipe path.
resp := s.sendRaw(gostC, "127.0.0.1", "8000",
"GET / HTTP/1.0\r\nHost: tcp-echo\r\n\r\n")
s.Assert().Contains(resp, "hello-gost",
"raw request through TCP forward should reach echo server")
}
// TestTCPForwardIdleTimeout verifies that idleTimeout closes the pipe after
// a period of inactivity. The forward handler's xnet.Pipe uses idleTimeout
// as a read deadline on the upstream connection — if no data flows for
// that duration, the pipe closes both directions.
func (s *ForwardSuite) TestTCPForwardIdleTimeout() {
gostC, err := RunGostContainerWithFiles(s.ctx, SharedNetworkName,
"testdata/forward/server_idle_timeout.yaml",
[]testcontainers.ContainerFile{
{HostFilePath: "scripts/tcp_idle_timeout.py", ContainerFilePath: "/scripts/tcp_idle_timeout.py", FileMode: 0644},
},
"8000/tcp")
s.Require().NoError(err)
defer gostC.Terminate(s.ctx)
// The Python script:
// 1. Connects to gost TCP forward port
// 2. Sends HTTP GET → expects "hello-gost" confirm pipe is alive
// 3. Waits > idleTimeout (3s + 2s buffer)
// 4. Sends more data — expects connection to be closed
code, out, err := gostC.Exec(s.ctx, []string{
"python3", "/scripts/tcp_idle_timeout.py",
"127.0.0.1", "8000", "3",
})
output, _ := io.ReadAll(out)
s.T().Logf("idle timeout output:\n%s", string(output))
if code != 0 {
DumpLogs(s.T(), s.ctx, "tcp-idle-timeout logs", gostC)
}
s.Require().Equal(0, code, "idle timeout test script should exit 0")
}
// TestUDPForward verifies basic UDP forwarding (handler: udp, listener: udp).
// The handler uses handleRawDatagram via the stateful UDP listener's
// per-client session conns (which implement net.PacketConn).
// A Python script inside the gost container sends a UDP datagram through
// the forward handler and verifies the echo response from udp-echo:5679.
func (s *ForwardSuite) TestUDPForward() {
gostC, err := RunGostContainerWithFiles(s.ctx, SharedNetworkName,
"testdata/forward/server_udp.yaml",
[]testcontainers.ContainerFile{
{HostFilePath: "scripts/udp_forward_test.py", ContainerFilePath: "/scripts/udp_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_forward_test.py",
"127.0.0.1", "9000",
})
output, _ := io.ReadAll(out)
s.T().Logf("udp forward output:\n%s", string(output))
if code != 0 {
DumpLogs(s.T(), s.ctx, "udp-forward logs", gostC)
}
s.Require().Equal(0, code, "udp forward test script should exit 0")
}
// TestUDPForwardStateless verifies UDP forwarding with stateless mode.
// Both listener and handler use stateless: true, so each datagram is a
// single request-response cycle with no per-client session tracking.
func (s *ForwardSuite) TestUDPForwardStateless() {
gostC, err := RunGostContainerWithFiles(s.ctx, SharedNetworkName,
"testdata/forward/server_udp_stateless.yaml",
[]testcontainers.ContainerFile{
{HostFilePath: "scripts/udp_forward_test.py", ContainerFilePath: "/scripts/udp_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_forward_test.py",
"127.0.0.1", "9000",
})
output, _ := io.ReadAll(out)
s.T().Logf("udp forward stateless output:\n%s", string(output))
if code != 0 {
DumpLogs(s.T(), s.ctx, "udp-forward-stateless logs", gostC)
}
s.Require().Equal(0, code, "udp forward stateless test script should exit 0")
}
// TestTCPForwardSniffingBypass verifies that when sniffing is enabled and a
// bypass rule blocks the target, the HTTP sniffer returns 403 Forbidden.
// The forward handler delegates to the HTTP sniffer (via handleSniffedProtocol),
// and the sniffer's resolveHTTPNode checks h.options.Bypass and returns 403
// when the destination is matched.
func (s *ForwardSuite) TestTCPForwardSniffingBypass() {
gostC, err := RunGostContainerWithPorts(s.ctx, SharedNetworkName,
"testdata/forward/server_bypass_sniffing.yaml", "8000/tcp")
s.Require().NoError(err)
defer gostC.Terminate(s.ctx)
// curl connects directly; sniffing detects HTTP. The sniffer bypass
// check matches 0.0.0.0/0 and returns 403 Forbidden.
cmd := []string{"curl", "-v", "-s", "-D", "-", "-o", "/dev/null",
"http://127.0.0.1:8000/"}
_, out, err := gostC.Exec(s.ctx, cmd)
s.Require().NoError(err)
body, _ := io.ReadAll(out)
output := string(body)
s.Assert().Contains(output, "403",
"bypass should return 403 Forbidden from sniffer")
}
// TestTCPForwardMultiNodeProtocol verifies that sniffing + protocol-filtered
// forwarder nodes correctly routes an HTTP request to the node with matching
// protocol (http), rather than the one with protocol: tls.
//
// Config has two nodes:
// - echo-http (protocol: http) → tcp-echo:5678 (works, returns "hello-gost")
// - echo-tls (protocol: tls) → tcp-echo:1 (closed port, would fail)
//
// With sniffing enabled, an HTTP request is detected as protocol "http",
// Select("http") filters to only the echo-http node. If protocol filtering
// fails and the tls node is selected instead, the connection to port 1
// fails and the test fails.
func (s *ForwardSuite) TestTCPForwardMultiNodeProtocol() {
gostC, err := RunGostContainerWithPorts(s.ctx, SharedNetworkName,
"testdata/forward/server_multi_node.yaml", "8000/tcp")
s.Require().NoError(err)
defer gostC.Terminate(s.ctx)
// curl sends an HTTP request → sniffed as "http" → Select("http")
// filters to echo-http (protocol: http) → pipes to tcp-echo:5678
cmd := []string{"curl", "-v", "-s", "http://127.0.0.1:8000/"}
code, out, err := gostC.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, "tcp-forward-multi-node logs", gostC)
}
s.Require().Equal(0, code)
s.Require().Contains(string(body), "hello-gost",
"HTTP request should route to the http-protocol node via protocol filtering")
}
func TestForwardSuite(t *testing.T) {
suite.Run(t, new(ForwardSuite))
}

View File

@ -0,0 +1,216 @@
package e2e
import (
"context"
"fmt"
"io"
"os"
"strings"
"sync"
"testing"
"github.com/stretchr/testify/suite"
"github.com/testcontainers/testcontainers-go"
)
// HTTP2Suite covers the HTTP/2 proxy handler (handler type: http2) together
// with the HTTP/2 listener (listener type: http2). The http2 listener wraps
// the underlying TCP listener with TLS and configures an h2 server, so the
// handler is always reached over HTTP/2 frames.
//
// Because Alpine's curl cannot be relied on to speak HTTP/2 proxy to the
// server directly, the suite uses the canonical GOST chaining pattern: a
// client container exposes a plain HTTP proxy which forwards through an
// http2 connector + http2 dialer to the http2 server. This exercises the
// h2 listener, the h2 handler, and the h2 connector/dialer together.
type HTTP2Suite struct {
suite.Suite
ctx context.Context
echoC testcontainers.Container
echoIP string
}
func (s *HTTP2Suite) 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 *HTTP2Suite) TearDownSuite() {
if s.echoC != nil {
s.echoC.Terminate(s.ctx)
}
}
// startChain brings up an http2 server (alias "h2-server") and an http client
// container that chains to it through connector/dialer type http2. The client
// exposes port 8080 for curl. The rendered client config is cleaned up by the
// caller-supplied template path resolving {{.ServerAddr}} to h2-server:8443.
func (s *HTTP2Suite) startChain(serverYAML, clientTmpl string) (testcontainers.Container, testcontainers.Container) {
s.T().Helper()
serverC, err := RunGostContainerWithOptions(s.ctx, SharedNetworkName,
serverYAML, []string{"h2-server"}, []string{"8443/tcp"})
s.Require().NoError(err)
rendered, err := RenderConfig(clientTmpl, ConfigData{ServerAddr: "h2-server:8443"})
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
// process exit code plus the captured body.
func (s *HTTP2Suite) curlEcho(clientC testcontainers.Container) (int, string) {
s.T().Helper()
cmd := []string{"curl", "-v", "-s", "-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)
}
func (s *HTTP2Suite) dump(label string, cs ...testcontainers.Container) {
for _, c := range cs {
DumpLogs(s.T(), s.ctx, label, c)
}
}
// TestHTTP2ForwardProxy verifies the core HTTP/2 proxy path: a plain HTTP
// request through the client proxy is tunneled to the http2 server via an h2
// CONNECT stream and reaches the echo backend.
//
// Covers:
// - listener type: http2 (TLS + h2 server)
// - handler type: http2 (CONNECT tunnel + bidirectional pipe)
// - connector type: http2, dialer type: http2 (h2 client)
func (s *HTTP2Suite) TestHTTP2ForwardProxy() {
serverC, clientC := s.startChain("testdata/http2/server.yaml", "testdata/http2/client.yaml")
defer serverC.Terminate(s.ctx)
defer clientC.Terminate(s.ctx)
code, body := s.curlEcho(clientC)
if code != 0 || !strings.Contains(body, "hello-gost") {
s.dump("http2-forward logs", clientC, serverC)
}
s.Require().Equal(0, code)
s.Require().Contains(body, "hello-gost")
}
// TestHTTP2Auth verifies proxy authentication over the h2 tunnel, plus
// authBasicRealm and hash metadata parsing on the http2 handler.
//
// - with auth: CONNECT succeeds → echo body returned
// - without auth: server rejects CONNECT (407) → client cannot reach echo
func (s *HTTP2Suite) TestHTTP2Auth() {
s.T().Run("with-auth-success", func(t *testing.T) {
serverC, clientC := s.startChain("testdata/http2/server_auth.yaml", "testdata/http2/client_auth.yaml")
defer serverC.Terminate(s.ctx)
defer clientC.Terminate(s.ctx)
code, body := s.curlEcho(clientC)
if code != 0 || !strings.Contains(body, "hello-gost") {
s.dump("http2-auth-success logs", clientC, serverC)
}
s.Require().Equal(0, code)
s.Require().Contains(body, "hello-gost")
})
s.T().Run("no-auth-fails", func(t *testing.T) {
// Server requires auth; client sends none → the http2 CONNECT is
// rejected with 407, so curl cannot obtain the echo body.
serverC, clientC := s.startChain("testdata/http2/server_auth.yaml", "testdata/http2/client.yaml")
defer serverC.Terminate(s.ctx)
defer clientC.Terminate(s.ctx)
code, body := s.curlEcho(clientC)
if strings.Contains(body, "hello-gost") {
s.dump("http2-auth-noauth logs", clientC, serverC)
}
s.Require().Zero(code,
"curl should still receive an HTTP response from the local proxy")
s.Require().NotContains(body, "hello-gost",
"without auth the echo backend must be unreachable")
})
}
// TestHTTP2Bypass verifies that a bypass matcher on the http2 handler blocks
// the CONNECT target after authentication succeeds. The server returns 403, so
// the client cannot reach the echo backend.
func (s *HTTP2Suite) TestHTTP2Bypass() {
serverC, clientC := s.startChain("testdata/http2/server_bypass.yaml", "testdata/http2/client_auth.yaml")
defer serverC.Terminate(s.ctx)
defer clientC.Terminate(s.ctx)
code, body := s.curlEcho(clientC)
if strings.Contains(body, "hello-gost") {
s.dump("http2-bypass logs", clientC, serverC)
}
s.Require().Zero(code,
"curl should still receive an HTTP response from the local proxy")
s.Require().NotContains(body, "hello-gost",
"bypass should prevent reaching the echo backend")
}
// TestHTTP2ProbeResist verifies that probeResist, header and authBasicRealm
// metadata parse cleanly on the http2 handler and do not break the success
// path. With correct credentials the request still reaches the echo backend.
func (s *HTTP2Suite) TestHTTP2ProbeResist() {
serverC, clientC := s.startChain("testdata/http2/server_proberesist.yaml", "testdata/http2/client_auth.yaml")
defer serverC.Terminate(s.ctx)
defer clientC.Terminate(s.ctx)
code, body := s.curlEcho(clientC)
if code != 0 || !strings.Contains(body, "hello-gost") {
s.dump("http2-proberesist logs", clientC, serverC)
}
s.Require().Equal(0, code)
s.Require().Contains(body, "hello-gost")
}
// TestHTTP2Multiplex verifies HTTP/2 stream multiplexing: many concurrent
// requests through the client proxy are tunneled as parallel h2 streams over a
// single underlying connection to the http2 server. All requests must reach
// the echo backend.
func (s *HTTP2Suite) TestHTTP2Multiplex() {
serverC, clientC := s.startChain("testdata/http2/server.yaml", "testdata/http2/client.yaml")
defer serverC.Terminate(s.ctx)
defer clientC.Terminate(s.ctx)
const n = 6
var wg sync.WaitGroup
wg.Add(n)
failed := make(chan string, n)
for i := 0; i < n; i++ {
go func() {
defer wg.Done()
_, body := s.curlEcho(clientC)
if !strings.Contains(body, "hello-gost") {
failed <- "missing echo body"
}
}()
}
wg.Wait()
close(failed)
if len(failed) > 0 {
s.dump("http2-multiplex logs", clientC, serverC)
}
s.Require().Empty(failed, "all concurrent h2 requests should reach the echo backend")
}
func TestHTTP2Suite(t *testing.T) {
suite.Run(t, new(HTTP2Suite))
}

View File

@ -0,0 +1,605 @@
package e2e
import (
"context"
"encoding/base64"
"fmt"
"io"
"os"
"strings"
"testing"
"github.com/stretchr/testify/suite"
"github.com/testcontainers/testcontainers-go"
)
type HTTPSuite struct {
suite.Suite
ctx context.Context
echoC testcontainers.Container
echoIP string
}
func (s *HTTPSuite) 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
}
// sendRaw sends a raw HTTP request via netcat and returns the response.
// Uses base64 to avoid shell quoting issues with CRLF bytes.
func (s *HTTPSuite) sendRaw(gostC testcontainers.Container, host, port, data string) string {
encoded := base64.StdEncoding.EncodeToString([]byte(data))
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)
}
func (s *HTTPSuite) TearDownSuite() {
if s.echoC != nil {
s.echoC.Terminate(s.ctx)
}
}
// TestHTTPProxy verifies basic HTTP forward proxy (no auth, no metadata).
// Covers: handler type http, listener type tcp, basic GET via proxy.
func (s *HTTPSuite) TestHTTPProxy() {
gostC, err := RunGostContainerWithPorts(s.ctx, SharedNetworkName,
"testdata/http/server.yaml", "8080/tcp")
s.Require().NoError(err)
defer gostC.Terminate(s.ctx)
// curl -x uses HTTP forward proxy (GET with absolute URL)
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)
if code != 0 || !strings.Contains(string(body), "hello-gost") {
DumpLogs(s.T(), s.ctx, "http-proxy logs", gostC)
}
s.Require().Equal(0, code)
s.Require().Contains(string(body), "hello-gost")
}
// TestHTTPProxyAuth verifies proxy authentication and the authBasicRealm
// metadata parameter.
//
// Covers:
// - auther: basic auth on HTTP proxy
// - authBasicRealm: custom realm string in 407 Proxy-Authenticate header
func (s *HTTPSuite) TestHTTPProxyAuth() {
gostC, err := RunGostContainerWithPorts(s.ctx, SharedNetworkName,
"testdata/http/server_auth.yaml", "8080/tcp")
s.Require().NoError(err)
defer gostC.Terminate(s.ctx)
s.T().Run("no-auth-407", func(t *testing.T) {
cmd := []string{"curl", "-v", "-s", "-D", "-", "-o", "/dev/null",
"-x", "http://127.0.0.1:8080",
fmt.Sprintf("http://%s:5678", s.echoIP)}
_, out, _ := gostC.Exec(s.ctx, cmd)
body, _ := io.ReadAll(out)
output := string(body)
s.Assert().Contains(output, "407",
"no auth should return 407")
s.Assert().Contains(output, "gost-e2e-realm",
"authBasicRealm should appear in 407 Proxy-Authenticate")
})
s.T().Run("wrong-auth-407", func(t *testing.T) {
cmd := []string{"curl", "-v", "-s", "-o", "/dev/null", "-w", "%{http_code}",
"-x", "http://wrong:pass@127.0.0.1:8080",
fmt.Sprintf("http://%s:5678", s.echoIP)}
_, out, _ := gostC.Exec(s.ctx, cmd)
body, _ := io.ReadAll(out)
s.Assert().Contains(string(body), "407",
"wrong password should return 407")
})
s.T().Run("with-auth-success", func(t *testing.T) {
cmd := []string{"curl", "-v", "-s", "-x", "http://user:pass@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)
if code != 0 || !strings.Contains(string(body), "hello-gost") {
DumpLogs(s.T(), s.ctx, "http-proxy-auth logs", gostC)
}
s.Require().Equal(0, code)
s.Require().Contains(string(body), "hello-gost")
})
}
// TestHTTPProxyMetadata verifies:
// - probeResist: code:404 — unauthorised clients see 404 instead of 407
// - keepalive: config parsing (applied at Init time)
// - compression: config parsing (applied at Init time)
func (s *HTTPSuite) TestHTTPProxyMetadata() {
gostC, err := RunGostContainerWithPorts(s.ctx, SharedNetworkName,
"testdata/http/server_metadata.yaml", "8080/tcp")
s.Require().NoError(err)
defer gostC.Terminate(s.ctx)
s.T().Run("probe-resist-404", func(t *testing.T) {
cmd := []string{"curl", "-v", "-s", "-o", "/dev/null", "-w", "%{http_code}",
"-x", "http://127.0.0.1:8080",
fmt.Sprintf("http://%s:5678", s.echoIP)}
_, out, _ := gostC.Exec(s.ctx, cmd)
body, _ := io.ReadAll(out)
output := string(body)
s.Assert().NotContains(output, "407",
"probeResist should hide 407 status")
s.Assert().Contains(output, "404",
"probeResist should return configured decoy code")
})
s.T().Run("with-auth-success", func(t *testing.T) {
cmd := []string{"curl", "-v", "-s", "-x", "http://user:pass@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)
if code != 0 || !strings.Contains(string(body), "hello-gost") {
DumpLogs(s.T(), s.ctx, "http-metadata logs", gostC)
}
s.Require().Equal(0, code)
s.Require().Contains(string(body), "hello-gost")
})
}
// TestHTTPProxyHeaders verifies:
// - header: custom response headers (X-Proxy-Info, X-Custom) on
// proxy-originated error responses. The header metadata is set on
// the skeleton response used for error paths after authentication.
// - proxyAgent: custom Proxy-Agent header in proxy responses.
//
// Uses a bypass matcher (0.0.0.0/0) that blocks all traffic after auth
// succeeds → requests hit 403 Forbidden path which carries custom headers.
func (s *HTTPSuite) TestHTTPProxyHeaders() {
gostC, err := RunGostContainerWithPorts(s.ctx, SharedNetworkName,
"testdata/http/server_headers.yaml", "8080/tcp")
s.Require().NoError(err)
defer gostC.Terminate(s.ctx)
// Auth succeeds, but target is bypassed → 403 Forbidden
// The 403 response is built from the skeleton resp which
// carries h.md.header (custom headers) and h.md.proxyAgent.
s.T().Log("bypass: expect 403 with custom headers")
cmd := []string{"curl", "-v", "-s", "-D", "-", "-o", "/dev/null",
"-x", "http://user:pass@127.0.0.1:8080",
fmt.Sprintf("http://%s:5678", s.echoIP)}
_, out, _ := gostC.Exec(s.ctx, cmd)
headers, _ := io.ReadAll(out)
output := string(headers)
// Bypass → 403 Forbidden
s.Assert().Contains(output, "403",
"bypass should return 403")
// header: custom X-Proxy-Info header on error response
s.Assert().Contains(output, "X-Proxy-Info: gost-e2e",
"custom header should appear in 403 response")
// header: custom X-Custom header on error response
s.Assert().Contains(output, "X-Custom: test-value",
"custom header should appear in 403 response")
// proxyAgent: custom Proxy-Agent header on error response
s.Assert().Contains(output, "Proxy-Agent: gost-e2e/1.0",
"proxyAgent should appear in 403 response")
}
// TestHTTPConnect verifies HTTP CONNECT tunnel:
// - sniffing-enabled: sniffing:true + sniffing.timeout:2s
// - no-sniffing: plain CONNECT (default, no metadata)
// - bypass-403: CONNECT blocked by (0.0.0.0/0) bypass
//
// Uses curl --proxytunnel (-p) to force CONNECT method instead of GET.
func (s *HTTPSuite) TestHTTPConnect() {
s.T().Run("sniffing-enabled", func(t *testing.T) {
gostC, err := RunGostContainerWithPorts(s.ctx, SharedNetworkName,
"testdata/http/server_connect.yaml", "8080/tcp")
s.Require().NoError(err)
defer gostC.Terminate(s.ctx)
cmd := []string{"curl", "-v", "-s", "-p", "-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)
if code != 0 || !strings.Contains(string(body), "hello-gost") {
DumpLogs(s.T(), s.ctx, "http-connect sniffing logs", gostC)
}
s.Require().Equal(0, code)
s.Require().Contains(string(body), "hello-gost")
})
s.T().Run("no-sniffing", func(t *testing.T) {
gostC, err := RunGostContainerWithPorts(s.ctx, SharedNetworkName,
"testdata/http/server.yaml", "8080/tcp")
s.Require().NoError(err)
defer gostC.Terminate(s.ctx)
cmd := []string{"curl", "-v", "-s", "-p", "-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)
if code != 0 || !strings.Contains(string(body), "hello-gost") {
DumpLogs(s.T(), s.ctx, "http-connect no-sniffing logs", gostC)
}
s.Require().Equal(0, code)
s.Require().Contains(string(body), "hello-gost")
})
s.T().Run("bypass-403", func(t *testing.T) {
gostC, err := RunGostContainerWithPorts(s.ctx, SharedNetworkName,
"testdata/http/server_connect_bypass.yaml", "8080/tcp")
s.Require().NoError(err)
defer gostC.Terminate(s.ctx)
cmd := []string{"curl", "-v", "-s", "-D", "-", "-o", "/dev/null",
"-p", "-x", "http://user:pass@127.0.0.1:8080",
fmt.Sprintf("http://%s:5678", s.echoIP)}
_, out, _ := gostC.Exec(s.ctx, cmd)
body, _ := io.ReadAll(out)
output := string(body)
s.Assert().Contains(output, "403",
"CONNECT bypass should return 403")
})
}
// TestHTTPProxyDirectRequest verifies that a non-proxy-form HTTP request
// (raw GET / sent to the proxy port) is rejected with 400 Bad Request.
// A raw request with empty Host header has no valid scheme to infer,
// so the handler returns 400.
func (s *HTTPSuite) TestHTTPProxyDirectRequest() {
gostC, err := RunGostContainerWithPorts(s.ctx, SharedNetworkName,
"testdata/http/server.yaml", "8080/tcp")
s.Require().NoError(err)
defer gostC.Terminate(s.ctx)
resp := s.sendRaw(gostC, "127.0.0.1", "8080",
"GET / HTTP/1.0\r\nHost: bad!\r\n\r\n")
s.Assert().Contains(resp, "400",
"non-proxy GET should return 400")
}
// TestHTTPProxyPOST verifies that POST method forwarding works through
// the HTTP forward proxy. The echo server only handles GET, so the proxy
// should forward the POST request and return whatever the upstream sends
// back (501 Not Implemented from the echo server — not a proxy error).
// 501 is the real upstream response, proving the proxy forwarded it.
func (s *HTTPSuite) TestHTTPProxyPOST() {
gostC, err := RunGostContainerWithPorts(s.ctx, SharedNetworkName,
"testdata/http/server.yaml", "8080/tcp")
s.Require().NoError(err)
defer gostC.Terminate(s.ctx)
cmd := []string{"curl", "-v", "-s", "-o", "/dev/null", "-w", "%{http_code}",
"-X", "POST", "-x", "http://127.0.0.1:8080",
fmt.Sprintf("http://%s:5678/", s.echoIP)}
_, out, _ := gostC.Exec(s.ctx, cmd)
body, _ := io.ReadAll(out)
output := string(body)
// The echo server's BaseHTTPRequestHandler defaults to 501 for POST.
// If we get 501 through the proxy, it means the method was forwarded.
s.Assert().Contains(output, "501",
"POST through proxy should be forwarded, echo server returns 501")
}
// TestHTTPConnectUnreachable verifies that CONNECT to an unreachable target
// returns 503 Service Unavailable.
func (s *HTTPSuite) TestHTTPConnectUnreachable() {
gostC, err := RunGostContainerWithPorts(s.ctx, SharedNetworkName,
"testdata/http/server_connect.yaml", "8080/tcp")
s.Require().NoError(err)
defer gostC.Terminate(s.ctx)
// Send a raw CONNECT request to a port where nothing is listening.
resp := s.sendRaw(gostC, "127.0.0.1", "8080",
fmt.Sprintf("CONNECT %s:1 HTTP/1.0\r\nHost: %s:1\r\n\r\n", s.echoIP, s.echoIP))
s.Assert().Contains(resp, "503",
"CONNECT to unreachable target should return 503")
}
// TestHTTPConnector verifies HTTP connector type: a gost client uses an HTTP
// connector to forward through an upstream HTTP proxy (no auth), reaching the
// target via the proxy chain. This covers the server+client two-container
// pattern where the client connects to the server via connector type: http.
func (s *HTTPSuite) TestHTTPConnector() {
// Start upstream HTTP proxy server (no auth).
serverC, err := RunGostContainerWithOptions(s.ctx, SharedNetworkName,
"testdata/http/server.yaml", []string{"http-server"}, []string{"8080/tcp"})
s.Require().NoError(err)
defer serverC.Terminate(s.ctx)
// Render client config that chains through the upstream server.
rendered, err := RenderConfig("testdata/http/client_connector.yaml",
ConfigData{ServerAddr: "http-server:8080"})
s.Require().NoError(err)
defer os.Remove(rendered)
// Start client with the rendered config.
clientC, err := RunGostContainerWithPorts(s.ctx, SharedNetworkName,
rendered, "8080/tcp")
s.Require().NoError(err)
defer clientC.Terminate(s.ctx)
// Request through client proxy → HTTP connector → upstream HTTP server → target.
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, "http-connector client logs", clientC)
DumpLogs(s.T(), s.ctx, "http-connector server logs", serverC)
}
s.Require().Equal(0, code)
s.Require().Contains(string(body), "hello-gost")
}
// TestHTTPConnectorAuth verifies HTTP connector with authentication:
// no-auth → 407, correct auth → success.
func (s *HTTPSuite) TestHTTPConnectorAuth() {
// Start upstream HTTP proxy with auth and network alias.
serverC, err := RunGostContainerWithOptions(s.ctx, SharedNetworkName,
"testdata/http/server_auth.yaml", []string{"http-server"}, []string{"8080/tcp"})
s.Require().NoError(err)
defer serverC.Terminate(s.ctx)
s.T().Run("no-auth-407", func(t *testing.T) {
// Send raw request directly to server container (no auth) → 407.
resp := s.sendRaw(serverC, "127.0.0.1", "8080",
"GET http://127.0.0.1:5678/ HTTP/1.0\r\nHost: 127.0.0.1\r\n\r\n")
s.Assert().Contains(resp, "407",
"no auth via upstream should return 407")
})
s.T().Run("correct-auth-success", func(t *testing.T) {
rendered, err := RenderConfig("testdata/http/client_connector_auth.yaml",
ConfigData{ServerAddr: "http-server:8080"})
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, "http-connector-auth client logs", clientC)
DumpLogs(s.T(), s.ctx, "http-connector-auth server logs", serverC)
}
s.Require().Equal(0, code)
s.Require().Contains(string(body), "hello-gost")
})
}
// TestHTTPProbeResistHost verifies probeResist host type.
// When auth fails, the handler pipes the raw request to
// the decoy host (tcp-echo:5678). The TCP echo server echoes
// back the request bytes rather than returning a 407.
func (s *HTTPSuite) TestHTTPProbeResistHost() {
gostC, err := RunGostContainerWithPorts(s.ctx, SharedNetworkName,
"testdata/http/server_proberesist_host.yaml", "8080/tcp")
s.Require().NoError(err)
defer gostC.Terminate(s.ctx)
// Without auth, probeResist host pipes the raw request to tcp-echo:5678.
// The HTTP echo server returns 200 OK with "hello-gost", proving the
// request reached the decoy host rather than getting a 407 response.
resp := s.sendRaw(gostC, "127.0.0.1", "8080",
"GET http://127.0.0.1:5678/ HTTP/1.0\r\nHost: 127.0.0.1\r\n\r\n")
s.Assert().NotContains(resp, "407",
"probeResist host should hide 407 response")
s.Assert().Contains(resp, "hello-gost",
"probeResist host should pipe request to decoy host")
}
// TestHTTPProbeResistKnock verifies probeResist knock mechanism.
// Clients that connect with a recognized knock Host header
// get a normal 407 auth challenge instead of the decoy response.
//
// Uses server_proberesist_knock.yaml:
// - probeResist: code:404 (decoy 404 for unknown hosts)
// - knock: secret.example.com (known hosts get normal 407)
func (s *HTTPSuite) TestHTTPProbeResistKnock() {
gostC, err := RunGostContainerWithPorts(s.ctx, SharedNetworkName,
"testdata/http/server_proberesist_knock.yaml", "8080/tcp")
s.Require().NoError(err)
defer gostC.Terminate(s.ctx)
s.T().Run("unknown-host-decoy", func(t *testing.T) {
// URL hostname doesn't match knock list → probeResist fires → 404
resp := s.sendRaw(gostC, "127.0.0.1", "8080",
"GET http://127.0.0.1:5678/ HTTP/1.0\r\nHost: 127.0.0.1\r\n\r\n")
s.Assert().Contains(resp, "404",
"unknown host should get decoy 404")
s.Assert().NotContains(resp, "407",
"unknown host should NOT get 407")
})
s.T().Run("knock-host-407", func(t *testing.T) {
// URL hostname matches knock list → normal 407 auth challenge.
// Note: knock checks req.URL.Hostname(), not the Host header.
resp := s.sendRaw(gostC, "127.0.0.1", "8080",
"GET http://secret.example.com:5678/ HTTP/1.0\r\nHost: secret.example.com\r\n\r\n")
s.Assert().Contains(resp, "407",
"knock host should get normal 407")
})
}
// TestHTTPConnectorTLS verifies HTTP connector using TLS dialer
// to upstream HTTPS proxy (listener type: tls).
// The TLS listener auto-generates a self-signed cert;
// the TLS dialer defaults to InsecureSkipVerify=true, so
// handshake succeeds without explicit cert configuration.
func (s *HTTPSuite) TestHTTPConnectorTLS() {
serverC, err := RunGostContainerWithOptions(s.ctx, SharedNetworkName,
"testdata/http/server_tls.yaml", []string{"tls-server"}, []string{"8443/tcp"})
s.Require().NoError(err)
defer serverC.Terminate(s.ctx)
rendered, err := RenderConfig("testdata/http/client_connector_tls.yaml",
ConfigData{ServerAddr: "tls-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, "http-connector-tls client logs", clientC)
DumpLogs(s.T(), s.ctx, "http-connector-tls server logs", serverC)
}
s.Require().Equal(0, code)
s.Require().Contains(string(body), "hello-gost")
}
// TestHTTPProbeResistWeb verifies probeResist web type.
// On auth failure, the handler fetches the decoy URL
// (tcp-echo:5678) and returns its response body.
func (s *HTTPSuite) TestHTTPProbeResistWeb() {
gostC, err := RunGostContainerWithPorts(s.ctx, SharedNetworkName,
"testdata/http/server_proberesist_web.yaml", "8080/tcp")
s.Require().NoError(err)
defer gostC.Terminate(s.ctx)
// Without auth, probeResist web fetches http://tcp-echo:5678
// and returns the echo server's response ("hello-gost").
resp := s.sendRaw(gostC, "127.0.0.1", "8080",
"GET http://127.0.0.1:5678/ HTTP/1.0\r\nHost: 127.0.0.1\r\n\r\n")
s.Assert().NotContains(resp, "407",
"probeResist web should hide 407 response")
s.Assert().Contains(resp, "hello-gost",
"probeResist web should return decoy response body")
}
// TestHTTPProbeResistFile verifies probeResist file type.
// On auth failure, the handler reads a local file and
// returns its contents as the HTTP response body.
func (s *HTTPSuite) TestHTTPProbeResistFile() {
gostC, err := RunGostContainerWithFiles(s.ctx, SharedNetworkName,
"testdata/http/server_proberesist_file.yaml",
[]testcontainers.ContainerFile{
{HostFilePath: "testdata/http/decoy.html", ContainerFilePath: "/tmp/decoy.html", FileMode: 0644},
},
"8080/tcp")
s.Require().NoError(err)
defer gostC.Terminate(s.ctx)
// Without auth, probeResist file reads /tmp/decoy.html and
// returns "decoy-response" rather than a 407.
resp := s.sendRaw(gostC, "127.0.0.1", "8080",
"GET http://127.0.0.1:5678/ HTTP/1.0\r\nHost: 127.0.0.1\r\n\r\n")
s.Assert().NotContains(resp, "407",
"probeResist file should hide 407 response")
s.Assert().Contains(resp, "decoy-response",
"probeResist file should return decoy file content")
}
// TestHTTPIdleTimeout verifies idleTimeout on CONNECT tunnels.
// After the configured idle timeout, the pipe between client
// and target should close.
func (s *HTTPSuite) TestHTTPIdleTimeout() {
gostC, err := RunGostContainerWithFiles(s.ctx, SharedNetworkName,
"testdata/http/server_idle_timeout.yaml",
[]testcontainers.ContainerFile{
{HostFilePath: "scripts/http_idle_timeout.py", ContainerFilePath: "/scripts/http_idle_timeout.py", FileMode: 0644},
},
"8080/tcp")
s.Require().NoError(err)
defer gostC.Terminate(s.ctx)
// The Python script:
// 1. Opens a CONNECT tunnel to tcp-echo:5678
// 2. Sends and receives data (confirm tunnel is alive)
// 3. Waits > idleTimeout (3s + 2s buffer)
// 4. Sends more data — expects connection to be closed
code, out, err := gostC.Exec(s.ctx, []string{
"python3", "/scripts/http_idle_timeout.py",
"127.0.0.1", "8080", "3",
})
output, _ := io.ReadAll(out)
s.T().Logf("idle timeout output:\n%s", string(output))
if code != 0 {
DumpLogs(s.T(), s.ctx, "http-idle-timeout logs", gostC)
}
s.Require().Equal(0, code, "idle timeout test script should exit 0")
}
// TestHTTPUDPRelay verifies UDP relay over HTTP. Uses
// X-Gost-Protocol: udp in the CONNECT request to establish
// a SOCKS5 UDP tunnel through the HTTP handler.
func (s *HTTPSuite) TestHTTPUDPRelay() {
// Start UDP echo container on the shared network.
udpC, err := RunUDPEchoContainer(s.ctx, SharedNetworkName)
s.Require().NoError(err)
defer udpC.Terminate(s.ctx)
gostC, err := RunGostContainerWithFiles(s.ctx, SharedNetworkName,
"testdata/http/server_udp.yaml",
[]testcontainers.ContainerFile{
{HostFilePath: "scripts/http_udp_relay.py", ContainerFilePath: "/scripts/http_udp_relay.py", FileMode: 0644},
},
"8080/tcp")
s.Require().NoError(err)
defer gostC.Terminate(s.ctx)
// The Python script:
// 1. Connects to gost HTTP proxy
// 2. Sends CONNECT with X-Gost-Protocol: udp
// 3. After 200 OK, sends a SOCKS5 UDP frame targeting udp-echo:5679
// 4. Reads back the echoed frame
code, out, err := gostC.Exec(s.ctx, []string{
"python3", "/scripts/http_udp_relay.py",
"127.0.0.1", "8080",
})
output, _ := io.ReadAll(out)
s.T().Logf("udp relay output:\n%s", string(output))
if code != 0 {
DumpLogs(s.T(), s.ctx, "http-udp logs", gostC)
}
s.Require().Equal(0, code, "udp relay test script should exit 0")
}
func TestHTTPSuite(t *testing.T) {
suite.Run(t, new(HTTPSuite))
}

View File

@ -0,0 +1,50 @@
package e2e
import (
"context"
"flag"
"fmt"
"os"
"os/exec"
"testing"
"github.com/testcontainers/testcontainers-go/network"
)
var SharedNetworkName string
func TestMain(m *testing.M) {
flag.Parse()
ctx := context.Background()
shouldCleanup := false
if GostBinPath == "" {
GostBinPath = "/tmp/gost-test-bin"
cmd := exec.Command("go", "build", "-o", GostBinPath, "../../cmd/gost")
cmd.Env = append(os.Environ(), "CGO_ENABLED=0")
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
if err := cmd.Run(); err != nil {
fmt.Printf("Failed to compile gost: %v\n", err)
os.Exit(1)
}
shouldCleanup = true
}
net, err := network.New(ctx)
if err != nil {
fmt.Printf("Failed to create network: %v\n", err)
os.Exit(1)
}
SharedNetworkName = net.Name
code := m.Run()
net.Remove(ctx)
if shouldCleanup {
os.Remove(GostBinPath)
}
os.Exit(code)
}

View File

@ -0,0 +1,289 @@
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
cmd := []string{"curl", "-v", "-s", "--connect-timeout", "5",
"--cacert", "/certs/ca.pem",
"--cert", "/certs/client.pem",
"--key", "/certs/client-key.pem",
"-x", "https://127.0.0.1:8443",
fmt.Sprintf("http://%s:5678", s.echoIP)}
code, out, err := gostC.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, "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
cmd := []string{"curl", "-v", "-s", "--connect-timeout", "5",
"--cacert", "/certs/ca.pem",
"-x", "https://127.0.0.1:8443",
fmt.Sprintf("http://%s:5678", s.echoIP)}
code, _, err := gostC.Exec(s.ctx, 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",
"--cacert", "/certs/ca.pem",
"--cert", "/certs/client.pem",
"--key", "/certs/client-key.pem",
"-x", "https://127.0.0.1:8443",
fmt.Sprintf("http://%s:5678", s.echoIP)}
code, out, err := gostC.Exec(s.ctx, 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)
}

View File

@ -0,0 +1,57 @@
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))
}

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))
}

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()

View File

@ -0,0 +1,162 @@
"""DNS query client for e2e tests.
Usage:
python3 dns_query.py udp host port qname qtype [expected_ip|empty]
python3 dns_query.py tcp host port qname qtype [expected_ip|empty]
Sends a DNS query and optionally checks the response:
- expected_ip: verify response contains this IP address
- "empty": verify response has zero answer records (NXDOMAIN or blocked)
- omitted: exit 0 on any valid DNS response
Exits 0 on success, 1 on failure.
"""
import struct
import socket
import sys
def encode_name(name):
parts = name.rstrip(".").split(".")
return b"".join(bytes([len(p)]) + p.encode() for p in parts) + b"\x00"
def skip_name(data, pos):
"""Skip one DNS name at pos, return position after it."""
while True:
length = data[pos]
if length == 0:
return pos + 1
if length & 0xC0:
return pos + 2
pos += length + 1
def build_query(qname, qtype):
tid = 0x1234
flags = 0x0100 # RD=1
qdcount = 1
header = struct.pack(">HHHHHH", tid, flags, qdcount, 0, 0, 0)
qname_enc = encode_name(qname)
question = qname_enc + struct.pack(">HH", qtype, 1) # QTYPE, QCLASS=IN
return header + question
def parse_response(data):
if len(data) < 12:
return [], -1
flags = struct.unpack(">H", data[2:4])[0]
rcode = flags & 0x0F
qdcount = struct.unpack(">H", data[4:6])[0]
ancount = struct.unpack(">H", data[6:8])[0]
# Skip question section
pos = 12
for _ in range(qdcount):
pos = skip_name(data, pos) + 4 # QTYPE + QCLASS
answers = []
for _ in range(ancount):
pos = skip_name(data, pos)
rtype, rclass, ttl, rdlength = struct.unpack(">HHIH", data[pos:pos + 10])
pos += 10
rdata = data[pos:pos + rdlength]
pos += rdlength
answers.append((rtype, rclass, ttl, rdata))
return answers, rcode
def query_udp(host, port, query):
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
sock.settimeout(5)
sock.sendto(query, (host, port))
data, _ = sock.recvfrom(4096)
sock.close()
return data
def query_tcp(host, port, query):
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.settimeout(5)
sock.connect((host, port))
sock.sendall(struct.pack(">H", len(query)) + query)
raw = sock.recv(2)
if len(raw) < 2:
sock.close()
return b""
msglen = struct.unpack(">H", raw)[0]
data = b""
while len(data) < msglen:
chunk = sock.recv(msglen - len(data))
if not chunk:
break
data += chunk
sock.close()
return data
def format_ip(rtype, rdata):
if rtype == 1 and len(rdata) == 4:
return socket.inet_ntoa(rdata)
if rtype == 28 and len(rdata) == 16:
return socket.inet_ntop(socket.AF_INET6, rdata)
return repr(rdata)
def main():
if len(sys.argv) < 5:
print("Usage: dns_query.py <udp|tcp> <host> <port> <qname> <qtype> [expected_ip|empty]")
sys.exit(1)
mode = sys.argv[1]
host = sys.argv[2]
port = int(sys.argv[3])
qname = sys.argv[4]
qtype_str = sys.argv[5]
expected = sys.argv[6] if len(sys.argv) > 6 else None
qtype_map = {"A": 1, "AAAA": 28}
qtype = qtype_map.get(qtype_str, 1)
query = build_query(qname, qtype)
try:
if mode == "tcp":
data = query_tcp(host, port, query)
else:
data = query_udp(host, port, query)
except Exception as e:
print(f"ERROR: {e}")
sys.exit(1)
if not data:
print("ERROR: empty response")
sys.exit(1)
answers, rcode = parse_response(data)
print(f"Got {len(answers)} answer(s), rcode={rcode}")
for rtype, _, _, rdata in answers:
print(f" {format_ip(rtype, rdata)}")
if expected == "empty":
if len(answers) == 0:
print("MATCH: empty answer (expected)")
sys.exit(0)
print(f"NO MATCH: expected empty, got {len(answers)} answers")
sys.exit(1)
elif expected:
for rtype, _, _, rdata in answers:
ip = format_ip(rtype, rdata)
if ip == expected:
print(f"MATCH: expected {expected}")
sys.exit(0)
print(f"NO MATCH: expected {expected}")
sys.exit(1)
else:
sys.exit(0)
if __name__ == "__main__":
main()

View File

@ -0,0 +1,88 @@
"""Simple authoritative DNS responder for e2e tests.
Listens on UDP port 5353 and responds with static records:
test.example.com. IN A 10.0.0.1
test2.example.com. IN A 10.0.0.2
example.com. IN AAAA ::1
All other queries receive NXDOMAIN.
"""
import socketserver
import struct
import socket
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
def encode_name(name):
parts = name.rstrip(".").split(".")
return b"".join(bytes([len(p)]) + p.encode() for p in parts) + b"\x00"
RECORDS = {
("test.example.com", 1): (1, 300, socket.inet_aton("10.0.0.1")),
("test2.example.com", 1): (1, 300, socket.inet_aton("10.0.0.2")),
("example.com", 28): (28, 300, socket.inet_pton(socket.AF_INET6, "::1")),
}
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]
key = (qname, qtype)
if key in RECORDS:
rcode = 0
ancount = 1
rtype, ttl, rdata = RECORDS[key]
else:
rcode = 3 # NXDOMAIN
ancount = 0
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__":
with socketserver.UDPServer(("0.0.0.0", 5353), DNSResponder) as srv:
srv.serve_forever()

View File

@ -0,0 +1,83 @@
"""Simple authoritative DNS responder for TCP mode e2e tests.
Listens on TCP port 5353 and responds with the same static records
as dns_responder.py, but over TCP (RFC 5966).
"""
import socketserver
import struct
import socket
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
RECORDS = {
("test.example.com", 1): (1, 300, socket.inet_aton("10.0.0.1")),
("test2.example.com", 1): (1, 300, socket.inet_aton("10.0.0.2")),
("example.com", 28): (28, 300, socket.inet_pton(socket.AF_INET6, "::1")),
}
class DNSResponder(socketserver.StreamRequestHandler):
def handle(self):
# TCP DNS: 2-byte length prefix
raw = self.rfile.read(2)
if len(raw) < 2:
return
msglen = struct.unpack(">H", raw)[0]
data = self.rfile.read(msglen)
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]
key = (qname, qtype)
if key in RECORDS:
rcode = 0
ancount = 1
rtype, ttl, rdata = RECORDS[key]
else:
rcode = 3 # NXDOMAIN
ancount = 0
rtype, ttl, rdata = 0, 0, b""
flags = 0x8000 | 0x0400 | 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)
+ struct.pack(">HI", qclass, ttl)
+ struct.pack(">H", len(rdata)) + rdata
)
dnsmsg = header + question + answer
self.wfile.write(struct.pack(">H", len(dnsmsg)) + dnsmsg)
if __name__ == "__main__":
with socketserver.TCPServer(("0.0.0.0", 5353), DNSResponder) as srv:
srv.serve_forever()

View File

@ -0,0 +1,140 @@
"""DNS query client over TLS for e2e tests.
Connects to the DNS-over-TLS endpoint and performs a DNS query.
Usage:
python3 dns_tls_query.py host port qname qtype [expected_ip]
"""
import struct
import socket
import ssl
import sys
def encode_name(name):
parts = name.rstrip(".").split(".")
return b"".join(bytes([len(p)]) + p.encode() for p in parts) + b"\x00"
def build_query(tid, qname, qtype):
flags = 0x0100
qdcount = 1
header = struct.pack(">HHHHHH", tid, flags, qdcount, 0, 0, 0)
qname_enc = encode_name(qname)
question = qname_enc + struct.pack(">HH", qtype, 1)
return header + question
def parse_response(data):
if len(data) < 12:
return [], -1
flags = struct.unpack(">H", data[2:4])[0]
rcode = flags & 0x0F
qdcount = struct.unpack(">H", data[4:6])[0]
ancount = struct.unpack(">H", data[6:8])[0]
pos = 12
for _ in range(qdcount):
while data[pos] != 0:
if data[pos] & 0xC0:
pos += 2
break
pos += data[pos] + 1
else:
pos += 1
pos += 4
answers = []
for _ in range(ancount):
if data[pos] & 0xC0:
pos += 2
else:
while data[pos] != 0:
pos += data[pos] + 1
pos += 1
rtype, rclass, ttl, rdlength = struct.unpack(">HHIH", data[pos:pos + 10])
pos += 10
rdata = data[pos:pos + rdlength]
pos += rdlength
answers.append((rtype, rdata))
return answers, rcode
def main():
if len(sys.argv) < 5:
print(f"Usage: {sys.argv[0]} host port qname qtype [expected_ip]")
sys.exit(1)
host = sys.argv[1]
port = int(sys.argv[2])
qname = sys.argv[3]
qtype_str = sys.argv[4]
expected = sys.argv[5] if len(sys.argv) > 5 else None
qtype_map = {"A": 1, "AAAA": 28}
qtype = qtype_map.get(qtype_str, 1)
query = build_query(0x1234, qname, qtype)
ctx = ssl.create_default_context()
ctx.check_hostname = False
ctx.verify_mode = ssl.CERT_NONE
try:
with socket.create_connection((host, port), timeout=5) as sock:
with ctx.wrap_socket(sock, server_hostname=host) as tls_sock:
# TCP DNS: 2-byte length prefix
tls_sock.sendall(struct.pack(">H", len(query)) + query)
raw = tls_sock.recv(2)
if len(raw) < 2:
print("ERROR: short response header")
sys.exit(1)
msglen = struct.unpack(">H", raw)[0]
data = b""
while len(data) < msglen:
chunk = tls_sock.recv(msglen - len(data))
if not chunk:
break
data += chunk
except Exception as e:
print(f"ERROR: {e}")
sys.exit(1)
if not data:
print("ERROR: empty response")
sys.exit(1)
answers, rcode = parse_response(data)
print(f"Got {len(answers)} answer(s), rcode={rcode}")
for rtype, rdata in answers:
if rtype == 1 and len(rdata) == 4:
ip = socket.inet_ntoa(rdata)
print(f" A {ip}")
elif rtype == 28 and len(rdata) == 16:
ip6 = socket.inet_ntop(socket.AF_INET6, rdata)
print(f" AAAA {ip6}")
else:
print(f" TYPE={rtype} RDATA={rdata.hex()}")
if expected:
for rtype, rdata in answers:
if rtype == 1 and len(rdata) == 4:
ip = socket.inet_ntoa(rdata)
if ip == expected:
print(f"MATCH: expected {expected}")
sys.exit(0)
elif rtype == 28 and len(rdata) == 16:
ip6 = socket.inet_ntop(socket.AF_INET6, rdata)
if ip6 == expected:
print(f"MATCH: expected {expected}")
sys.exit(0)
print(f"NO MATCH: expected {expected}")
sys.exit(1)
sys.exit(0)
if __name__ == "__main__":
main()

View File

@ -0,0 +1,68 @@
import socket
import sys
import time
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 8080
idle_timeout = int(sys.argv[3]) if len(sys.argv) > 3 else 3
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.settimeout(10)
s.connect((host, port))
# CONNECT to echo server
req = b"CONNECT tcp-echo:5678 HTTP/1.1\r\nHost: tcp-echo:5678\r\n\r\n"
s.sendall(req)
# Read 200 OK response
resp = b""
while b"\r\n\r\n" not in resp:
chunk = s.recv(4096)
if not chunk:
break
resp += chunk
if b"200" not in resp:
print(f"FAIL: expected 200, got {resp.decode(errors='replace')}")
sys.exit(1)
# Send an HTTP GET through the CONNECT tunnel. The tunnel target
# is an HTTP echo server (BaseHTTPRequestHandler), so we must speak
# HTTP or the connection closes.
req = b"GET / HTTP/1.0\r\nHost: tcp-echo\r\n\r\n"
s.sendall(req)
data = b""
while b"hello-gost" not in data:
chunk = s.recv(4096)
if not chunk:
break
data += chunk
if b"hello-gost" not in data:
print(f"FAIL: expected hello-gost echo, got {data!r}")
sys.exit(1)
print(f"PASS: first request through tunnel succeeded")
# Wait longer than idleTimeout
wait = idle_timeout + 2
print(f"Waiting {wait}s for idle timeout...")
time.sleep(wait)
# Try to send more data — idle timeout should have closed the pipe
try:
s.sendall(b"ping-2\n")
data = s.recv(4096)
if not data:
print("PASS: connection closed after idle timeout (empty recv)")
sys.exit(0)
# Got data means the pipe is still alive
print(f"FAIL: connection still alive after idle timeout, got {data!r}")
sys.exit(1)
except (socket.timeout, ConnectionResetError, BrokenPipeError, OSError) as e:
print(f"PASS: connection closed after idle timeout: {e}")
sys.exit(0)
if __name__ == "__main__":
main()

View File

@ -0,0 +1,135 @@
import socket
import struct
import sys
UDP_ECHO_HOST = "udp-echo"
UDP_ECHO_PORT = 5679
def encode_socks5_addr(host, port):
"""Encode (host, port) as SOCKS5 ADDRESS + PORT.
Uses ATYP=3 (domain name) so Docker DNS resolves the host.
Returns (atyp, address_bytes, port_bytes).
"""
host_bytes = host.encode()
addr_bytes = struct.pack("!B", len(host_bytes)) + host_bytes
port_bytes = struct.pack("!H", port)
return 0x03, addr_bytes, port_bytes
def build_udp_frame(payload, host, port):
"""Build a SOCKS5 UDP relay frame over TCP.
gost HTTP UDP relay uses RSV=data-length and FRAG=0xff.
Frame: [RSV:2][FRAG:1][ATYP:1][DST.ADDR][DST.PORT:2][DATA]
"""
atyp, addr_bytes, port_bytes = encode_socks5_addr(host, port)
rsv = struct.pack("!H", len(payload))
frag = b"\xff"
atyp_byte = struct.pack("!B", atyp)
return rsv + frag + atyp_byte + addr_bytes + port_bytes + payload
def recvn(sock, n):
"""Receive exactly n bytes from socket."""
buf = b""
while len(buf) < n:
chunk = sock.recv(n - len(buf))
if not chunk:
raise ConnectionError("connection closed while reading")
buf += chunk
return buf
def read_socks5_frame(sock):
"""Read one SOCKS5 UDP relay frame from the TCP connection.
Returns (addr, port, data).
"""
# Read header: RSV(2) + FRAG(1) + ATYP(1)
header = recvn(sock, 4)
rsv = struct.unpack("!H", header[:2])[0]
frag = header[2]
atyp = header[3]
# Read address based on ATYP
if atyp == 1: # IPv4
addr_bytes = recvn(sock, 4)
addr = socket.inet_ntoa(addr_bytes)
elif atyp == 3: # Domain name
domain_len = recvn(sock, 1)
addr = recvn(sock, domain_len[0]).decode()
elif atyp == 4: # IPv6
addr_bytes = recvn(sock, 16)
addr = socket.inet_ntop(socket.AF_INET6, addr_bytes)
else:
raise ValueError(f"unknown ATYP: {atyp}")
port_bytes = recvn(sock, 2)
port = struct.unpack("!H", port_bytes)[0]
# Read data
if rsv > 0:
data = recvn(sock, rsv)
else:
# Standard SOCKS5 UDP: read remaining
data = b""
while True:
chunk = sock.recv(4096)
if not chunk:
break
data += chunk
return addr, port, data
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 8080
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.settimeout(10)
s.connect((host, port))
# Send CONNECT with X-Gost-Protocol: udp
req = (
b"CONNECT 0.0.0.0:0 HTTP/1.1\r\n"
b"Host: 0.0.0.0:0\r\n"
b"X-Gost-Protocol: udp\r\n"
b"Proxy-Connection: keep-alive\r\n"
b"\r\n"
)
s.sendall(req)
# Read 200 OK
resp = b""
while b"\r\n\r\n" not in resp:
chunk = s.recv(4096)
if not chunk:
break
resp += chunk
if b"200" not in resp:
print(f"FAIL: expected 200, got {resp.decode(errors='replace')}")
sys.exit(1)
# Build and send UDP relay frame
payload = b"hello-gost"
frame = build_udp_frame(payload, UDP_ECHO_HOST, UDP_ECHO_PORT)
s.sendall(frame)
# Read response frame
addr, rport, data = read_socks5_frame(s)
if b"hello-gost" in data:
print(f"PASS: received expected data from {addr}:{rport}")
sys.exit(0)
else:
print(f"FAIL: expected hello-gost in response, got {data!r}")
sys.exit(1)
if __name__ == "__main__":
main()

View File

@ -0,0 +1,16 @@
from http.server import BaseHTTPRequestHandler, HTTPServer
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
HTTPServer(("0.0.0.0", 5678), Handler).serve_forever()

View File

@ -0,0 +1,53 @@
import socket
import sys
import time
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 8000
idle_timeout = int(sys.argv[3]) if len(sys.argv) > 3 else 3
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.settimeout(10)
s.connect((host, port))
# Send an HTTP GET request — the forward handler pipes us to tcp-echo:5678
req = b"GET / HTTP/1.0\r\nHost: tcp-echo\r\n\r\n"
s.sendall(req)
# Read response — should contain "hello-gost" (echo server reply)
resp = b""
while b"hello-gost" not in resp:
chunk = s.recv(4096)
if not chunk:
break
resp += chunk
if b"hello-gost" not in resp:
print(f"FAIL: expected hello-gost in response, got {resp.decode(errors='replace')}")
sys.exit(1)
print(f"PASS: first request through forward pipe succeeded")
# Wait longer than idleTimeout
wait = idle_timeout + 2
print(f"Waiting {wait}s for idle timeout...")
time.sleep(wait)
# Try to send more data — idle timeout should have closed the pipe
try:
s.sendall(b"GET / HTTP/1.0\r\nHost: tcp-echo\r\n\r\n")
data = s.recv(4096)
if not data:
print("PASS: connection closed after idle timeout (empty recv)")
sys.exit(0)
# Got data means the pipe is still alive
print(f"FAIL: connection still alive after idle timeout, got {data!r}")
sys.exit(1)
except (socket.timeout, ConnectionResetError, BrokenPipeError, OSError) as e:
print(f"PASS: connection closed after idle timeout: {e}")
sys.exit(0)
if __name__ == "__main__":
main()

View File

@ -0,0 +1,9 @@
import socket
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
sock.bind(("0.0.0.0", 5679))
while True:
data, addr = sock.recvfrom(2048)
sock.sendto(data, addr)

View File

@ -0,0 +1,25 @@
import socket
import sys
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
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
sock.settimeout(5)
payload = b"hello-gost-udp"
sock.sendto(payload, (host, port))
data, addr = sock.recvfrom(2048)
if data == payload:
print("PASS: received echo")
sys.exit(0)
else:
print(f"FAIL: expected {payload!r}, got {data!r}")
sys.exit(1)
if __name__ == "__main__":
main()

View File

@ -0,0 +1,160 @@
package e2e
import (
"context"
"fmt"
"io"
"net"
"os"
"strings"
"testing"
"time"
"github.com/stretchr/testify/suite"
"github.com/testcontainers/testcontainers-go"
)
type ShadowsocksSuite struct {
suite.Suite
ctx context.Context
echoC testcontainers.Container
echoIP string
udpC testcontainers.Container
}
func (s *ShadowsocksSuite) 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
s.T().Logf("start udp echo container...")
udpC, err := RunUDPEchoContainer(s.ctx, SharedNetworkName)
s.Require().NoError(err)
s.udpC = udpC
}
func (s *ShadowsocksSuite) TearDownSuite() {
if s.echoC != nil {
s.echoC.Terminate(s.ctx)
}
if s.udpC != nil {
s.udpC.Terminate(s.ctx)
}
}
func (s *ShadowsocksSuite) TestShadowsocksTCP() {
s.runTCPCase("aes256gcm", "testdata/shadowsocks/tcp_server_aes256gcm.yaml", "testdata/shadowsocks/tcp_client_aes256gcm.yaml")
s.runTCPCase("chacha20", "testdata/shadowsocks/tcp_server_chacha20.yaml", "testdata/shadowsocks/tcp_client_chacha20.yaml")
}
func (s *ShadowsocksSuite) TestShadowsocks2022TCP() {
s.runTCPCase("2022-aes128", "testdata/shadowsocks/tcp_server_2022_aes128.yaml", "testdata/shadowsocks/tcp_client_2022_aes128.yaml")
s.runTCPCase("2022-aes256", "testdata/shadowsocks/tcp_server_2022_aes256.yaml", "testdata/shadowsocks/tcp_client_2022_aes256.yaml")
}
func (s *ShadowsocksSuite) TestShadowsocks2022TCPMultiPSK() {
s.runTCPCase("2022-aes128-multipsk", "testdata/shadowsocks/tcp_server_2022_aes128_multipsk.yaml", "testdata/shadowsocks/tcp_client_2022_aes128_multipsk.yaml")
s.runTCPCase("2022-aes256-multipsk", "testdata/shadowsocks/tcp_server_2022_aes256_multipsk.yaml", "testdata/shadowsocks/tcp_client_2022_aes256_multipsk.yaml")
}
func (s *ShadowsocksSuite) TestShadowsocksUDP() {
s.runUDPCase("aes256gcm", "testdata/shadowsocks/udp_server_aes256gcm.yaml", "testdata/shadowsocks/udp_client_aes256gcm.yaml")
}
func (s *ShadowsocksSuite) TestShadowsocks2022UDP() {
s.runUDPCase("2022-aes128", "testdata/shadowsocks/udp_server_2022_aes128.yaml", "testdata/shadowsocks/udp_client_2022_aes128.yaml")
s.runUDPCase("2022-aes256", "testdata/shadowsocks/udp_server_2022_aes256.yaml", "testdata/shadowsocks/udp_client_2022_aes256.yaml")
}
func (s *ShadowsocksSuite) runUDPCase(name, serverConfig, clientConfig string) {
s.T().Run(name, func(t *testing.T) {
serverAlias := name + "-ssu-server"
serverC, err := RunGostContainerWithOptions(s.ctx, SharedNetworkName, serverConfig, []string{serverAlias}, []string{"8389/udp"})
s.Require().NoError(err)
defer serverC.Terminate(s.ctx)
rendered, err := RenderConfig(clientConfig, ConfigData{ServerAddr: serverAlias + ":8389"})
s.Require().NoError(err)
defer os.Remove(rendered)
clientC, err := RunGostContainerWithPorts(
s.ctx,
SharedNetworkName,
rendered,
"9000/udp",
)
s.Require().NoError(err)
defer clientC.Terminate(s.ctx)
host, err := clientC.Host(s.ctx)
s.Require().NoError(err)
port, err := clientC.MappedPort(s.ctx, "9000/udp")
s.Require().NoError(err)
conn, err := net.DialTimeout("udp", net.JoinHostPort(host, port.Port()), 5*time.Second)
s.Require().NoError(err)
defer conn.Close()
payload := []byte("hello-gost-udp")
buf := make([]byte, 2048)
var n int
for i := range 5 {
_, err = conn.Write(payload)
s.Require().NoError(err)
_ = conn.SetReadDeadline(time.Now().Add(2 * time.Second))
n, err = conn.Read(buf)
if err == nil {
break
}
s.T().Logf("udp read attempt %d failed: %v, retrying...", i+1, err)
time.Sleep(2 * time.Second)
}
if err != nil {
DumpLogs(s.T(), s.ctx, name+" udp client logs", clientC)
DumpLogs(s.T(), s.ctx, name+" udp server logs", serverC)
}
s.Require().NoError(err)
s.Require().Contains(string(buf[:n]), "hello-gost")
})
}
func TestShadowsocksSuite(t *testing.T) {
suite.Run(t, new(ShadowsocksSuite))
}
func (s *ShadowsocksSuite) runTCPCase(name, serverConfig, clientConfig string) {
s.T().Run(name, func(t *testing.T) {
serverAlias := name + "-ss-server"
serverC, err := RunGostContainerWithOptions(s.ctx, SharedNetworkName, serverConfig, []string{serverAlias}, []string{"8388/tcp"})
s.Require().NoError(err)
defer serverC.Terminate(s.ctx)
rendered, err := RenderConfig(clientConfig, ConfigData{ServerAddr: serverAlias + ":8388"})
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, name+" client logs", clientC)
DumpLogs(s.T(), s.ctx, name+" server logs", serverC)
}
s.Require().Equal(0, code)
s.Require().Contains(string(body), "hello-gost")
})
}

View File

@ -0,0 +1,16 @@
services:
- name: dns
addr: :1053
handler:
type: dns
metadata:
dns: udp://dns-server:5353
timeout: 5s
listener:
type: dns
bypass: block-example
bypasses:
- name: block-example
matchers:
- test.example.com

View File

@ -0,0 +1,10 @@
services:
- name: dns
addr: :1053
handler:
type: dns
metadata:
dns: udp://127.0.0.1:1
timeout: 2s
listener:
type: dns

View File

@ -0,0 +1,22 @@
services:
- name: dns
addr: :1053
handler:
type: dns
metadata:
# Unreachable upstream so the system-DNS fallback is bypassed.
# The host mapper resolves mapped names before the exchanger is
# reached; unmapped names must hit the exchanger and fail.
dns: udp://127.0.0.1:1
timeout: 2s
listener:
type: dns
hosts: my-hosts
hosts:
- name: my-hosts
mappings:
- ip: 10.0.0.100
hostname: mapped.example.com
- ip: 192.168.1.200
hostname: aaaa.mapped.example.com

View File

@ -0,0 +1,16 @@
services:
- name: dns
addr: :1053
handler:
type: dns
metadata:
dns: udp://dns-server:5353
timeout: 5s
listener:
type: dns
rlimiter: limiter-0
rlimiters:
- name: limiter-0
limits:
- "$ 1000"

View File

@ -0,0 +1,12 @@
services:
- name: dns
addr: :1053
handler:
type: dns
metadata:
dns: tcp://dns-server:5353
timeout: 5s
listener:
type: dns
metadata:
mode: tcp

View File

@ -0,0 +1,12 @@
services:
- name: dns
addr: :1053
handler:
type: dns
metadata:
dns: udp://dns-server:5353
timeout: 5s
listener:
type: dns
metadata:
mode: tls

View File

@ -0,0 +1,10 @@
services:
- name: dns
addr: :1053
handler:
type: dns
metadata:
dns: udp://dns-server:5353
timeout: 5s
listener:
type: dns

0
tests/e2e/testdata/file/.empty vendored 100644
View File

View File

@ -0,0 +1 @@
<html><body>gost file index</body></html>

View File

@ -0,0 +1,9 @@
services:
- name: file-server
addr: :8080
handler:
type: file
metadata:
file.dir: /srv/files
listener:
type: tcp

View File

@ -0,0 +1,15 @@
services:
- name: file-server-auth
addr: :8080
handler:
type: file
auther: auther-0
metadata:
file.dir: /srv/files
listener:
type: tcp
authers:
- name: auther-0
auths:
- username: user
password: pass

View File

@ -0,0 +1,10 @@
services:
- name: file-server-put
addr: :8080
handler:
type: file
metadata:
file.dir: /srv/files
file.put: true
listener:
type: tcp

View File

@ -0,0 +1,11 @@
services:
- name: forward-tcp
addr: :8000
handler:
type: tcp
listener:
type: tcp
forwarder:
nodes:
- name: echo
addr: tcp-echo:5678

View File

@ -0,0 +1,20 @@
services:
- name: forward-tcp
addr: :8000
handler:
type: tcp
metadata:
sniffing: true
sniffing.timeout: 2s
listener:
type: tcp
bypass: block-all
forwarder:
nodes:
- name: echo
addr: tcp-echo:5678
bypasses:
- name: block-all
matchers:
- 0.0.0.0/0

View File

@ -0,0 +1,13 @@
services:
- name: forward-tcp
addr: :8000
handler:
type: tcp
metadata:
idleTimeout: 3s
listener:
type: tcp
forwarder:
nodes:
- name: echo
addr: tcp-echo:5678

View File

@ -0,0 +1,18 @@
services:
- name: forward-tcp
addr: :8000
handler:
type: tcp
metadata:
sniffing: true
sniffing.timeout: 2s
listener:
type: tcp
forwarder:
nodes:
- name: echo-http
addr: tcp-echo:5678
protocol: http
- name: echo-tls
addr: tcp-echo:1
protocol: tls

View File

@ -0,0 +1,14 @@
services:
- name: forward-tcp
addr: :8000
handler:
type: tcp
metadata:
sniffing: true
sniffing.timeout: 2s
listener:
type: tcp
forwarder:
nodes:
- name: echo
addr: tcp-echo:5678

View File

@ -0,0 +1,11 @@
services:
- name: udp-forward
addr: :9000
handler:
type: udp
listener:
type: udp
forwarder:
nodes:
- name: echo
addr: udp-echo:5679

View File

@ -0,0 +1,15 @@
services:
- name: udp-forward
addr: :9000
handler:
type: udp
metadata:
stateless: true
listener:
type: udp
metadata:
stateless: true
forwarder:
nodes:
- name: echo
addr: udp-echo:5679

View File

@ -0,0 +1,20 @@
services:
- name: http-proxy
addr: :8080
handler:
type: http
chain: http-chain
listener:
type: tcp
chains:
- name: http-chain
hops:
- name: hop-0
nodes:
- name: node-0
addr: {{.ServerAddr}}
connector:
type: http
dialer:
type: tcp

View File

@ -0,0 +1,23 @@
services:
- name: http-proxy
addr: :8080
handler:
type: http
chain: http-chain
listener:
type: tcp
chains:
- name: http-chain
hops:
- name: hop-0
nodes:
- name: node-0
addr: {{.ServerAddr}}
connector:
type: http
auth:
username: user
password: pass
dialer:
type: tcp

View File

@ -0,0 +1,20 @@
services:
- name: http-proxy
addr: :8080
handler:
type: http
chain: https-chain
listener:
type: tcp
chains:
- name: https-chain
hops:
- name: hop-0
nodes:
- name: node-0
addr: {{.ServerAddr}}
connector:
type: http
dialer:
type: tls

View File

@ -0,0 +1 @@
<html><body>decoy-response</body></html>

View File

@ -0,0 +1,7 @@
services:
- name: http-proxy
addr: :8080
handler:
type: http
listener:
type: tcp

View File

@ -0,0 +1,16 @@
services:
- name: http-proxy-auth
addr: :8080
handler:
type: http
auther: auther-0
metadata:
# authBasicRealm: custom realm in 407 Proxy-Authenticate header
authBasicRealm: gost-e2e-realm
listener:
type: tcp
authers:
- name: auther-0
auths:
- username: user
password: pass

View File

@ -0,0 +1,12 @@
services:
- name: http-connect
addr: :8080
handler:
type: http
metadata:
# sniffing: enable protocol sniffing on CONNECT tunnels
sniffing: true
# sniffing.timeout: timeout for initial sniff read (2s)
sniffing.timeout: 2s
listener:
type: tcp

View File

@ -0,0 +1,20 @@
bypasses:
- name: bypass-0
matchers:
- 0.0.0.0/0
services:
- name: http-connect-bypass
addr: :8080
handler:
type: http
auther: auther-0
listener:
type: tcp
bypass: bypass-0
authers:
- name: auther-0
auths:
- username: user
password: pass

View File

@ -0,0 +1,25 @@
bypasses:
- name: bypass-0
matchers:
- 0.0.0.0/0
services:
- name: http-headers
addr: :8080
handler:
type: http
auther: auther-0
metadata:
header:
X-Proxy-Info: gost-e2e
X-Custom: test-value
proxyAgent: gost-e2e/1.0
listener:
type: tcp
bypass: bypass-0
authers:
- name: auther-0
auths:
- username: user
password: pass

View File

@ -0,0 +1,9 @@
services:
- name: http-idle
addr: :8080
handler:
type: http
metadata:
idleTimeout: 3s
listener:
type: tcp

View File

@ -0,0 +1,20 @@
services:
- name: http-proxy-meta
addr: :8080
handler:
type: http
auther: auther-0
metadata:
# probeResist: decoy response on auth failure (code:404 hides the proxy)
probeResist: code:404
# keepalive: enable HTTP keep-alive on upstream transport (parse test)
keepalive: true
# compression: enable HTTP compression on upstream transport (parse test)
compression: true
listener:
type: tcp
authers:
- name: auther-0
auths:
- username: user
password: pass

View File

@ -0,0 +1,16 @@
services:
- name: http-proxy
addr: :8080
handler:
type: http
auther: auther-0
metadata:
probeResist: file:/tmp/decoy.html
listener:
type: tcp
authers:
- name: auther-0
auths:
- username: user
password: pass

View File

@ -0,0 +1,16 @@
services:
- name: http-proxy
addr: :8080
handler:
type: http
auther: auther-0
metadata:
probeResist: host:tcp-echo:5678
listener:
type: tcp
authers:
- name: auther-0
auths:
- username: user
password: pass

View File

@ -0,0 +1,17 @@
services:
- name: http-proxy
addr: :8080
handler:
type: http
auther: auther-0
metadata:
probeResist: code:404
knock: secret.example.com
listener:
type: tcp
authers:
- name: auther-0
auths:
- username: user
password: pass

View File

@ -0,0 +1,16 @@
services:
- name: http-proxy
addr: :8080
handler:
type: http
auther: auther-0
metadata:
probeResist: web:tcp-echo:5678
listener:
type: tcp
authers:
- name: auther-0
auths:
- username: user
password: pass

View File

@ -0,0 +1,7 @@
services:
- name: https-server
addr: :8443
handler:
type: http
listener:
type: tls

View File

@ -0,0 +1,9 @@
services:
- name: http-udp
addr: :8080
handler:
type: http
metadata:
udp: true
listener:
type: tcp

View File

@ -0,0 +1,20 @@
services:
- name: http-proxy
addr: :8080
handler:
type: http
chain: h2-chain
listener:
type: tcp
chains:
- name: h2-chain
hops:
- name: hop-0
nodes:
- name: node-0
addr: {{.ServerAddr}}
connector:
type: http2
dialer:
type: http2

View File

@ -0,0 +1,23 @@
services:
- name: http-proxy
addr: :8080
handler:
type: http
chain: h2-chain
listener:
type: tcp
chains:
- name: h2-chain
hops:
- name: hop-0
nodes:
- name: node-0
addr: {{.ServerAddr}}
connector:
type: http2
auth:
username: user
password: pass
dialer:
type: http2

View File

@ -0,0 +1,7 @@
services:
- name: h2-proxy
addr: :8443
handler:
type: http2
listener:
type: http2

View File

@ -0,0 +1,18 @@
services:
- name: h2-proxy-auth
addr: :8443
handler:
type: http2
auther: auther-0
metadata:
# authBasicRealm: custom realm in 407 Proxy-Authenticate header
authBasicRealm: gost-e2e-realm
# hash: pin upstream selection by request host (metadata parse coverage)
hash: host
listener:
type: http2
authers:
- name: auther-0
auths:
- username: user
password: pass

View File

@ -0,0 +1,20 @@
bypasses:
- name: bypass-0
matchers:
- 0.0.0.0/0
services:
- name: h2-proxy-bypass
addr: :8443
handler:
type: http2
auther: auther-0
listener:
type: http2
bypass: bypass-0
authers:
- name: auther-0
auths:
- username: user
password: pass

View File

@ -0,0 +1,20 @@
services:
- name: h2-proxy-pr
addr: :8443
handler:
type: http2
auther: auther-0
metadata:
# probeResist: hide the proxy behind a 404 decoy on auth failure
probeResist: code:404
# header: custom response headers set on proxy responses
header:
X-Proxy-Info: gost-e2e
authBasicRealm: gost-e2e-realm
listener:
type: http2
authers:
- name: auther-0
auths:
- username: user
password: pass

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

View File

@ -0,0 +1,30 @@
services:
- name: proxy
addr: :8080
handler:
type: http
chain: my-chain
listener:
type: tcp
- name: dummy-1
addr: :18081
handler:
type: http
chains:
- name: my-chain
hops:
- name: hop-1
selector:
strategy: parallel
nodes:
- name: node-1
addr: 127.0.0.1:18081
connector:
type: http
# non existed node
- name: node-2
addr: 127.0.0.1:18082
connector:
type: http

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

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

View File

@ -0,0 +1,7 @@
services:
- name: pht-server
addr: :8443
handler:
type: http
listener:
type: pht

View File

@ -0,0 +1,7 @@
services:
- name: phts-server
addr: :8443
handler:
type: http
listener:
type: phts

View File

@ -0,0 +1,23 @@
services:
- name: http-proxy
addr: :8080
handler:
type: http
chain: ss-tcp-chain
listener:
type: tcp
chains:
- name: ss-tcp-chain
hops:
- name: ss-hop
nodes:
- name: ss-node
addr: {{.ServerAddr}}
connector:
type: ss
auth:
username: 2022-blake3-aes-128-gcm
password: MTIzNDU2Nzg5MDEyMzQ1Ng==
dialer:
type: tcp

View File

@ -0,0 +1,23 @@
services:
- name: http-proxy
addr: :8080
handler:
type: http
chain: ss-tcp-chain
listener:
type: tcp
chains:
- name: ss-tcp-chain
hops:
- name: ss-hop
nodes:
- name: ss-node
addr: {{.ServerAddr}}
connector:
type: ss
auth:
username: 2022-blake3-aes-128-gcm
password: MTIzNDU2Nzg5MDEyMzQ1Ng==:Vbwi6yqCwvPMPR1bCi32Dg==
dialer:
type: tcp

View File

@ -0,0 +1,23 @@
services:
- name: http-proxy
addr: :8080
handler:
type: http
chain: ss-tcp-chain
listener:
type: tcp
chains:
- name: ss-tcp-chain
hops:
- name: ss-hop
nodes:
- name: ss-node
addr: {{.ServerAddr}}
connector:
type: ss
auth:
username: 2022-blake3-aes-256-gcm
password: MTIzNDU2Nzg5MDEyMzQ1NjEyMzQ1Njc4OTAxMjM0NTY=
dialer:
type: tcp

View File

@ -0,0 +1,23 @@
services:
- name: http-proxy
addr: :8080
handler:
type: http
chain: ss-tcp-chain
listener:
type: tcp
chains:
- name: ss-tcp-chain
hops:
- name: ss-hop
nodes:
- name: ss-node
addr: {{.ServerAddr}}
connector:
type: ss
auth:
username: 2022-blake3-aes-256-gcm
password: MTIzNDU2Nzg5MDEyMzQ1NjEyMzQ1Njc4OTAxMjM0NTY=:YWJjZGVmZ2hpamtsbW5vcHFyc3R1dnd4eXoxMjM0NTY=
dialer:
type: tcp

View File

@ -0,0 +1,23 @@
services:
- name: http-proxy
addr: :8080
handler:
type: http
chain: ss-tcp-chain
listener:
type: tcp
chains:
- name: ss-tcp-chain
hops:
- name: ss-hop
nodes:
- name: ss-node
addr: {{.ServerAddr}}
connector:
type: ss
auth:
username: aes-256-gcm
password: secret
dialer:
type: tcp

View File

@ -0,0 +1,23 @@
services:
- name: http-proxy
addr: :8080
handler:
type: http
chain: ss-tcp-chain
listener:
type: tcp
chains:
- name: ss-tcp-chain
hops:
- name: ss-hop
nodes:
- name: ss-node
addr: {{.ServerAddr}}
connector:
type: ss
auth:
username: chacha20-ietf-poly1305
password: secret
dialer:
type: tcp

View File

@ -0,0 +1,10 @@
services:
- name: ss-server
addr: :8388
handler:
type: ss
auth:
username: 2022-blake3-aes-128-gcm
password: MTIzNDU2Nzg5MDEyMzQ1Ng==
listener:
type: tcp

Some files were not shown because too many files have changed in this diff Show More