Compare commits

..

1 Commits

Author SHA1 Message Date
wenyifan b8b4e99aec dev 2023-11-21 10:22:09 +08:00
103 changed files with 1766 additions and 5403 deletions
+70 -43
View File
@@ -3,62 +3,89 @@
name: docker name: docker
on: on:
push: push:
branches: branches:
- master - master
tags: tags:
- 'v*' - 'v*'
permissions:
contents: read
# New pushes cancel an in-progress build for the same ref. Tags are isolated
# by ref, so re-pushing a release tag only de-dupes itself, never another tag.
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs: jobs:
build: build:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Prepare
id: prepare
run: |
DOCKER_IMAGE=${{ secrets.DOCKER_IMAGE }}
VERSION=latest
# If this is git tag, use the tag name as a docker tag
if [[ $GITHUB_REF == refs/tags/* ]]; then
VERSION=${GITHUB_REF#refs/tags/v}
fi
TAGS="${DOCKER_IMAGE}:${VERSION}"
# If the VERSION looks like a version number, assume that
# this is the most recent version of the image and also
# tag it 'latest'.
if [[ $VERSION =~ ^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}$ ]]; then
MAJOR_VERSION=`echo $VERSION | awk '{split($0,a,"."); print a[1]}'`
MINOR_VERSION=`echo $VERSION | awk '{split($0,a,"."); print a[2]}'`
TAGS="$TAGS,${DOCKER_IMAGE}:${MAJOR_VERSION},${DOCKER_IMAGE}:${MAJOR_VERSION}.${MINOR_VERSION},${DOCKER_IMAGE}:latest"
fi
# Set output parameters.
echo ::set-output name=tags::${TAGS}
echo ::set-output name=docker_image::${DOCKER_IMAGE}
echo ::set-output name=docker_platforms::linux/386,linux/amd64,linux/arm/v6,linux/arm/v7,linux/arm64/v8,linux/s390x
# https://github.com/crazy-max/ghaction-docker-buildx
- name: Set up Docker Buildx
id: buildx
uses: crazy-max/ghaction-docker-buildx@v1
with:
version: latest
- name: Environment
run: |
echo home=$HOME
echo git_ref=$GITHUB_REF
echo git_sha=$GITHUB_SHA
echo version=${{ steps.prepare.outputs.version }}
echo image=${{ steps.prepare.outputs.docker_image }}
echo platforms=${{ steps.prepare.outputs.docker_platforms }}
echo avail_platforms=${{ steps.buildx.outputs.platforms }}
# https://github.com/actions/checkout
- name: Checkout - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v4
# Auto-generates image tags, replacing the former hand-rolled shell: - name: Docker Buildx (no push)
# - master push -> :latest run: |
# - clean release tag vX.Y.Z -> :X.Y.Z :X :X.Y :latest docker buildx bake \
# - nightly tag vX.Y.Z-nightly.<date> -> :X.Y.Z-nightly.<date> (no major/minor/latest) --set ${{ github.event.repository.name }}.platform=${{ steps.prepare.outputs.docker_platforms }} \
- name: Docker meta --set ${{ github.event.repository.name }}.output=type=image,push=false \
id: meta --set ${{ github.event.repository.name }}.tags="${{ steps.prepare.outputs.tags }}" \
uses: docker/metadata-action@v5 --file docker-compose.yaml
with:
images: ${{ secrets.DOCKER_IMAGE }}
flavor: latest=false
tags: |
type=raw,value=latest,enable=${{ github.ref == format('refs/heads/{0}', github.event.repository.default_branch) || (startsWith(github.ref, 'refs/tags/v') && !contains(github.ref_name, '-')) }}
type=semver,pattern={{version}}
type=semver,pattern={{major}},enable=${{ !contains(github.ref_name, '-') }}
type=semver,pattern={{major}}.{{minor}},enable=${{ !contains(github.ref_name, '-') }}
- name: Set up Docker Buildx - name: Docker Login
id: buildx if: success()
uses: docker/setup-buildx-action@v3 env:
DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }}
run: |
echo "${DOCKER_PASSWORD}" | docker login --username "${{ secrets.DOCKER_USERNAME }}" --password-stdin
- name: Login to DockerHub - name: Docker Buildx (push)
if: github.event_name != 'pull_request' if: success()
uses: docker/login-action@v3 run: |
with: docker buildx bake \
username: ${{ secrets.DOCKER_USERNAME }} --set ${{ github.event.repository.name }}.platform=${{ steps.prepare.outputs.docker_platforms }} \
password: ${{ secrets.DOCKER_PASSWORD }} --set ${{ github.event.repository.name }}.output=type=image,push=true \
--set ${{ github.event.repository.name }}.tags="${{ steps.prepare.outputs.tags }}" \
--file docker-compose.yaml
- name: Buildx and push - name: Clear
uses: docker/build-push-action@v6 if: always()
with: run: |
platforms: linux/386,linux/amd64,linux/arm/v6,linux/arm/v7,linux/arm64/v8,linux/s390x,linux/riscv64 rm -f ${HOME}/.docker/config.json
push: true
tags: ${{ steps.meta.outputs.tags }}
# Reuse build layers across runs via the GitHub Actions cache.
cache-from: type=gha
cache-to: type=gha,mode=max
+8 -10
View File
@@ -8,11 +8,8 @@ on:
permissions: permissions:
contents: write contents: write
# packages: write
# Never cancel an in-flight release; only de-dupe a re-pushed tag. # issues: write
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: false
jobs: jobs:
goreleaser: goreleaser:
@@ -22,16 +19,17 @@ jobs:
with: with:
fetch-depth: 0 fetch-depth: 0
- run: git fetch --force --tags - run: git fetch --force --tags
- uses: actions/setup-go@v5 - uses: actions/setup-go@v4
with: with:
go-version: '1.26' go-version: '1.21'
cache: true cache: true
# UPX is disabled in .goreleaser.yaml (see #863); the install step was removed. # More assembly might be required: Docker logins, GPG, etc. It all depends
- uses: goreleaser/goreleaser-action@v6 # on your needs.
- uses: goreleaser/goreleaser-action@v5
with: with:
# either 'goreleaser' (default) or 'goreleaser-pro': # either 'goreleaser' (default) or 'goreleaser-pro':
distribution: goreleaser distribution: goreleaser
version: '~> v2' version: latest
args: release --clean args: release --clean
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+8 -36
View File
@@ -6,32 +6,8 @@ on:
- cron: '00 15 * * *' - cron: '00 15 * * *'
workflow_dispatch: workflow_dispatch:
# Only one nightly run at a time; a new run cancels a stale one.
concurrency:
group: ${{ github.workflow }}
cancel-in-progress: true
jobs: jobs:
check_date:
runs-on: ubuntu-latest
name: Check latest commit
outputs:
should_run: ${{ steps.should_run.outputs.should_run }}
steps:
- uses: actions/checkout@v4
- name: print latest_commit
run: echo ${{ github.sha }}
- id: should_run
name: Skip if no commit in the last 24 hours
if: ${{ github.event_name == 'schedule' }}
run: |
if [ "$(git rev-list --count --after='24 hours' ${{ github.sha }})" -eq 0 ]; then
echo "should_run=false" >> $GITHUB_OUTPUT
fi
trigger-nightly: trigger-nightly:
needs: check_date
if: ${{ needs.check_date.outputs.should_run != 'false' }}
name: Push tag for nightly build name: Push tag for nightly build
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
@@ -53,19 +29,15 @@ jobs:
DESCRIBE=`git tag -l --sort=-v:refname | grep -v nightly | head -n 1` DESCRIBE=`git tag -l --sort=-v:refname | grep -v nightly | head -n 1`
MAJOR_VERSION=`echo $DESCRIBE | awk '{split($0,a,"."); print a[1]}'` MAJOR_VERSION=`echo $DESCRIBE | awk '{split($0,a,"."); print a[1]}'`
MINOR_VERSION=`echo $DESCRIBE | awk '{split($0,a,"."); print a[2]}'` MINOR_VERSION=`echo $DESCRIBE | awk '{split($0,a,"."); print a[2]}'`
PATCH_VERSION=`echo $DESCRIBE | awk '{split($0,a,"."); print a[3]}'` # MINOR_VERSION="$((${MINOR_VERSION} + 1))"
PATCH_VERSION="$((${PATCH_VERSION} + 1))" TAG="${MAJOR_VERSION}.${MINOR_VERSION}.0-nightly.$(date +'%Y%m%d')"
TAG="${MAJOR_VERSION}.${MINOR_VERSION}.${PATCH_VERSION}-nightly.$(date +'%Y%m%d')"
git tag -a $TAG -m "$TAG: nightly build" git tag -a $TAG -m "$TAG: nightly build"
git push origin $TAG git push origin $TAG
- name: 'Clean up nightly releases' - name: 'Clean up nightly releases'
uses: dev-drprasad/delete-older-releases@v0.3.2
with:
keep_latest: 2
delete_tags: true
delete_tag_pattern: nightly
env: env:
GH_TOKEN: ${{ secrets.NIGHTLY_BUILD_GH_TOKEN }} GITHUB_TOKEN: ${{ secrets.NIGHTLY_BUILD_GH_TOKEN }}
run: |
# Keep the 2 most recent nightly releases; delete the rest and their tags.
# Replaces the archived dev-drprasad/delete-older-releases action.
gh release list --json tagName,createdAt --limit 100 \
--jq '[.[] | select(.tagName | test("nightly"))] | sort_by(.createdAt) | reverse | .[2:] | .[].tagName' \
| while read -r tag; do
gh release delete "$tag" --yes --cleanup-tag || true
done
+1 -2
View File
@@ -32,10 +32,9 @@ _testmain.go
*.bak *.bak
cmd/gost/gost cmd/gost/gost
gost
snap snap
*.pem *.pem
/*.yaml *.yaml
*.txt *.txt
dist/ dist/
-8
View File
@@ -28,23 +28,15 @@ builds:
- linux_mips64le - linux_mips64le
- linux_s390x - linux_s390x
- linux_riscv64 - linux_riscv64
- linux_loong64
- freebsd_386 - freebsd_386
- freebsd_amd64 - freebsd_amd64
- windows_386 - windows_386
- windows_amd64 - windows_amd64
- windows_amd64_v3 - windows_amd64_v3
- windows_arm64 - windows_arm64
- android_arm64
ldflags: ldflags:
- "-s -w -X 'main.version={{ .Tag }}'" - "-s -w -X 'main.version={{ .Tag }}'"
upx:
- # UPX compression. Disabled by default (see #863): upx --best/--lzma/--brute
# adds ~3s startup time on low-spec systems (linux/arm). Release builds can
# opt in via GORELEASER_UPX=true environment variable.
enabled: false
archives: archives:
- format: tar.gz - format: tar.gz
# use zip for windows archives # use zip for windows archives
-66
View File
@@ -1,66 +0,0 @@
# CLAUDE.md — gost/
CLI binary entry point for GOST (GO Simple Tunnel). This module compiles the `gost` command.
## Build & Run
```bash
cd gost && go build ./cmd/gost/...
# Cross-compile
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags="-s -w" ./cmd/gost/...
# Build all platforms
make all
# Run
./gost -L "http://:8080" -F "socks5://:1080"
./gost -C gost.yml
```
## Structure
| File | Purpose |
|------|---------|
| `cmd/gost/main.go` | CLI entry point: flag parsing, multi-worker mode (`--` separator), program launch via `svc.Run` |
| `cmd/gost/program.go` | Service lifecycle (`Init`/`Start`/`Stop`), config loading, API/metrics/profiling servers, SIGHUP reload |
| `cmd/gost/register.go` | Blank imports for all built-in handlers, listeners, dialers, connectors — triggers `init()` registration |
| `cmd/gost/version.go` | Version string (`3.3.0`) |
## CLI Flags
| Flag | Usage |
|------|-------|
| `-L` | Inline service definition (repeatable) |
| `-F` | Inline chain node definition (repeatable) |
| `-C` | Path to config file (YAML/JSON) |
| `-D` / `-DD` | Debug / trace logging |
| `-api` | API service address (e.g. `:8080`) |
| `-metrics` | Prometheus metrics address |
| `-O` | Output merged config as yaml or json, then exit |
| `-V` | Print version and exit |
## Multi-Worker Mode
Arguments separated by ` -- ` spawn multiple worker processes via `exec.CommandContext`. Each worker runs as a child process with `_GOST_ID` set. Any worker's exit cancels all others. This is triggered in `init()` before flag parsing.
## Service Lifecycle
- `Init` → calls `parser.Init()` with all CLI/config inputs
- `Start``parser.Parse()``loader.Load()``p.run()` (starts services, API, metrics, profiling)
- `Stop` → cancels reload context, closes all services
- SIGHUP → `reloadConfig()` re-parses and re-runs without restarting the process
## Key Dependencies
- `github.com/go-gost/core` — interface definitions
- `github.com/go-gost/x` — all implementations, config, registry
- `github.com/judwhite/go-svc` — OS service framework (handles daemon/SIGHUP/SIGTERM)
## Registration Pattern
All components register via `init()` side-effects in their packages. `cmd/gost/register.go` triggers them with blank imports. When adding a new built-in handler/listener/dialer/connector, add a blank import here.
## Tests
Tests are in `tests/e2e/` — integration tests using the built binary. No unit tests. Run with `go test ./tests/e2e/...`.
+13 -22
View File
@@ -1,37 +1,28 @@
FROM --platform=$BUILDPLATFORM tonistiigi/xx:1.6.1 AS xx FROM --platform=$BUILDPLATFORM golang:1.21-alpine as builder
FROM --platform=$BUILDPLATFORM golang:1.26-alpine3.23 AS builder # Convert TARGETPLATFORM to GOARCH format
# https://github.com/tonistiigi/xx
# UPX compression disabled by default (see #863): upx --best adds ~3s startup COPY --from=tonistiigi/xx:golang / /
# time on low-spec systems (linux/arm). Builds can opt in by uncommenting the
# upx install line and adding upx --best to the build command below.
# RUN apk add --no-cache upx || echo "upx not found"
COPY --from=xx / /
ARG TARGETPLATFORM ARG TARGETPLATFORM
RUN xx-info env RUN apk add --no-cache musl-dev git gcc
ENV CGO_ENABLED=0 ADD . /src
ENV XX_VERIFY_STATIC=1 WORKDIR /src
WORKDIR /app ENV GO111MODULE=on
COPY . . RUN cd cmd/gost && go env && go build
RUN cd cmd/gost && \ FROM alpine:latest
xx-go build -ldflags "-s -w" && \
xx-verify gost
FROM alpine:3.23 # add iptables for tun/tap
RUN apk add --no-cache iptables
# add iptables/nftables for tun/tap
RUN apk add --no-cache iptables nftables
WORKDIR /bin/ WORKDIR /bin/
COPY --from=builder /app/cmd/gost/gost . COPY --from=builder /src/cmd/gost/gost .
ENTRYPOINT ["/bin/gost"] ENTRYPOINT ["/bin/gost"]
+5 -17
View File
@@ -2,7 +2,7 @@
### GO语言实现的安全隧道 ### GO语言实现的安全隧道
[![zh](https://img.shields.io/badge/Chinese%20README-green)](README.md) [![en](https://img.shields.io/badge/English%20README-gray)](README_en.md) [English README](README_en.md)
## 功能特性 ## 功能特性
@@ -13,7 +13,7 @@
- [x] [反向代理](https://gost.run/tutorials/reverse-proxy/)和[隧道](https://gost.run/tutorials/reverse-proxy-tunnel/) - [x] [反向代理](https://gost.run/tutorials/reverse-proxy/)和[隧道](https://gost.run/tutorials/reverse-proxy-tunnel/)
- [x] [TCP/UDP透明代理](https://gost.run/tutorials/redirect/) - [x] [TCP/UDP透明代理](https://gost.run/tutorials/redirect/)
- [x] DNS[解析](https://gost.run/concepts/resolver/)和[代理](https://gost.run/tutorials/dns/) - [x] DNS[解析](https://gost.run/concepts/resolver/)和[代理](https://gost.run/tutorials/dns/)
- [x] [TUN/TAP设备](https://gost.run/tutorials/tuntap/)与[TUN2SOCKS](https://gost.run/tutorials/tungo/) - [x] [TUN/TAP设备](https://gost.run/tutorials/tuntap/)
- [x] [负载均衡](https://gost.run/concepts/selector/) - [x] [负载均衡](https://gost.run/concepts/selector/)
- [x] [路由控制](https://gost.run/concepts/bypass/) - [x] [路由控制](https://gost.run/concepts/bypass/)
- [x] [准入控制](https://gost.run/concepts/admission/) - [x] [准入控制](https://gost.run/concepts/admission/)
@@ -22,7 +22,7 @@
- [x] [Prometheus监控指标](https://gost.run/tutorials/metrics/) - [x] [Prometheus监控指标](https://gost.run/tutorials/metrics/)
- [x] [动态配置](https://gost.run/tutorials/api/config/) - [x] [动态配置](https://gost.run/tutorials/api/config/)
- [x] [Web API](https://gost.run/tutorials/api/overview/) - [x] [Web API](https://gost.run/tutorials/api/overview/)
- [x] [GUI](https://github.com/go-gost/gostctl)/[WebUI](https://github.com/go-gost/gost-ui) - [ ] Web UI
## 概览 ## 概览
@@ -79,27 +79,15 @@ go build
docker run --rm gogost/gost -V docker run --rm gogost/gost -V
``` ```
## 工具
### GUI
[go-gost/gostctl](https://github.com/go-gost/gostctl)
### WebUI
[go-gost/gost-ui](https://github.com/go-gost/gost-ui)
### Shadowsocks Android插件 ### Shadowsocks Android插件
[hamid-nazari/ShadowsocksGostPlugin](https://github.com/hamid-nazari/ShadowsocksGostPlugin) [xausky/ShadowsocksGostPlugin](https://github.com/xausky/ShadowsocksGostPlugin)
## 帮助与支持 ## 帮助与支持
Wiki站点:[https://gost.run](https://gost.run) Wiki站点:[https://gost.run](https://gost.run)
YouTube: [https://www.youtube.com/@gost-tunnel](https://www.youtube.com/@gost-tunnel) Telegram讨论群:[https://t.me/gogost](https://t.me/gogost)
Telegram[https://t.me/gogost](https://t.me/gogost)
Google讨论组:[https://groups.google.com/d/forum/go-gost](https://groups.google.com/d/forum/go-gost) Google讨论组:[https://groups.google.com/d/forum/go-gost](https://groups.google.com/d/forum/go-gost)
+3 -17
View File
@@ -2,8 +2,6 @@
### A simple security tunnel written in golang ### A simple security tunnel written in golang
[![en](https://img.shields.io/badge/English%20README-green)](README_en.md) [![zh](https://img.shields.io/badge/Chinese%20README-gray)](README.md)
## Features ## Features
- [x] [Listening on multiple ports](https://gost.run/en/getting-started/quick-start/) - [x] [Listening on multiple ports](https://gost.run/en/getting-started/quick-start/)
@@ -13,7 +11,7 @@
- [x] [Reverse Proxy](https://gost.run/en/tutorials/reverse-proxy/) and [Tunnel](https://gost.run/en/tutorials/reverse-proxy-tunnel/) - [x] [Reverse Proxy](https://gost.run/en/tutorials/reverse-proxy/) and [Tunnel](https://gost.run/en/tutorials/reverse-proxy-tunnel/)
- [x] [TCP/UDP transparent proxy](https://gost.run/en/tutorials/redirect/) - [x] [TCP/UDP transparent proxy](https://gost.run/en/tutorials/redirect/)
- [x] DNS [resolver](https://gost.run/en/concepts/resolver/) and [proxy](https://gost.run/en/tutorials/dns/) - [x] DNS [resolver](https://gost.run/en/concepts/resolver/) and [proxy](https://gost.run/en/tutorials/dns/)
- [x] [TUN/TAP device](https://gost.run/en/tutorials/tuntap/) and [TUN2SOCKS](https://gost.run/en/tutorials/tungo/) - [x] [TUN/TAP device](https://gost.run/en/tutorials/tuntap/)
- [x] [Load balancing](https://gost.run/en/concepts/selector/) - [x] [Load balancing](https://gost.run/en/concepts/selector/)
- [x] [Routing control](https://gost.run/en/concepts/bypass/) - [x] [Routing control](https://gost.run/en/concepts/bypass/)
- [x] [Admission control](https://gost.run/en/concepts/limiter/) - [x] [Admission control](https://gost.run/en/concepts/limiter/)
@@ -22,7 +20,7 @@
- [x] [Prometheus metrics](https://gost.run/en/tutorials/metrics/) - [x] [Prometheus metrics](https://gost.run/en/tutorials/metrics/)
- [x] [Dynamic configuration](https://gost.run/en/tutorials/api/config/) - [x] [Dynamic configuration](https://gost.run/en/tutorials/api/config/)
- [x] [Web API](https://gost.run/en/tutorials/api/overview/) - [x] [Web API](https://gost.run/en/tutorials/api/overview/)
- [x] [GUI](https://github.com/go-gost/gostctl)/[WebUI](https://github.com/go-gost/gost-ui) - [ ] Web UI
## Overview ## Overview
@@ -79,26 +77,14 @@ go build
docker run --rm gogost/gost -V docker run --rm gogost/gost -V
``` ```
## Tools
### GUI
[go-gost/gostctl](https://github.com/go-gost/gostctl)
### WebUI
[go-gost/gost-ui](https://github.com/go-gost/gost-ui)
### Shadowsocks Android ### Shadowsocks Android
[hamid-nazari/ShadowsocksGostPlugin](https://github.com/hamid-nazari/ShadowsocksGostPlugin) [xausky/ShadowsocksGostPlugin](https://github.com/xausky/ShadowsocksGostPlugin)
## Support ## Support
Wiki: [https://gost.run](https://gost.run/en/) Wiki: [https://gost.run](https://gost.run/en/)
YouTube: [https://www.youtube.com/@gost-tunnel](https://www.youtube.com/@gost-tunnel)
Telegram: [https://t.me/gogost](https://t.me/gogost) Telegram: [https://t.me/gogost](https://t.me/gogost)
Google group: [https://groups.google.com/d/forum/go-gost](https://groups.google.com/d/forum/go-gost) Google group: [https://groups.google.com/d/forum/go-gost](https://groups.google.com/d/forum/go-gost)
+596
View File
@@ -0,0 +1,596 @@
package main
import (
"encoding/base64"
"errors"
"fmt"
"net/url"
"os"
"strconv"
"strings"
"time"
mdutil "github.com/go-gost/core/metadata/util"
"github.com/go-gost/x/config"
"github.com/go-gost/x/limiter/conn"
"github.com/go-gost/x/limiter/traffic"
mdx "github.com/go-gost/x/metadata"
"github.com/go-gost/x/registry"
)
var (
ErrInvalidCmd = errors.New("invalid cmd")
ErrInvalidNode = errors.New("invalid node")
)
type stringList []string
func (l *stringList) String() string {
return fmt.Sprintf("%s", *l)
}
func (l *stringList) Set(value string) error {
*l = append(*l, value)
return nil
}
func buildConfigFromCmd(services, nodes stringList) (*config.Config, error) {
namePrefix := ""
cfg := &config.Config{}
var chain *config.ChainConfig
if len(nodes) > 0 {
chain = &config.ChainConfig{
Name: fmt.Sprintf("%schain-0", namePrefix),
}
cfg.Chains = append(cfg.Chains, chain)
}
for i, node := range nodes {
url, err := normCmd(node)
if err != nil {
return nil, err
}
nodeConfig, err := buildNodeConfig(url)
if err != nil {
return nil, err
}
nodeConfig.Name = fmt.Sprintf("%snode-0", namePrefix)
var nodes []*config.NodeConfig
for _, host := range strings.Split(nodeConfig.Addr, ",") {
if host == "" {
continue
}
nodeCfg := &config.NodeConfig{}
*nodeCfg = *nodeConfig
nodeCfg.Name = fmt.Sprintf("%snode-%d", namePrefix, len(nodes))
nodeCfg.Addr = host
nodes = append(nodes, nodeCfg)
}
mc := nodeConfig.Connector.Metadata
md := mdx.NewMetadata(mc)
hopConfig := &config.HopConfig{
Name: fmt.Sprintf("%shop-%d", namePrefix, i),
Selector: parseSelector(mc),
Nodes: nodes,
}
if v := mdutil.GetString(md, "bypass"); v != "" {
bypassCfg := &config.BypassConfig{
Name: fmt.Sprintf("%sbypass-%d", namePrefix, len(cfg.Bypasses)),
}
if v[0] == '~' {
bypassCfg.Whitelist = true
v = v[1:]
}
for _, s := range strings.Split(v, ",") {
if s == "" {
continue
}
bypassCfg.Matchers = append(bypassCfg.Matchers, s)
}
hopConfig.Bypass = bypassCfg.Name
cfg.Bypasses = append(cfg.Bypasses, bypassCfg)
delete(mc, "bypass")
}
if v := mdutil.GetString(md, "resolver"); v != "" {
resolverCfg := &config.ResolverConfig{
Name: fmt.Sprintf("%sresolver-%d", namePrefix, len(cfg.Resolvers)),
}
for _, rs := range strings.Split(v, ",") {
if rs == "" {
continue
}
resolverCfg.Nameservers = append(
resolverCfg.Nameservers,
&config.NameserverConfig{
Addr: rs,
},
)
}
hopConfig.Resolver = resolverCfg.Name
cfg.Resolvers = append(cfg.Resolvers, resolverCfg)
delete(mc, "resolver")
}
if v := mdutil.GetString(md, "hosts"); v != "" {
hostsCfg := &config.HostsConfig{
Name: fmt.Sprintf("%shosts-%d", namePrefix, len(cfg.Hosts)),
}
for _, s := range strings.Split(v, ",") {
ss := strings.SplitN(s, ":", 2)
if len(ss) != 2 {
continue
}
hostsCfg.Mappings = append(
hostsCfg.Mappings,
&config.HostMappingConfig{
Hostname: ss[0],
IP: ss[1],
},
)
}
hopConfig.Hosts = hostsCfg.Name
cfg.Hosts = append(cfg.Hosts, hostsCfg)
delete(mc, "hosts")
}
if v := mdutil.GetString(md, "interface"); v != "" {
hopConfig.Interface = v
delete(mc, "interface")
}
if v := mdutil.GetInt(md, "so_mark"); v > 0 {
hopConfig.SockOpts = &config.SockOptsConfig{
Mark: v,
}
delete(mc, "so_mark")
}
chain.Hops = append(chain.Hops, hopConfig)
}
for i, svc := range services {
url, err := normCmd(svc)
if err != nil {
return nil, err
}
service, err := buildServiceConfig(url)
if err != nil {
return nil, err
}
service.Name = fmt.Sprintf("%sservice-%d", namePrefix, i)
if chain != nil {
if service.Listener.Type == "rtcp" || service.Listener.Type == "rudp" {
service.Listener.Chain = chain.Name
} else {
service.Handler.Chain = chain.Name
}
}
cfg.Services = append(cfg.Services, service)
mh := service.Handler.Metadata
md := mdx.NewMetadata(mh)
if v := mdutil.GetInt(md, "retries"); v > 0 {
service.Handler.Retries = v
delete(mh, "retries")
}
if v := mdutil.GetString(md, "admission"); v != "" {
admCfg := &config.AdmissionConfig{
Name: fmt.Sprintf("%sadmission-%d", namePrefix, len(cfg.Admissions)),
}
if v[0] == '~' {
admCfg.Whitelist = true
v = v[1:]
}
for _, s := range strings.Split(v, ",") {
if s == "" {
continue
}
admCfg.Matchers = append(admCfg.Matchers, s)
}
service.Admission = admCfg.Name
cfg.Admissions = append(cfg.Admissions, admCfg)
delete(mh, "admission")
}
if v := mdutil.GetString(md, "bypass"); v != "" {
bypassCfg := &config.BypassConfig{
Name: fmt.Sprintf("%sbypass-%d", namePrefix, len(cfg.Bypasses)),
}
if v[0] == '~' {
bypassCfg.Whitelist = true
v = v[1:]
}
for _, s := range strings.Split(v, ",") {
if s == "" {
continue
}
bypassCfg.Matchers = append(bypassCfg.Matchers, s)
}
service.Bypass = bypassCfg.Name
cfg.Bypasses = append(cfg.Bypasses, bypassCfg)
delete(mh, "bypass")
}
if v := mdutil.GetString(md, "resolver"); v != "" {
resolverCfg := &config.ResolverConfig{
Name: fmt.Sprintf("%sresolver-%d", namePrefix, len(cfg.Resolvers)),
}
for _, rs := range strings.Split(v, ",") {
if rs == "" {
continue
}
resolverCfg.Nameservers = append(
resolverCfg.Nameservers,
&config.NameserverConfig{
Addr: rs,
Prefer: mdutil.GetString(md, "prefer"),
},
)
}
service.Resolver = resolverCfg.Name
cfg.Resolvers = append(cfg.Resolvers, resolverCfg)
delete(mh, "resolver")
}
if v := mdutil.GetString(md, "hosts"); v != "" {
hostsCfg := &config.HostsConfig{
Name: fmt.Sprintf("%shosts-%d", namePrefix, len(cfg.Hosts)),
}
for _, s := range strings.Split(v, ",") {
ss := strings.SplitN(s, ":", 2)
if len(ss) != 2 {
continue
}
hostsCfg.Mappings = append(
hostsCfg.Mappings,
&config.HostMappingConfig{
Hostname: ss[0],
IP: ss[1],
},
)
}
service.Hosts = hostsCfg.Name
cfg.Hosts = append(cfg.Hosts, hostsCfg)
delete(mh, "hosts")
}
in := mdutil.GetString(md, "limiter.in")
out := mdutil.GetString(md, "limiter.out")
cin := mdutil.GetString(md, "limiter.conn.in")
cout := mdutil.GetString(md, "limiter.conn.out")
if in != "" || cin != "" || out != "" || cout != "" {
limiter := &config.LimiterConfig{
Name: fmt.Sprintf("%slimiter-%d", namePrefix, len(cfg.Limiters)),
}
if in != "" || out != "" {
limiter.Limits = append(limiter.Limits,
fmt.Sprintf("%s %s %s", traffic.GlobalLimitKey, in, out))
}
if cin != "" || cout != "" {
limiter.Limits = append(limiter.Limits,
fmt.Sprintf("%s %s %s", traffic.ConnLimitKey, cin, cout))
}
service.Limiter = limiter.Name
cfg.Limiters = append(cfg.Limiters, limiter)
delete(mh, "limiter.in")
delete(mh, "limiter.out")
delete(mh, "limiter.conn.in")
delete(mh, "limiter.conn.out")
}
if climit := mdutil.GetInt(md, "climiter"); climit > 0 {
limiter := &config.LimiterConfig{
Name: fmt.Sprintf("%sclimiter-%d", namePrefix, len(cfg.CLimiters)),
Limits: []string{fmt.Sprintf("%s %d", conn.GlobalLimitKey, climit)},
}
service.CLimiter = limiter.Name
cfg.CLimiters = append(cfg.CLimiters, limiter)
delete(mh, "climiter")
}
if rlimit := mdutil.GetFloat(md, "rlimiter"); rlimit > 0 {
limiter := &config.LimiterConfig{
Name: fmt.Sprintf("%srlimiter-%d", namePrefix, len(cfg.RLimiters)),
Limits: []string{fmt.Sprintf("%s %s", conn.GlobalLimitKey, strconv.FormatFloat(rlimit, 'f', -1, 64))},
}
service.RLimiter = limiter.Name
cfg.RLimiters = append(cfg.RLimiters, limiter)
delete(mh, "rlimiter")
}
}
return cfg, nil
}
func buildServiceConfig(url *url.URL) (*config.ServiceConfig, error) {
namePrefix := ""
if v := os.Getenv("_GOST_ID"); v != "" {
namePrefix = fmt.Sprintf("go-%s@", v)
}
var handler, listener string
schemes := strings.Split(url.Scheme, "+")
if len(schemes) == 1 {
handler = schemes[0]
listener = schemes[0]
}
if len(schemes) == 2 {
handler = schemes[0]
listener = schemes[1]
}
svc := &config.ServiceConfig{
Addr: url.Host,
}
if h := registry.HandlerRegistry().Get(handler); h == nil {
handler = "auto"
}
if ln := registry.ListenerRegistry().Get(listener); ln == nil {
listener = "tcp"
if handler == "ssu" {
listener = "udp"
}
}
// forward mode
if remotes := strings.Trim(url.EscapedPath(), "/"); remotes != "" {
svc.Forwarder = &config.ForwarderConfig{
// Targets: strings.Split(remotes, ","),
}
for i, addr := range strings.Split(remotes, ",") {
svc.Forwarder.Nodes = append(svc.Forwarder.Nodes,
&config.ForwardNodeConfig{
Name: fmt.Sprintf("%starget-%d", namePrefix, i),
Addr: addr,
})
}
if handler != "relay" {
if listener == "tcp" || listener == "udp" ||
listener == "rtcp" || listener == "rudp" ||
listener == "tun" || listener == "tap" ||
listener == "dns" || listener == "unix" ||
listener == "serial" {
handler = listener
} else {
handler = "forward"
}
}
}
var auth *config.AuthConfig
if url.User != nil {
auth = &config.AuthConfig{
Username: url.User.Username(),
}
auth.Password, _ = url.User.Password()
}
m := map[string]any{}
for k, v := range url.Query() {
if len(v) > 0 {
m[k] = v[0]
}
}
md := mdx.NewMetadata(m)
if sa := mdutil.GetString(md, "auth"); sa != "" {
au, err := parseAuthFromCmd(sa)
if err != nil {
return nil, err
}
auth = au
}
delete(m, "auth")
tlsConfig := &config.TLSConfig{
CertFile: mdutil.GetString(md, "certFile", "cert"),
KeyFile: mdutil.GetString(md, "keyFile", "key"),
CAFile: mdutil.GetString(md, "caFile", "ca"),
}
delete(m, "certFile")
delete(m, "cert")
delete(m, "keyFile")
delete(m, "key")
delete(m, "caFile")
delete(m, "ca")
if tlsConfig.CertFile == "" {
tlsConfig = nil
}
if v := mdutil.GetString(md, "dns"); v != "" {
md.Set("dns", strings.Split(v, ","))
}
if svc.Forwarder != nil {
svc.Forwarder.Selector = parseSelector(m)
}
svc.Handler = &config.HandlerConfig{
Type: handler,
Auth: auth,
Metadata: m,
}
svc.Listener = &config.ListenerConfig{
Type: listener,
TLS: tlsConfig,
Metadata: m,
}
svc.Metadata = m
if svc.Listener.Type == "ssh" || svc.Listener.Type == "sshd" {
svc.Handler.Auth = nil
svc.Listener.Auth = auth
}
return svc, nil
}
func buildNodeConfig(url *url.URL) (*config.NodeConfig, error) {
var connector, dialer string
schemes := strings.Split(url.Scheme, "+")
if len(schemes) == 1 {
connector = schemes[0]
dialer = schemes[0]
}
if len(schemes) == 2 {
connector = schemes[0]
dialer = schemes[1]
}
node := &config.NodeConfig{
Addr: url.Host,
}
if c := registry.ConnectorRegistry().Get(connector); c == nil {
connector = "http"
}
if d := registry.DialerRegistry().Get(dialer); d == nil {
dialer = "tcp"
if connector == "ssu" {
dialer = "udp"
}
}
var auth *config.AuthConfig
if url.User != nil {
auth = &config.AuthConfig{
Username: url.User.Username(),
}
auth.Password, _ = url.User.Password()
}
m := map[string]any{}
for k, v := range url.Query() {
if len(v) > 0 {
m[k] = v[0]
}
}
md := mdx.NewMetadata(m)
if sauth := mdutil.GetString(md, "auth"); sauth != "" && auth == nil {
au, err := parseAuthFromCmd(sauth)
if err != nil {
return nil, err
}
auth = au
}
delete(m, "auth")
tlsConfig := &config.TLSConfig{
CertFile: mdutil.GetString(md, "certFile", "cert"),
KeyFile: mdutil.GetString(md, "keyFile", "key"),
CAFile: mdutil.GetString(md, "caFile", "ca"),
Secure: mdutil.GetBool(md, "secure"),
ServerName: mdutil.GetString(md, "serverName"),
}
if tlsConfig.ServerName == "" {
tlsConfig.ServerName = url.Hostname()
}
delete(m, "certFile")
delete(m, "cert")
delete(m, "keyFile")
delete(m, "key")
delete(m, "caFile")
delete(m, "ca")
delete(m, "secure")
delete(m, "serverName")
if !tlsConfig.Secure && tlsConfig.CertFile == "" && tlsConfig.CAFile == "" && tlsConfig.ServerName == "" {
tlsConfig = nil
}
node.Connector = &config.ConnectorConfig{
Type: connector,
Auth: auth,
Metadata: m,
}
node.Dialer = &config.DialerConfig{
Type: dialer,
TLS: tlsConfig,
Metadata: m,
}
if node.Dialer.Type == "ssh" || node.Dialer.Type == "sshd" {
node.Connector.Auth = nil
node.Dialer.Auth = auth
}
return node, nil
}
func normCmd(s string) (*url.URL, error) {
s = strings.TrimSpace(s)
if s == "" {
return nil, ErrInvalidCmd
}
if s[0] == ':' || !strings.Contains(s, "://") {
s = "auto://" + s
}
url, err := url.Parse(s)
if err != nil {
return nil, err
}
if url.Scheme == "https" {
url.Scheme = "http+tls"
}
return url, nil
}
func parseAuthFromCmd(sa string) (*config.AuthConfig, error) {
v, err := base64.StdEncoding.DecodeString(sa)
if err != nil {
return nil, err
}
cs := string(v)
n := strings.IndexByte(cs, ':')
if n < 0 {
return &config.AuthConfig{
Username: cs,
}, nil
}
return &config.AuthConfig{
Username: cs[:n],
Password: cs[n+1:],
}, nil
}
func parseSelector(m map[string]any) *config.SelectorConfig {
md := mdx.NewMetadata(m)
strategy := mdutil.GetString(md, "strategy")
maxFails := mdutil.GetInt(md, "maxFails", "max_fails")
failTimeout := mdutil.GetDuration(md, "failTimeout", "fail_timeout")
if strategy == "" && maxFails <= 0 && failTimeout <= 0 {
return nil
}
if strategy == "" {
strategy = "round"
}
if maxFails <= 0 {
maxFails = 1
}
if failTimeout <= 0 {
failTimeout = 30 * time.Second
}
delete(m, "strategy")
delete(m, "maxFails")
delete(m, "max_fails")
delete(m, "failTimeout")
delete(m, "fail_timeout")
return &config.SelectorConfig{
Strategy: strategy,
MaxFails: maxFails,
FailTimeout: failTimeout,
}
}
+230
View File
@@ -0,0 +1,230 @@
package main
import (
"io"
"os"
"path/filepath"
"github.com/go-gost/core/logger"
"github.com/go-gost/core/service"
"github.com/go-gost/x/api"
"github.com/go-gost/x/config"
admission_parser "github.com/go-gost/x/config/parsing/admission"
auth_parser "github.com/go-gost/x/config/parsing/auth"
bypass_parser "github.com/go-gost/x/config/parsing/bypass"
chain_parser "github.com/go-gost/x/config/parsing/chain"
hop_parser "github.com/go-gost/x/config/parsing/hop"
hosts_parser "github.com/go-gost/x/config/parsing/hosts"
ingress_parser "github.com/go-gost/x/config/parsing/ingress"
limiter_parser "github.com/go-gost/x/config/parsing/limiter"
recorder_parser "github.com/go-gost/x/config/parsing/recorder"
resolver_parser "github.com/go-gost/x/config/parsing/resolver"
sd_parser "github.com/go-gost/x/config/parsing/sd"
service_parser "github.com/go-gost/x/config/parsing/service"
xlogger "github.com/go-gost/x/logger"
metrics "github.com/go-gost/x/metrics/service"
"github.com/go-gost/x/registry"
"gopkg.in/natefinch/lumberjack.v2"
)
func buildService(cfg *config.Config) (services []service.Service) {
if cfg == nil {
return
}
log := logger.Default()
for _, autherCfg := range cfg.Authers {
if auther := auth_parser.ParseAuther(autherCfg); auther != nil {
if err := registry.AutherRegistry().Register(autherCfg.Name, auther); err != nil {
log.Fatal(err)
}
}
}
for _, admissionCfg := range cfg.Admissions {
if adm := admission_parser.ParseAdmission(admissionCfg); adm != nil {
if err := registry.AdmissionRegistry().Register(admissionCfg.Name, adm); err != nil {
log.Fatal(err)
}
}
}
for _, bypassCfg := range cfg.Bypasses {
if bp := bypass_parser.ParseBypass(bypassCfg); bp != nil {
if err := registry.BypassRegistry().Register(bypassCfg.Name, bp); err != nil {
log.Fatal(err)
}
}
}
for _, resolverCfg := range cfg.Resolvers {
r, err := resolver_parser.ParseResolver(resolverCfg)
if err != nil {
log.Fatal(err)
}
if r != nil {
if err := registry.ResolverRegistry().Register(resolverCfg.Name, r); err != nil {
log.Fatal(err)
}
}
}
for _, hostsCfg := range cfg.Hosts {
if h := hosts_parser.ParseHostMapper(hostsCfg); h != nil {
if err := registry.HostsRegistry().Register(hostsCfg.Name, h); err != nil {
log.Fatal(err)
}
}
}
for _, ingressCfg := range cfg.Ingresses {
if h := ingress_parser.ParseIngress(ingressCfg); h != nil {
if err := registry.IngressRegistry().Register(ingressCfg.Name, h); err != nil {
log.Fatal(err)
}
}
}
for _, sdCfg := range cfg.SDs {
if h := sd_parser.ParseSD(sdCfg); h != nil {
if err := registry.SDRegistry().Register(sdCfg.Name, h); err != nil {
log.Fatal(err)
}
}
}
for _, recorderCfg := range cfg.Recorders {
if h := recorder_parser.ParseRecorder(recorderCfg); h != nil {
if err := registry.RecorderRegistry().Register(recorderCfg.Name, h); err != nil {
log.Fatal(err)
}
}
}
for _, limiterCfg := range cfg.Limiters {
if h := limiter_parser.ParseTrafficLimiter(limiterCfg); h != nil {
if err := registry.TrafficLimiterRegistry().Register(limiterCfg.Name, h); err != nil {
log.Fatal(err)
}
}
}
for _, limiterCfg := range cfg.CLimiters {
if h := limiter_parser.ParseConnLimiter(limiterCfg); h != nil {
if err := registry.ConnLimiterRegistry().Register(limiterCfg.Name, h); err != nil {
log.Fatal(err)
}
}
}
for _, limiterCfg := range cfg.RLimiters {
if h := limiter_parser.ParseRateLimiter(limiterCfg); h != nil {
if err := registry.RateLimiterRegistry().Register(limiterCfg.Name, h); err != nil {
log.Fatal(err)
}
}
}
for _, hopCfg := range cfg.Hops {
hop, err := hop_parser.ParseHop(hopCfg)
if err != nil {
log.Fatal(err)
}
if hop != nil {
if err := registry.HopRegistry().Register(hopCfg.Name, hop); err != nil {
log.Fatal(err)
}
}
}
for _, chainCfg := range cfg.Chains {
c, err := chain_parser.ParseChain(chainCfg)
if err != nil {
log.Fatal(err)
}
if c != nil {
if err := registry.ChainRegistry().Register(chainCfg.Name, c); err != nil {
log.Fatal(err)
}
}
}
for _, svcCfg := range cfg.Services {
svc, err := service_parser.ParseService(svcCfg)
if err != nil {
log.Fatal(err)
}
if svc != nil {
if err := registry.ServiceRegistry().Register(svcCfg.Name, svc); err != nil {
log.Fatal(err)
}
}
services = append(services, svc)
}
return
}
func logFromConfig(cfg *config.LogConfig) logger.Logger {
if cfg == nil {
cfg = &config.LogConfig{}
}
opts := []xlogger.LoggerOption{
xlogger.FormatLoggerOption(logger.LogFormat(cfg.Format)),
xlogger.LevelLoggerOption(logger.LogLevel(cfg.Level)),
}
var out io.Writer = os.Stderr
switch cfg.Output {
case "none", "null":
return xlogger.Nop()
case "stdout":
out = os.Stdout
case "stderr", "":
out = os.Stderr
default:
if cfg.Rotation != nil {
out = &lumberjack.Logger{
Filename: cfg.Output,
MaxSize: cfg.Rotation.MaxSize,
MaxAge: cfg.Rotation.MaxAge,
MaxBackups: cfg.Rotation.MaxBackups,
LocalTime: cfg.Rotation.LocalTime,
Compress: cfg.Rotation.Compress,
}
} else {
os.MkdirAll(filepath.Dir(cfg.Output), 0755)
f, err := os.OpenFile(cfg.Output, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666)
if err != nil {
logger.Default().Warn(err)
} else {
out = f
}
}
}
opts = append(opts, xlogger.OutputLoggerOption(out))
return xlogger.NewLogger(opts...)
}
func buildAPIService(cfg *config.APIConfig) (service.Service, error) {
auther := auth_parser.ParseAutherFromAuth(cfg.Auth)
if cfg.Auther != "" {
auther = registry.AutherRegistry().Get(cfg.Auther)
}
return api.NewService(
cfg.Addr,
api.PathPrefixOption(cfg.PathPrefix),
api.AccessLogOption(cfg.AccessLog),
api.AutherOption(auther),
)
}
func buildMetricsService(cfg *config.MetricsConfig) (service.Service, error) {
auther := auth_parser.ParseAutherFromAuth(cfg.Auth)
if cfg.Auther != "" {
auther = registry.AutherRegistry().Get(cfg.Auther)
}
return metrics.NewService(
cfg.Addr,
metrics.PathOption(cfg.Path),
metrics.AutherOption(auther),
)
}
+22 -46
View File
@@ -11,35 +11,21 @@ import (
"runtime" "runtime"
"strings" "strings"
"sync" "sync"
"sync/atomic"
"time"
"github.com/go-gost/core/logger" "github.com/go-gost/core/logger"
xlogger "github.com/go-gost/x/logger" xlogger "github.com/go-gost/x/logger"
"github.com/judwhite/go-svc" "github.com/judwhite/go-svc"
) )
type stringList []string
func (l *stringList) String() string {
return fmt.Sprintf("%s", *l)
}
func (l *stringList) Set(value string) error {
*l = append(*l, value)
return nil
}
var ( var (
cfgFiles stringList cfgFile string
outputFormat string outputFormat string
services stringList services stringList
nodes stringList nodes stringList
debug bool debug bool
trace bool showConfig bool
apiAddr string apiAddr string
metricsAddr string metricsAddr string
reload time.Duration
showConfig bool
) )
func init() { func init() {
@@ -50,43 +36,37 @@ func init() {
if strings.Contains(args, " -- ") { if strings.Contains(args, " -- ") {
var ( var (
wg sync.WaitGroup wg sync.WaitGroup
ret atomic.Int32 ret int
) )
wargsList := strings.Split(" "+args+" ", " -- ")
ctx, cancel := context.WithCancel(context.Background()) ctx, cancel := context.WithCancel(context.Background())
defer cancel()
for wid, wargs := range wargsList { for wid, wargs := range strings.Split(" "+args+" ", " -- ") {
wg.Go(func() { wg.Add(1)
worker(wid, strings.Split(strings.TrimSpace(wargs), " "), ctx, &ret) go func(wid int, wargs string) {
}) defer wg.Done()
defer cancel()
worker(wid, strings.Split(wargs, " "), &ctx, &ret)
}(wid, strings.TrimSpace(wargs))
} }
wg.Wait() wg.Wait()
cancel()
os.Exit(int(ret.Load())) os.Exit(ret)
} }
} }
func worker(id int, args []string, ctx context.Context, ret *atomic.Int32) { func worker(id int, args []string, ctx *context.Context, ret *int) {
cmd := exec.CommandContext(ctx, os.Args[0], args...) cmd := exec.CommandContext(*ctx, os.Args[0], args...)
cmd.Stdout = os.Stdout cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr cmd.Stderr = os.Stderr
cmd.Env = append(os.Environ(), fmt.Sprintf("_GOST_ID=%d", id)) cmd.Env = append(os.Environ(), fmt.Sprintf("_GOST_ID=%d", id))
if err := cmd.Run(); err != nil { cmd.Run()
// Context cancellation is expected when one worker exits early. if cmd.ProcessState.Exited() {
// Only log fatal on other errors. *ret = cmd.ProcessState.ExitCode()
if ctx.Err() == nil {
log.Fatal(err)
}
return
}
if cmd.ProcessState != nil && cmd.ProcessState.Exited() {
ret.Store(int32(cmd.ProcessState.ExitCode()))
} }
} }
@@ -95,15 +75,13 @@ func init() {
flag.Var(&services, "L", "service list") flag.Var(&services, "L", "service list")
flag.Var(&nodes, "F", "chain node list") flag.Var(&nodes, "F", "chain node list")
flag.Var(&cfgFiles, "C", "config file(s), URL(s), or inline JSON") flag.StringVar(&cfgFile, "C", "", "configuration file")
flag.BoolVar(&printVersion, "V", false, "print version") flag.BoolVar(&printVersion, "V", false, "print version")
flag.StringVar(&outputFormat, "O", "", "output format, one of yaml|json format") flag.StringVar(&outputFormat, "O", "", "output format, one of yaml|json format")
flag.BoolVar(&debug, "D", false, "debug mode") flag.BoolVar(&debug, "D", false, "debug mode")
flag.BoolVar(&trace, "DD", false, "trace mode") flag.BoolVar(&showConfig, "P", false, "print config only")
flag.StringVar(&apiAddr, "api", "", "api service address") flag.StringVar(&apiAddr, "api", "", "api service address")
flag.StringVar(&metricsAddr, "metrics", "", "metrics service address") flag.StringVar(&metricsAddr, "metrics", "", "metrics service address")
flag.DurationVar(&reload, "R", 0, "auto reload period (e.g. 30s, 1m)")
flag.BoolVar(&showConfig, "P", false, "print config only")
flag.Parse() flag.Parse()
if printVersion { if printVersion {
@@ -111,15 +89,13 @@ func init() {
version, runtime.Version(), runtime.GOOS, runtime.GOARCH) version, runtime.Version(), runtime.GOOS, runtime.GOARCH)
os.Exit(0) os.Exit(0)
} }
logger.SetDefault(xlogger.NewLogger())
} }
func main() { func main() {
log := xlogger.NewLogger()
logger.SetDefault(log)
p := &program{} p := &program{}
if err := svc.Run(p); err != nil { if err := svc.Run(p); err != nil {
logger.Default().Fatal(err) log.Fatal(err)
} }
} }
+137 -238
View File
@@ -1,60 +1,81 @@
package main package main
import ( import (
"context"
"encoding/json"
"errors"
"fmt" "fmt"
"gopkg.in/yaml.v2"
"net/http" "net/http"
"os" "os"
"os/signal"
"strings"
"syscall"
"time"
"github.com/go-gost/core/auth"
"github.com/go-gost/core/logger" "github.com/go-gost/core/logger"
"github.com/go-gost/core/service"
api_service "github.com/go-gost/x/api/service"
xauth "github.com/go-gost/x/auth"
"github.com/go-gost/x/config" "github.com/go-gost/x/config"
"github.com/go-gost/x/config/loader" "github.com/go-gost/x/config/parsing"
auth_parser "github.com/go-gost/x/config/parsing/auth"
"github.com/go-gost/x/config/parsing/parser"
xmetrics "github.com/go-gost/x/metrics" xmetrics "github.com/go-gost/x/metrics"
metrics "github.com/go-gost/x/metrics/service"
"github.com/go-gost/x/registry" "github.com/go-gost/x/registry"
"github.com/judwhite/go-svc" "github.com/judwhite/go-svc"
"gopkg.in/yaml.v3"
) )
type program struct { type program struct {
srvApi service.Service
srvMetrics service.Service
srvProfiling *http.Server
cancel context.CancelFunc
} }
func (p *program) Init(env svc.Environment) error { func (p *program) Init(env svc.Environment) error {
parser.Init(parser.Args{ cfg := &config.Config{}
CfgFiles: cfgFiles, if cfgFile != "" {
Services: services, if err := cfg.ReadFile(cfgFile); err != nil {
Nodes: nodes, return err
Debug: debug, }
Trace: trace, }
ApiAddr: apiAddr,
MetricsAddr: metricsAddr,
})
return nil cmdCfg, err := buildConfigFromCmd(services, nodes)
}
func (p *program) Start() error {
cfg, err := parser.Parse()
if err != nil { if err != nil {
return err return err
} }
cfg = p.mergeConfig(cfg, cmdCfg)
if len(cfg.Services) == 0 && apiAddr == "" {
if err := cfg.Load(); err != nil {
return err
}
}
if v := os.Getenv("GOST_API"); v != "" {
cfg.API = &config.APIConfig{
Addr: v,
}
}
if v := os.Getenv("GOST_LOGGER_LEVEL"); v != "" {
cfg.Log = &config.LogConfig{
Level: v,
}
}
if v := os.Getenv("GOST_PROFILING"); v != "" {
cfg.Profiling = &config.ProfilingConfig{
Addr: v,
}
}
if v := os.Getenv("GOST_METRICS"); v != "" {
cfg.Metrics = &config.MetricsConfig{
Addr: v,
}
}
if apiAddr != "" {
cfg.API = &config.APIConfig{
Addr: apiAddr,
}
}
if debug {
if cfg.Log == nil {
cfg.Log = &config.LogConfig{}
}
cfg.Log.Level = string(logger.DebugLevel)
}
if metricsAddr != "" {
cfg.Metrics = &config.MetricsConfig{
Addr: metricsAddr,
}
}
logger.SetDefault(logFromConfig(cfg.Log))
if outputFormat != "" { if outputFormat != "" {
if err := cfg.Write(os.Stdout, outputFormat); err != nil { if err := cfg.Write(os.Stdout, outputFormat); err != nil {
@@ -63,119 +84,65 @@ func (p *program) Start() error {
os.Exit(0) os.Exit(0)
} }
parsing.BuildDefaultTLSConfig(cfg.TLS)
config.Set(cfg) config.Set(cfg)
// Enable metrics before loading services so that listener wrappers
// can observe the enabled state at Init time.
if cfg.Metrics != nil && cfg.Metrics.Addr != "" {
xmetrics.Enable(true)
}
if err := loader.Load(cfg); err != nil {
return err
}
if err := p.run(cfg); err != nil {
return err
}
ctx, cancel := context.WithCancel(context.Background())
p.cancel = cancel
go p.reload(ctx)
return nil return nil
} }
func (p *program) run(cfg *config.Config) error { func (p *program) Start() error {
for _, svc := range registry.ServiceRegistry().GetAll() { log := logger.Default()
svc := svc cfg := config.Global()
go func() {
svc.Serve()
}()
}
if showConfig { if showConfig {
if outputFormat == "json" { marshal, _ := yaml.Marshal(cfg)
marshal, _ := json.Marshal(cfg) fmt.Println(string(marshal))
fmt.Println(string(marshal))
} else {
marshal, _ := yaml.Marshal(cfg)
fmt.Println(string(marshal))
}
os.Exit(0) os.Exit(0)
return nil return nil
} }
if p.srvApi != nil {
p.srvApi.Close()
p.srvApi = nil
}
if cfg.API != nil { if cfg.API != nil {
s, err := buildApiService(cfg.API) s, err := buildAPIService(cfg.API)
if err != nil { if err != nil {
return err return err
} }
p.srvApi = s
go func() { go func() {
defer s.Close() defer s.Close()
log.Info("api service on ", s.Addr())
log := logger.Default().WithFields(map[string]any{"kind": "service", "service": "@api"}) log.Fatal(s.Serve())
log.Info("listening on ", s.Addr())
if err := s.Serve(); !errors.Is(err, http.ErrServerClosed) {
log.Error(err)
}
}() }()
} }
if p.srvMetrics != nil {
p.srvMetrics.Close()
p.srvMetrics = nil
}
if cfg.Metrics != nil && cfg.Metrics.Addr != "" {
s, err := buildMetricsService(cfg.Metrics)
if err != nil {
return err
}
p.srvMetrics = s
go func() {
defer s.Close()
log := logger.Default().WithFields(map[string]any{"kind": "service", "service": "@metrics"})
log.Info("listening on ", s.Addr())
if err := s.Serve(); !errors.Is(err, http.ErrServerClosed) {
log.Error(err)
}
}()
}
if p.srvProfiling != nil {
p.srvProfiling.Close()
p.srvProfiling = nil
}
if cfg.Profiling != nil { if cfg.Profiling != nil {
addr := cfg.Profiling.Addr
if addr == "" {
addr = ":6060"
}
s := &http.Server{
Addr: addr,
}
p.srvProfiling = s
go func() { go func() {
defer s.Close() addr := cfg.Profiling.Addr
if addr == "" {
log := logger.Default().WithFields(map[string]any{"kind": "service", "service": "@profiling"}) addr = ":6060"
log.Info("listening on ", addr)
if err := s.ListenAndServe(); !errors.Is(err, http.ErrServerClosed) {
log.Error(err)
} }
log.Info("profiling server on ", addr)
log.Fatal(http.ListenAndServe(addr, nil))
}()
}
if cfg.Metrics != nil {
xmetrics.Init(xmetrics.NewMetrics())
if cfg.Metrics.Addr != "" {
s, err := buildMetricsService(cfg.Metrics)
if err != nil {
log.Fatal(err)
}
go func() {
defer s.Close()
log.Info("metrics service on ", s.Addr())
log.Fatal(s.Serve())
}()
}
}
for _, svc := range buildService(cfg) {
svc := svc
go func() {
svc.Serve()
}() }()
} }
@@ -183,125 +150,57 @@ func (p *program) run(cfg *config.Config) error {
} }
func (p *program) Stop() error { func (p *program) Stop() error {
if p.cancel != nil {
p.cancel()
}
for name, srv := range registry.ServiceRegistry().GetAll() { for name, srv := range registry.ServiceRegistry().GetAll() {
srv.Close() srv.Close()
logger.Default().Debugf("service %s shutdown", name) logger.Default().Debugf("service %s shutdown", name)
} }
if p.srvApi != nil {
p.srvApi.Close()
logger.Default().Debug("service @api shutdown")
}
if p.srvMetrics != nil {
p.srvMetrics.Close()
logger.Default().Debug("service @metrics shutdown")
}
if p.srvProfiling != nil {
p.srvProfiling.Close()
logger.Default().Debug("service @profiling shutdown")
}
return nil return nil
} }
func (p *program) reload(ctx context.Context) { func (p *program) mergeConfig(cfg1, cfg2 *config.Config) *config.Config {
c := make(chan os.Signal, 1) if cfg1 == nil {
signal.Notify(c, syscall.SIGHUP) return cfg2
}
var ticker <-chan time.Time if cfg2 == nil {
if reload > 0 { return cfg1
t := time.NewTicker(reload)
defer t.Stop()
ticker = t.C
} }
for { cfg := &config.Config{
select { Services: append(cfg1.Services, cfg2.Services...),
case <-c: Chains: append(cfg1.Chains, cfg2.Chains...),
if err := p.reloadConfig(); err != nil { Hops: append(cfg1.Hops, cfg2.Hops...),
logger.Default().Error(err) Authers: append(cfg1.Authers, cfg2.Authers...),
} else { Admissions: append(cfg1.Admissions, cfg2.Admissions...),
logger.Default().Info("config reloaded") Bypasses: append(cfg1.Bypasses, cfg2.Bypasses...),
} Resolvers: append(cfg1.Resolvers, cfg2.Resolvers...),
Hosts: append(cfg1.Hosts, cfg2.Hosts...),
case <-ticker: Ingresses: append(cfg1.Ingresses, cfg2.Ingresses...),
if err := p.reloadConfig(); err != nil { SDs: append(cfg1.SDs, cfg2.SDs...),
logger.Default().Errorf("auto reload: %v", err) Recorders: append(cfg1.Recorders, cfg2.Recorders...),
} else { Limiters: append(cfg1.Limiters, cfg2.Limiters...),
logger.Default().Debug("config auto reloaded") CLimiters: append(cfg1.CLimiters, cfg2.CLimiters...),
} RLimiters: append(cfg1.RLimiters, cfg2.RLimiters...),
TLS: cfg1.TLS,
case <-ctx.Done(): Log: cfg1.Log,
return API: cfg1.API,
} Metrics: cfg1.Metrics,
Profiling: cfg1.Profiling,
} }
} if cfg2.TLS != nil {
cfg.TLS = cfg2.TLS
func (p *program) reloadConfig() error { }
cfg, err := parser.Parse() if cfg2.Log != nil {
if err != nil { cfg.Log = cfg2.Log
return err }
} if cfg2.API != nil {
config.Set(cfg) cfg.API = cfg2.API
}
if err := loader.Load(cfg); err != nil { if cfg2.Metrics != nil {
return err cfg.Metrics = cfg2.Metrics
} }
if cfg2.Profiling != nil {
if err := p.run(cfg); err != nil { cfg.Profiling = cfg2.Profiling
return err }
}
return cfg
return nil
}
func buildApiService(cfg *config.APIConfig) (service.Service, error) {
var authers []auth.Authenticator
if auther := auth_parser.ParseAutherFromAuth(cfg.Auth); auther != nil {
authers = append(authers, auther)
}
if cfg.Auther != "" {
authers = append(authers, registry.AutherRegistry().Get(cfg.Auther))
}
var auther auth.Authenticator
if len(authers) > 0 {
auther = xauth.AuthenticatorGroup(authers...)
}
network := "tcp"
addr := cfg.Addr
if strings.HasPrefix(addr, "unix://") {
network = "unix"
addr = strings.TrimPrefix(addr, "unix://")
}
return api_service.NewService(
network, addr,
api_service.PathPrefixOption(cfg.PathPrefix),
api_service.AccessLogOption(cfg.AccessLog),
api_service.AutherOption(auther),
)
}
func buildMetricsService(cfg *config.MetricsConfig) (service.Service, error) {
auther := auth_parser.ParseAutherFromAuth(cfg.Auth)
if cfg.Auther != "" {
auther = registry.AutherRegistry().Get(cfg.Auther)
}
network := "tcp"
addr := cfg.Addr
if strings.HasPrefix(addr, "unix://") {
network = "unix"
addr = strings.TrimPrefix(addr, "unix://")
}
return metrics.NewService(
network, addr,
metrics.PathOption(cfg.Path),
metrics.AutherOption(auther),
)
} }
-11
View File
@@ -6,9 +6,7 @@ import (
_ "github.com/go-gost/x/connector/forward" _ "github.com/go-gost/x/connector/forward"
_ "github.com/go-gost/x/connector/http" _ "github.com/go-gost/x/connector/http"
_ "github.com/go-gost/x/connector/http2" _ "github.com/go-gost/x/connector/http2"
_ "github.com/go-gost/x/connector/masque"
_ "github.com/go-gost/x/connector/relay" _ "github.com/go-gost/x/connector/relay"
_ "github.com/go-gost/x/connector/router"
_ "github.com/go-gost/x/connector/serial" _ "github.com/go-gost/x/connector/serial"
_ "github.com/go-gost/x/connector/sni" _ "github.com/go-gost/x/connector/sni"
_ "github.com/go-gost/x/connector/socks/v4" _ "github.com/go-gost/x/connector/socks/v4"
@@ -28,7 +26,6 @@ import (
_ "github.com/go-gost/x/dialer/http2" _ "github.com/go-gost/x/dialer/http2"
_ "github.com/go-gost/x/dialer/http2/h2" _ "github.com/go-gost/x/dialer/http2/h2"
_ "github.com/go-gost/x/dialer/http3" _ "github.com/go-gost/x/dialer/http3"
_ "github.com/go-gost/x/dialer/http3/masque"
_ "github.com/go-gost/x/dialer/http3/wt" _ "github.com/go-gost/x/dialer/http3/wt"
_ "github.com/go-gost/x/dialer/icmp" _ "github.com/go-gost/x/dialer/icmp"
_ "github.com/go-gost/x/dialer/kcp" _ "github.com/go-gost/x/dialer/kcp"
@@ -46,11 +43,9 @@ import (
_ "github.com/go-gost/x/dialer/tls" _ "github.com/go-gost/x/dialer/tls"
_ "github.com/go-gost/x/dialer/udp" _ "github.com/go-gost/x/dialer/udp"
_ "github.com/go-gost/x/dialer/unix" _ "github.com/go-gost/x/dialer/unix"
_ "github.com/go-gost/x/dialer/utls"
_ "github.com/go-gost/x/dialer/ws" _ "github.com/go-gost/x/dialer/ws"
// Register handlers // Register handlers
_ "github.com/go-gost/x/handler/api"
_ "github.com/go-gost/x/handler/auto" _ "github.com/go-gost/x/handler/auto"
_ "github.com/go-gost/x/handler/dns" _ "github.com/go-gost/x/handler/dns"
_ "github.com/go-gost/x/handler/file" _ "github.com/go-gost/x/handler/file"
@@ -59,12 +54,10 @@ import (
_ "github.com/go-gost/x/handler/http" _ "github.com/go-gost/x/handler/http"
_ "github.com/go-gost/x/handler/http2" _ "github.com/go-gost/x/handler/http2"
_ "github.com/go-gost/x/handler/http3" _ "github.com/go-gost/x/handler/http3"
_ "github.com/go-gost/x/handler/masque"
_ "github.com/go-gost/x/handler/metrics" _ "github.com/go-gost/x/handler/metrics"
_ "github.com/go-gost/x/handler/redirect/tcp" _ "github.com/go-gost/x/handler/redirect/tcp"
_ "github.com/go-gost/x/handler/redirect/udp" _ "github.com/go-gost/x/handler/redirect/udp"
_ "github.com/go-gost/x/handler/relay" _ "github.com/go-gost/x/handler/relay"
_ "github.com/go-gost/x/handler/router"
_ "github.com/go-gost/x/handler/serial" _ "github.com/go-gost/x/handler/serial"
_ "github.com/go-gost/x/handler/sni" _ "github.com/go-gost/x/handler/sni"
_ "github.com/go-gost/x/handler/socks/v4" _ "github.com/go-gost/x/handler/socks/v4"
@@ -74,7 +67,6 @@ import (
_ "github.com/go-gost/x/handler/sshd" _ "github.com/go-gost/x/handler/sshd"
_ "github.com/go-gost/x/handler/tap" _ "github.com/go-gost/x/handler/tap"
_ "github.com/go-gost/x/handler/tun" _ "github.com/go-gost/x/handler/tun"
_ "github.com/go-gost/x/handler/tungo"
_ "github.com/go-gost/x/handler/tunnel" _ "github.com/go-gost/x/handler/tunnel"
_ "github.com/go-gost/x/handler/unix" _ "github.com/go-gost/x/handler/unix"
@@ -101,16 +93,13 @@ import (
_ "github.com/go-gost/x/listener/redirect/udp" _ "github.com/go-gost/x/listener/redirect/udp"
_ "github.com/go-gost/x/listener/rtcp" _ "github.com/go-gost/x/listener/rtcp"
_ "github.com/go-gost/x/listener/rudp" _ "github.com/go-gost/x/listener/rudp"
_ "github.com/go-gost/x/listener/runix"
_ "github.com/go-gost/x/listener/serial" _ "github.com/go-gost/x/listener/serial"
_ "github.com/go-gost/x/listener/stdio"
_ "github.com/go-gost/x/listener/ssh" _ "github.com/go-gost/x/listener/ssh"
_ "github.com/go-gost/x/listener/sshd" _ "github.com/go-gost/x/listener/sshd"
_ "github.com/go-gost/x/listener/tap" _ "github.com/go-gost/x/listener/tap"
_ "github.com/go-gost/x/listener/tcp" _ "github.com/go-gost/x/listener/tcp"
_ "github.com/go-gost/x/listener/tls" _ "github.com/go-gost/x/listener/tls"
_ "github.com/go-gost/x/listener/tun" _ "github.com/go-gost/x/listener/tun"
_ "github.com/go-gost/x/listener/tungo"
_ "github.com/go-gost/x/listener/udp" _ "github.com/go-gost/x/listener/udp"
_ "github.com/go-gost/x/listener/unix" _ "github.com/go-gost/x/listener/unix"
_ "github.com/go-gost/x/listener/ws" _ "github.com/go-gost/x/listener/ws"
+1 -1
View File
@@ -1,5 +1,5 @@
package main package main
var ( var (
version = "3.3.0" version = "3.0.0"
) )
+4
View File
@@ -0,0 +1,4 @@
version: "3.4"
services:
gost:
build: .
+82 -136
View File
@@ -1,174 +1,120 @@
module github.com/go-gost/gost module github.com/go-gost/gost
go 1.26.3 go 1.21
replace github.com/templexxx/cpu v0.0.7 => github.com/templexxx/cpu v0.0.10-0.20211111114238-98168dcec14a
require ( require (
github.com/go-gost/core v0.5.1 github.com/go-gost/core v0.0.0-20231113123850-a916f0401649
github.com/go-gost/x v0.13.0 github.com/go-gost/x v0.0.0-20231116134211-2c82233b4f2f
github.com/judwhite/go-svc v1.2.1 github.com/judwhite/go-svc v1.2.1
github.com/moby/moby/client v0.4.0 gopkg.in/natefinch/lumberjack.v2 v2.0.0
github.com/stretchr/testify v1.11.1
github.com/testcontainers/testcontainers-go v0.42.0
) )
require ( require (
dario.cat/mergo v1.0.2 // indirect github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da // indirect
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c // indirect
github.com/Microsoft/go-winio v0.6.2 // indirect
github.com/alecthomas/units v0.0.0-20211218093645-b94a6e3cc137 // indirect github.com/alecthomas/units v0.0.0-20211218093645-b94a6e3cc137 // indirect
github.com/alessio/shellescape v1.4.1 // indirect
github.com/andybalholm/brotli v1.0.6 // indirect
github.com/asaskevich/govalidator v0.0.0-20210307081110-f21760c49a8d // indirect github.com/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/sonic v1.11.6 // indirect github.com/bytedance/sonic v1.9.1 // indirect
github.com/bytedance/sonic/loader v0.1.1 // indirect github.com/cespare/xxhash/v2 v2.2.0 // indirect
github.com/cenkalti/backoff/v4 v4.3.0 // indirect github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/cloudwego/base64x v0.1.4 // indirect
github.com/cloudwego/iasm v0.2.0 // indirect
github.com/containerd/errdefs v1.0.0 // indirect
github.com/containerd/errdefs/pkg v0.3.0 // indirect
github.com/containerd/log v0.1.0 // indirect
github.com/containerd/platforms v0.2.1 // indirect
github.com/coreos/go-iptables v0.5.0 // indirect github.com/coreos/go-iptables v0.5.0 // indirect
github.com/cpuguy83/dockercfg v0.3.2 // indirect
github.com/danieljoos/wincred v1.2.0 // indirect
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
github.com/distribution/reference v0.6.0 // indirect github.com/fsnotify/fsnotify v1.6.0 // indirect
github.com/docker/go-connections v0.7.0 // indirect github.com/gabriel-vasile/mimetype v1.4.2 // indirect
github.com/docker/go-units v0.5.0 // indirect github.com/gin-contrib/cors v1.3.1 // indirect
github.com/dunglas/httpsfv v1.1.0 // indirect
github.com/ebitengine/purego v0.10.0 // indirect
github.com/felixge/httpsnoop v1.0.4 // indirect
github.com/fsnotify/fsnotify v1.7.0 // indirect
github.com/gabriel-vasile/mimetype v1.4.3 // indirect
github.com/gin-contrib/cors v1.7.2 // indirect
github.com/gin-contrib/sse v0.1.0 // indirect github.com/gin-contrib/sse v0.1.0 // indirect
github.com/gin-gonic/gin v1.10.1 // indirect github.com/gin-gonic/gin v1.9.1 // indirect
github.com/go-gost/go-shadowsocks2 v0.1.3 // indirect github.com/go-gost/gosocks4 v0.0.1 // indirect
github.com/go-gost/gosocks4 v0.1.0 // indirect github.com/go-gost/gosocks5 v0.4.0 // indirect
github.com/go-gost/gosocks5 v0.5.0 // indirect github.com/go-gost/plugin v0.0.0-20231109123346-0ae4157b9d25 // indirect
github.com/go-gost/plugin v0.4.0 // indirect github.com/go-gost/relay v0.4.1-0.20230916134211-828f314ddfe7 // indirect
github.com/go-gost/relay v0.6.1 // indirect github.com/go-gost/tls-dissector v0.0.2-0.20220408131628-aac992c27451 // indirect
github.com/go-gost/tls-dissector v0.2.0 // indirect
github.com/go-logr/logr v1.4.3 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/go-ole/go-ole v1.2.6 // indirect
github.com/go-playground/locales v0.14.1 // indirect github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/go-playground/validator/v10 v10.20.0 // indirect github.com/go-playground/validator/v10 v10.14.0 // indirect
github.com/go-redis/redis/v8 v8.11.5 // indirect github.com/go-redis/redis/v8 v8.11.5 // indirect
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 // indirect
github.com/gobwas/glob v0.2.3 // indirect github.com/gobwas/glob v0.2.3 // indirect
github.com/goccy/go-json v0.10.2 // indirect github.com/goccy/go-json v0.10.2 // indirect
github.com/godbus/dbus/v5 v5.1.0 // indirect github.com/golang/protobuf v1.5.3 // indirect
github.com/golang/snappy v0.0.4 // indirect github.com/golang/snappy v0.0.4 // indirect
github.com/google/btree v1.1.3 // indirect
github.com/google/gopacket v1.1.19 // indirect github.com/google/gopacket v1.1.19 // indirect
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect github.com/google/pprof v0.0.0-20230912144702-c363fe2c2ed8 // indirect
github.com/google/uuid v1.6.0 // indirect github.com/google/uuid v1.4.0 // indirect
github.com/gorilla/websocket v1.5.3 // indirect github.com/gorilla/websocket v1.5.0 // 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/json-iterator/go v1.1.12 // indirect github.com/json-iterator/go v1.1.12 // indirect
github.com/klauspost/compress v1.18.5 // indirect github.com/klauspost/cpuid v1.3.1 // indirect
github.com/klauspost/cpuid/v2 v2.2.7 // indirect github.com/klauspost/cpuid/v2 v2.2.4 // indirect
github.com/klauspost/reedsolomon v1.11.8 // indirect github.com/klauspost/reedsolomon v1.9.9 // indirect
github.com/leodido/go-urn v1.4.0 // indirect github.com/leodido/go-urn v1.2.4 // indirect
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect github.com/magiconair/properties v1.8.7 // indirect
github.com/magiconair/properties v1.8.10 // indirect github.com/mattn/go-isatty v0.0.19 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect
github.com/miekg/dns v1.1.61 // indirect github.com/miekg/dns v1.1.56 // 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/mmcloughlin/avo v0.0.0-20200803215136-443f81d77104 // indirect
github.com/moby/go-archive v0.2.0 // indirect
github.com/moby/moby/api v1.54.1 // indirect
github.com/moby/patternmatcher v0.6.1 // indirect
github.com/moby/sys/sequential v0.6.0 // indirect
github.com/moby/sys/user v0.4.0 // indirect
github.com/moby/sys/userns v0.1.0 // indirect
github.com/moby/term v0.5.2 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/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/opencontainers/go-digest v1.0.0 // indirect github.com/onsi/ginkgo/v2 v2.12.0 // 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.2 // indirect github.com/pelletier/go-toml v1.9.5 // indirect
github.com/pion/dtls/v3 v3.1.1 // indirect github.com/pelletier/go-toml/v2 v2.0.8 // indirect
github.com/pion/logging v0.2.4 // indirect github.com/pion/dtls/v2 v2.2.6 // indirect
github.com/pion/transport/v4 v4.0.1 // indirect github.com/pion/logging v0.2.2 // indirect
github.com/pires/go-proxyproto v0.8.1 // indirect github.com/pion/transport/v2 v2.0.2 // indirect
github.com/pion/udp/v2 v2.0.1 // indirect
github.com/pires/go-proxyproto v0.7.0 // 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/prometheus/client_golang v1.17.0 // indirect
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect github.com/prometheus/client_model v0.4.1-0.20230718164431-9a2bf3000d16 // indirect
github.com/prometheus/client_golang v1.19.1 // indirect github.com/prometheus/common v0.44.0 // indirect
github.com/prometheus/client_model v0.6.0 // indirect github.com/prometheus/procfs v0.11.1 // indirect
github.com/prometheus/common v0.48.0 // indirect github.com/quic-go/qpack v0.4.0 // indirect
github.com/prometheus/procfs v0.12.0 // indirect github.com/quic-go/qtls-go1-20 v0.3.4 // indirect
github.com/quic-go/qpack v0.6.0 // indirect github.com/quic-go/quic-go v0.39.0 // indirect
github.com/quic-go/quic-go v0.59.1 // indirect github.com/quic-go/webtransport-go v0.6.0 // indirect
github.com/quic-go/webtransport-go v0.10.0 // indirect
github.com/refraction-networking/utls v1.8.2 // indirect
github.com/riobard/go-bloom v0.0.0-20200614022211-cdc8013cb5b3 // indirect github.com/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/shadowsocks/go-shadowsocks2 v0.1.5 // indirect
github.com/sagikazarmark/slog-shim v0.1.0 // indirect github.com/shadowsocks/shadowsocks-go v0.0.0-20200409064450-3e585ff90601 // indirect
github.com/shadowsocks/go-shadowsocks2 v0.1.6-0.20241020092332-e1fe9ea73740 // indirect github.com/sirupsen/logrus v1.8.1 // indirect
github.com/shirou/gopsutil/v3 v3.24.5 // indirect
github.com/shirou/gopsutil/v4 v4.26.3 // indirect
github.com/shoenig/go-m1cpu v0.1.6 // indirect
github.com/sirupsen/logrus v1.9.4 // indirect
github.com/songgao/water v0.0.0-20200317203138-2b4b6d7c09d8 // indirect github.com/songgao/water v0.0.0-20200317203138-2b4b6d7c09d8 // indirect
github.com/sourcegraph/conc v0.3.0 // indirect github.com/spf13/afero v1.9.3 // indirect
github.com/spf13/afero v1.11.0 // indirect github.com/spf13/cast v1.5.0 // indirect
github.com/spf13/cast v1.6.0 // indirect github.com/spf13/jwalterweatherman v1.1.0 // indirect
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.14.0 // indirect
github.com/subosito/gotenv v1.6.0 // indirect github.com/subosito/gotenv v1.4.1 // indirect
github.com/templexxx/cpu v0.1.1 // indirect github.com/templexxx/cpu v0.0.7 // indirect
github.com/templexxx/xorsimd v0.4.3 // indirect github.com/templexxx/xorsimd v0.4.1 // indirect
github.com/tjfoc/gmsm v1.4.1 // indirect github.com/tjfoc/gmsm v1.3.2 // indirect
github.com/tklauser/go-sysconf v0.3.16 // indirect
github.com/tklauser/numcpus v0.11.0 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/ugorji/go/codec v1.2.12 // indirect github.com/ugorji/go/codec v1.2.11 // indirect
github.com/vishvananda/netlink v1.1.1-0.20211118161826-650dca95af54 // indirect github.com/vishvananda/netlink v1.1.0 // indirect
github.com/vishvananda/netns v0.0.4 // indirect github.com/vishvananda/netns v0.0.0-20191106174202-0a2b9b5464df // indirect
github.com/vulcand/predicate v1.2.0 // indirect github.com/xtaci/kcp-go/v5 v5.6.1 // indirect
github.com/xjasonlyu/tun2socks/v2 v2.6.0 // indirect github.com/xtaci/smux v1.5.24 // indirect
github.com/xtaci/kcp-go/v5 v5.6.5 // indirect
github.com/xtaci/smux v1.5.31 // indirect
github.com/xtaci/tcpraw v1.2.25 // indirect github.com/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 go.uber.org/mock v0.3.0 // indirect
github.com/zalando/go-keyring v0.2.4 // indirect golang.org/x/arch v0.3.0 // indirect
github.com/zeebo/blake3 v0.2.4 // indirect golang.org/x/crypto v0.14.0 // indirect
go.opentelemetry.io/auto/sdk v1.2.1 // indirect golang.org/x/exp v0.0.0-20230905200255-921286631fa9 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0 // indirect golang.org/x/mod v0.12.0 // indirect
go.opentelemetry.io/otel v1.41.0 // indirect golang.org/x/net v0.17.0 // indirect
go.opentelemetry.io/otel/metric v1.41.0 // indirect golang.org/x/sys v0.13.0 // indirect
go.opentelemetry.io/otel/trace v1.41.0 // indirect golang.org/x/text v0.13.0 // indirect
go.uber.org/multierr v1.11.0 // indirect golang.org/x/time v0.3.0 // indirect
golang.org/x/arch v0.8.0 // indirect golang.org/x/tools v0.13.0 // indirect
golang.org/x/crypto v0.50.0 // indirect golang.zx2c4.com/wintun v0.0.0-20211104114900-415007cec224 // indirect
golang.org/x/exp v0.0.0-20241210194714-1829a127f884 // indirect golang.zx2c4.com/wireguard v0.0.0-20220703234212-c31a7b1ab478 // indirect
golang.org/x/mod v0.34.0 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20231016165738-49dd2c1f3d0b // indirect
golang.org/x/net v0.53.0 // indirect google.golang.org/grpc v1.59.0 // indirect
golang.org/x/sync v0.20.0 // indirect google.golang.org/protobuf v1.31.0 // indirect
golang.org/x/sys v0.43.0 // indirect
golang.org/x/term v0.42.0 // indirect
golang.org/x/text v0.36.0 // indirect
golang.org/x/time v0.12.0 // indirect
golang.org/x/tools v0.43.0 // indirect
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 // indirect
golang.zx2c4.com/wireguard v0.0.0-20250521234502-f333402bd9cb // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217 // indirect
google.golang.org/grpc v1.79.3 // indirect
google.golang.org/protobuf v1.36.10 // indirect
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/yaml.v2 v2.4.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect
gvisor.dev/gvisor v0.0.0-20250523182742-eede7a881b20 // indirect
) )
+574 -321
View File
File diff suppressed because it is too large Load Diff
+12 -14
View File
@@ -1,4 +1,4 @@
#!/usr/bin/env bash #!/bin/bash
# Check Root User # Check Root User
@@ -42,7 +42,7 @@ install_gost() {
armv7*) armv7*)
cpu_arch="armv7" cpu_arch="armv7"
;; ;;
aarch64|arm64) aarch64)
cpu_arch="arm64" cpu_arch="arm64"
;; ;;
i686) i686)
@@ -57,31 +57,29 @@ install_gost() {
mipsel*) mipsel*)
cpu_arch="mipsle" cpu_arch="mipsle"
;; ;;
riscv64)
cpu_arch="riscv64"
;;
*) *)
echo "Unsupported CPU architecture." echo "Unsupported CPU architecture."
exit 1 exit 1
;; ;;
esac esac
get_download_url="$base_url/tags/$version" get_download_url="$base_url/tags/$version"
download_url=$(curl -s "$get_download_url" | awk -F'"' -v re=".*${os}.*${cpu_arch}.*" '/"browser_download_url":/ && $4 ~ re { print $4 }' | head -n 1) download_url=$(curl -s "$get_download_url" | grep -Eo "\"browser_download_url\": \".*${os}.*${cpu_arch}.*\"" | awk -F'["]' '{print $4}')
# Download and install the binary # Download the binary
install_path="/usr/local/bin" echo "Downloading gost version $version..."
echo "Downloading and installing gost version $version..." curl -fsSL -o gost.tar.gz $download_url
curl -fsSL "$download_url" | tar -xzC "$install_path" gost
chmod +x "$install_path/gost"
# Remove binary from macOS quarantine when installing for first time # Extract and install the binary
[[ "$os" == "darwin" ]] && { xattr -d com.apple.quarantine "$install_path/gost" 2>&-; } echo "Installing gost..."
tar -xzf gost.tar.gz
chmod +x gost
mv gost /usr/local/bin/gost
echo "gost installation completed!" echo "gost installation completed!"
} }
# Retrieve available versions from GitHub API # Retrieve available versions from GitHub API
versions=$(curl -s "$base_url" | awk -F'"' '/"tag_name":/ {print $4}') versions=$(curl -s "$base_url" | grep -oP 'tag_name": "\K[^"]+')
# Check if --install option provided # Check if --install option provided
if [[ "$1" == "--install" ]]; then if [[ "$1" == "--install" ]]; then
-4
View File
@@ -1,4 +0,0 @@
FROM alpine:3.22
# add tools needed by e2e containers and health checks
RUN apk add --no-cache iptables curl netcat-openbsd python3
-276
View File
@@ -1,276 +0,0 @@
# 执行计划:HTTP Handler E2E 测试 + utils.go 重构
## 背景
从 PLAN.md 的 Step 0 和 Step 1A 开始实施。HTTP proxy 是 gost 最基础的协议,目前 e2e 测试仅覆盖 shadowsocks 和 parallel selector。本计划先重构 utils.go 提取公共 helper,然后用 helper 写 HTTP handler 测试套件,最后同步重构 shadowsocks_test.go。
## 变更文件清单
| 文件 | 操作 | 说明 |
|------|------|------|
| `tests/e2e/utils.go` | 修改 | 新增 `RunTCPCase` / `RunUDPCase` |
| `tests/e2e/http_test.go` | 新建 | HTTP proxy 测试套件 |
| `tests/e2e/testdata/http/server.yaml` | 新建 | 无 auth 的 HTTP proxy 服务器 |
| `tests/e2e/testdata/http/server_auth.yaml` | 新建 | 带 auth 的 HTTP proxy 服务器 |
| `tests/e2e/shadowsocks_test.go` | 修改 | 重构为使用 `RunTCPCase` / `RunUDPCase` |
---
## Step 1: 修改 `utils.go` — 提取 RunTCPCase / RunUDPCase
`shadowsocks_test.go``runTCPCase``runUDPCase` 提取为包级函数。关键差异:参数需要传入 echo container IP 和 echo container 本身(用于 dump logs)。
### RunTCPCase 签名与逻辑
```go
// RunTCPCase runs a full TCP proxy test case:
// start server → render client config → start client → curl assertion → cleanup.
func RunTCPCase(t *testing.T, ctx context.Context, networkName, echoIP string,
name, serverConfig, clientConfig string)
```
逻辑(从 shadowsocks_test.go:132-160 提取,一字不差):
1. 生成 serverAlias = `name + "-server"`
2. `RunGostContainerWithOptions(ctx, networkName, serverConfig, [serverAlias], ["8388/tcp"])`
3. `defer serverC.Terminate(ctx)`
4. `RenderConfig(clientConfig, {ServerAddr: serverAlias + ":8388"})`
5. `defer os.Remove(rendered)`
6. `RunGostContainerWithPorts(ctx, networkName, rendered, "8080/tcp")`
7. `defer clientC.Terminate(ctx)`
8. `clientC.Exec(ctx, ["curl", "-v", "-s", "-x", "http://127.0.0.1:8080", "http://<echoIP>:5678"])`
9. 断言 exitCode==0 且 body 包含 `"hello-gost"`,失败时 DumpLogs
**问题:端口硬编码**。shadowsocks 用 8388 作为服务端口,但其他协议可能用不同端口。需要参数化。
### 修正设计 — 参数化端口
```go
type TCPOptions struct {
ServerPort string // 容器内服务端口,默认 "8388/tcp"
ClientPort string // 客户端代理端口,默认 "8080/tcp"
}
func RunTCPCase(t *testing.T, ctx context.Context, networkName, echoIP string,
name, serverConfig, clientConfig string, opts ...TCPOptions)
```
`opts` 为空时使用默认值 `{ServerPort: "8388/tcp", ClientPort: "8080/tcp"}`。这样 shadowsocks 测试无需任何改动,而 HTTP 测试可传 `TCPOptions{ServerPort: "8080/tcp", ClientPort: "8080/tcp"}`
等一下 — 仔细看 shadowsocks 的 server 容器暴露的是 `8388/tcp`HTTP proxy 场景下只有一个 gost 容器(不做 server→client 链路),直接暴露 proxy 端口。
**重新思考:HTTP proxy 不需要 server+client 两容器模式。**
HTTP proxy 测试是最简单的模式 — 只需一个 gost 容器运行 HTTP proxy,然后容器内 curl 通过它访问 echo server。这和 parallel_selector_test.go 的模式一致。但为了统一框架和后续协议(SOCKS5、relay 等都需要 server+client 模式),我们应该:
- HTTP 的"无 auth"用例:**单容器模式**(同 parallel_selector
- HTTP 的"带 auth"用例:**也可以单容器**,只需在 curl 加 `--proxy-user`
所以 HTTP proxy 不需要 server/client 分离,但 RunTCPCase helper 仍然服务于 SOCKS5/relay 等需要链路的协议。
### 最终 RunTCPCase 设计
```go
func RunTCPCase(t *testing.T, ctx context.Context, networkName, echoIP, name string,
serverConfig, clientConfig string, serverPort, clientPort string)
```
- `serverPort`: 服务端容器暴露端口,如 `"8388/tcp"`
- `clientPort`: 客户端容器暴露端口,如 `"8080/tcp"`
- 渲染模板时用 `ServerAddr: serverAlias + ":" + strings.TrimSuffix(serverPort, "/tcp")`
### RunUDPCase 设计
```go
func RunUDPCase(t *testing.T, ctx context.Context, networkName, name string,
serverConfig, clientConfig string, serverPort, clientPort string)
```
逻辑从 shadowsocks_test.go:76-126 提取。
---
## Step 2: 新建 `testdata/http/` 配置文件
### `testdata/http/server.yaml` — 无 auth 的 HTTP proxy
```yaml
services:
- name: http-proxy
addr: :8080
handler:
type: http
listener:
type: tcp
```
单容器即可测试:gost 启动后在 :8080 提供 HTTP proxy 服务。
### `testdata/http/server_auth.yaml` — 带 auth 的 HTTP proxy
```yaml
services:
- name: http-proxy-auth
addr: :8080
handler:
type: http
auther: auther-0
listener:
type: tcp
authers:
- name: auther-0
auths:
- username: user
password: pass
```
---
## Step 3: 新建 `http_test.go`
### 套件结构
```go
type HTTPSuite struct {
suite.Suite
ctx context.Context
echoC testcontainers.Container
echoIP string
}
func (s *HTTPSuite) SetupSuite() // 启动 TCP echo
func (s *HTTPSuite) TearDownSuite() // 关闭 echo
func (s *HTTPSuite) TestHTTPProxy() // 无 auth
func (s *HTTPSuite) TestHTTPProxyAuth() // 带 auth
func TestHTTPSuite(t *testing.T) {
suite.Run(t, new(HTTPSuite))
}
```
### TestHTTPProxy — 单容器模式(同 parallel_selector
```go
func (s *HTTPSuite) TestHTTPProxy() {
gostC, err := RunGostContainerWithPorts(s.ctx, SharedNetworkName,
"testdata/http/server.yaml", "8080/tcp")
s.Require().NoError(err)
defer gostC.Terminate(s.ctx)
cmd := []string{"curl", "-v", "-s", "-x", "http://127.0.0.1:8080",
fmt.Sprintf("http://%s:5678", s.echoIP)}
code, out, err := gostC.Exec(s.ctx, cmd)
s.Require().NoError(err)
body, err := io.ReadAll(out)
s.Require().NoError(err)
if code != 0 || !strings.Contains(string(body), "hello-gost") {
DumpLogs(s.T(), s.ctx, "http-proxy logs", gostC)
}
s.Require().Equal(0, code)
s.Require().Contains(string(body), "hello-gost")
}
```
### TestHTTPProxyAuth — 带 auth 验证
```go
func (s *HTTPSuite) TestHTTPProxyAuth() {
gostC, err := RunGostContainerWithPorts(s.ctx, SharedNetworkName,
"testdata/http/server_auth.yaml", "8080/tcp")
s.Require().NoError(err)
defer gostC.Terminate(s.ctx)
// 测试1:无 auth 应失败(407)
s.T().Run("no-auth-should-fail", func(t *testing.T) {
cmd := []string{"curl", "-s", "-o", "/dev/null", "-w", "%{http_code}",
"-x", "http://127.0.0.1:8080", fmt.Sprintf("http://%s:5678", s.echoIP)}
code, out, _ := gostC.Exec(s.ctx, cmd)
body, _ := io.ReadAll(out)
// curl exit code 非 0 或 HTTP status 是 407
// 注意:gost HTTP proxy 在无 auth 时返回 407curl 不会自动重试
// 所以这里验证返回码是 407 或 curl 返回非零
httpStatus := strings.TrimSpace(string(body))
s.Assert().True(code != 0 || httpStatus == "407",
"expected auth failure, got status: %s, exit code: %d", httpStatus, code)
})
// 测试2:正确 auth 应成功
s.T().Run("with-auth-should-succeed", func(t *testing.T) {
cmd := []string{"curl", "-v", "-s", "-x",
"http://user:pass@127.0.0.1:8080",
fmt.Sprintf("http://%s:5678", s.echoIP)}
code, out, err := gostC.Exec(s.ctx, cmd)
s.Require().NoError(err)
body, err := io.ReadAll(out)
s.Require().NoError(err)
if code != 0 || !strings.Contains(string(body), "hello-gost") {
DumpLogs(s.T(), s.ctx, "http-proxy-auth logs", gostC)
}
s.Require().Equal(0, code)
s.Require().Contains(string(body), "hello-gost")
})
}
```
### 注意事项
- HTTP proxy 是**单容器模式**,不需要 server/client 分离(不像 shadowsocks 需要解密/加密链路)
- auth 测试分两步:先验证无 auth 被拒(407),再验证正确 auth 成功
- curl 使用 `http://user:pass@host:port` 格式传递 proxy auth
---
## Step 4: 重构 `shadowsocks_test.go`
`runTCPCase``runUDPCase` 方法替换为调用 `utils.go` 中的 `RunTCPCase` / `RunUDPCase`
**Before**:
```go
func (s *ShadowsocksSuite) TestShadowsocksTCP() {
s.runTCPCase("aes256gcm", "testdata/shadowsocks/tcp_server_aes256gcm.yaml", "testdata/shadowsocks/tcp_client_aes256gcm.yaml")
s.runTCPCase("chacha20", "testdata/shadowsocks/tcp_server_chacha20.yaml", "testdata/shadowsocks/tcp_client_chacha20.yaml")
}
```
**After**:
```go
func (s *ShadowsocksSuite) TestShadowsocksTCP() {
RunTCPCase(s.T(), s.ctx, SharedNetworkName, s.echoIP, "aes256gcm",
"testdata/shadowsocks/tcp_server_aes256gcm.yaml",
"testdata/shadowsocks/tcp_client_aes256gcm.yaml",
"8388/tcp", "8080/tcp")
RunTCPCase(s.T(), s.ctx, SharedNetworkName, s.echoIP, "chacha20",
"testdata/shadowsocks/tcp_server_chacha20.yaml",
"testdata/shadowsocks/tcp_client_chacha20.yaml",
"8388/tcp", "8080/tcp")
}
```
删除 `runTCPCase``runUDPCase` 方法。UDP 测试同理。
---
## 实施顺序
1. **`utils.go`** — 添加 `RunTCPCase` + `RunUDPCase`(不删除任何现有函数)
2. **`testdata/http/`** — 创建 `server.yaml` + `server_auth.yaml`
3. **`http_test.go`** — 创建 HTTP 套件
4. **编译验证**`cd gost && go build ./... && go vet ./tests/e2e/...`
5. **`shadowsocks_test.go`** — 重构为使用新 helper
6. **最终编译验证**`go build ./... && go vet ./tests/e2e/...`
7. **运行测试**`go test ./tests/e2e/ -v -run TestHTTPSuite -timeout 5m`
8. **回归测试**`go test ./tests/e2e/ -v -run TestShadowsocksSuite -timeout 5m`
---
## 验证
```bash
cd /config/workspace/go-gost/gost
go build ./...
go vet ./tests/e2e/...
go test ./tests/e2e/ -v -run TestHTTPSuite -timeout 5m
go test ./tests/e2e/ -v -run TestShadowsocksSuite -timeout 5m
```
-183
View File
@@ -1,183 +0,0 @@
# Plan: Expand E2E Test Suite
## Context
The e2e test framework (`tests/e2e/`) currently covers only **2 protocols** (Shadowsocks, parallel selector) out of **~30+ registered protocols** in gost. This plan systematically adds test coverage for all major protocols, following the existing Docker-based testcontainers pattern exactly.
**Key insight**: gost auto-generates self-signed TLS certs when none are provided (`x/config/parsing/tls.go``BuildDefaultTLSConfig`), and clients with default config skip cert verification. This means TLS-based protocols (ws→wss, h2c→h2, quic, etc.) can be tested **without mounting cert files**.
---
## Step 0: Refactor `utils.go` — Add Reusable Test Helpers
**File**: `tests/e2e/utils.go`
Extract the repeated patterns from `shadowsocks_test.go` into reusable helpers:
1. **`RunTCPCase(t, ctx, networkName, echoIP, name, serverConfig, clientConfig string)`** — Full lifecycle: start server → render client config → start client → curl assertion → cleanup. Extracts the logic from `ShadowsocksSuite.runTCPCase`.
2. **`RunUDPCase(t, ctx, networkName, name, serverConfig, clientConfig string)`** — Same for UDP: start server → render client → start client → dial UDP → retry loop → assertion → cleanup. Extracts from `runUDPCase`.
These eliminate ~40 lines of boilerplate per suite. Each suite's test methods become one-liners:
```go
func (s *SOCKS5Suite) TestSOCKS5TCP() {
RunTCPCase(s.T(), s.ctx, SharedNetworkName, s.echoIP, "socks5-tcp",
"testdata/socks5/tcp_server.yaml", "testdata/socks5/tcp_client.yaml")
}
```
---
## Step 1: Tier 1 — Core Protocols (Plain TCP, No Special Infrastructure)
Each suite follows the exact pattern: suite struct → `SetupSuite` (echo containers) → `TearDownSuite` → test methods calling `RunTCPCase`/`RunUDPCase``suite.Run()` entry point.
### 1A. HTTP Proxy Suite
- **Files**: `http_test.go`, `testdata/http/` (2 configs)
- **Configs**: `server.yaml` (handler `http`, listener `tcp`), `server_auth.yaml` (+ basic auth)
- **Tests**: `TestHTTPProxy`, `TestHTTPProxyAuth` (curl with `--proxy-user`)
- **Note**: Simplest test — single gost container acts as HTTP proxy
### 1B. SOCKS5 Suite
- **Files**: `socks5_test.go`, `testdata/socks5/` (4 configs)
- **Configs**: `tcp_server.yaml` (handler `socks5`), `tcp_client.yaml` (connector `socks5`, dialer `tcp`), + auth variants
- **Tests**: `TestSOCKS5TCP`, `TestSOCKS5TCPAuth`
### 1C. SOCKS4 Suite
- **Files**: `socks4_test.go`, `testdata/socks4/` (4 configs)
- **Configs**: `tcp_server.yaml` (handler `socks4`), `tcp_client.yaml` (connector `socks4`), + socks4a variants
- **Tests**: `TestSOCKS4TCP`, `TestSOCKS4aTCP`
### 1D. HTTP2 Cleartext Suite
- **Files**: `http2_test.go`, `testdata/http2/` (2 configs)
- **Configs**: `tcp_server.yaml` (handler `http2`, listener `http2`), `tcp_client.yaml` (connector `http2`, dialer `http2`)
- **Tests**: `TestHTTP2TCP`
### 1E. Relay Suite
- **Files**: `relay_test.go`, `testdata/relay/` (4 configs)
- **Configs**: `tcp_server.yaml` (handler `relay`, listener `tcp`), `tcp_client.yaml` (connector `relay`, dialer `tcp`), + auth variants
- **Tests**: `TestRelayTCP`, `TestRelayTCPAuth`
---
## Step 2: Tier 2 — TLS/Transport Protocols
Same pattern but using TLS listeners/dialers. No explicit cert files needed — gost auto-generates them.
### 2A. TLS Suite
- **Files**: `tls_test.go`, `testdata/tls/` (4 configs)
- **Tests**: `TestTLSHTTPProxy` (handler `http` + listener `tls`), `TestTLSSOCKS5` (handler `socks5` + listener `tls`)
### 2B. WebSocket Suite
- **Files**: `ws_test.go`, `testdata/ws/` (4 configs)
- **Tests**: `TestWSSOCKS5` (dialer `ws`), `TestWSSSOCKS5` (dialer `wss`)
### 2C. gRPC Suite
- **Files**: `grpc_test.go`, `testdata/grpc/` (2 configs)
- **Tests**: `TestGRPCSOCKS5` (listener `grpc`, metadata `insecure: true`)
### 2D. HTTP/2 TLS (h2) Suite
- **Files**: `h2_test.go`, `testdata/h2/` (2 configs)
- **Tests**: `TestH2HTTP2Proxy` (listener `h2`, dialer `h2`)
### 2E. QUIC Suite
- **Files**: `quic_test.go`, `testdata/quic/` (2 configs)
- **Tests**: `TestQUICHTTP` (listener `quic`, handler `http` — uses UDP ports)
### 2F. KCP Suite
- **Files**: `kcp_test.go`, `testdata/kcp/` (2 configs)
- **Tests**: `TestKCPSOCKS5` (listener `kcp`, handler `socks5` — uses UDP)
### 2G. Multiplex Variants (mws/mwss/mtls/mtcp)
- **Files**: `mux_test.go`, `testdata/mux/` (6 configs)
- **Tests**: `TestMWSSOCKS5`, `TestMWSSSOCKS5`, `TestMtlSSOCKS5`, `TestMtcpSOCKS5`
### 2H. Obfuscation (ohttp/otls)
- **Files**: `obfs_test.go`, `testdata/obfs/` (4 configs)
- **Tests**: `TestOHTTP`, `TestOTLS`
### 2I. PHT (HTTP Pipe)
- **Files**: `pht_test.go`, `testdata/pht/` (4 configs)
- **Tests**: `TestPHT`, `TestPHTS`
---
## Step 3: Tier 3 — Special Infrastructure (Deferred)
These require Docker capabilities (`NET_ADMIN`, `NET_RAW`), SSH key generation, or iptables setup. Implemented after Tiers 1-2 are stable.
| Protocol | Challenge | Approach |
|----------|-----------|----------|
| SSH/sshd | Host key generation | Generate SSH key at test time, mount into container |
| HTTP/3/WebTransport | QUIC+UDP, TLS | Uses UDP like QUIC suite; auto certs work |
| TUN/TAP | `CAP_NET_ADMIN`, `/dev/net/tun` | Privileged container + routing setup |
| Tungo/MASQUE | Same as TUN | Same approach |
| ICMP | `CAP_NET_RAW`, raw sockets | Privileged container |
| Redirect (red/redu) | iptables rules | Privileged + iptables setup in container |
| DTLS | UDP + TLS | Auto certs; UDP port mapping |
---
## Files to Create/Modify
### New files (per suite):
- `tests/e2e/<protocol>_test.go` — Suite struct + test methods
- `tests/e2e/testdata/<protocol>/` — Server + client YAML configs
### Modified files:
- `tests/e2e/utils.go` — Add `RunTCPCase`, `RunUDPCase` helpers
- `tests/e2e/shadowsocks_test.go` — Refactor to use new helpers (optional, reduces duplication)
- `tests/e2e/README.md` — Document new suites
### Reference files (read-only):
- `tests/e2e/shadowsocks_test.go` — Pattern template for all suites
- `tests/e2e/testdata/shadowsocks/tcp_server_aes256gcm.yaml` — Server config template
- `tests/e2e/testdata/shadowsocks/tcp_client_aes256gcm.yaml` — Client config template
---
## Config Pattern Reference
**Server** (all protocols):
```yaml
services:
- name: <proto>-server
addr: :<port>
handler:
type: <handler-type>
listener:
type: <listener-type>
```
**Client** (all protocols):
```yaml
services:
- name: http-proxy
addr: :8080
handler:
type: http
chain: <proto>-chain
listener:
type: tcp
chains:
- name: <proto>-chain
hops:
- name: <proto>-hop
nodes:
- name: <proto>-node
addr: {{.ServerAddr}}
connector:
type: <connector-type>
dialer:
type: <dialer-type>
```
---
## Verification
After each suite is implemented:
1. `cd gost && go build ./...` — Ensure compilation
2. `go vet ./tests/e2e/` — Static analysis
3. `go test ./tests/e2e/ -v -run TestXxxSuite -timeout 5m` — Run individual suite
4. `go test ./tests/e2e/ -v -timeout 10m` — Run all suites together
-53
View File
@@ -1,53 +0,0 @@
# End-to-End Tests
Integration tests that spin up real gost instances inside Docker containers and verify protocol behavior over the network.
## Prerequisites
- [Docker](https://docs.docker.com/get-docker/) (running daemon)
- Go toolchain (for compiling the gost binary under test)
## Running
From the repository root:
```bash
# Run all e2e tests
go test ./tests/e2e/ -v -timeout 10m
# Run a specific test suite
go test ./tests/e2e/ -v -run TestShadowsocksSuite -timeout 5m
go test ./tests/e2e/ -v -run TestParallelSelectorSuite -timeout 5m
# Use a pre-built gost binary (skips compilation)
go test ./tests/e2e/ -v -gost-bin /path/to/gost
```
## Architecture
```
tests/e2e/
├── Dockerfile # Shared base image (Alpine + curl, python3, etc.)
├── main_test.go # TestMain: compiles gost, creates Docker network
├── utils.go # Helpers: container lifecycle, config rendering
├── scripts/
│ ├── tcp_echo.py # HTTP echo server (responds with "hello-gost")
│ └── udp_echo.py # UDP echo server (reflects payloads)
├── testdata/ # config files or data files for running cases
├── shadowsocks_test.go # Shadowsocks protocol tests
└── parallel_selector_test.go # Parallel node selector tests
```
### How it works
1. **TestMain** (`main_test.go`) compiles the gost binary from `../../cmd/gost` (unless `-gost-bin` is provided) and creates a shared Docker network for all containers.
2. Each test suite starts echo server containers (TCP/UDP), then launches separate gost containers for server and client roles.
3. Client configs use Go template syntax (`{{.ServerAddr}}`) so the server address is injected at runtime.
4. Tests verify end-to-end connectivity by sending traffic through the gost proxy chain and checking that the echo server responds correctly.
## Tips
- Increase `-timeout` for CI or slow networks. Container image builds on first run take extra time.
- Use `-gost-bin` to avoid recompiling when iterating on tests locally.
- Add `-v` to see container log output on failure.
- RunGostContainer will wait for exposedPorts automatically, but it's not reliable for udp ports. So, you should check the readiness of udp ports inside cases.
-360
View File
@@ -1,360 +0,0 @@
package e2e
import (
"context"
"encoding/base64"
"fmt"
"io"
"strings"
"testing"
"time"
"github.com/stretchr/testify/suite"
"github.com/testcontainers/testcontainers-go"
)
type DNSSuite struct {
suite.Suite
ctx context.Context
}
func (s *DNSSuite) SetupSuite() {
s.ctx = context.Background()
}
// dnsQuery sends a DNS query via the Python client and retries on failure.
// expected can be an IP string, "empty" (expect zero answer records), or "" (no check).
func (s *DNSSuite) dnsQuery(gostC testcontainers.Container, mode, host, port, qname, qtype, expected string) {
args := []string{"python3", "/scripts/dns_query.py", mode, host, port, qname, qtype}
if expected != "" {
args = append(args, expected)
}
for i := range 5 {
code, out, err := gostC.Exec(s.ctx, args)
if err != nil {
s.T().Logf("query attempt %d exec error: %v", i+1, err)
time.Sleep(time.Second)
continue
}
body, err := io.ReadAll(out)
if err != nil {
s.T().Logf("query attempt %d read error: %v", i+1, err)
time.Sleep(time.Second)
continue
}
output := string(body)
s.T().Logf("query attempt %d: %s", i+1, strings.TrimSpace(output))
if code == 0 {
return
}
time.Sleep(time.Second)
}
s.T().Fatalf("DNS query %s %s %s %s failed after 5 retries", mode, qname, qtype, host)
}
// dnsQueryOnce sends a single DNS query with no retries. Returns exit code and output.
func (s *DNSSuite) dnsQueryOnce(gostC testcontainers.Container, mode, host, port, qname, qtype string) (int, string) {
args := []string{"python3", "/scripts/dns_query.py", mode, host, port, qname, qtype}
code, out, err := gostC.Exec(s.ctx, args)
if err != nil {
return 1, fmt.Sprintf("exec error: %v", err)
}
body, _ := io.ReadAll(out)
return code, string(body)
}
// dnsQueryWithDelay calls dnsQuery with a small wait before the first attempt.
func (s *DNSSuite) dnsQueryWithDelay(gostC testcontainers.Container, mode, host, port, qname, qtype, expected string) {
time.Sleep(500 * time.Millisecond)
s.dnsQuery(gostC, mode, host, port, qname, qtype, expected)
}
// sendRaw sends raw data via base64+nc and returns the response.
func (s *DNSSuite) sendRaw(gostC testcontainers.Container, host, port, data string) string {
encoded := base64.StdEncoding.EncodeToString([]byte(data))
cmd := []string{"sh", "-c",
fmt.Sprintf("echo %s | base64 -d | nc -w 3 -u %s %s", encoded, host, port)}
_, out, _ := gostC.Exec(s.ctx, cmd)
b, _ := io.ReadAll(out)
return string(b)
}
// startDNSResponder starts the UDP DNS responder and returns the container.
func (s *DNSSuite) startDNSResponder() testcontainers.Container {
dnsC, err := RunDNSResponderContainer(s.ctx, SharedNetworkName)
s.Require().NoError(err)
return dnsC
}
// startGostWithQueryScript starts a gost container with dns_query.py mounted.
func (s *DNSSuite) startGostWithQueryScript(yamlPath, exposedPort string) testcontainers.Container {
gostC, err := RunGostContainerWithFiles(s.ctx, SharedNetworkName,
yamlPath,
[]testcontainers.ContainerFile{
{HostFilePath: "scripts/dns_query.py", ContainerFilePath: "/scripts/dns_query.py", FileMode: 0644},
},
exposedPort)
s.Require().NoError(err)
return gostC
}
// ---------------------------------------------------------------------------
// Upstream resolution
// ---------------------------------------------------------------------------
func (s *DNSSuite) TestDNSUpstream() {
dnsC := s.startDNSResponder()
defer dnsC.Terminate(s.ctx)
gostC := s.startGostWithQueryScript("testdata/dns/server_upstream.yaml", "1053/udp")
defer gostC.Terminate(s.ctx)
s.T().Run("a-record", func(t *testing.T) {
s.dnsQueryWithDelay(gostC, "udp", "127.0.0.1", "1053", "test.example.com", "A", "10.0.0.1")
})
s.T().Run("aaaa-record", func(t *testing.T) {
s.dnsQuery(gostC, "udp", "127.0.0.1", "1053", "example.com", "AAAA", "::1")
})
s.T().Run("second-a-record", func(t *testing.T) {
s.dnsQuery(gostC, "udp", "127.0.0.1", "1053", "test2.example.com", "A", "10.0.0.2")
})
}
// ---------------------------------------------------------------------------
// TCP mode
// ---------------------------------------------------------------------------
func (s *DNSSuite) TestDNSTCP() {
s.T().Log("start TCP DNS responder container...")
dnsC, err := RunTCPDNSResponderContainer(s.ctx, SharedNetworkName)
s.Require().NoError(err)
defer dnsC.Terminate(s.ctx)
gostC := s.startGostWithQueryScript("testdata/dns/server_tcp.yaml", "1053/tcp")
defer gostC.Terminate(s.ctx)
s.dnsQueryWithDelay(gostC, "tcp", "127.0.0.1", "1053", "test.example.com", "A", "10.0.0.1")
s.dnsQuery(gostC, "tcp", "127.0.0.1", "1053", "test2.example.com", "A", "10.0.0.2")
}
// ---------------------------------------------------------------------------
// Bypass rules
// ---------------------------------------------------------------------------
func (s *DNSSuite) TestDNSBypass() {
dnsC := s.startDNSResponder()
defer dnsC.Terminate(s.ctx)
gostC := s.startGostWithQueryScript("testdata/dns/server_bypass.yaml", "1053/udp")
defer gostC.Terminate(s.ctx)
s.T().Run("blocked-domain-empty-answer", func(t *testing.T) {
s.dnsQueryWithDelay(gostC, "udp", "127.0.0.1", "1053",
"test.example.com", "A", "empty")
})
s.T().Run("non-blocked-domain", func(t *testing.T) {
s.dnsQuery(gostC, "udp", "127.0.0.1", "1053",
"test2.example.com", "A", "10.0.0.2")
})
}
// ---------------------------------------------------------------------------
// Host mapper
// ---------------------------------------------------------------------------
// TestDNSHostMapper verifies DNS resolution via the host mapper before
// reaching the upstream exchanger.
//
// Config: server_hosts.yaml maps mapped.example.com → 10.0.0.100 and
// points the handler at an unreachable upstream (udp://127.0.0.1:1).
// The handler checks the host mapper before the exchange path, so
// mapped names resolve without needing any upstream DNS. Unmapped
// names must fall through to the exchanger, which fails — confirming
// the host-mapper path was the only reason mapped names worked.
func (s *DNSSuite) TestDNSHostMapper() {
gostC, err := RunGostContainerWithFiles(s.ctx, SharedNetworkName,
"testdata/dns/server_hosts.yaml",
[]testcontainers.ContainerFile{
{HostFilePath: "scripts/dns_query.py", ContainerFilePath: "/scripts/dns_query.py", FileMode: 0644},
},
"1053/udp")
s.Require().NoError(err)
defer gostC.Terminate(s.ctx)
s.T().Run("mapped-a-record", func(t *testing.T) {
// mapped.example.com → host mapper returns 10.0.0.100
// without contacting any upstream.
s.dnsQueryWithDelay(gostC, "udp", "127.0.0.1", "1053",
"mapped.example.com", "A", "10.0.0.100")
})
s.T().Run("unmapped-domain", func(t *testing.T) {
// test.example.com is not in the hosts mapping. The handler
// will fall through to the exchanger path, which fails because
// no upstream is configured. Query should return empty/NXDOMAIN.
code, output := s.dnsQueryOnce(gostC, "udp", "127.0.0.1", "1053",
"test.example.com", "A")
s.T().Logf("unmapped query: code=%d, %s", code, strings.TrimSpace(output))
// Non-zero exit indicates no valid response — expected since
// there's no working upstream.
s.Assert().NotEqual(0, code,
"unmapped domain should fail with no upstream")
})
}
// ---------------------------------------------------------------------------
// Exchange failure (unreachable upstream)
// ---------------------------------------------------------------------------
// TestDNSExchangeFailure verifies graceful handling when the upstream DNS
// exchanger is unreachable.
//
// Config: server_exchange_failure.yaml points to udp://127.0.0.1:1 which
// is unreachable. The handler must not crash or leak goroutines when the
// upstream exchange fails.
func (s *DNSSuite) TestDNSExchangeFailure() {
gostC, err := RunGostContainerWithFiles(s.ctx, SharedNetworkName,
"testdata/dns/server_exchange_failure.yaml",
[]testcontainers.ContainerFile{
{HostFilePath: "scripts/dns_query.py", ContainerFilePath: "/scripts/dns_query.py", FileMode: 0644},
},
"1053/udp")
s.Require().NoError(err)
defer gostC.Terminate(s.ctx)
s.T().Run("unreachable-upstream", func(t *testing.T) {
// Send query once — the exchange will fail (timeout / ICMP
// unreachable). No response is expected.
code, output := s.dnsQueryOnce(gostC, "udp", "127.0.0.1", "1053",
"test.example.com", "A")
s.T().Logf("exchange failure result: code=%d, %s",
code, strings.TrimSpace(output))
// Non-zero exit is expected since the exchange fails.
s.Assert().NotEqual(0, code,
"exchange failure should return non-zero exit")
})
s.T().Run("container-alive-after-failure", func(t *testing.T) {
// Verify the gost container is still running after the
// failed exchange — proves no crash or hang.
aliveCode, _, aliveErr := gostC.Exec(s.ctx, []string{"true"})
s.Require().NoError(aliveErr,
"container exec should succeed after exchange failure")
s.Require().Equal(0, aliveCode,
"gost container should be alive after exchange failure")
})
}
// ---------------------------------------------------------------------------
// Rate limiter
// ---------------------------------------------------------------------------
// TestDNSRateLimiter verifies that rate limiting configuration is accepted
// and the handler processes queries through the rate limiter path without
// crashing. Uses a generous global limit (1000/s) so queries pass.
func (s *DNSSuite) TestDNSRateLimiter() {
dnsC := s.startDNSResponder()
defer dnsC.Terminate(s.ctx)
gostC, err := RunGostContainerWithFiles(s.ctx, SharedNetworkName,
"testdata/dns/server_rlimiter.yaml",
[]testcontainers.ContainerFile{
{HostFilePath: "scripts/dns_query.py", ContainerFilePath: "/scripts/dns_query.py", FileMode: 0644},
},
"1053/udp")
s.Require().NoError(err)
defer gostC.Terminate(s.ctx)
s.T().Run("query-with-limiter", func(t *testing.T) {
s.dnsQueryWithDelay(gostC, "udp", "127.0.0.1", "1053",
"test.example.com", "A", "10.0.0.1")
})
s.T().Run("second-query-with-limiter", func(t *testing.T) {
s.dnsQuery(gostC, "udp", "127.0.0.1", "1053",
"test2.example.com", "A", "10.0.0.2")
})
}
// ---------------------------------------------------------------------------
// Invalid query
// ---------------------------------------------------------------------------
// TestDNSInvalidQuery verifies the DNS listener gracefully handles malformed
// DNS messages. Sends garbage bytes and checks the gost container is
// unaffected (no crash, no hang).
func (s *DNSSuite) TestDNSInvalidQuery() {
gostC := s.startGostWithQueryScript("testdata/dns/server_upstream.yaml", "1053/udp")
defer gostC.Terminate(s.ctx)
s.T().Run("send-garbage-bytes", func(t *testing.T) {
// Send 5 bytes of garbage via UDP. The miekg/dns server
// will fail to parse them and discard the packet.
s.sendRaw(gostC, "127.0.0.1", "1053", "garbage!")
})
s.T().Run("container-alive-after-garbage", func(t *testing.T) {
// Verify the gost container is still alive after receiving
// invalid data.
aliveCode, _, aliveErr := gostC.Exec(s.ctx, []string{"true"})
s.Require().NoError(aliveErr,
"container exec should succeed after invalid query")
s.Require().Equal(0, aliveCode,
"gost container should be alive after invalid query")
})
}
// ---------------------------------------------------------------------------
// DNS over TLS
// ---------------------------------------------------------------------------
// TestDNSTLS verifies the DNS listener in TLS mode (DNS over TLS).
// Starts the UDP DNS responder (unencrypted), a gost DNS server with
// listener mode: tls, and queries through the TLS endpoint.
func (s *DNSSuite) TestDNSTLS() {
dnsC := s.startDNSResponder()
defer dnsC.Terminate(s.ctx)
gostC, err := RunGostContainerWithFiles(s.ctx, SharedNetworkName,
"testdata/dns/server_tls.yaml",
[]testcontainers.ContainerFile{
{HostFilePath: "scripts/dns_tls_query.py", ContainerFilePath: "/scripts/dns_tls_query.py", FileMode: 0644},
},
"1053/tcp")
s.Require().NoError(err)
defer gostC.Terminate(s.ctx)
// Query via TLS
args := []string{"python3", "/scripts/dns_tls_query.py",
"127.0.0.1", "1053", "test.example.com", "A", "10.0.0.1"}
for i := range 5 {
code, out, err := gostC.Exec(s.ctx, args)
if err != nil {
s.T().Logf("tls query attempt %d exec error: %v", i+1, err)
time.Sleep(time.Second)
continue
}
body, err := io.ReadAll(out)
if err != nil {
s.T().Logf("tls query attempt %d read error: %v", i+1, err)
time.Sleep(time.Second)
continue
}
output := string(body)
s.T().Logf("tls query attempt %d: %s", i+1, strings.TrimSpace(output))
if code == 0 {
return
}
time.Sleep(time.Second)
}
s.T().Fatal("TLS query failed after 5 retries")
}
func TestDNSSuite(t *testing.T) {
suite.Run(t, new(DNSSuite))
}
-194
View File
@@ -1,194 +0,0 @@
package e2e
import (
"context"
"io"
"strings"
"testing"
"github.com/stretchr/testify/suite"
"github.com/testcontainers/testcontainers-go"
)
type FileSuite struct {
suite.Suite
ctx context.Context
}
func (s *FileSuite) SetupSuite() {
s.ctx = context.Background()
}
func (s *FileSuite) TearDownSuite() {}
// TestFileGetExisting verifies that GET on an existing file returns
// the file content with HTTP 200. Covers the basic file serving path:
// file handler → http.FileServer → file read.
func (s *FileSuite) TestFileGetExisting() {
gostC, err := RunGostContainerWithFiles(s.ctx, SharedNetworkName,
"testdata/file/server.yaml",
[]testcontainers.ContainerFile{
{HostFilePath: "testdata/file/hello.txt", ContainerFilePath: "/srv/files/hello.txt", FileMode: 0644},
},
"8080/tcp")
s.Require().NoError(err)
defer gostC.Terminate(s.ctx)
cmd := []string{"curl", "-v", "-s", "http://127.0.0.1:8080/hello.txt"}
code, out, err := gostC.Exec(s.ctx, cmd)
s.Require().NoError(err)
body, err := io.ReadAll(out)
s.Require().NoError(err)
if code != 0 || !strings.Contains(string(body), "hello-gost-file") {
DumpLogs(s.T(), s.ctx, "file-server logs", gostC)
}
s.Require().Equal(0, code)
s.Require().Contains(string(body), "hello-gost-file")
}
// TestFileGetNotFound verifies that GET on a nonexistent file returns
// 404 Not Found.
func (s *FileSuite) TestFileGetNotFound() {
gostC, err := RunGostContainerWithFiles(s.ctx, SharedNetworkName,
"testdata/file/server.yaml",
[]testcontainers.ContainerFile{
{HostFilePath: "testdata/file/hello.txt", ContainerFilePath: "/srv/files/hello.txt", FileMode: 0644},
},
"8080/tcp")
s.Require().NoError(err)
defer gostC.Terminate(s.ctx)
cmd := []string{"curl", "-v", "-s", "-o", "/dev/null", "-w", "%{http_code}",
"http://127.0.0.1:8080/nonexistent.txt"}
_, out, err := gostC.Exec(s.ctx, cmd)
s.Require().NoError(err)
body, _ := io.ReadAll(out)
s.Assert().Contains(string(body), "404")
}
// TestFileGetIndexHtml verifies that GET / serves index.html when
// present in the served directory. Covers the default index document
// behavior of http.FileServer.
func (s *FileSuite) TestFileGetIndexHtml() {
gostC, err := RunGostContainerWithFiles(s.ctx, SharedNetworkName,
"testdata/file/server.yaml",
[]testcontainers.ContainerFile{
{HostFilePath: "testdata/file/index.html", ContainerFilePath: "/srv/files/index.html", FileMode: 0644},
},
"8080/tcp")
s.Require().NoError(err)
defer gostC.Terminate(s.ctx)
cmd := []string{"curl", "-v", "-s", "http://127.0.0.1:8080/"}
code, out, err := gostC.Exec(s.ctx, cmd)
s.Require().NoError(err)
body, err := io.ReadAll(out)
s.Require().NoError(err)
if code != 0 || !strings.Contains(string(body), "gost file index") {
DumpLogs(s.T(), s.ctx, "file-server index logs", gostC)
}
s.Require().Equal(0, code)
s.Require().Contains(string(body), "gost file index")
}
// TestFilePutUpload verifies that PUT uploads a file when file.put is
// enabled. Uploads a file and then reads it back via GET to confirm
// the content was persisted.
func (s *FileSuite) TestFilePutUpload() {
gostC, err := RunGostContainerWithFiles(s.ctx, SharedNetworkName,
"testdata/file/server_put.yaml",
[]testcontainers.ContainerFile{
{HostFilePath: "testdata/file/.empty", ContainerFilePath: "/srv/files/.empty", FileMode: 0644},
},
"8080/tcp")
s.Require().NoError(err)
defer gostC.Terminate(s.ctx)
// Create source content inside the container.
_, _, err = gostC.Exec(s.ctx, []string{
"sh", "-c", "printf 'uploaded-content' > /tmp/upload_src.txt",
})
s.Require().NoError(err)
// Upload via PUT.
_, out, err := gostC.Exec(s.ctx, []string{
"curl", "-v", "-s", "-T", "/tmp/upload_src.txt",
"http://127.0.0.1:8080/uploaded.txt",
})
s.Require().NoError(err)
putBody, _ := io.ReadAll(out)
s.T().Logf("PUT response:\n%s", string(putBody))
// Read back the uploaded file via GET.
_, out2, err := gostC.Exec(s.ctx, []string{
"curl", "-v", "-s",
"http://127.0.0.1:8080/uploaded.txt",
})
s.Require().NoError(err)
body, _ := io.ReadAll(out2)
s.Assert().Contains(string(body), "uploaded-content")
}
// TestFilePutNoPermission verifies that PUT returns 405 Method Not
// Allowed when file.put is not enabled (default: false).
func (s *FileSuite) TestFilePutNoPermission() {
gostC, err := RunGostContainerWithFiles(s.ctx, SharedNetworkName,
"testdata/file/server.yaml",
[]testcontainers.ContainerFile{
{HostFilePath: "testdata/file/.empty", ContainerFilePath: "/srv/files/.empty", FileMode: 0644},
},
"8080/tcp")
s.Require().NoError(err)
defer gostC.Terminate(s.ctx)
cmd := []string{"curl", "-v", "-s", "-o", "/dev/null", "-w", "%{http_code}",
"-T", "/dev/null", "http://127.0.0.1:8080/test.txt"}
_, out, err := gostC.Exec(s.ctx, cmd)
s.Require().NoError(err)
body, _ := io.ReadAll(out)
s.Assert().Contains(string(body), "405")
}
// TestFileAuth verifies authentication on the file handler.
// Without credentials the handler returns 401 Unauthorized,
// with valid credentials it serves the file.
func (s *FileSuite) TestFileAuth() {
gostC, err := RunGostContainerWithFiles(s.ctx, SharedNetworkName,
"testdata/file/server_auth.yaml",
[]testcontainers.ContainerFile{
{HostFilePath: "testdata/file/hello.txt", ContainerFilePath: "/srv/files/hello.txt", FileMode: 0644},
},
"8080/tcp")
s.Require().NoError(err)
defer gostC.Terminate(s.ctx)
s.T().Run("no-auth-401", func(t *testing.T) {
cmd := []string{"curl", "-v", "-s", "-o", "/dev/null", "-w", "%{http_code}",
"http://127.0.0.1:8080/hello.txt"}
_, out, _ := gostC.Exec(s.ctx, cmd)
body, _ := io.ReadAll(out)
s.Assert().Contains(string(body), "401")
})
s.T().Run("with-auth-success", func(t *testing.T) {
cmd := []string{"curl", "-v", "-s", "-u", "user:pass",
"http://127.0.0.1:8080/hello.txt"}
code, out, err := gostC.Exec(s.ctx, cmd)
s.Require().NoError(err)
body, err := io.ReadAll(out)
s.Require().NoError(err)
if code != 0 || !strings.Contains(string(body), "hello-gost-file") {
DumpLogs(s.T(), s.ctx, "file-server auth logs", gostC)
}
s.Require().Equal(0, code)
s.Require().Contains(string(body), "hello-gost-file")
})
}
func TestFileSuite(t *testing.T) {
suite.Run(t, new(FileSuite))
}
-291
View File
@@ -1,291 +0,0 @@
package e2e
import (
"context"
"encoding/base64"
"fmt"
"io"
"strings"
"testing"
"github.com/stretchr/testify/suite"
"github.com/testcontainers/testcontainers-go"
)
type ForwardSuite struct {
suite.Suite
ctx context.Context
echoC testcontainers.Container
echoIP string
udpC testcontainers.Container
}
func (s *ForwardSuite) SetupSuite() {
s.ctx = context.Background()
s.T().Logf("start tcp echo container...")
echoC, err := RunEchoContainer(s.ctx, SharedNetworkName)
s.Require().NoError(err)
s.echoC = echoC
echoIP, err := echoC.ContainerIP(s.ctx)
s.Require().NoError(err)
s.echoIP = echoIP
s.T().Logf("start udp echo container...")
udpC, err := RunUDPEchoContainer(s.ctx, SharedNetworkName)
s.Require().NoError(err)
s.udpC = udpC
}
// sendRaw sends a raw HTTP request via netcat and returns the response.
// Uses base64 to avoid shell quoting issues with CRLF bytes.
func (s *ForwardSuite) sendRaw(gostC testcontainers.Container, host, port, data string) string {
encoded := base64.StdEncoding.EncodeToString([]byte(data))
cmd := []string{"sh", "-c",
fmt.Sprintf("echo %s | base64 -d | nc -w 5 %s %s", encoded, host, port)}
_, out, _ := gostC.Exec(s.ctx, cmd)
b, _ := io.ReadAll(out)
return string(b)
}
func (s *ForwardSuite) TearDownSuite() {
if s.echoC != nil {
s.echoC.Terminate(s.ctx)
}
if s.udpC != nil {
s.udpC.Terminate(s.ctx)
}
}
// TestTCPForward verifies basic TCP forward handler (handler type: tcp).
// The forward handler pipes raw TCP connections to the configured forwarder
// node (tcp-echo:5678). curl connects directly to the handler port and sends
// an HTTP request, expecting the echo server's "hello-gost" response.
func (s *ForwardSuite) TestTCPForward() {
gostC, err := RunGostContainerWithPorts(s.ctx, SharedNetworkName,
"testdata/forward/server.yaml", "8000/tcp")
s.Require().NoError(err)
defer gostC.Terminate(s.ctx)
// curl directly to the handler port (not via -x proxy flag).
// The TCP forward handler pipes our connection to tcp-echo:5678.
cmd := []string{"curl", "-v", "-s", "http://127.0.0.1:8000/"}
code, out, err := gostC.Exec(s.ctx, cmd)
s.Require().NoError(err)
body, err := io.ReadAll(out)
s.Require().NoError(err)
if code != 0 || !strings.Contains(string(body), "hello-gost") {
DumpLogs(s.T(), s.ctx, "tcp-forward logs", gostC)
}
s.Require().Equal(0, code)
s.Require().Contains(string(body), "hello-gost")
}
// TestForwardAlias verifies that the "forward" handler type (alias for "tcp")
// works identically to the "tcp" handler type.
func (s *ForwardSuite) TestForwardAlias() {
gostC, err := RunGostContainerWithPorts(s.ctx, SharedNetworkName,
"testdata/forward/server.yaml", "8000/tcp")
s.Require().NoError(err)
defer gostC.Terminate(s.ctx)
cmd := []string{"curl", "-v", "-s", "http://127.0.0.1:8000/"}
code, out, err := gostC.Exec(s.ctx, cmd)
s.Require().NoError(err)
body, err := io.ReadAll(out)
s.Require().NoError(err)
if code != 0 || !strings.Contains(string(body), "hello-gost") {
DumpLogs(s.T(), s.ctx, "forward-alias logs", gostC)
}
s.Require().Equal(0, code)
s.Require().Contains(string(body), "hello-gost")
}
// TestTCPForwardSniffing verifies TCP forward handler with sniffing enabled.
// When sniffing is enabled and the connection starts with HTTP data, the handler
// detects the protocol via sniffing.Sniff and delegates to the HTTP sniffer
// for protocol-aware forwarding. The result is the same as raw forwarding:
// "hello-gost" from the echo server.
func (s *ForwardSuite) TestTCPForwardSniffing() {
gostC, err := RunGostContainerWithPorts(s.ctx, SharedNetworkName,
"testdata/forward/server_sniffing.yaml", "8000/tcp")
s.Require().NoError(err)
defer gostC.Terminate(s.ctx)
// curl directly — sniffing detects HTTP and handles via sniffer.
cmd := []string{"curl", "-v", "-s", "http://127.0.0.1:8000/"}
code, out, err := gostC.Exec(s.ctx, cmd)
s.Require().NoError(err)
body, err := io.ReadAll(out)
s.Require().NoError(err)
if code != 0 || !strings.Contains(string(body), "hello-gost") {
DumpLogs(s.T(), s.ctx, "tcp-forward-sniffing logs", gostC)
}
s.Require().Equal(0, code)
s.Require().Contains(string(body), "hello-gost")
}
// TestTCPForwardRaw verifies raw TCP forwarding by sending an HTTP request
// via netcat through the forward handler. This tests the handleRawForwarding
// code path (no sniffing), proving that raw bytes are piped through correctly.
func (s *ForwardSuite) TestTCPForwardRaw() {
gostC, err := RunGostContainerWithPorts(s.ctx, SharedNetworkName,
"testdata/forward/server.yaml", "8000/tcp")
s.Require().NoError(err)
defer gostC.Terminate(s.ctx)
// Send raw HTTP request via nc (not curl) to exercise the raw pipe path.
resp := s.sendRaw(gostC, "127.0.0.1", "8000",
"GET / HTTP/1.0\r\nHost: tcp-echo\r\n\r\n")
s.Assert().Contains(resp, "hello-gost",
"raw request through TCP forward should reach echo server")
}
// TestTCPForwardIdleTimeout verifies that idleTimeout closes the pipe after
// a period of inactivity. The forward handler's xnet.Pipe uses idleTimeout
// as a read deadline on the upstream connection — if no data flows for
// that duration, the pipe closes both directions.
func (s *ForwardSuite) TestTCPForwardIdleTimeout() {
gostC, err := RunGostContainerWithFiles(s.ctx, SharedNetworkName,
"testdata/forward/server_idle_timeout.yaml",
[]testcontainers.ContainerFile{
{HostFilePath: "scripts/tcp_idle_timeout.py", ContainerFilePath: "/scripts/tcp_idle_timeout.py", FileMode: 0644},
},
"8000/tcp")
s.Require().NoError(err)
defer gostC.Terminate(s.ctx)
// The Python script:
// 1. Connects to gost TCP forward port
// 2. Sends HTTP GET → expects "hello-gost" confirm pipe is alive
// 3. Waits > idleTimeout (3s + 2s buffer)
// 4. Sends more data — expects connection to be closed
code, out, err := gostC.Exec(s.ctx, []string{
"python3", "/scripts/tcp_idle_timeout.py",
"127.0.0.1", "8000", "3",
})
output, _ := io.ReadAll(out)
s.T().Logf("idle timeout output:\n%s", string(output))
if code != 0 {
DumpLogs(s.T(), s.ctx, "tcp-idle-timeout logs", gostC)
}
s.Require().Equal(0, code, "idle timeout test script should exit 0")
}
// TestUDPForward verifies basic UDP forwarding (handler: udp, listener: udp).
// The handler uses handleRawDatagram via the stateful UDP listener's
// per-client session conns (which implement net.PacketConn).
// A Python script inside the gost container sends a UDP datagram through
// the forward handler and verifies the echo response from udp-echo:5679.
func (s *ForwardSuite) TestUDPForward() {
gostC, err := RunGostContainerWithFiles(s.ctx, SharedNetworkName,
"testdata/forward/server_udp.yaml",
[]testcontainers.ContainerFile{
{HostFilePath: "scripts/udp_forward_test.py", ContainerFilePath: "/scripts/udp_forward_test.py", FileMode: 0644},
},
"9000/udp")
s.Require().NoError(err)
defer gostC.Terminate(s.ctx)
code, out, err := gostC.Exec(s.ctx, []string{
"python3", "/scripts/udp_forward_test.py",
"127.0.0.1", "9000",
})
output, _ := io.ReadAll(out)
s.T().Logf("udp forward output:\n%s", string(output))
if code != 0 {
DumpLogs(s.T(), s.ctx, "udp-forward logs", gostC)
}
s.Require().Equal(0, code, "udp forward test script should exit 0")
}
// TestUDPForwardStateless verifies UDP forwarding with stateless mode.
// Both listener and handler use stateless: true, so each datagram is a
// single request-response cycle with no per-client session tracking.
func (s *ForwardSuite) TestUDPForwardStateless() {
gostC, err := RunGostContainerWithFiles(s.ctx, SharedNetworkName,
"testdata/forward/server_udp_stateless.yaml",
[]testcontainers.ContainerFile{
{HostFilePath: "scripts/udp_forward_test.py", ContainerFilePath: "/scripts/udp_forward_test.py", FileMode: 0644},
},
"9000/udp")
s.Require().NoError(err)
defer gostC.Terminate(s.ctx)
code, out, err := gostC.Exec(s.ctx, []string{
"python3", "/scripts/udp_forward_test.py",
"127.0.0.1", "9000",
})
output, _ := io.ReadAll(out)
s.T().Logf("udp forward stateless output:\n%s", string(output))
if code != 0 {
DumpLogs(s.T(), s.ctx, "udp-forward-stateless logs", gostC)
}
s.Require().Equal(0, code, "udp forward stateless test script should exit 0")
}
// TestTCPForwardSniffingBypass verifies that when sniffing is enabled and a
// bypass rule blocks the target, the HTTP sniffer returns 403 Forbidden.
// The forward handler delegates to the HTTP sniffer (via handleSniffedProtocol),
// and the sniffer's resolveHTTPNode checks h.options.Bypass and returns 403
// when the destination is matched.
func (s *ForwardSuite) TestTCPForwardSniffingBypass() {
gostC, err := RunGostContainerWithPorts(s.ctx, SharedNetworkName,
"testdata/forward/server_bypass_sniffing.yaml", "8000/tcp")
s.Require().NoError(err)
defer gostC.Terminate(s.ctx)
// curl connects directly; sniffing detects HTTP. The sniffer bypass
// check matches 0.0.0.0/0 and returns 403 Forbidden.
cmd := []string{"curl", "-v", "-s", "-D", "-", "-o", "/dev/null",
"http://127.0.0.1:8000/"}
_, out, err := gostC.Exec(s.ctx, cmd)
s.Require().NoError(err)
body, _ := io.ReadAll(out)
output := string(body)
s.Assert().Contains(output, "403",
"bypass should return 403 Forbidden from sniffer")
}
// TestTCPForwardMultiNodeProtocol verifies that sniffing + protocol-filtered
// forwarder nodes correctly routes an HTTP request to the node with matching
// protocol (http), rather than the one with protocol: tls.
//
// Config has two nodes:
// - echo-http (protocol: http) → tcp-echo:5678 (works, returns "hello-gost")
// - echo-tls (protocol: tls) → tcp-echo:1 (closed port, would fail)
//
// With sniffing enabled, an HTTP request is detected as protocol "http",
// Select("http") filters to only the echo-http node. If protocol filtering
// fails and the tls node is selected instead, the connection to port 1
// fails and the test fails.
func (s *ForwardSuite) TestTCPForwardMultiNodeProtocol() {
gostC, err := RunGostContainerWithPorts(s.ctx, SharedNetworkName,
"testdata/forward/server_multi_node.yaml", "8000/tcp")
s.Require().NoError(err)
defer gostC.Terminate(s.ctx)
// curl sends an HTTP request → sniffed as "http" → Select("http")
// filters to echo-http (protocol: http) → pipes to tcp-echo:5678
cmd := []string{"curl", "-v", "-s", "http://127.0.0.1:8000/"}
code, out, err := gostC.Exec(s.ctx, cmd)
s.Require().NoError(err)
body, err := io.ReadAll(out)
s.Require().NoError(err)
if code != 0 || !strings.Contains(string(body), "hello-gost") {
DumpLogs(s.T(), s.ctx, "tcp-forward-multi-node logs", gostC)
}
s.Require().Equal(0, code)
s.Require().Contains(string(body), "hello-gost",
"HTTP request should route to the http-protocol node via protocol filtering")
}
func TestForwardSuite(t *testing.T) {
suite.Run(t, new(ForwardSuite))
}
-216
View File
@@ -1,216 +0,0 @@
package e2e
import (
"context"
"fmt"
"io"
"os"
"strings"
"sync"
"testing"
"github.com/stretchr/testify/suite"
"github.com/testcontainers/testcontainers-go"
)
// HTTP2Suite covers the HTTP/2 proxy handler (handler type: http2) together
// with the HTTP/2 listener (listener type: http2). The http2 listener wraps
// the underlying TCP listener with TLS and configures an h2 server, so the
// handler is always reached over HTTP/2 frames.
//
// Because Alpine's curl cannot be relied on to speak HTTP/2 proxy to the
// server directly, the suite uses the canonical GOST chaining pattern: a
// client container exposes a plain HTTP proxy which forwards through an
// http2 connector + http2 dialer to the http2 server. This exercises the
// h2 listener, the h2 handler, and the h2 connector/dialer together.
type HTTP2Suite struct {
suite.Suite
ctx context.Context
echoC testcontainers.Container
echoIP string
}
func (s *HTTP2Suite) SetupSuite() {
s.ctx = context.Background()
s.T().Logf("start tcp echo container...")
echoC, err := RunEchoContainer(s.ctx, SharedNetworkName)
s.Require().NoError(err)
s.echoC = echoC
echoIP, err := echoC.ContainerIP(s.ctx)
s.Require().NoError(err)
s.echoIP = echoIP
}
func (s *HTTP2Suite) TearDownSuite() {
if s.echoC != nil {
s.echoC.Terminate(s.ctx)
}
}
// startChain brings up an http2 server (alias "h2-server") and an http client
// container that chains to it through connector/dialer type http2. The client
// exposes port 8080 for curl. The rendered client config is cleaned up by the
// caller-supplied template path resolving {{.ServerAddr}} to h2-server:8443.
func (s *HTTP2Suite) startChain(serverYAML, clientTmpl string) (testcontainers.Container, testcontainers.Container) {
s.T().Helper()
serverC, err := RunGostContainerWithOptions(s.ctx, SharedNetworkName,
serverYAML, []string{"h2-server"}, []string{"8443/tcp"})
s.Require().NoError(err)
rendered, err := RenderConfig(clientTmpl, ConfigData{ServerAddr: "h2-server:8443"})
s.Require().NoError(err)
s.T().Cleanup(func() { os.Remove(rendered) })
clientC, err := RunGostContainerWithPorts(s.ctx, SharedNetworkName,
rendered, "8080/tcp")
s.Require().NoError(err)
return serverC, clientC
}
// curlEcho runs curl through the client's local http proxy and returns the
// process exit code plus the captured body.
func (s *HTTP2Suite) curlEcho(clientC testcontainers.Container) (int, string) {
s.T().Helper()
cmd := []string{"curl", "-v", "-s", "-x", "http://127.0.0.1:8080",
fmt.Sprintf("http://%s:5678", s.echoIP)}
code, out, _ := clientC.Exec(s.ctx, cmd)
body, _ := io.ReadAll(out)
return code, string(body)
}
func (s *HTTP2Suite) dump(label string, cs ...testcontainers.Container) {
for _, c := range cs {
DumpLogs(s.T(), s.ctx, label, c)
}
}
// TestHTTP2ForwardProxy verifies the core HTTP/2 proxy path: a plain HTTP
// request through the client proxy is tunneled to the http2 server via an h2
// CONNECT stream and reaches the echo backend.
//
// Covers:
// - listener type: http2 (TLS + h2 server)
// - handler type: http2 (CONNECT tunnel + bidirectional pipe)
// - connector type: http2, dialer type: http2 (h2 client)
func (s *HTTP2Suite) TestHTTP2ForwardProxy() {
serverC, clientC := s.startChain("testdata/http2/server.yaml", "testdata/http2/client.yaml")
defer serverC.Terminate(s.ctx)
defer clientC.Terminate(s.ctx)
code, body := s.curlEcho(clientC)
if code != 0 || !strings.Contains(body, "hello-gost") {
s.dump("http2-forward logs", clientC, serverC)
}
s.Require().Equal(0, code)
s.Require().Contains(body, "hello-gost")
}
// TestHTTP2Auth verifies proxy authentication over the h2 tunnel, plus
// authBasicRealm and hash metadata parsing on the http2 handler.
//
// - with auth: CONNECT succeeds → echo body returned
// - without auth: server rejects CONNECT (407) → client cannot reach echo
func (s *HTTP2Suite) TestHTTP2Auth() {
s.T().Run("with-auth-success", func(t *testing.T) {
serverC, clientC := s.startChain("testdata/http2/server_auth.yaml", "testdata/http2/client_auth.yaml")
defer serverC.Terminate(s.ctx)
defer clientC.Terminate(s.ctx)
code, body := s.curlEcho(clientC)
if code != 0 || !strings.Contains(body, "hello-gost") {
s.dump("http2-auth-success logs", clientC, serverC)
}
s.Require().Equal(0, code)
s.Require().Contains(body, "hello-gost")
})
s.T().Run("no-auth-fails", func(t *testing.T) {
// Server requires auth; client sends none → the http2 CONNECT is
// rejected with 407, so curl cannot obtain the echo body.
serverC, clientC := s.startChain("testdata/http2/server_auth.yaml", "testdata/http2/client.yaml")
defer serverC.Terminate(s.ctx)
defer clientC.Terminate(s.ctx)
code, body := s.curlEcho(clientC)
if strings.Contains(body, "hello-gost") {
s.dump("http2-auth-noauth logs", clientC, serverC)
}
s.Require().Zero(code,
"curl should still receive an HTTP response from the local proxy")
s.Require().NotContains(body, "hello-gost",
"without auth the echo backend must be unreachable")
})
}
// TestHTTP2Bypass verifies that a bypass matcher on the http2 handler blocks
// the CONNECT target after authentication succeeds. The server returns 403, so
// the client cannot reach the echo backend.
func (s *HTTP2Suite) TestHTTP2Bypass() {
serverC, clientC := s.startChain("testdata/http2/server_bypass.yaml", "testdata/http2/client_auth.yaml")
defer serverC.Terminate(s.ctx)
defer clientC.Terminate(s.ctx)
code, body := s.curlEcho(clientC)
if strings.Contains(body, "hello-gost") {
s.dump("http2-bypass logs", clientC, serverC)
}
s.Require().Zero(code,
"curl should still receive an HTTP response from the local proxy")
s.Require().NotContains(body, "hello-gost",
"bypass should prevent reaching the echo backend")
}
// TestHTTP2ProbeResist verifies that probeResist, header and authBasicRealm
// metadata parse cleanly on the http2 handler and do not break the success
// path. With correct credentials the request still reaches the echo backend.
func (s *HTTP2Suite) TestHTTP2ProbeResist() {
serverC, clientC := s.startChain("testdata/http2/server_proberesist.yaml", "testdata/http2/client_auth.yaml")
defer serverC.Terminate(s.ctx)
defer clientC.Terminate(s.ctx)
code, body := s.curlEcho(clientC)
if code != 0 || !strings.Contains(body, "hello-gost") {
s.dump("http2-proberesist logs", clientC, serverC)
}
s.Require().Equal(0, code)
s.Require().Contains(body, "hello-gost")
}
// TestHTTP2Multiplex verifies HTTP/2 stream multiplexing: many concurrent
// requests through the client proxy are tunneled as parallel h2 streams over a
// single underlying connection to the http2 server. All requests must reach
// the echo backend.
func (s *HTTP2Suite) TestHTTP2Multiplex() {
serverC, clientC := s.startChain("testdata/http2/server.yaml", "testdata/http2/client.yaml")
defer serverC.Terminate(s.ctx)
defer clientC.Terminate(s.ctx)
const n = 6
var wg sync.WaitGroup
wg.Add(n)
failed := make(chan string, n)
for i := 0; i < n; i++ {
go func() {
defer wg.Done()
_, body := s.curlEcho(clientC)
if !strings.Contains(body, "hello-gost") {
failed <- "missing echo body"
}
}()
}
wg.Wait()
close(failed)
if len(failed) > 0 {
s.dump("http2-multiplex logs", clientC, serverC)
}
s.Require().Empty(failed, "all concurrent h2 requests should reach the echo backend")
}
func TestHTTP2Suite(t *testing.T) {
suite.Run(t, new(HTTP2Suite))
}
-605
View File
@@ -1,605 +0,0 @@
package e2e
import (
"context"
"encoding/base64"
"fmt"
"io"
"os"
"strings"
"testing"
"github.com/stretchr/testify/suite"
"github.com/testcontainers/testcontainers-go"
)
type HTTPSuite struct {
suite.Suite
ctx context.Context
echoC testcontainers.Container
echoIP string
}
func (s *HTTPSuite) SetupSuite() {
s.ctx = context.Background()
s.T().Logf("start tcp echo container...")
echoC, err := RunEchoContainer(s.ctx, SharedNetworkName)
s.Require().NoError(err)
s.echoC = echoC
echoIP, err := echoC.ContainerIP(s.ctx)
s.Require().NoError(err)
s.echoIP = echoIP
}
// sendRaw sends a raw HTTP request via netcat and returns the response.
// Uses base64 to avoid shell quoting issues with CRLF bytes.
func (s *HTTPSuite) sendRaw(gostC testcontainers.Container, host, port, data string) string {
encoded := base64.StdEncoding.EncodeToString([]byte(data))
cmd := []string{"sh", "-c",
fmt.Sprintf("echo %s | base64 -d | nc -w 5 %s %s", encoded, host, port)}
_, out, _ := gostC.Exec(s.ctx, cmd)
b, _ := io.ReadAll(out)
return string(b)
}
func (s *HTTPSuite) TearDownSuite() {
if s.echoC != nil {
s.echoC.Terminate(s.ctx)
}
}
// TestHTTPProxy verifies basic HTTP forward proxy (no auth, no metadata).
// Covers: handler type http, listener type tcp, basic GET via proxy.
func (s *HTTPSuite) TestHTTPProxy() {
gostC, err := RunGostContainerWithPorts(s.ctx, SharedNetworkName,
"testdata/http/server.yaml", "8080/tcp")
s.Require().NoError(err)
defer gostC.Terminate(s.ctx)
// curl -x uses HTTP forward proxy (GET with absolute URL)
cmd := []string{"curl", "-v", "-s", "-x", "http://127.0.0.1:8080",
fmt.Sprintf("http://%s:5678", s.echoIP)}
code, out, err := gostC.Exec(s.ctx, cmd)
s.Require().NoError(err)
body, err := io.ReadAll(out)
s.Require().NoError(err)
if code != 0 || !strings.Contains(string(body), "hello-gost") {
DumpLogs(s.T(), s.ctx, "http-proxy logs", gostC)
}
s.Require().Equal(0, code)
s.Require().Contains(string(body), "hello-gost")
}
// TestHTTPProxyAuth verifies proxy authentication and the authBasicRealm
// metadata parameter.
//
// Covers:
// - auther: basic auth on HTTP proxy
// - authBasicRealm: custom realm string in 407 Proxy-Authenticate header
func (s *HTTPSuite) TestHTTPProxyAuth() {
gostC, err := RunGostContainerWithPorts(s.ctx, SharedNetworkName,
"testdata/http/server_auth.yaml", "8080/tcp")
s.Require().NoError(err)
defer gostC.Terminate(s.ctx)
s.T().Run("no-auth-407", func(t *testing.T) {
cmd := []string{"curl", "-v", "-s", "-D", "-", "-o", "/dev/null",
"-x", "http://127.0.0.1:8080",
fmt.Sprintf("http://%s:5678", s.echoIP)}
_, out, _ := gostC.Exec(s.ctx, cmd)
body, _ := io.ReadAll(out)
output := string(body)
s.Assert().Contains(output, "407",
"no auth should return 407")
s.Assert().Contains(output, "gost-e2e-realm",
"authBasicRealm should appear in 407 Proxy-Authenticate")
})
s.T().Run("wrong-auth-407", func(t *testing.T) {
cmd := []string{"curl", "-v", "-s", "-o", "/dev/null", "-w", "%{http_code}",
"-x", "http://wrong:pass@127.0.0.1:8080",
fmt.Sprintf("http://%s:5678", s.echoIP)}
_, out, _ := gostC.Exec(s.ctx, cmd)
body, _ := io.ReadAll(out)
s.Assert().Contains(string(body), "407",
"wrong password should return 407")
})
s.T().Run("with-auth-success", func(t *testing.T) {
cmd := []string{"curl", "-v", "-s", "-x", "http://user:pass@127.0.0.1:8080",
fmt.Sprintf("http://%s:5678", s.echoIP)}
code, out, err := gostC.Exec(s.ctx, cmd)
s.Require().NoError(err)
body, err := io.ReadAll(out)
s.Require().NoError(err)
if code != 0 || !strings.Contains(string(body), "hello-gost") {
DumpLogs(s.T(), s.ctx, "http-proxy-auth logs", gostC)
}
s.Require().Equal(0, code)
s.Require().Contains(string(body), "hello-gost")
})
}
// TestHTTPProxyMetadata verifies:
// - probeResist: code:404 — unauthorised clients see 404 instead of 407
// - keepalive: config parsing (applied at Init time)
// - compression: config parsing (applied at Init time)
func (s *HTTPSuite) TestHTTPProxyMetadata() {
gostC, err := RunGostContainerWithPorts(s.ctx, SharedNetworkName,
"testdata/http/server_metadata.yaml", "8080/tcp")
s.Require().NoError(err)
defer gostC.Terminate(s.ctx)
s.T().Run("probe-resist-404", func(t *testing.T) {
cmd := []string{"curl", "-v", "-s", "-o", "/dev/null", "-w", "%{http_code}",
"-x", "http://127.0.0.1:8080",
fmt.Sprintf("http://%s:5678", s.echoIP)}
_, out, _ := gostC.Exec(s.ctx, cmd)
body, _ := io.ReadAll(out)
output := string(body)
s.Assert().NotContains(output, "407",
"probeResist should hide 407 status")
s.Assert().Contains(output, "404",
"probeResist should return configured decoy code")
})
s.T().Run("with-auth-success", func(t *testing.T) {
cmd := []string{"curl", "-v", "-s", "-x", "http://user:pass@127.0.0.1:8080",
fmt.Sprintf("http://%s:5678", s.echoIP)}
code, out, err := gostC.Exec(s.ctx, cmd)
s.Require().NoError(err)
body, err := io.ReadAll(out)
s.Require().NoError(err)
if code != 0 || !strings.Contains(string(body), "hello-gost") {
DumpLogs(s.T(), s.ctx, "http-metadata logs", gostC)
}
s.Require().Equal(0, code)
s.Require().Contains(string(body), "hello-gost")
})
}
// TestHTTPProxyHeaders verifies:
// - header: custom response headers (X-Proxy-Info, X-Custom) on
// proxy-originated error responses. The header metadata is set on
// the skeleton response used for error paths after authentication.
// - proxyAgent: custom Proxy-Agent header in proxy responses.
//
// Uses a bypass matcher (0.0.0.0/0) that blocks all traffic after auth
// succeeds → requests hit 403 Forbidden path which carries custom headers.
func (s *HTTPSuite) TestHTTPProxyHeaders() {
gostC, err := RunGostContainerWithPorts(s.ctx, SharedNetworkName,
"testdata/http/server_headers.yaml", "8080/tcp")
s.Require().NoError(err)
defer gostC.Terminate(s.ctx)
// Auth succeeds, but target is bypassed → 403 Forbidden
// The 403 response is built from the skeleton resp which
// carries h.md.header (custom headers) and h.md.proxyAgent.
s.T().Log("bypass: expect 403 with custom headers")
cmd := []string{"curl", "-v", "-s", "-D", "-", "-o", "/dev/null",
"-x", "http://user:pass@127.0.0.1:8080",
fmt.Sprintf("http://%s:5678", s.echoIP)}
_, out, _ := gostC.Exec(s.ctx, cmd)
headers, _ := io.ReadAll(out)
output := string(headers)
// Bypass → 403 Forbidden
s.Assert().Contains(output, "403",
"bypass should return 403")
// header: custom X-Proxy-Info header on error response
s.Assert().Contains(output, "X-Proxy-Info: gost-e2e",
"custom header should appear in 403 response")
// header: custom X-Custom header on error response
s.Assert().Contains(output, "X-Custom: test-value",
"custom header should appear in 403 response")
// proxyAgent: custom Proxy-Agent header on error response
s.Assert().Contains(output, "Proxy-Agent: gost-e2e/1.0",
"proxyAgent should appear in 403 response")
}
// TestHTTPConnect verifies HTTP CONNECT tunnel:
// - sniffing-enabled: sniffing:true + sniffing.timeout:2s
// - no-sniffing: plain CONNECT (default, no metadata)
// - bypass-403: CONNECT blocked by (0.0.0.0/0) bypass
//
// Uses curl --proxytunnel (-p) to force CONNECT method instead of GET.
func (s *HTTPSuite) TestHTTPConnect() {
s.T().Run("sniffing-enabled", func(t *testing.T) {
gostC, err := RunGostContainerWithPorts(s.ctx, SharedNetworkName,
"testdata/http/server_connect.yaml", "8080/tcp")
s.Require().NoError(err)
defer gostC.Terminate(s.ctx)
cmd := []string{"curl", "-v", "-s", "-p", "-x", "http://127.0.0.1:8080",
fmt.Sprintf("http://%s:5678", s.echoIP)}
code, out, err := gostC.Exec(s.ctx, cmd)
s.Require().NoError(err)
body, err := io.ReadAll(out)
s.Require().NoError(err)
if code != 0 || !strings.Contains(string(body), "hello-gost") {
DumpLogs(s.T(), s.ctx, "http-connect sniffing logs", gostC)
}
s.Require().Equal(0, code)
s.Require().Contains(string(body), "hello-gost")
})
s.T().Run("no-sniffing", func(t *testing.T) {
gostC, err := RunGostContainerWithPorts(s.ctx, SharedNetworkName,
"testdata/http/server.yaml", "8080/tcp")
s.Require().NoError(err)
defer gostC.Terminate(s.ctx)
cmd := []string{"curl", "-v", "-s", "-p", "-x", "http://127.0.0.1:8080",
fmt.Sprintf("http://%s:5678", s.echoIP)}
code, out, err := gostC.Exec(s.ctx, cmd)
s.Require().NoError(err)
body, err := io.ReadAll(out)
s.Require().NoError(err)
if code != 0 || !strings.Contains(string(body), "hello-gost") {
DumpLogs(s.T(), s.ctx, "http-connect no-sniffing logs", gostC)
}
s.Require().Equal(0, code)
s.Require().Contains(string(body), "hello-gost")
})
s.T().Run("bypass-403", func(t *testing.T) {
gostC, err := RunGostContainerWithPorts(s.ctx, SharedNetworkName,
"testdata/http/server_connect_bypass.yaml", "8080/tcp")
s.Require().NoError(err)
defer gostC.Terminate(s.ctx)
cmd := []string{"curl", "-v", "-s", "-D", "-", "-o", "/dev/null",
"-p", "-x", "http://user:pass@127.0.0.1:8080",
fmt.Sprintf("http://%s:5678", s.echoIP)}
_, out, _ := gostC.Exec(s.ctx, cmd)
body, _ := io.ReadAll(out)
output := string(body)
s.Assert().Contains(output, "403",
"CONNECT bypass should return 403")
})
}
// TestHTTPProxyDirectRequest verifies that a non-proxy-form HTTP request
// (raw GET / sent to the proxy port) is rejected with 400 Bad Request.
// A raw request with empty Host header has no valid scheme to infer,
// so the handler returns 400.
func (s *HTTPSuite) TestHTTPProxyDirectRequest() {
gostC, err := RunGostContainerWithPorts(s.ctx, SharedNetworkName,
"testdata/http/server.yaml", "8080/tcp")
s.Require().NoError(err)
defer gostC.Terminate(s.ctx)
resp := s.sendRaw(gostC, "127.0.0.1", "8080",
"GET / HTTP/1.0\r\nHost: bad!\r\n\r\n")
s.Assert().Contains(resp, "400",
"non-proxy GET should return 400")
}
// TestHTTPProxyPOST verifies that POST method forwarding works through
// the HTTP forward proxy. The echo server only handles GET, so the proxy
// should forward the POST request and return whatever the upstream sends
// back (501 Not Implemented from the echo server — not a proxy error).
// 501 is the real upstream response, proving the proxy forwarded it.
func (s *HTTPSuite) TestHTTPProxyPOST() {
gostC, err := RunGostContainerWithPorts(s.ctx, SharedNetworkName,
"testdata/http/server.yaml", "8080/tcp")
s.Require().NoError(err)
defer gostC.Terminate(s.ctx)
cmd := []string{"curl", "-v", "-s", "-o", "/dev/null", "-w", "%{http_code}",
"-X", "POST", "-x", "http://127.0.0.1:8080",
fmt.Sprintf("http://%s:5678/", s.echoIP)}
_, out, _ := gostC.Exec(s.ctx, cmd)
body, _ := io.ReadAll(out)
output := string(body)
// The echo server's BaseHTTPRequestHandler defaults to 501 for POST.
// If we get 501 through the proxy, it means the method was forwarded.
s.Assert().Contains(output, "501",
"POST through proxy should be forwarded, echo server returns 501")
}
// TestHTTPConnectUnreachable verifies that CONNECT to an unreachable target
// returns 503 Service Unavailable.
func (s *HTTPSuite) TestHTTPConnectUnreachable() {
gostC, err := RunGostContainerWithPorts(s.ctx, SharedNetworkName,
"testdata/http/server_connect.yaml", "8080/tcp")
s.Require().NoError(err)
defer gostC.Terminate(s.ctx)
// Send a raw CONNECT request to a port where nothing is listening.
resp := s.sendRaw(gostC, "127.0.0.1", "8080",
fmt.Sprintf("CONNECT %s:1 HTTP/1.0\r\nHost: %s:1\r\n\r\n", s.echoIP, s.echoIP))
s.Assert().Contains(resp, "503",
"CONNECT to unreachable target should return 503")
}
// TestHTTPConnector verifies HTTP connector type: a gost client uses an HTTP
// connector to forward through an upstream HTTP proxy (no auth), reaching the
// target via the proxy chain. This covers the server+client two-container
// pattern where the client connects to the server via connector type: http.
func (s *HTTPSuite) TestHTTPConnector() {
// Start upstream HTTP proxy server (no auth).
serverC, err := RunGostContainerWithOptions(s.ctx, SharedNetworkName,
"testdata/http/server.yaml", []string{"http-server"}, []string{"8080/tcp"})
s.Require().NoError(err)
defer serverC.Terminate(s.ctx)
// Render client config that chains through the upstream server.
rendered, err := RenderConfig("testdata/http/client_connector.yaml",
ConfigData{ServerAddr: "http-server:8080"})
s.Require().NoError(err)
defer os.Remove(rendered)
// Start client with the rendered config.
clientC, err := RunGostContainerWithPorts(s.ctx, SharedNetworkName,
rendered, "8080/tcp")
s.Require().NoError(err)
defer clientC.Terminate(s.ctx)
// Request through client proxy → HTTP connector → upstream HTTP server → target.
cmd := []string{"curl", "-v", "-s", "-x", "http://127.0.0.1:8080",
fmt.Sprintf("http://%s:5678", s.echoIP)}
code, out, err := clientC.Exec(s.ctx, cmd)
s.Require().NoError(err)
body, err := io.ReadAll(out)
s.Require().NoError(err)
if code != 0 || !strings.Contains(string(body), "hello-gost") {
DumpLogs(s.T(), s.ctx, "http-connector client logs", clientC)
DumpLogs(s.T(), s.ctx, "http-connector server logs", serverC)
}
s.Require().Equal(0, code)
s.Require().Contains(string(body), "hello-gost")
}
// TestHTTPConnectorAuth verifies HTTP connector with authentication:
// no-auth → 407, correct auth → success.
func (s *HTTPSuite) TestHTTPConnectorAuth() {
// Start upstream HTTP proxy with auth and network alias.
serverC, err := RunGostContainerWithOptions(s.ctx, SharedNetworkName,
"testdata/http/server_auth.yaml", []string{"http-server"}, []string{"8080/tcp"})
s.Require().NoError(err)
defer serverC.Terminate(s.ctx)
s.T().Run("no-auth-407", func(t *testing.T) {
// Send raw request directly to server container (no auth) → 407.
resp := s.sendRaw(serverC, "127.0.0.1", "8080",
"GET http://127.0.0.1:5678/ HTTP/1.0\r\nHost: 127.0.0.1\r\n\r\n")
s.Assert().Contains(resp, "407",
"no auth via upstream should return 407")
})
s.T().Run("correct-auth-success", func(t *testing.T) {
rendered, err := RenderConfig("testdata/http/client_connector_auth.yaml",
ConfigData{ServerAddr: "http-server:8080"})
s.Require().NoError(err)
defer os.Remove(rendered)
clientC, err := RunGostContainerWithPorts(s.ctx, SharedNetworkName,
rendered, "8080/tcp")
s.Require().NoError(err)
defer clientC.Terminate(s.ctx)
cmd := []string{"curl", "-v", "-s", "-x", "http://127.0.0.1:8080",
fmt.Sprintf("http://%s:5678", s.echoIP)}
code, out, err := clientC.Exec(s.ctx, cmd)
s.Require().NoError(err)
body, err := io.ReadAll(out)
s.Require().NoError(err)
if code != 0 || !strings.Contains(string(body), "hello-gost") {
DumpLogs(s.T(), s.ctx, "http-connector-auth client logs", clientC)
DumpLogs(s.T(), s.ctx, "http-connector-auth server logs", serverC)
}
s.Require().Equal(0, code)
s.Require().Contains(string(body), "hello-gost")
})
}
// TestHTTPProbeResistHost verifies probeResist host type.
// When auth fails, the handler pipes the raw request to
// the decoy host (tcp-echo:5678). The TCP echo server echoes
// back the request bytes rather than returning a 407.
func (s *HTTPSuite) TestHTTPProbeResistHost() {
gostC, err := RunGostContainerWithPorts(s.ctx, SharedNetworkName,
"testdata/http/server_proberesist_host.yaml", "8080/tcp")
s.Require().NoError(err)
defer gostC.Terminate(s.ctx)
// Without auth, probeResist host pipes the raw request to tcp-echo:5678.
// The HTTP echo server returns 200 OK with "hello-gost", proving the
// request reached the decoy host rather than getting a 407 response.
resp := s.sendRaw(gostC, "127.0.0.1", "8080",
"GET http://127.0.0.1:5678/ HTTP/1.0\r\nHost: 127.0.0.1\r\n\r\n")
s.Assert().NotContains(resp, "407",
"probeResist host should hide 407 response")
s.Assert().Contains(resp, "hello-gost",
"probeResist host should pipe request to decoy host")
}
// TestHTTPProbeResistKnock verifies probeResist knock mechanism.
// Clients that connect with a recognized knock Host header
// get a normal 407 auth challenge instead of the decoy response.
//
// Uses server_proberesist_knock.yaml:
// - probeResist: code:404 (decoy 404 for unknown hosts)
// - knock: secret.example.com (known hosts get normal 407)
func (s *HTTPSuite) TestHTTPProbeResistKnock() {
gostC, err := RunGostContainerWithPorts(s.ctx, SharedNetworkName,
"testdata/http/server_proberesist_knock.yaml", "8080/tcp")
s.Require().NoError(err)
defer gostC.Terminate(s.ctx)
s.T().Run("unknown-host-decoy", func(t *testing.T) {
// URL hostname doesn't match knock list → probeResist fires → 404
resp := s.sendRaw(gostC, "127.0.0.1", "8080",
"GET http://127.0.0.1:5678/ HTTP/1.0\r\nHost: 127.0.0.1\r\n\r\n")
s.Assert().Contains(resp, "404",
"unknown host should get decoy 404")
s.Assert().NotContains(resp, "407",
"unknown host should NOT get 407")
})
s.T().Run("knock-host-407", func(t *testing.T) {
// URL hostname matches knock list → normal 407 auth challenge.
// Note: knock checks req.URL.Hostname(), not the Host header.
resp := s.sendRaw(gostC, "127.0.0.1", "8080",
"GET http://secret.example.com:5678/ HTTP/1.0\r\nHost: secret.example.com\r\n\r\n")
s.Assert().Contains(resp, "407",
"knock host should get normal 407")
})
}
// TestHTTPConnectorTLS verifies HTTP connector using TLS dialer
// to upstream HTTPS proxy (listener type: tls).
// The TLS listener auto-generates a self-signed cert;
// the TLS dialer defaults to InsecureSkipVerify=true, so
// handshake succeeds without explicit cert configuration.
func (s *HTTPSuite) TestHTTPConnectorTLS() {
serverC, err := RunGostContainerWithOptions(s.ctx, SharedNetworkName,
"testdata/http/server_tls.yaml", []string{"tls-server"}, []string{"8443/tcp"})
s.Require().NoError(err)
defer serverC.Terminate(s.ctx)
rendered, err := RenderConfig("testdata/http/client_connector_tls.yaml",
ConfigData{ServerAddr: "tls-server:8443"})
s.Require().NoError(err)
defer os.Remove(rendered)
clientC, err := RunGostContainerWithPorts(s.ctx, SharedNetworkName,
rendered, "8080/tcp")
s.Require().NoError(err)
defer clientC.Terminate(s.ctx)
cmd := []string{"curl", "-v", "-s", "-x", "http://127.0.0.1:8080",
fmt.Sprintf("http://%s:5678", s.echoIP)}
code, out, err := clientC.Exec(s.ctx, cmd)
s.Require().NoError(err)
body, err := io.ReadAll(out)
s.Require().NoError(err)
if code != 0 || !strings.Contains(string(body), "hello-gost") {
DumpLogs(s.T(), s.ctx, "http-connector-tls client logs", clientC)
DumpLogs(s.T(), s.ctx, "http-connector-tls server logs", serverC)
}
s.Require().Equal(0, code)
s.Require().Contains(string(body), "hello-gost")
}
// TestHTTPProbeResistWeb verifies probeResist web type.
// On auth failure, the handler fetches the decoy URL
// (tcp-echo:5678) and returns its response body.
func (s *HTTPSuite) TestHTTPProbeResistWeb() {
gostC, err := RunGostContainerWithPorts(s.ctx, SharedNetworkName,
"testdata/http/server_proberesist_web.yaml", "8080/tcp")
s.Require().NoError(err)
defer gostC.Terminate(s.ctx)
// Without auth, probeResist web fetches http://tcp-echo:5678
// and returns the echo server's response ("hello-gost").
resp := s.sendRaw(gostC, "127.0.0.1", "8080",
"GET http://127.0.0.1:5678/ HTTP/1.0\r\nHost: 127.0.0.1\r\n\r\n")
s.Assert().NotContains(resp, "407",
"probeResist web should hide 407 response")
s.Assert().Contains(resp, "hello-gost",
"probeResist web should return decoy response body")
}
// TestHTTPProbeResistFile verifies probeResist file type.
// On auth failure, the handler reads a local file and
// returns its contents as the HTTP response body.
func (s *HTTPSuite) TestHTTPProbeResistFile() {
gostC, err := RunGostContainerWithFiles(s.ctx, SharedNetworkName,
"testdata/http/server_proberesist_file.yaml",
[]testcontainers.ContainerFile{
{HostFilePath: "testdata/http/decoy.html", ContainerFilePath: "/tmp/decoy.html", FileMode: 0644},
},
"8080/tcp")
s.Require().NoError(err)
defer gostC.Terminate(s.ctx)
// Without auth, probeResist file reads /tmp/decoy.html and
// returns "decoy-response" rather than a 407.
resp := s.sendRaw(gostC, "127.0.0.1", "8080",
"GET http://127.0.0.1:5678/ HTTP/1.0\r\nHost: 127.0.0.1\r\n\r\n")
s.Assert().NotContains(resp, "407",
"probeResist file should hide 407 response")
s.Assert().Contains(resp, "decoy-response",
"probeResist file should return decoy file content")
}
// TestHTTPIdleTimeout verifies idleTimeout on CONNECT tunnels.
// After the configured idle timeout, the pipe between client
// and target should close.
func (s *HTTPSuite) TestHTTPIdleTimeout() {
gostC, err := RunGostContainerWithFiles(s.ctx, SharedNetworkName,
"testdata/http/server_idle_timeout.yaml",
[]testcontainers.ContainerFile{
{HostFilePath: "scripts/http_idle_timeout.py", ContainerFilePath: "/scripts/http_idle_timeout.py", FileMode: 0644},
},
"8080/tcp")
s.Require().NoError(err)
defer gostC.Terminate(s.ctx)
// The Python script:
// 1. Opens a CONNECT tunnel to tcp-echo:5678
// 2. Sends and receives data (confirm tunnel is alive)
// 3. Waits > idleTimeout (3s + 2s buffer)
// 4. Sends more data — expects connection to be closed
code, out, err := gostC.Exec(s.ctx, []string{
"python3", "/scripts/http_idle_timeout.py",
"127.0.0.1", "8080", "3",
})
output, _ := io.ReadAll(out)
s.T().Logf("idle timeout output:\n%s", string(output))
if code != 0 {
DumpLogs(s.T(), s.ctx, "http-idle-timeout logs", gostC)
}
s.Require().Equal(0, code, "idle timeout test script should exit 0")
}
// TestHTTPUDPRelay verifies UDP relay over HTTP. Uses
// X-Gost-Protocol: udp in the CONNECT request to establish
// a SOCKS5 UDP tunnel through the HTTP handler.
func (s *HTTPSuite) TestHTTPUDPRelay() {
// Start UDP echo container on the shared network.
udpC, err := RunUDPEchoContainer(s.ctx, SharedNetworkName)
s.Require().NoError(err)
defer udpC.Terminate(s.ctx)
gostC, err := RunGostContainerWithFiles(s.ctx, SharedNetworkName,
"testdata/http/server_udp.yaml",
[]testcontainers.ContainerFile{
{HostFilePath: "scripts/http_udp_relay.py", ContainerFilePath: "/scripts/http_udp_relay.py", FileMode: 0644},
},
"8080/tcp")
s.Require().NoError(err)
defer gostC.Terminate(s.ctx)
// The Python script:
// 1. Connects to gost HTTP proxy
// 2. Sends CONNECT with X-Gost-Protocol: udp
// 3. After 200 OK, sends a SOCKS5 UDP frame targeting udp-echo:5679
// 4. Reads back the echoed frame
code, out, err := gostC.Exec(s.ctx, []string{
"python3", "/scripts/http_udp_relay.py",
"127.0.0.1", "8080",
})
output, _ := io.ReadAll(out)
s.T().Logf("udp relay output:\n%s", string(output))
if code != 0 {
DumpLogs(s.T(), s.ctx, "http-udp logs", gostC)
}
s.Require().Equal(0, code, "udp relay test script should exit 0")
}
func TestHTTPSuite(t *testing.T) {
suite.Run(t, new(HTTPSuite))
}
-50
View File
@@ -1,50 +0,0 @@
package e2e
import (
"context"
"flag"
"fmt"
"os"
"os/exec"
"testing"
"github.com/testcontainers/testcontainers-go/network"
)
var SharedNetworkName string
func TestMain(m *testing.M) {
flag.Parse()
ctx := context.Background()
shouldCleanup := false
if GostBinPath == "" {
GostBinPath = "/tmp/gost-test-bin"
cmd := exec.Command("go", "build", "-o", GostBinPath, "../../cmd/gost")
cmd.Env = append(os.Environ(), "CGO_ENABLED=0")
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
if err := cmd.Run(); err != nil {
fmt.Printf("Failed to compile gost: %v\n", err)
os.Exit(1)
}
shouldCleanup = true
}
net, err := network.New(ctx)
if err != nil {
fmt.Printf("Failed to create network: %v\n", err)
os.Exit(1)
}
SharedNetworkName = net.Name
code := m.Run()
net.Remove(ctx)
if shouldCleanup {
os.Remove(GostBinPath)
}
os.Exit(code)
}
-57
View File
@@ -1,57 +0,0 @@
package e2e
import (
"context"
"fmt"
"io"
"testing"
"github.com/stretchr/testify/suite"
"github.com/testcontainers/testcontainers-go"
)
type ParallelSelectorSuite struct {
suite.Suite
ctx context.Context
echoC testcontainers.Container
echoIP string
}
func (s *ParallelSelectorSuite) SetupSuite() {
s.ctx = context.Background()
echoC, err := RunEchoContainer(s.ctx, SharedNetworkName)
s.Require().NoError(err)
s.echoC = echoC
echoIP, err := echoC.ContainerIP(s.ctx)
s.Require().NoError(err)
s.echoIP = echoIP
}
func (s *ParallelSelectorSuite) TearDownSuite() {
if s.echoC != nil {
s.echoC.Terminate(s.ctx)
}
}
func (s *ParallelSelectorSuite) TestParallelSelector() {
gostC, err := RunGostContainerWithPorts(s.ctx, SharedNetworkName, "testdata/parallel_selector/server.yaml", "8080/tcp")
s.Require().NoError(err)
defer gostC.Terminate(s.ctx)
// Test the proxy by running curl inside the gost container
cmd := []string{"curl", "-v", "-s", "-x", "http://127.0.0.1:8080", fmt.Sprintf("http://%s:5678", s.echoIP)}
code, out, err := gostC.Exec(s.ctx, cmd)
s.Require().NoError(err)
body, err := io.ReadAll(out)
s.Require().NoError(err)
s.Require().Equal(0, code)
s.Require().Contains(string(body), "hello-gost")
}
func TestParallelSelectorSuite(t *testing.T) {
suite.Run(t, new(ParallelSelectorSuite))
}
-162
View File
@@ -1,162 +0,0 @@
"""DNS query client for e2e tests.
Usage:
python3 dns_query.py udp host port qname qtype [expected_ip|empty]
python3 dns_query.py tcp host port qname qtype [expected_ip|empty]
Sends a DNS query and optionally checks the response:
- expected_ip: verify response contains this IP address
- "empty": verify response has zero answer records (NXDOMAIN or blocked)
- omitted: exit 0 on any valid DNS response
Exits 0 on success, 1 on failure.
"""
import struct
import socket
import sys
def encode_name(name):
parts = name.rstrip(".").split(".")
return b"".join(bytes([len(p)]) + p.encode() for p in parts) + b"\x00"
def skip_name(data, pos):
"""Skip one DNS name at pos, return position after it."""
while True:
length = data[pos]
if length == 0:
return pos + 1
if length & 0xC0:
return pos + 2
pos += length + 1
def build_query(qname, qtype):
tid = 0x1234
flags = 0x0100 # RD=1
qdcount = 1
header = struct.pack(">HHHHHH", tid, flags, qdcount, 0, 0, 0)
qname_enc = encode_name(qname)
question = qname_enc + struct.pack(">HH", qtype, 1) # QTYPE, QCLASS=IN
return header + question
def parse_response(data):
if len(data) < 12:
return [], -1
flags = struct.unpack(">H", data[2:4])[0]
rcode = flags & 0x0F
qdcount = struct.unpack(">H", data[4:6])[0]
ancount = struct.unpack(">H", data[6:8])[0]
# Skip question section
pos = 12
for _ in range(qdcount):
pos = skip_name(data, pos) + 4 # QTYPE + QCLASS
answers = []
for _ in range(ancount):
pos = skip_name(data, pos)
rtype, rclass, ttl, rdlength = struct.unpack(">HHIH", data[pos:pos + 10])
pos += 10
rdata = data[pos:pos + rdlength]
pos += rdlength
answers.append((rtype, rclass, ttl, rdata))
return answers, rcode
def query_udp(host, port, query):
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
sock.settimeout(5)
sock.sendto(query, (host, port))
data, _ = sock.recvfrom(4096)
sock.close()
return data
def query_tcp(host, port, query):
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.settimeout(5)
sock.connect((host, port))
sock.sendall(struct.pack(">H", len(query)) + query)
raw = sock.recv(2)
if len(raw) < 2:
sock.close()
return b""
msglen = struct.unpack(">H", raw)[0]
data = b""
while len(data) < msglen:
chunk = sock.recv(msglen - len(data))
if not chunk:
break
data += chunk
sock.close()
return data
def format_ip(rtype, rdata):
if rtype == 1 and len(rdata) == 4:
return socket.inet_ntoa(rdata)
if rtype == 28 and len(rdata) == 16:
return socket.inet_ntop(socket.AF_INET6, rdata)
return repr(rdata)
def main():
if len(sys.argv) < 5:
print("Usage: dns_query.py <udp|tcp> <host> <port> <qname> <qtype> [expected_ip|empty]")
sys.exit(1)
mode = sys.argv[1]
host = sys.argv[2]
port = int(sys.argv[3])
qname = sys.argv[4]
qtype_str = sys.argv[5]
expected = sys.argv[6] if len(sys.argv) > 6 else None
qtype_map = {"A": 1, "AAAA": 28}
qtype = qtype_map.get(qtype_str, 1)
query = build_query(qname, qtype)
try:
if mode == "tcp":
data = query_tcp(host, port, query)
else:
data = query_udp(host, port, query)
except Exception as e:
print(f"ERROR: {e}")
sys.exit(1)
if not data:
print("ERROR: empty response")
sys.exit(1)
answers, rcode = parse_response(data)
print(f"Got {len(answers)} answer(s), rcode={rcode}")
for rtype, _, _, rdata in answers:
print(f" {format_ip(rtype, rdata)}")
if expected == "empty":
if len(answers) == 0:
print("MATCH: empty answer (expected)")
sys.exit(0)
print(f"NO MATCH: expected empty, got {len(answers)} answers")
sys.exit(1)
elif expected:
for rtype, _, _, rdata in answers:
ip = format_ip(rtype, rdata)
if ip == expected:
print(f"MATCH: expected {expected}")
sys.exit(0)
print(f"NO MATCH: expected {expected}")
sys.exit(1)
else:
sys.exit(0)
if __name__ == "__main__":
main()
-88
View File
@@ -1,88 +0,0 @@
"""Simple authoritative DNS responder for e2e tests.
Listens on UDP port 5353 and responds with static records:
test.example.com. IN A 10.0.0.1
test2.example.com. IN A 10.0.0.2
example.com. IN AAAA ::1
All other queries receive NXDOMAIN.
"""
import socketserver
import struct
import socket
def decode_name(data, offset):
labels = []
while True:
length = data[offset]
if length == 0:
offset += 1
break
if length & 0xC0:
offset += 2
break
offset += 1
labels.append(data[offset:offset + length].decode())
offset += length
return '.'.join(labels), offset
def encode_name(name):
parts = name.rstrip(".").split(".")
return b"".join(bytes([len(p)]) + p.encode() for p in parts) + b"\x00"
RECORDS = {
("test.example.com", 1): (1, 300, socket.inet_aton("10.0.0.1")),
("test2.example.com", 1): (1, 300, socket.inet_aton("10.0.0.2")),
("example.com", 28): (28, 300, socket.inet_pton(socket.AF_INET6, "::1")),
}
class DNSResponder(socketserver.DatagramRequestHandler):
def handle(self):
data = self.rfile.read(512)
if len(data) < 12:
return
tid = struct.unpack(">H", data[:2])[0]
qdcount = struct.unpack(">H", data[4:6])[0]
if qdcount == 0:
return
qname, pos = decode_name(data, 12)
qtype = struct.unpack(">H", data[pos:pos + 2])[0]
qclass = struct.unpack(">H", data[pos + 2:pos + 4])[0]
key = (qname, qtype)
if key in RECORDS:
rcode = 0
ancount = 1
rtype, ttl, rdata = RECORDS[key]
else:
rcode = 3 # NXDOMAIN
ancount = 0
rtype, ttl, rdata = 0, 0, b""
flags = 0x8000 | 0x0400 | rcode # QR=1, AA=1, rcode
header = struct.pack(">HHHHHH", tid, flags, qdcount, ancount, 0, 0)
question = data[12:pos + 4]
answer = b""
if ancount:
answer = (
struct.pack(">HH", 0xC00C, rtype) # NAME pointer + TYPE
+ struct.pack(">HI", qclass, ttl) # CLASS + TTL
+ struct.pack(">H", len(rdata)) + rdata # RDLENGTH + RDATA
)
self.wfile.write(header + question + answer)
if __name__ == "__main__":
with socketserver.UDPServer(("0.0.0.0", 5353), DNSResponder) as srv:
srv.serve_forever()
-83
View File
@@ -1,83 +0,0 @@
"""Simple authoritative DNS responder for TCP mode e2e tests.
Listens on TCP port 5353 and responds with the same static records
as dns_responder.py, but over TCP (RFC 5966).
"""
import socketserver
import struct
import socket
def decode_name(data, offset):
labels = []
while True:
length = data[offset]
if length == 0:
offset += 1
break
if length & 0xC0:
offset += 2
break
offset += 1
labels.append(data[offset:offset + length].decode())
offset += length
return '.'.join(labels), offset
RECORDS = {
("test.example.com", 1): (1, 300, socket.inet_aton("10.0.0.1")),
("test2.example.com", 1): (1, 300, socket.inet_aton("10.0.0.2")),
("example.com", 28): (28, 300, socket.inet_pton(socket.AF_INET6, "::1")),
}
class DNSResponder(socketserver.StreamRequestHandler):
def handle(self):
# TCP DNS: 2-byte length prefix
raw = self.rfile.read(2)
if len(raw) < 2:
return
msglen = struct.unpack(">H", raw)[0]
data = self.rfile.read(msglen)
if len(data) < 12:
return
tid = struct.unpack(">H", data[:2])[0]
qdcount = struct.unpack(">H", data[4:6])[0]
if qdcount == 0:
return
qname, pos = decode_name(data, 12)
qtype = struct.unpack(">H", data[pos:pos + 2])[0]
qclass = struct.unpack(">H", data[pos + 2:pos + 4])[0]
key = (qname, qtype)
if key in RECORDS:
rcode = 0
ancount = 1
rtype, ttl, rdata = RECORDS[key]
else:
rcode = 3 # NXDOMAIN
ancount = 0
rtype, ttl, rdata = 0, 0, b""
flags = 0x8000 | 0x0400 | rcode
header = struct.pack(">HHHHHH", tid, flags, qdcount, ancount, 0, 0)
question = data[12:pos + 4]
answer = b""
if ancount:
answer = (
struct.pack(">HH", 0xC00C, rtype)
+ struct.pack(">HI", qclass, ttl)
+ struct.pack(">H", len(rdata)) + rdata
)
dnsmsg = header + question + answer
self.wfile.write(struct.pack(">H", len(dnsmsg)) + dnsmsg)
if __name__ == "__main__":
with socketserver.TCPServer(("0.0.0.0", 5353), DNSResponder) as srv:
srv.serve_forever()
-140
View File
@@ -1,140 +0,0 @@
"""DNS query client over TLS for e2e tests.
Connects to the DNS-over-TLS endpoint and performs a DNS query.
Usage:
python3 dns_tls_query.py host port qname qtype [expected_ip]
"""
import struct
import socket
import ssl
import sys
def encode_name(name):
parts = name.rstrip(".").split(".")
return b"".join(bytes([len(p)]) + p.encode() for p in parts) + b"\x00"
def build_query(tid, qname, qtype):
flags = 0x0100
qdcount = 1
header = struct.pack(">HHHHHH", tid, flags, qdcount, 0, 0, 0)
qname_enc = encode_name(qname)
question = qname_enc + struct.pack(">HH", qtype, 1)
return header + question
def parse_response(data):
if len(data) < 12:
return [], -1
flags = struct.unpack(">H", data[2:4])[0]
rcode = flags & 0x0F
qdcount = struct.unpack(">H", data[4:6])[0]
ancount = struct.unpack(">H", data[6:8])[0]
pos = 12
for _ in range(qdcount):
while data[pos] != 0:
if data[pos] & 0xC0:
pos += 2
break
pos += data[pos] + 1
else:
pos += 1
pos += 4
answers = []
for _ in range(ancount):
if data[pos] & 0xC0:
pos += 2
else:
while data[pos] != 0:
pos += data[pos] + 1
pos += 1
rtype, rclass, ttl, rdlength = struct.unpack(">HHIH", data[pos:pos + 10])
pos += 10
rdata = data[pos:pos + rdlength]
pos += rdlength
answers.append((rtype, rdata))
return answers, rcode
def main():
if len(sys.argv) < 5:
print(f"Usage: {sys.argv[0]} host port qname qtype [expected_ip]")
sys.exit(1)
host = sys.argv[1]
port = int(sys.argv[2])
qname = sys.argv[3]
qtype_str = sys.argv[4]
expected = sys.argv[5] if len(sys.argv) > 5 else None
qtype_map = {"A": 1, "AAAA": 28}
qtype = qtype_map.get(qtype_str, 1)
query = build_query(0x1234, qname, qtype)
ctx = ssl.create_default_context()
ctx.check_hostname = False
ctx.verify_mode = ssl.CERT_NONE
try:
with socket.create_connection((host, port), timeout=5) as sock:
with ctx.wrap_socket(sock, server_hostname=host) as tls_sock:
# TCP DNS: 2-byte length prefix
tls_sock.sendall(struct.pack(">H", len(query)) + query)
raw = tls_sock.recv(2)
if len(raw) < 2:
print("ERROR: short response header")
sys.exit(1)
msglen = struct.unpack(">H", raw)[0]
data = b""
while len(data) < msglen:
chunk = tls_sock.recv(msglen - len(data))
if not chunk:
break
data += chunk
except Exception as e:
print(f"ERROR: {e}")
sys.exit(1)
if not data:
print("ERROR: empty response")
sys.exit(1)
answers, rcode = parse_response(data)
print(f"Got {len(answers)} answer(s), rcode={rcode}")
for rtype, rdata in answers:
if rtype == 1 and len(rdata) == 4:
ip = socket.inet_ntoa(rdata)
print(f" A {ip}")
elif rtype == 28 and len(rdata) == 16:
ip6 = socket.inet_ntop(socket.AF_INET6, rdata)
print(f" AAAA {ip6}")
else:
print(f" TYPE={rtype} RDATA={rdata.hex()}")
if expected:
for rtype, rdata in answers:
if rtype == 1 and len(rdata) == 4:
ip = socket.inet_ntoa(rdata)
if ip == expected:
print(f"MATCH: expected {expected}")
sys.exit(0)
elif rtype == 28 and len(rdata) == 16:
ip6 = socket.inet_ntop(socket.AF_INET6, rdata)
if ip6 == expected:
print(f"MATCH: expected {expected}")
sys.exit(0)
print(f"NO MATCH: expected {expected}")
sys.exit(1)
sys.exit(0)
if __name__ == "__main__":
main()
-68
View File
@@ -1,68 +0,0 @@
import socket
import sys
import time
def main():
host = sys.argv[1] if len(sys.argv) > 1 else "127.0.0.1"
port = int(sys.argv[2]) if len(sys.argv) > 2 else 8080
idle_timeout = int(sys.argv[3]) if len(sys.argv) > 3 else 3
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.settimeout(10)
s.connect((host, port))
# CONNECT to echo server
req = b"CONNECT tcp-echo:5678 HTTP/1.1\r\nHost: tcp-echo:5678\r\n\r\n"
s.sendall(req)
# Read 200 OK response
resp = b""
while b"\r\n\r\n" not in resp:
chunk = s.recv(4096)
if not chunk:
break
resp += chunk
if b"200" not in resp:
print(f"FAIL: expected 200, got {resp.decode(errors='replace')}")
sys.exit(1)
# Send an HTTP GET through the CONNECT tunnel. The tunnel target
# is an HTTP echo server (BaseHTTPRequestHandler), so we must speak
# HTTP or the connection closes.
req = b"GET / HTTP/1.0\r\nHost: tcp-echo\r\n\r\n"
s.sendall(req)
data = b""
while b"hello-gost" not in data:
chunk = s.recv(4096)
if not chunk:
break
data += chunk
if b"hello-gost" not in data:
print(f"FAIL: expected hello-gost echo, got {data!r}")
sys.exit(1)
print(f"PASS: first request through tunnel succeeded")
# Wait longer than idleTimeout
wait = idle_timeout + 2
print(f"Waiting {wait}s for idle timeout...")
time.sleep(wait)
# Try to send more data — idle timeout should have closed the pipe
try:
s.sendall(b"ping-2\n")
data = s.recv(4096)
if not data:
print("PASS: connection closed after idle timeout (empty recv)")
sys.exit(0)
# Got data means the pipe is still alive
print(f"FAIL: connection still alive after idle timeout, got {data!r}")
sys.exit(1)
except (socket.timeout, ConnectionResetError, BrokenPipeError, OSError) as e:
print(f"PASS: connection closed after idle timeout: {e}")
sys.exit(0)
if __name__ == "__main__":
main()
-135
View File
@@ -1,135 +0,0 @@
import socket
import struct
import sys
UDP_ECHO_HOST = "udp-echo"
UDP_ECHO_PORT = 5679
def encode_socks5_addr(host, port):
"""Encode (host, port) as SOCKS5 ADDRESS + PORT.
Uses ATYP=3 (domain name) so Docker DNS resolves the host.
Returns (atyp, address_bytes, port_bytes).
"""
host_bytes = host.encode()
addr_bytes = struct.pack("!B", len(host_bytes)) + host_bytes
port_bytes = struct.pack("!H", port)
return 0x03, addr_bytes, port_bytes
def build_udp_frame(payload, host, port):
"""Build a SOCKS5 UDP relay frame over TCP.
gost HTTP UDP relay uses RSV=data-length and FRAG=0xff.
Frame: [RSV:2][FRAG:1][ATYP:1][DST.ADDR][DST.PORT:2][DATA]
"""
atyp, addr_bytes, port_bytes = encode_socks5_addr(host, port)
rsv = struct.pack("!H", len(payload))
frag = b"\xff"
atyp_byte = struct.pack("!B", atyp)
return rsv + frag + atyp_byte + addr_bytes + port_bytes + payload
def recvn(sock, n):
"""Receive exactly n bytes from socket."""
buf = b""
while len(buf) < n:
chunk = sock.recv(n - len(buf))
if not chunk:
raise ConnectionError("connection closed while reading")
buf += chunk
return buf
def read_socks5_frame(sock):
"""Read one SOCKS5 UDP relay frame from the TCP connection.
Returns (addr, port, data).
"""
# Read header: RSV(2) + FRAG(1) + ATYP(1)
header = recvn(sock, 4)
rsv = struct.unpack("!H", header[:2])[0]
frag = header[2]
atyp = header[3]
# Read address based on ATYP
if atyp == 1: # IPv4
addr_bytes = recvn(sock, 4)
addr = socket.inet_ntoa(addr_bytes)
elif atyp == 3: # Domain name
domain_len = recvn(sock, 1)
addr = recvn(sock, domain_len[0]).decode()
elif atyp == 4: # IPv6
addr_bytes = recvn(sock, 16)
addr = socket.inet_ntop(socket.AF_INET6, addr_bytes)
else:
raise ValueError(f"unknown ATYP: {atyp}")
port_bytes = recvn(sock, 2)
port = struct.unpack("!H", port_bytes)[0]
# Read data
if rsv > 0:
data = recvn(sock, rsv)
else:
# Standard SOCKS5 UDP: read remaining
data = b""
while True:
chunk = sock.recv(4096)
if not chunk:
break
data += chunk
return addr, port, data
def main():
host = sys.argv[1] if len(sys.argv) > 1 else "127.0.0.1"
port = int(sys.argv[2]) if len(sys.argv) > 2 else 8080
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.settimeout(10)
s.connect((host, port))
# Send CONNECT with X-Gost-Protocol: udp
req = (
b"CONNECT 0.0.0.0:0 HTTP/1.1\r\n"
b"Host: 0.0.0.0:0\r\n"
b"X-Gost-Protocol: udp\r\n"
b"Proxy-Connection: keep-alive\r\n"
b"\r\n"
)
s.sendall(req)
# Read 200 OK
resp = b""
while b"\r\n\r\n" not in resp:
chunk = s.recv(4096)
if not chunk:
break
resp += chunk
if b"200" not in resp:
print(f"FAIL: expected 200, got {resp.decode(errors='replace')}")
sys.exit(1)
# Build and send UDP relay frame
payload = b"hello-gost"
frame = build_udp_frame(payload, UDP_ECHO_HOST, UDP_ECHO_PORT)
s.sendall(frame)
# Read response frame
addr, rport, data = read_socks5_frame(s)
if b"hello-gost" in data:
print(f"PASS: received expected data from {addr}:{rport}")
sys.exit(0)
else:
print(f"FAIL: expected hello-gost in response, got {data!r}")
sys.exit(1)
if __name__ == "__main__":
main()
-16
View File
@@ -1,16 +0,0 @@
from http.server import BaseHTTPRequestHandler, HTTPServer
class Handler(BaseHTTPRequestHandler):
def do_GET(self):
body = b"hello-gost"
self.send_response(200)
self.send_header("Content-Length", str(len(body)))
self.end_headers()
self.wfile.write(body)
def log_message(self, format, *args):
return
HTTPServer(("0.0.0.0", 5678), Handler).serve_forever()
-53
View File
@@ -1,53 +0,0 @@
import socket
import sys
import time
def main():
host = sys.argv[1] if len(sys.argv) > 1 else "127.0.0.1"
port = int(sys.argv[2]) if len(sys.argv) > 2 else 8000
idle_timeout = int(sys.argv[3]) if len(sys.argv) > 3 else 3
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.settimeout(10)
s.connect((host, port))
# Send an HTTP GET request — the forward handler pipes us to tcp-echo:5678
req = b"GET / HTTP/1.0\r\nHost: tcp-echo\r\n\r\n"
s.sendall(req)
# Read response — should contain "hello-gost" (echo server reply)
resp = b""
while b"hello-gost" not in resp:
chunk = s.recv(4096)
if not chunk:
break
resp += chunk
if b"hello-gost" not in resp:
print(f"FAIL: expected hello-gost in response, got {resp.decode(errors='replace')}")
sys.exit(1)
print(f"PASS: first request through forward pipe succeeded")
# Wait longer than idleTimeout
wait = idle_timeout + 2
print(f"Waiting {wait}s for idle timeout...")
time.sleep(wait)
# Try to send more data — idle timeout should have closed the pipe
try:
s.sendall(b"GET / HTTP/1.0\r\nHost: tcp-echo\r\n\r\n")
data = s.recv(4096)
if not data:
print("PASS: connection closed after idle timeout (empty recv)")
sys.exit(0)
# Got data means the pipe is still alive
print(f"FAIL: connection still alive after idle timeout, got {data!r}")
sys.exit(1)
except (socket.timeout, ConnectionResetError, BrokenPipeError, OSError) as e:
print(f"PASS: connection closed after idle timeout: {e}")
sys.exit(0)
if __name__ == "__main__":
main()
-9
View File
@@ -1,9 +0,0 @@
import socket
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
sock.bind(("0.0.0.0", 5679))
while True:
data, addr = sock.recvfrom(2048)
sock.sendto(data, addr)
-25
View File
@@ -1,25 +0,0 @@
import socket
import sys
def main():
host = sys.argv[1] if len(sys.argv) > 1 else "127.0.0.1"
port = int(sys.argv[2]) if len(sys.argv) > 2 else 9000
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
sock.settimeout(5)
payload = b"hello-gost-udp"
sock.sendto(payload, (host, port))
data, addr = sock.recvfrom(2048)
if data == payload:
print("PASS: received echo")
sys.exit(0)
else:
print(f"FAIL: expected {payload!r}, got {data!r}")
sys.exit(1)
if __name__ == "__main__":
main()
-160
View File
@@ -1,160 +0,0 @@
package e2e
import (
"context"
"fmt"
"io"
"net"
"os"
"strings"
"testing"
"time"
"github.com/stretchr/testify/suite"
"github.com/testcontainers/testcontainers-go"
)
type ShadowsocksSuite struct {
suite.Suite
ctx context.Context
echoC testcontainers.Container
echoIP string
udpC testcontainers.Container
}
func (s *ShadowsocksSuite) SetupSuite() {
s.ctx = context.Background()
s.T().Logf("start tcp echo container...")
echoC, err := RunEchoContainer(s.ctx, SharedNetworkName)
s.Require().NoError(err)
s.echoC = echoC
echoIP, err := echoC.ContainerIP(s.ctx)
s.Require().NoError(err)
s.echoIP = echoIP
s.T().Logf("start udp echo container...")
udpC, err := RunUDPEchoContainer(s.ctx, SharedNetworkName)
s.Require().NoError(err)
s.udpC = udpC
}
func (s *ShadowsocksSuite) TearDownSuite() {
if s.echoC != nil {
s.echoC.Terminate(s.ctx)
}
if s.udpC != nil {
s.udpC.Terminate(s.ctx)
}
}
func (s *ShadowsocksSuite) TestShadowsocksTCP() {
s.runTCPCase("aes256gcm", "testdata/shadowsocks/tcp_server_aes256gcm.yaml", "testdata/shadowsocks/tcp_client_aes256gcm.yaml")
s.runTCPCase("chacha20", "testdata/shadowsocks/tcp_server_chacha20.yaml", "testdata/shadowsocks/tcp_client_chacha20.yaml")
}
func (s *ShadowsocksSuite) TestShadowsocks2022TCP() {
s.runTCPCase("2022-aes128", "testdata/shadowsocks/tcp_server_2022_aes128.yaml", "testdata/shadowsocks/tcp_client_2022_aes128.yaml")
s.runTCPCase("2022-aes256", "testdata/shadowsocks/tcp_server_2022_aes256.yaml", "testdata/shadowsocks/tcp_client_2022_aes256.yaml")
}
func (s *ShadowsocksSuite) TestShadowsocks2022TCPMultiPSK() {
s.runTCPCase("2022-aes128-multipsk", "testdata/shadowsocks/tcp_server_2022_aes128_multipsk.yaml", "testdata/shadowsocks/tcp_client_2022_aes128_multipsk.yaml")
s.runTCPCase("2022-aes256-multipsk", "testdata/shadowsocks/tcp_server_2022_aes256_multipsk.yaml", "testdata/shadowsocks/tcp_client_2022_aes256_multipsk.yaml")
}
func (s *ShadowsocksSuite) TestShadowsocksUDP() {
s.runUDPCase("aes256gcm", "testdata/shadowsocks/udp_server_aes256gcm.yaml", "testdata/shadowsocks/udp_client_aes256gcm.yaml")
}
func (s *ShadowsocksSuite) TestShadowsocks2022UDP() {
s.runUDPCase("2022-aes128", "testdata/shadowsocks/udp_server_2022_aes128.yaml", "testdata/shadowsocks/udp_client_2022_aes128.yaml")
s.runUDPCase("2022-aes256", "testdata/shadowsocks/udp_server_2022_aes256.yaml", "testdata/shadowsocks/udp_client_2022_aes256.yaml")
}
func (s *ShadowsocksSuite) runUDPCase(name, serverConfig, clientConfig string) {
s.T().Run(name, func(t *testing.T) {
serverAlias := name + "-ssu-server"
serverC, err := RunGostContainerWithOptions(s.ctx, SharedNetworkName, serverConfig, []string{serverAlias}, []string{"8389/udp"})
s.Require().NoError(err)
defer serverC.Terminate(s.ctx)
rendered, err := RenderConfig(clientConfig, ConfigData{ServerAddr: serverAlias + ":8389"})
s.Require().NoError(err)
defer os.Remove(rendered)
clientC, err := RunGostContainerWithPorts(
s.ctx,
SharedNetworkName,
rendered,
"9000/udp",
)
s.Require().NoError(err)
defer clientC.Terminate(s.ctx)
host, err := clientC.Host(s.ctx)
s.Require().NoError(err)
port, err := clientC.MappedPort(s.ctx, "9000/udp")
s.Require().NoError(err)
conn, err := net.DialTimeout("udp", net.JoinHostPort(host, port.Port()), 5*time.Second)
s.Require().NoError(err)
defer conn.Close()
payload := []byte("hello-gost-udp")
buf := make([]byte, 2048)
var n int
for i := range 5 {
_, err = conn.Write(payload)
s.Require().NoError(err)
_ = conn.SetReadDeadline(time.Now().Add(2 * time.Second))
n, err = conn.Read(buf)
if err == nil {
break
}
s.T().Logf("udp read attempt %d failed: %v, retrying...", i+1, err)
time.Sleep(2 * time.Second)
}
if err != nil {
DumpLogs(s.T(), s.ctx, name+" udp client logs", clientC)
DumpLogs(s.T(), s.ctx, name+" udp server logs", serverC)
}
s.Require().NoError(err)
s.Require().Contains(string(buf[:n]), "hello-gost")
})
}
func TestShadowsocksSuite(t *testing.T) {
suite.Run(t, new(ShadowsocksSuite))
}
func (s *ShadowsocksSuite) runTCPCase(name, serverConfig, clientConfig string) {
s.T().Run(name, func(t *testing.T) {
serverAlias := name + "-ss-server"
serverC, err := RunGostContainerWithOptions(s.ctx, SharedNetworkName, serverConfig, []string{serverAlias}, []string{"8388/tcp"})
s.Require().NoError(err)
defer serverC.Terminate(s.ctx)
rendered, err := RenderConfig(clientConfig, ConfigData{ServerAddr: serverAlias + ":8388"})
s.Require().NoError(err)
defer os.Remove(rendered)
clientC, err := RunGostContainerWithPorts(s.ctx, SharedNetworkName, rendered, "8080/tcp")
s.Require().NoError(err)
defer clientC.Terminate(s.ctx)
cmd := []string{"curl", "-v", "-s", "-x", "http://127.0.0.1:8080", fmt.Sprintf("http://%s:5678", s.echoIP)}
code, out, err := clientC.Exec(s.ctx, cmd)
s.Require().NoError(err)
body, err := io.ReadAll(out)
s.Require().NoError(err)
if code != 0 || !strings.Contains(string(body), "hello-gost") {
DumpLogs(s.T(), s.ctx, name+" client logs", clientC)
DumpLogs(s.T(), s.ctx, name+" server logs", serverC)
}
s.Require().Equal(0, code)
s.Require().Contains(string(body), "hello-gost")
})
}
-16
View File
@@ -1,16 +0,0 @@
services:
- name: dns
addr: :1053
handler:
type: dns
metadata:
dns: udp://dns-server:5353
timeout: 5s
listener:
type: dns
bypass: block-example
bypasses:
- name: block-example
matchers:
- test.example.com
-10
View File
@@ -1,10 +0,0 @@
services:
- name: dns
addr: :1053
handler:
type: dns
metadata:
dns: udp://127.0.0.1:1
timeout: 2s
listener:
type: dns
-22
View File
@@ -1,22 +0,0 @@
services:
- name: dns
addr: :1053
handler:
type: dns
metadata:
# Unreachable upstream so the system-DNS fallback is bypassed.
# The host mapper resolves mapped names before the exchanger is
# reached; unmapped names must hit the exchanger and fail.
dns: udp://127.0.0.1:1
timeout: 2s
listener:
type: dns
hosts: my-hosts
hosts:
- name: my-hosts
mappings:
- ip: 10.0.0.100
hostname: mapped.example.com
- ip: 192.168.1.200
hostname: aaaa.mapped.example.com
-16
View File
@@ -1,16 +0,0 @@
services:
- name: dns
addr: :1053
handler:
type: dns
metadata:
dns: udp://dns-server:5353
timeout: 5s
listener:
type: dns
rlimiter: limiter-0
rlimiters:
- name: limiter-0
limits:
- "$ 1000"
-12
View File
@@ -1,12 +0,0 @@
services:
- name: dns
addr: :1053
handler:
type: dns
metadata:
dns: tcp://dns-server:5353
timeout: 5s
listener:
type: dns
metadata:
mode: tcp
-12
View File
@@ -1,12 +0,0 @@
services:
- name: dns
addr: :1053
handler:
type: dns
metadata:
dns: udp://dns-server:5353
timeout: 5s
listener:
type: dns
metadata:
mode: tls
-10
View File
@@ -1,10 +0,0 @@
services:
- name: dns
addr: :1053
handler:
type: dns
metadata:
dns: udp://dns-server:5353
timeout: 5s
listener:
type: dns
View File
-1
View File
@@ -1 +0,0 @@
<html><body>gost file index</body></html>
-9
View File
@@ -1,9 +0,0 @@
services:
- name: file-server
addr: :8080
handler:
type: file
metadata:
file.dir: /srv/files
listener:
type: tcp
-15
View File
@@ -1,15 +0,0 @@
services:
- name: file-server-auth
addr: :8080
handler:
type: file
auther: auther-0
metadata:
file.dir: /srv/files
listener:
type: tcp
authers:
- name: auther-0
auths:
- username: user
password: pass
-10
View File
@@ -1,10 +0,0 @@
services:
- name: file-server-put
addr: :8080
handler:
type: file
metadata:
file.dir: /srv/files
file.put: true
listener:
type: tcp
-11
View File
@@ -1,11 +0,0 @@
services:
- name: forward-tcp
addr: :8000
handler:
type: tcp
listener:
type: tcp
forwarder:
nodes:
- name: echo
addr: tcp-echo:5678
-20
View File
@@ -1,20 +0,0 @@
services:
- name: forward-tcp
addr: :8000
handler:
type: tcp
metadata:
sniffing: true
sniffing.timeout: 2s
listener:
type: tcp
bypass: block-all
forwarder:
nodes:
- name: echo
addr: tcp-echo:5678
bypasses:
- name: block-all
matchers:
- 0.0.0.0/0
-13
View File
@@ -1,13 +0,0 @@
services:
- name: forward-tcp
addr: :8000
handler:
type: tcp
metadata:
idleTimeout: 3s
listener:
type: tcp
forwarder:
nodes:
- name: echo
addr: tcp-echo:5678
-18
View File
@@ -1,18 +0,0 @@
services:
- name: forward-tcp
addr: :8000
handler:
type: tcp
metadata:
sniffing: true
sniffing.timeout: 2s
listener:
type: tcp
forwarder:
nodes:
- name: echo-http
addr: tcp-echo:5678
protocol: http
- name: echo-tls
addr: tcp-echo:1
protocol: tls
-14
View File
@@ -1,14 +0,0 @@
services:
- name: forward-tcp
addr: :8000
handler:
type: tcp
metadata:
sniffing: true
sniffing.timeout: 2s
listener:
type: tcp
forwarder:
nodes:
- name: echo
addr: tcp-echo:5678
-11
View File
@@ -1,11 +0,0 @@
services:
- name: udp-forward
addr: :9000
handler:
type: udp
listener:
type: udp
forwarder:
nodes:
- name: echo
addr: udp-echo:5679
-15
View File
@@ -1,15 +0,0 @@
services:
- name: udp-forward
addr: :9000
handler:
type: udp
metadata:
stateless: true
listener:
type: udp
metadata:
stateless: true
forwarder:
nodes:
- name: echo
addr: udp-echo:5679
-20
View File
@@ -1,20 +0,0 @@
services:
- name: http-proxy
addr: :8080
handler:
type: http
chain: http-chain
listener:
type: tcp
chains:
- name: http-chain
hops:
- name: hop-0
nodes:
- name: node-0
addr: {{.ServerAddr}}
connector:
type: http
dialer:
type: tcp
-23
View File
@@ -1,23 +0,0 @@
services:
- name: http-proxy
addr: :8080
handler:
type: http
chain: http-chain
listener:
type: tcp
chains:
- name: http-chain
hops:
- name: hop-0
nodes:
- name: node-0
addr: {{.ServerAddr}}
connector:
type: http
auth:
username: user
password: pass
dialer:
type: tcp
-20
View File
@@ -1,20 +0,0 @@
services:
- name: http-proxy
addr: :8080
handler:
type: http
chain: https-chain
listener:
type: tcp
chains:
- name: https-chain
hops:
- name: hop-0
nodes:
- name: node-0
addr: {{.ServerAddr}}
connector:
type: http
dialer:
type: tls
-1
View File
@@ -1 +0,0 @@
<html><body>decoy-response</body></html>
-7
View File
@@ -1,7 +0,0 @@
services:
- name: http-proxy
addr: :8080
handler:
type: http
listener:
type: tcp
-16
View File
@@ -1,16 +0,0 @@
services:
- name: http-proxy-auth
addr: :8080
handler:
type: http
auther: auther-0
metadata:
# authBasicRealm: custom realm in 407 Proxy-Authenticate header
authBasicRealm: gost-e2e-realm
listener:
type: tcp
authers:
- name: auther-0
auths:
- username: user
password: pass
-12
View File
@@ -1,12 +0,0 @@
services:
- name: http-connect
addr: :8080
handler:
type: http
metadata:
# sniffing: enable protocol sniffing on CONNECT tunnels
sniffing: true
# sniffing.timeout: timeout for initial sniff read (2s)
sniffing.timeout: 2s
listener:
type: tcp
-20
View File
@@ -1,20 +0,0 @@
bypasses:
- name: bypass-0
matchers:
- 0.0.0.0/0
services:
- name: http-connect-bypass
addr: :8080
handler:
type: http
auther: auther-0
listener:
type: tcp
bypass: bypass-0
authers:
- name: auther-0
auths:
- username: user
password: pass
-25
View File
@@ -1,25 +0,0 @@
bypasses:
- name: bypass-0
matchers:
- 0.0.0.0/0
services:
- name: http-headers
addr: :8080
handler:
type: http
auther: auther-0
metadata:
header:
X-Proxy-Info: gost-e2e
X-Custom: test-value
proxyAgent: gost-e2e/1.0
listener:
type: tcp
bypass: bypass-0
authers:
- name: auther-0
auths:
- username: user
password: pass
-9
View File
@@ -1,9 +0,0 @@
services:
- name: http-idle
addr: :8080
handler:
type: http
metadata:
idleTimeout: 3s
listener:
type: tcp
-20
View File
@@ -1,20 +0,0 @@
services:
- name: http-proxy-meta
addr: :8080
handler:
type: http
auther: auther-0
metadata:
# probeResist: decoy response on auth failure (code:404 hides the proxy)
probeResist: code:404
# keepalive: enable HTTP keep-alive on upstream transport (parse test)
keepalive: true
# compression: enable HTTP compression on upstream transport (parse test)
compression: true
listener:
type: tcp
authers:
- name: auther-0
auths:
- username: user
password: pass
-16
View File
@@ -1,16 +0,0 @@
services:
- name: http-proxy
addr: :8080
handler:
type: http
auther: auther-0
metadata:
probeResist: file:/tmp/decoy.html
listener:
type: tcp
authers:
- name: auther-0
auths:
- username: user
password: pass
-16
View File
@@ -1,16 +0,0 @@
services:
- name: http-proxy
addr: :8080
handler:
type: http
auther: auther-0
metadata:
probeResist: host:tcp-echo:5678
listener:
type: tcp
authers:
- name: auther-0
auths:
- username: user
password: pass
-17
View File
@@ -1,17 +0,0 @@
services:
- name: http-proxy
addr: :8080
handler:
type: http
auther: auther-0
metadata:
probeResist: code:404
knock: secret.example.com
listener:
type: tcp
authers:
- name: auther-0
auths:
- username: user
password: pass
-16
View File
@@ -1,16 +0,0 @@
services:
- name: http-proxy
addr: :8080
handler:
type: http
auther: auther-0
metadata:
probeResist: web:tcp-echo:5678
listener:
type: tcp
authers:
- name: auther-0
auths:
- username: user
password: pass
-7
View File
@@ -1,7 +0,0 @@
services:
- name: https-server
addr: :8443
handler:
type: http
listener:
type: tls
-9
View File
@@ -1,9 +0,0 @@
services:
- name: http-udp
addr: :8080
handler:
type: http
metadata:
udp: true
listener:
type: tcp
-20
View File
@@ -1,20 +0,0 @@
services:
- name: http-proxy
addr: :8080
handler:
type: http
chain: h2-chain
listener:
type: tcp
chains:
- name: h2-chain
hops:
- name: hop-0
nodes:
- name: node-0
addr: {{.ServerAddr}}
connector:
type: http2
dialer:
type: http2
-23
View File
@@ -1,23 +0,0 @@
services:
- name: http-proxy
addr: :8080
handler:
type: http
chain: h2-chain
listener:
type: tcp
chains:
- name: h2-chain
hops:
- name: hop-0
nodes:
- name: node-0
addr: {{.ServerAddr}}
connector:
type: http2
auth:
username: user
password: pass
dialer:
type: http2
-7
View File
@@ -1,7 +0,0 @@
services:
- name: h2-proxy
addr: :8443
handler:
type: http2
listener:
type: http2
-18
View File
@@ -1,18 +0,0 @@
services:
- name: h2-proxy-auth
addr: :8443
handler:
type: http2
auther: auther-0
metadata:
# authBasicRealm: custom realm in 407 Proxy-Authenticate header
authBasicRealm: gost-e2e-realm
# hash: pin upstream selection by request host (metadata parse coverage)
hash: host
listener:
type: http2
authers:
- name: auther-0
auths:
- username: user
password: pass
-20
View File
@@ -1,20 +0,0 @@
bypasses:
- name: bypass-0
matchers:
- 0.0.0.0/0
services:
- name: h2-proxy-bypass
addr: :8443
handler:
type: http2
auther: auther-0
listener:
type: http2
bypass: bypass-0
authers:
- name: auther-0
auths:
- username: user
password: pass
-20
View File
@@ -1,20 +0,0 @@
services:
- name: h2-proxy-pr
addr: :8443
handler:
type: http2
auther: auther-0
metadata:
# probeResist: hide the proxy behind a 404 decoy on auth failure
probeResist: code:404
# header: custom response headers set on proxy responses
header:
X-Proxy-Info: gost-e2e
authBasicRealm: gost-e2e-realm
listener:
type: http2
authers:
- name: auther-0
auths:
- username: user
password: pass
-30
View File
@@ -1,30 +0,0 @@
services:
- name: proxy
addr: :8080
handler:
type: http
chain: my-chain
listener:
type: tcp
- name: dummy-1
addr: :18081
handler:
type: http
chains:
- name: my-chain
hops:
- name: hop-1
selector:
strategy: parallel
nodes:
- name: node-1
addr: 127.0.0.1:18081
connector:
type: http
# non existed node
- name: node-2
addr: 127.0.0.1:18082
connector:
type: http
@@ -1,23 +0,0 @@
services:
- name: http-proxy
addr: :8080
handler:
type: http
chain: ss-tcp-chain
listener:
type: tcp
chains:
- name: ss-tcp-chain
hops:
- name: ss-hop
nodes:
- name: ss-node
addr: {{.ServerAddr}}
connector:
type: ss
auth:
username: 2022-blake3-aes-128-gcm
password: MTIzNDU2Nzg5MDEyMzQ1Ng==
dialer:
type: tcp
@@ -1,23 +0,0 @@
services:
- name: http-proxy
addr: :8080
handler:
type: http
chain: ss-tcp-chain
listener:
type: tcp
chains:
- name: ss-tcp-chain
hops:
- name: ss-hop
nodes:
- name: ss-node
addr: {{.ServerAddr}}
connector:
type: ss
auth:
username: 2022-blake3-aes-128-gcm
password: MTIzNDU2Nzg5MDEyMzQ1Ng==:Vbwi6yqCwvPMPR1bCi32Dg==
dialer:
type: tcp
@@ -1,23 +0,0 @@
services:
- name: http-proxy
addr: :8080
handler:
type: http
chain: ss-tcp-chain
listener:
type: tcp
chains:
- name: ss-tcp-chain
hops:
- name: ss-hop
nodes:
- name: ss-node
addr: {{.ServerAddr}}
connector:
type: ss
auth:
username: 2022-blake3-aes-256-gcm
password: MTIzNDU2Nzg5MDEyMzQ1NjEyMzQ1Njc4OTAxMjM0NTY=
dialer:
type: tcp
@@ -1,23 +0,0 @@
services:
- name: http-proxy
addr: :8080
handler:
type: http
chain: ss-tcp-chain
listener:
type: tcp
chains:
- name: ss-tcp-chain
hops:
- name: ss-hop
nodes:
- name: ss-node
addr: {{.ServerAddr}}
connector:
type: ss
auth:
username: 2022-blake3-aes-256-gcm
password: MTIzNDU2Nzg5MDEyMzQ1NjEyMzQ1Njc4OTAxMjM0NTY=:YWJjZGVmZ2hpamtsbW5vcHFyc3R1dnd4eXoxMjM0NTY=
dialer:
type: tcp
@@ -1,23 +0,0 @@
services:
- name: http-proxy
addr: :8080
handler:
type: http
chain: ss-tcp-chain
listener:
type: tcp
chains:
- name: ss-tcp-chain
hops:
- name: ss-hop
nodes:
- name: ss-node
addr: {{.ServerAddr}}
connector:
type: ss
auth:
username: aes-256-gcm
password: secret
dialer:
type: tcp
-23
View File
@@ -1,23 +0,0 @@
services:
- name: http-proxy
addr: :8080
handler:
type: http
chain: ss-tcp-chain
listener:
type: tcp
chains:
- name: ss-tcp-chain
hops:
- name: ss-hop
nodes:
- name: ss-node
addr: {{.ServerAddr}}
connector:
type: ss
auth:
username: chacha20-ietf-poly1305
password: secret
dialer:
type: tcp
@@ -1,10 +0,0 @@
services:
- name: ss-server
addr: :8388
handler:
type: ss
auth:
username: 2022-blake3-aes-128-gcm
password: MTIzNDU2Nzg5MDEyMzQ1Ng==
listener:
type: tcp
@@ -1,13 +0,0 @@
services:
- name: ss-server
addr: :8388
handler:
type: ss
auth:
username: 2022-blake3-aes-128-gcm
password: MTIzNDU2Nzg5MDEyMzQ1Ng==
metadata:
users:
test: Vbwi6yqCwvPMPR1bCi32Dg==
listener:
type: tcp
@@ -1,10 +0,0 @@
services:
- name: ss-server
addr: :8388
handler:
type: ss
auth:
username: 2022-blake3-aes-256-gcm
password: MTIzNDU2Nzg5MDEyMzQ1NjEyMzQ1Njc4OTAxMjM0NTY=
listener:
type: tcp
@@ -1,13 +0,0 @@
services:
- name: ss-server
addr: :8388
handler:
type: ss
auth:
username: 2022-blake3-aes-256-gcm
password: MTIzNDU2Nzg5MDEyMzQ1NjEyMzQ1Njc4OTAxMjM0NTY=
metadata:
users:
test: YWJjZGVmZ2hpamtsbW5vcHFyc3R1dnd4eXoxMjM0NTY=
listener:
type: tcp
@@ -1,10 +0,0 @@
services:
- name: ss-server
addr: :8388
handler:
type: ss
auth:
username: aes-256-gcm
password: secret
listener:
type: tcp
-10
View File
@@ -1,10 +0,0 @@
services:
- name: ss-server
addr: :8388
handler:
type: ss
auth:
username: chacha20-ietf-poly1305
password: secret
listener:
type: tcp
@@ -1,27 +0,0 @@
services:
- name: udp-proxy
addr: :9000
handler:
type: udp
chain: ssu-chain
forwarder:
nodes:
- name: udp-echo
addr: udp-echo:5679
listener:
type: udp
chains:
- name: ssu-chain
hops:
- name: ssu-hop
nodes:
- name: ssu-node
addr: {{.ServerAddr}}
connector:
type: ssu
auth:
username: 2022-blake3-aes-128-gcm
password: MTIzNDU2Nzg5MDEyMzQ1Ng==
dialer:
type: udp
@@ -1,27 +0,0 @@
services:
- name: udp-proxy
addr: :9000
handler:
type: udp
chain: ssu-chain
forwarder:
nodes:
- name: udp-echo
addr: udp-echo:5679
listener:
type: udp
chains:
- name: ssu-chain
hops:
- name: ssu-hop
nodes:
- name: ssu-node
addr: {{.ServerAddr}}
connector:
type: ssu
auth:
username: 2022-blake3-aes-256-gcm
password: MTIzNDU2Nzg5MDEyMzQ1NjEyMzQ1Njc4OTAxMjM0NTY=
dialer:
type: udp
@@ -1,27 +0,0 @@
services:
- name: udp-proxy
addr: :9000
handler:
type: udp
chain: ssu-chain
forwarder:
nodes:
- name: udp-echo
addr: udp-echo:5679
listener:
type: udp
chains:
- name: ssu-chain
hops:
- name: ssu-hop
nodes:
- name: ssu-node
addr: {{.ServerAddr}}
connector:
type: ssu
auth:
username: aes-256-gcm
password: secret
dialer:
type: udp
@@ -1,10 +0,0 @@
services:
- name: ssu-server
addr: :8389
handler:
type: ssu
auth:
username: 2022-blake3-aes-128-gcm
password: MTIzNDU2Nzg5MDEyMzQ1Ng==
listener:
type: udp

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