commit 2c10b421b406de2f5b5e7bb780bdaa1318f83c29 Author: mm644706215 Date: Tue Sep 30 20:45:51 2025 +0800 first add diff --git a/README.md b/README.md new file mode 100644 index 0000000..d13d7f2 --- /dev/null +++ b/README.md @@ -0,0 +1,93 @@ +# Proxy Stack — VLESS(WS+TLS) with your custom Caddy (Cloudflare/Alidns + caddy-l4) + +这包兼容两种部署模式: + +- **A. 标准模式(无 L4)**:`server/docker-compose.yml` 直接让 Caddy 占用 80/443;v2ray 走 `/ray` 路由,适合多数“网站 + 代理共存”的情况。 +- **B. L4 前置模式**:`server/docker-compose-l4.yml` 用 caddy-l4 独占 80/443,根据 SNI 把 `DERPER`/其他 TLS 流量直接四层转发,`caddy-http` 仅监听 8443;适合你需要在 443 上同时承载多种非 HTTP TLS upstream(如 Tailscale derper)时。 + +> 两种模式均保持 **复用 443 端口**;未来增加站点/服务仅需改 Caddyfile 或 l4.json。 + +--- + +## 目录 +``` +proxy-stack-vless-wss-caddy-v2/ +├─ README.md +├─ server/ +│ ├─ Dockerfile.caddy-l4 # 你的 caddy 构建(含 cloudflare/alidns/caddy-l4) +│ ├─ docker-compose.yml # 模式 A(无 L4,Caddy 直接 80/443) +│ ├─ docker-compose-l4.yml # 模式 B(有 L4,Caddy-HTTP 监听 8443) +│ ├─ caddy/ +│ │ ├─ Caddyfile # 站点与 /ray 反代(模式A/B都用) +│ │ ├─ l4.json # 仅模式B使用:L4 路由(SNI → upstream) +│ │ ├─ env/ +│ │ │ └─ caddy.env.example # DNS 证书与 ACME 环境变量示例 +│ │ └─ site/index.html # 占位站点 +│ └─ v2ray/config.json # VLESS WS 入站(/ray) +└─ client/ + ├─ docker-compose.yml + └─ v2ray-client.json +``` + +--- + +## 1) 准备域名与证书环境变量 + +- DNS A 记录:将 `` 指向 VPS 公网 IP(如需子域 `proxy.`,请自行替换)。 +- 将 `server/caddy/env/caddy.env.example` 复制为 `caddy.env` 并填入: + - `ACME_EMAIL`:Let's Encrypt/ZeroSSL 证书邮箱 + - `CF_API_TOKEN`(可选):Cloudflare DNS API Token + - `ALIYUN_ACCESS_KEY_ID` / `ALIYUN_ACCESS_KEY_SECRET`(可选):阿里云 DNS AK/SK + - `DOMAIN`:你的域名(与上面解析一致) + - `DERPER_HOST`(可选,L4 模式匹配用) + +> 你可同时放 Cloudflare 与阿里云变量,Caddyfile 里按需选择 DNS Provider。 + +--- + +## 2) 模式 A(推荐,简单):Caddy 直接占用 80/443 + +```bash +cd server +docker compose up -d --build +``` + +- 将自动构建你自定义的 caddy 镜像,并用 `caddy/Caddyfile` 配置站点与 `/ray` 反代。 +- 健康检查:访问 `https:///_health` 返回 200。 + +--- + +## 3) 模式 B(进阶):caddy-l4 前置 + caddy-http 监听 8443 + +```bash +cd server +docker compose -f docker-compose-l4.yml up -d --build +``` + +- `caddy-l4` 独占 80/443,并按 `caddy/l4.json` 的 **SNI** 规则把 `DERPER_HOST` 直接四层转发,其他 TLS 走 `caddy-http:8443`。 +- `caddy-http` 同样装的是你自定义的 caddy(可自动签发证书),承担网站与 `/ray`。 + +--- + +## 4) 客户端 + +```bash +cd client +# 修改 v2ray-client.json 的 后: +docker compose up -d +# 得到本机代理:SOCKS5 127.0.0.1:1080;HTTP 127.0.0.1:8080 +``` + +**Docker 构建走代理:** +```bash +DOCKER_BUILDKIT=1 docker build --build-arg HTTP_PROXY=http://127.0.0.1:8080 --build-arg HTTPS_PROXY=http://127.0.0.1:8080 -t img:latest . +``` + +--- + +## 5) 常见问题 +- 证书失败:检查 80/443 是否放行;DNS 是否生效;`caddy.env` 中 DNS Token 是否正确。 +- 端口复用:在 `Caddyfile` 新增 `handle`/`route` 或在 `l4.json` 新增 SNI 分流;无需额外占端口。 +- 性能:选好 LA 节点/带宽;必要时在 v2ray 启用 `mux`。 + +> 法律合规自行确认;仅用于合法用途。 diff --git a/client/docker-compose.yml b/client/docker-compose.yml new file mode 100644 index 0000000..fb98e86 --- /dev/null +++ b/client/docker-compose.yml @@ -0,0 +1,13 @@ +version: "3.9" + +services: + v2ray-client: + image: v2fly/v2fly-core:latest + container_name: v2ray-client + restart: unless-stopped + volumes: + - ./v2ray-client.json:/etc/v2ray/config.json:ro + command: ["run", "-c", "/etc/v2ray/config.json"] + ports: + - "1080:1080" # SOCKS5 + - "8080:8080" # HTTP diff --git a/client/test-speed.ps1 b/client/test-speed.ps1 new file mode 100644 index 0000000..d53c166 --- /dev/null +++ b/client/test-speed.ps1 @@ -0,0 +1,58 @@ +param( + [ValidateSet("http","socks")] + [string]$Mode = "http", + + [int]$SizeMB = 50 +) + +# Build proxy value and target URL +$proxy = if ($Mode -eq "http") { "http://127.0.0.1:8080" } else { "socks5h://127.0.0.1:1080" } +$envVar = if ($Mode -eq "http") { "HTTPS_PROXY" } else { "ALL_PROXY" } +$bytes = [int64]$SizeMB * 1MB +$testUrl = "https://speed.cloudflare.com/__down?bytes=$bytes" + +Write-Host "Mode: $Mode Proxy: $proxy Size: $SizeMB MB" -ForegroundColor Cyan + +# Set proxy env only for this process +[System.Environment]::SetEnvironmentVariable($envVar, $proxy, 'Process') + +function Test-Health { + try { + Write-Host "Checking server health via proxy..." -ForegroundColor Yellow + $health = & curl.exe -I https://molecular.eu.org/_health 2>&1 + $line = ($health | Select-String -Pattern "HTTP/").ToString() + Write-Host $line -ForegroundColor Green + } catch { + Write-Host "Health check failed: $_" -ForegroundColor Red + } +} + +function Test-Download { + param([string]$Url) + Write-Host "Running download test: $Url" -ForegroundColor Yellow + $result = & curl.exe -sS -o NUL -w "time_namelookup=%{time_namelookup} time_connect=%{time_connect} time_starttransfer=%{time_starttransfer} time_total=%{time_total} speed_download=%{speed_download}\n" $Url + if (-not $result) { Write-Host "curl produced no output." -ForegroundColor Red; return } + + $kv = @{} + foreach ($pair in $result.Trim().Split(' ')) { + if ($pair -match "=") { + $k,$v = $pair.Split('='); $kv[$k] = [double]$v + } + } + $mbps = [math]::Round(($kv['speed_download'] * 8.0) / 1e6, 2) + Write-Host ("Latency(connect): {0} s | TTFB: {1} s | Total: {2} s | Throughput: {3} Mbps" -f ` + $kv['time_connect'], $kv['time_starttransfer'], $kv['time_total'], $mbps) -ForegroundColor Green +} + +Test-Health +Test-Download -Url $testUrl + +# Optional: run a smaller warm-up and a larger sustained test +if ($SizeMB -lt 20) { + Test-Download -Url "https://speed.cloudflare.com/__down?bytes=20000000" # 20MB +} +if ($SizeMB -lt 100) { + Test-Download -Url "https://speed.cloudflare.com/__down?bytes=100000000" # 100MB +} + +Write-Host "Done." -ForegroundColor Cyan diff --git a/client/v2ray-client-grpc.json b/client/v2ray-client-grpc.json new file mode 100644 index 0000000..d758792 --- /dev/null +++ b/client/v2ray-client-grpc.json @@ -0,0 +1,47 @@ +{ + "log": { + "loglevel": "info" + }, + "inbounds": [ + { + "port": 1080, + "listen": "0.0.0.0", + "protocol": "socks", + "settings": { "auth": "noauth", "udp": true } + }, + { + "port": 8080, + "listen": "0.0.0.0", + "protocol": "http", + "settings": {} + } + ], + "outbounds": [ + { + "protocol": "vless", + "settings": { + "vnext": [ + { + "address": "molecular.eu.org", + "port": 443, + "users": [ + { "id": "8f5e3b57-5a3d-4a3e-9f3c-9c6c2d6a9f1e", "encryption": "none" } + ] + } + ] + }, + "streamSettings": { + "network": "grpc", + "security": "tls", + "tlsSettings": { + "serverName": "molecular.eu.org", + "alpn": ["h2"] + }, + "grpcSettings": { + "serviceName": "grpc", + "multiMode": true + } + } + } + ] +} diff --git a/client/v2ray-client.json b/client/v2ray-client.json new file mode 100644 index 0000000..fee05ef --- /dev/null +++ b/client/v2ray-client.json @@ -0,0 +1,43 @@ +{ + "log": { + "loglevel": "info" + }, + "inbounds": [ + { + "port": 1080, + "listen": "0.0.0.0", + "protocol": "socks", + "settings": { "auth": "noauth", "udp": true } + }, + { + "port": 8080, + "listen": "0.0.0.0", + "protocol": "http", + "settings": {} + } + ], + "outbounds": [ + { + "protocol": "vless", + "settings": { + "vnext": [ + { + "address": "molecular.eu.org", + "port": 443, + "users": [ + { "id": "8f5e3b57-5a3d-4a3e-9f3c-9c6c2d6a9f1e", "encryption": "none" } + ] + } + ] + }, + "streamSettings": { + "network": "grpc", + "security": "tls", + "grpcSettings": { + "serviceName": "grpc", + "multiMode": true + } + } + } + ] +} diff --git a/client/v2ray-clientbak.json b/client/v2ray-clientbak.json new file mode 100644 index 0000000..0f0ada1 --- /dev/null +++ b/client/v2ray-clientbak.json @@ -0,0 +1,48 @@ +{ + "log": { + "loglevel": "info" + }, + "inbounds": [ + { + "port": 1080, + "listen": "0.0.0.0", + "protocol": "socks", + "settings": { + "auth": "noauth", + "udp": true + } + }, + { + "port": 8080, + "listen": "0.0.0.0", + "protocol": "http", + "settings": {} + } + ], + "outbounds": [ + { + "protocol": "vless", + "settings": { + "vnext": [ + { + "address": "molecular.eu.org", + "port": 443, + "users": [ + { + "id": "8f5e3b57-5a3d-4a3e-9f3c-9c6c2d6a9f1e", + "encryption": "none" + } + ] + } + ] + }, + "streamSettings": { + "network": "ws", + "security": "tls", + "wsSettings": { + "path": "/ray" + } + } + } + ] +} \ No newline at end of file diff --git a/server/Dockerfile.caddy-l4 b/server/Dockerfile.caddy-l4 new file mode 100644 index 0000000..2e2e6bf --- /dev/null +++ b/server/Dockerfile.caddy-l4 @@ -0,0 +1,36 @@ +# ========= Stage 1: (可选)你的前端静态资源构建 ========= +FROM node:lts-alpine AS ui-builder +WORKDIR /src +COPY headscale-ui/package*.json ./ +RUN npm ci +COPY headscale-ui/ . +RUN npm run build + +# ========= Stage 2: 用 Go 1.25 (Alpine) 构建 Caddy + 插件 ========= +FROM golang:1.25.0-alpine3.22 AS caddy-builder + +RUN echo "https://mirrors.aliyun.com/alpine/v3.22/main" > /etc/apk/repositories && \ + echo "https://mirrors.aliyun.com/alpine/v3.22/community" >> /etc/apk/repositories && \ + apk update && \ + apk add --no-cache git build-base ca-certificates + +ENV GOPROXY=https://goproxy.cn,direct +ENV GOTOOLCHAIN=local + +RUN go install github.com/caddyserver/xcaddy/cmd/xcaddy@latest + +# 用 xcaddy 构建 (保持与用户提供版本一致/可调整) +RUN /go/bin/xcaddy build v2.10.1 \ + --output /usr/local/bin/caddy \ + --with github.com/caddy-dns/alidns@v1.0.26 \ + --with github.com/caddy-dns/cloudflare@v0.2.1 \ + --with github.com/mholt/caddy-l4 + +# ========= Stage 3: 运行时镜像(Alpine) ========= +FROM caddy:2.10.0-alpine +COPY --from=caddy-builder /usr/local/bin/caddy /usr/bin/caddy +COPY --from=ui-builder /src/build /usr/src/www + +RUN apk update && \ + apk add --no-cache bind-tools netcat-openbsd jq curl && \ + mkdir -p /data/log/headscale diff --git a/server/caddy/Caddyfile b/server/caddy/Caddyfile new file mode 100644 index 0000000..2ddfa97 --- /dev/null +++ b/server/caddy/Caddyfile @@ -0,0 +1,54 @@ +{ + email {env.ACME_EMAIL} + # 如果你想强制使用 Cloudflare/阿里云 DNS 验证,可在每个站点 tls 块中指定 `dns`。 +} + +# 主站点(HTTPS 复用 443;/ray 提供 VLESS-WS 反代) +molecular.eu.org { + log { + output stdout + format console + } + # 若你需要 http->https 强制跳转,可加: + # redir https://{uri} permanent + + encode zstd gzip + + @vless_ws path /ray /ray/* + handle @vless_ws { + reverse_proxy v2ray:10000 { + header_up -Origin + } + } + + # VLESS gRPC over h2c at /grpc + @vless_grpc path /grpc /grpc/* + handle @vless_grpc { + reverse_proxy { + transport http { + versions h2c + } + to v2ray:10001 + } + } + + # 健康检查 + handle_path /_health* { + respond 200 + } + + # 静态站点或你的网站反代 + handle { + root * /usr/src/www + file_server + } + + # 证书:可根据你的 DNS 服务商二选一(或只保留其一) + tls { + dns cloudflare {env.CF_API_TOKEN} + #dns alidns { + # access_key_id {env.ALIYUN_ACCESS_KEY_ID} + # access_key_secret {env.ALIYUN_ACCESS_KEY_SECRET} + #} + } +} diff --git a/server/caddy/env/caddy.env b/server/caddy/env/caddy.env new file mode 100644 index 0000000..d7f17ea --- /dev/null +++ b/server/caddy/env/caddy.env @@ -0,0 +1,10 @@ +# ===== Caddy / ACME / DNS (Cloudflare) ===== +# 用于在 Let's Encrypt/ZeroSSL 注册证书的邮箱 +ACME_EMAIL=pylyzeng@gmail.com + +# 你的站点域名(当前 Caddyfile 已直接写死为 molecular.eu.org,此变量仅作记录) +DOMAIN=molecular.eu.org + +# Cloudflare API Token(至少授予:Zone:Read 与 Zone:DNS:Edit) +# 在 Caddyfile 中通过 {env.CF_API_TOKEN} 使用 +CF_API_TOKEN=ofvUY4Wo9-VN__AMglXHf8fVM1xtBRFfGN_Bsd-C diff --git a/server/caddy/env/caddy.env.example b/server/caddy/env/caddy.env.example new file mode 100644 index 0000000..234e89d --- /dev/null +++ b/server/caddy/env/caddy.env.example @@ -0,0 +1,14 @@ +# ===== Caddy / ACME / DNS ===== +ACME_EMAIL= +DOMAIN= + +# Cloudflare (可选其一) +CLOUDFLARE_API_TOKEN= +CF_API_TOKEN= + +# 阿里云 (可选其一) +ALIYUN_ACCESS_KEY_ID= +ALIYUN_ACCESS_KEY_SECRET= + +# (L4 模式可选) 指定需要直达 L4 转发的 SNI +DERPER_HOST= diff --git a/server/caddy/l4.json b/server/caddy/l4.json new file mode 100644 index 0000000..c839a42 --- /dev/null +++ b/server/caddy/l4.json @@ -0,0 +1,73 @@ +{ + "apps": { + "layer4": { + "servers": { + "tcp80": { + "listen": [ + "tcp/:80" + ], + "routes": [ + { + "handle": [ + { + "handler": "proxy", + "upstreams": [ + { + "dial": [ + "caddy-http:80" + ] + } + ] + } + ] + } + ] + }, + "tcp443": { + "listen": [ + "tcp/:443" + ], + "routes": [ + { + "match": [ + { + "tls": { + "sni": [ + "" + ] + } + } + ], + "handle": [ + { + "handler": "proxy", + "upstreams": [ + { + "dial": [ + "derper:443" + ] + } + ] + } + ] + }, + { + "handle": [ + { + "handler": "proxy", + "upstreams": [ + { + "dial": [ + "caddy-http:8443" + ] + } + ] + } + ] + } + ] + } + } + } + } +} \ No newline at end of file diff --git a/server/caddy/site/index.html b/server/caddy/site/index.html new file mode 100644 index 0000000..e2efa9f --- /dev/null +++ b/server/caddy/site/index.html @@ -0,0 +1,3 @@ + +OK +

It works.

HTTPS is up. VLESS-WS is on /ray.

diff --git a/server/docker-compose-l4.yml b/server/docker-compose-l4.yml new file mode 100644 index 0000000..f3f7d95 --- /dev/null +++ b/server/docker-compose-l4.yml @@ -0,0 +1,49 @@ +version: "3.9" + +services: + caddy-l4: + image: caddy-l4:latest + build: + context: . + dockerfile: Dockerfile.caddy-l4 + container_name: caddy-l4 + restart: unless-stopped + ports: + - "80:80/tcp" + - "443:443/tcp" + command: ["caddy","run","--config","/etc/caddy/caddy.json"] + env_file: + - ./caddy/env/caddy.env + volumes: + - ./caddy/l4.json:/etc/caddy/caddy.json:ro + - ./caddy/log:/data/log + depends_on: + - caddy-http + + caddy-http: + image: caddy-l4:latest + container_name: caddy-http + restart: unless-stopped + expose: + - "8443/tcp" + - "80/tcp" + environment: + - CADDY_LISTEN_HTTPS=:8443 + - CADDY_LISTEN_HTTP=:80 + - CADDY_ADMIN=:2019 + env_file: + - ./caddy/env/caddy.env + volumes: + - ./caddy/Caddyfile:/etc/caddy/Caddyfile:ro + - ./caddy/site:/usr/src/www:ro + - ./caddy/ssl:/data/caddy/certificates + - ./caddy/log:/data/log + depends_on: + - v2ray + + v2ray: + image: v2fly/v2fly-core:latest + container_name: v2ray + restart: unless-stopped + volumes: + - ./v2ray/config.json:/etc/v2ray/config.json:ro diff --git a/server/docker-compose.yml b/server/docker-compose.yml new file mode 100644 index 0000000..bfc6476 --- /dev/null +++ b/server/docker-compose.yml @@ -0,0 +1,32 @@ +version: "3.9" + +services: + caddy-http: + image: caddy-l4:latest + build: + context: . + dockerfile: Dockerfile.caddy-l4 + container_name: caddy-http + restart: unless-stopped + ports: + - "80:80/tcp" + - "443:443/tcp" + environment: + - CADDY_ADMIN=:2019 + env_file: + - ./caddy/env/caddy.env + volumes: + - ./caddy/Caddyfile:/etc/caddy/Caddyfile:ro + - ./caddy/site:/usr/src/www:ro + - ./caddy/ssl:/data/caddy/certificates + - ./caddy/log:/data/log + depends_on: + - v2ray + + v2ray: + image: v2fly/v2fly-core:latest + container_name: v2ray + restart: unless-stopped + volumes: + - ./v2ray/config.json:/etc/v2ray/config.json:ro + command: ["run", "-c", "/etc/v2ray/config.json"] diff --git a/server/v2ray/config.json b/server/v2ray/config.json new file mode 100644 index 0000000..6fc3478 --- /dev/null +++ b/server/v2ray/config.json @@ -0,0 +1,52 @@ +{ + "log": { + "loglevel": "warning" + }, + "inbounds": [ + { + "port": 10000, + "listen": "0.0.0.0", + "protocol": "vless", + "settings": { + "clients": [ + { + "id": "8f5e3b57-5a3d-4a3e-9f3c-9c6c2d6a9f1e" + } + ], + "decryption": "none" + }, + "streamSettings": { + "network": "ws", + "wsSettings": { + "path": "/ray" + } + } + }, + { + "port": 10001, + "listen": "0.0.0.0", + "protocol": "vless", + "settings": { + "clients": [ + { + "id": "8f5e3b57-5a3d-4a3e-9f3c-9c6c2d6a9f1e" + } + ], + "decryption": "none" + }, + "streamSettings": { + "network": "grpc", + "grpcSettings": { + "serviceName": "grpc", + "multiMode": true + } + } + } + ], + "outbounds": [ + { + "protocol": "freedom", + "settings": {} + } + ] +} \ No newline at end of file