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