chore(ubuntu): pin xray runtime and simplify tests
This commit is contained in:
@@ -1 +1,3 @@
|
||||
logs
|
||||
.env
|
||||
.test-case-*.sh
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
- 除非处于YOLO模式,不然你应该每一步都是先告诉我思路和原理或者问题,等我确认之后再下一步
|
||||
- 代码保持简洁明了的风格,禁止过度设计
|
||||
@@ -1,4 +0,0 @@
|
||||
IMAGE=ubuntu-xray-transparent-test
|
||||
XRAY_URL=vless://07e41bec-d78a-4e17-9c1f-682c70ef256b@xui.akko.pchuan.site:25748?encryption=none&flow=xtls-rprx-vision&security=reality&sni=yahoo.com&fp=firefox&pbk=n2Y247czvV-3_DbiAy4mmk6s8QWaeKCmB5KI5E_831w&sid=f0&spx=%2F&type=tcp&headerType=none#5-pc
|
||||
XRAY_IPV6_ENABLED=0
|
||||
XRAY_DEBUG_CONFIG=0
|
||||
@@ -0,0 +1,4 @@
|
||||
IMAGE=ubuntu-xray-transparent-test
|
||||
XRAY_URL=vless://00000000-0000-0000-0000-000000000000@example.com:443?encryption=none&flow=xtls-rprx-vision&security=reality&type=tcp&headerType=none&sni=example.com&fp=chrome&pbk=REPLACE_WITH_PUBLIC_KEY&sid=00&spx=%2F
|
||||
XRAY_IPV6_ENABLED=0
|
||||
XRAY_DEBUG_CONFIG=0
|
||||
+10
-8
@@ -1,9 +1,12 @@
|
||||
FROM ubuntu:24.04
|
||||
|
||||
ARG XRAY_VERSION=latest
|
||||
ARG XRAY_VERSION=v26.3.27
|
||||
ARG XRAY_ZIP=Xray-linux-64.zip
|
||||
ARG S6_OVERLAY_VERSION=3.2.1.0
|
||||
ARG XRAY_ZIP_SHA256=23cd9af937744d97776ee35ecad4972cf4b2109d1e0fe6be9930467608f7c8ae
|
||||
ARG S6_OVERLAY_VERSION=3.2.3.0
|
||||
ARG S6_OVERLAY_ARCH=x86_64
|
||||
ARG S6_OVERLAY_NOARCH_SHA256=b720f9d9340efc8bb07528b9743813c836e4b02f8693d90241f047998b4c53cf
|
||||
ARG S6_OVERLAY_ARCH_SHA256=a93f02882c6ed46b21e7adb5c0add86154f01236c93cd82c7d682722e8840563
|
||||
ARG SOURCE_VERSION=unknown
|
||||
|
||||
# Use USTC Ubuntu mirrors and install runtime tools.
|
||||
@@ -21,7 +24,7 @@ RUN set -eux; \
|
||||
{} +; \
|
||||
fi; \
|
||||
apt-get update; \
|
||||
apt-get install -y --no-install-recommends ca-certificates curl iptables unzip xz-utils; \
|
||||
apt-get install -y --no-install-recommends ca-certificates curl iptables jq unzip xz-utils; \
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Install s6-overlay as PID 1 / service supervisor.
|
||||
@@ -29,6 +32,8 @@ RUN set -eux; \
|
||||
s6_base="https://github.com/just-containers/s6-overlay/releases/download/v${S6_OVERLAY_VERSION}"; \
|
||||
curl -fsSL "${s6_base}/s6-overlay-noarch.tar.xz" -o /tmp/s6-overlay-noarch.tar.xz; \
|
||||
curl -fsSL "${s6_base}/s6-overlay-${S6_OVERLAY_ARCH}.tar.xz" -o /tmp/s6-overlay-${S6_OVERLAY_ARCH}.tar.xz; \
|
||||
printf '%s %s\n' "${S6_OVERLAY_NOARCH_SHA256}" /tmp/s6-overlay-noarch.tar.xz | sha256sum -c -; \
|
||||
printf '%s %s\n' "${S6_OVERLAY_ARCH_SHA256}" /tmp/s6-overlay-${S6_OVERLAY_ARCH}.tar.xz | sha256sum -c -; \
|
||||
tar -C / -Jxpf /tmp/s6-overlay-noarch.tar.xz; \
|
||||
tar -C / -Jxpf /tmp/s6-overlay-${S6_OVERLAY_ARCH}.tar.xz; \
|
||||
rm -f /tmp/s6-overlay-noarch.tar.xz /tmp/s6-overlay-${S6_OVERLAY_ARCH}.tar.xz
|
||||
@@ -36,12 +41,9 @@ RUN set -eux; \
|
||||
# Install Xray from the official release archive.
|
||||
RUN set -eux; \
|
||||
download_base="https://github.com/XTLS/Xray-core/releases"; \
|
||||
if [ "$XRAY_VERSION" = "latest" ]; then \
|
||||
download_url="$download_base/latest/download/$XRAY_ZIP"; \
|
||||
else \
|
||||
download_url="$download_base/download/$XRAY_VERSION/$XRAY_ZIP"; \
|
||||
fi; \
|
||||
download_url="$download_base/download/$XRAY_VERSION/$XRAY_ZIP"; \
|
||||
curl -fsSL "$download_url" -o /tmp/xray.zip; \
|
||||
printf '%s %s\n' "${XRAY_ZIP_SHA256}" /tmp/xray.zip | sha256sum -c -; \
|
||||
unzip /tmp/xray.zip -d /tmp/xray; \
|
||||
install -m 0755 /tmp/xray/xray /usr/local/bin/xray; \
|
||||
mkdir -p /usr/local/share/xray /etc/xray; \
|
||||
|
||||
+24
-14
@@ -33,7 +33,7 @@ flowchart LR
|
||||
```mermaid
|
||||
flowchart TD
|
||||
Base[ubuntu:24.04] --> Mirror[替换 apt 源为中科大镜像]
|
||||
Mirror --> Packages[安装 curl / iptables / unzip / xz-utils]
|
||||
Mirror --> Packages[安装 curl / iptables / jq / unzip / xz-utils]
|
||||
Packages --> S6Install[安装 s6-overlay]
|
||||
S6Install --> XrayInstall[下载并安装 Xray-core]
|
||||
XrayInstall --> User[创建 xray 系统用户]
|
||||
@@ -49,8 +49,8 @@ flowchart TD
|
||||
Ubuntu[Ubuntu/] --> EnvFile[.env]
|
||||
Ubuntu --> ComposeFile[compose.yml]
|
||||
Ubuntu --> Dockerfile[Dockerfile]
|
||||
Ubuntu --> EnvExample[.env.example]
|
||||
Ubuntu --> Test[test.bat]
|
||||
Ubuntu --> Runner[test-runner.sh]
|
||||
Ubuntu --> Scripts[scripts/]
|
||||
Ubuntu --> Rootfs[rootfs/]
|
||||
|
||||
@@ -73,11 +73,15 @@ flowchart TD
|
||||
|
||||
`.env`
|
||||
|
||||
运行时配置文件。默认包含 `IMAGE`、`XRAY_URL`、`XRAY_IPV6_ENABLED`、`XRAY_DEBUG_CONFIG`。
|
||||
运行时配置文件。默认包含 `IMAGE`、`XRAY_URL`、`XRAY_IPV6_ENABLED`、`XRAY_DEBUG_CONFIG`。真实 `.env` 不入库,使用 `.env.example` 复制后填写真实节点。
|
||||
|
||||
`.env.example`
|
||||
|
||||
可提交的配置模板,不包含真实节点密钥。
|
||||
|
||||
`compose.yml`
|
||||
|
||||
定义测试服务,并把 `test-runner.sh` 只读挂载到容器内。测试脚本不会进入最终镜像。
|
||||
定义测试服务和运行时环境。测试命令由 `test.bat` 传入,不再维护额外的测试 runner 文件。
|
||||
|
||||
`Dockerfile`
|
||||
|
||||
@@ -85,15 +89,11 @@ flowchart TD
|
||||
|
||||
`test.bat`
|
||||
|
||||
本地测试脚本。调用 Docker Compose 构建镜像,并依次运行 `proxy-on` 与 `proxy-off`。
|
||||
|
||||
`test-runner.sh`
|
||||
|
||||
容器测试入口。只通过 Docker Compose 临时挂载到 `/tmp/test-runner.sh`,不被 `Dockerfile` 复制进镜像。
|
||||
本地测试脚本。调用 Docker Compose 构建镜像,并依次运行 IPv6 与代理开关测试。
|
||||
|
||||
`scripts/xray-url-to-config.sh`
|
||||
|
||||
把受支持的 `vless://...` 转换成 Xray JSON 配置,写到 `/tmp/xray.generated.json`。当前不是通用转换器,只支持下面两类链接:
|
||||
把受支持的 `vless://...` 转换成 Xray JSON 配置,写到 `/tmp/xray.generated.json`。配置由 `jq` 生成,并先写入临时文件,成功后再原子替换目标文件。当前不是通用转换器,只支持下面两类链接:
|
||||
|
||||
```text
|
||||
vless + tcp + reality + flow=xtls-rprx-vision + headerType=none
|
||||
@@ -283,8 +283,6 @@ Ubuntu\test.bat
|
||||
```mermaid
|
||||
flowchart TD
|
||||
Build[docker compose build] --> On[proxy-on]
|
||||
Mount[test-runner.sh 只读挂载] --> On
|
||||
Mount --> Off
|
||||
On --> OnRun[读取 .env 并启用透明代理]
|
||||
OnRun --> OnCurl[curl google.com]
|
||||
OnCurl --> Off[proxy-off]
|
||||
@@ -297,8 +295,8 @@ flowchart TD
|
||||
|
||||
```bat
|
||||
docker compose --env-file .env -f compose.yml build
|
||||
docker compose --env-file .env -f compose.yml run --rm proxy-on
|
||||
docker compose --env-file .env -f compose.yml run --rm --no-deps proxy-off
|
||||
docker compose --env-file .env -f compose.yml run --rm --no-deps proxy-on bash
|
||||
docker compose --env-file .env -f compose.yml run --rm --no-deps proxy-off bash
|
||||
```
|
||||
|
||||
`proxy-on` 日志里应该出现:
|
||||
@@ -315,6 +313,18 @@ docker compose --env-file .env -f compose.yml run --rm --no-deps proxy-off
|
||||
Xray disabled by XRAY_ENABLED=0.
|
||||
```
|
||||
|
||||
如果当前网络允许直连,`proxy-off` 会输出:
|
||||
|
||||
```text
|
||||
[INFO] HTTP request without xray succeeded directly
|
||||
```
|
||||
|
||||
如果当前网络不允许直连,`proxy-off` 会输出:
|
||||
|
||||
```text
|
||||
[PASS] HTTP request without xray is blocked
|
||||
```
|
||||
|
||||
测试脚本内部使用:
|
||||
|
||||
```sh
|
||||
|
||||
@@ -4,8 +4,6 @@ x-test-image: &test-image
|
||||
args:
|
||||
SOURCE_VERSION: ${SOURCE_VERSION:-local}
|
||||
image: ${IMAGE:-ubuntu-xray-transparent-test}
|
||||
volumes:
|
||||
- ./test-runner.sh:/tmp/test-runner.sh:ro
|
||||
|
||||
services:
|
||||
ipv6-default-off:
|
||||
@@ -16,10 +14,6 @@ services:
|
||||
XRAY_ENABLED: "0"
|
||||
XRAY_IPV6_ENABLED: "0"
|
||||
S6_LOGGING: "0"
|
||||
command:
|
||||
- bash
|
||||
- /tmp/test-runner.sh
|
||||
- ipv6-default-off
|
||||
|
||||
ipv6-enabled:
|
||||
<<: *test-image
|
||||
@@ -27,10 +21,6 @@ services:
|
||||
XRAY_ENABLED: "0"
|
||||
XRAY_IPV6_ENABLED: "1"
|
||||
S6_LOGGING: "0"
|
||||
command:
|
||||
- bash
|
||||
- /tmp/test-runner.sh
|
||||
- ipv6-enabled
|
||||
|
||||
proxy-on:
|
||||
<<: *test-image
|
||||
@@ -40,10 +30,6 @@ services:
|
||||
- .env
|
||||
environment:
|
||||
S6_LOGGING: "0"
|
||||
command:
|
||||
- bash
|
||||
- /tmp/test-runner.sh
|
||||
- proxy-on
|
||||
|
||||
proxy-off:
|
||||
<<: *test-image
|
||||
@@ -51,7 +37,3 @@ services:
|
||||
XRAY_ENABLED: "0"
|
||||
XRAY_IPV6_ENABLED: ${XRAY_IPV6_ENABLED:-0}
|
||||
S6_LOGGING: "0"
|
||||
command:
|
||||
- bash
|
||||
- /tmp/test-runner.sh
|
||||
- proxy-off
|
||||
|
||||
@@ -6,11 +6,6 @@ if [ -f "${XRAY_CONFIG}" ]; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if [ -f "${XRAY_GENERATED_CONFIG:-}" ]; then
|
||||
printf '%s\n' "${XRAY_GENERATED_CONFIG}"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if [ -n "${XRAY_URL:-}" ]; then
|
||||
config="$(/usr/local/bin/scripts/xray-url-to-config.sh "${XRAY_URL}")"
|
||||
if [ "${XRAY_DEBUG_CONFIG:-0}" = "1" ]; then
|
||||
@@ -21,6 +16,11 @@ if [ -n "${XRAY_URL:-}" ]; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if [ -f "${XRAY_GENERATED_CONFIG:-}" ]; then
|
||||
printf '%s\n' "${XRAY_GENERATED_CONFIG}"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "Xray config not found: ${XRAY_CONFIG}" >&2
|
||||
echo "Set XRAY_URL or mount a config file to ${XRAY_CONFIG}." >&2
|
||||
exit 1
|
||||
|
||||
@@ -27,13 +27,6 @@ fail() {
|
||||
exit 1
|
||||
}
|
||||
|
||||
json_escape() {
|
||||
local value="$1"
|
||||
value="${value//\\/\\\\}"
|
||||
value="${value//\"/\\\"}"
|
||||
printf '%s' "${value}"
|
||||
}
|
||||
|
||||
validate_port() {
|
||||
[[ "$1" =~ ^[0-9]+$ ]] || fail "Invalid port: $1"
|
||||
[ "$1" -ge 1 ] && [ "$1" -le 65535 ] || fail "Invalid port: $1"
|
||||
@@ -71,131 +64,152 @@ validate_path() {
|
||||
[[ "${value}" != *$'\r'* ]] || fail "Invalid ${name}: carriage return is not allowed"
|
||||
}
|
||||
|
||||
write_common_prefix() {
|
||||
validate_port "${XRAY_REDIRECT_PORT}"
|
||||
cat >"${generated_config}" <<EOF
|
||||
{
|
||||
"log": {
|
||||
"loglevel": "warning"
|
||||
},
|
||||
"inbounds": [
|
||||
{
|
||||
"tag": "transparent",
|
||||
"listen": "127.0.0.1",
|
||||
"port": ${XRAY_REDIRECT_PORT},
|
||||
"protocol": "dokodemo-door",
|
||||
"settings": {
|
||||
"network": "tcp",
|
||||
"followRedirect": true
|
||||
},
|
||||
"sniffing": {
|
||||
"enabled": true,
|
||||
"destOverride": ["http", "tls"]
|
||||
}
|
||||
}
|
||||
],
|
||||
"outbounds": [
|
||||
EOF
|
||||
}
|
||||
|
||||
write_common_suffix() {
|
||||
cat >>"${generated_config}" <<'EOF'
|
||||
,
|
||||
{
|
||||
"tag": "direct",
|
||||
"protocol": "freedom"
|
||||
},
|
||||
{
|
||||
"tag": "block",
|
||||
"protocol": "blackhole"
|
||||
}
|
||||
],
|
||||
"routing": {
|
||||
"domainStrategy": "AsIs",
|
||||
"rules": [
|
||||
{
|
||||
"type": "field",
|
||||
"ip": ["geoip:private"],
|
||||
"outboundTag": "direct"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
EOF
|
||||
}
|
||||
|
||||
write_reality_tcp_outbound() {
|
||||
cat >>"${generated_config}" <<EOF
|
||||
{
|
||||
"tag": "proxy",
|
||||
"protocol": "vless",
|
||||
"settings": {
|
||||
"vnext": [
|
||||
{
|
||||
"address": "$(json_escape "${address}")",
|
||||
"port": ${port},
|
||||
"users": [
|
||||
build_reality_tcp_outbound() {
|
||||
jq -n \
|
||||
--arg address "${address}" \
|
||||
--argjson port "${port}" \
|
||||
--arg uuid "${uuid}" \
|
||||
--arg encryption "${encryption}" \
|
||||
--arg flow "${flow}" \
|
||||
--arg sni "${sni}" \
|
||||
--arg fp "${fp}" \
|
||||
--arg pbk "${pbk}" \
|
||||
--arg sid "${sid}" \
|
||||
--arg spx "${spx}" \
|
||||
'{
|
||||
tag: "proxy",
|
||||
protocol: "vless",
|
||||
settings: {
|
||||
vnext: [
|
||||
{
|
||||
"id": "$(json_escape "${uuid}")",
|
||||
"encryption": "$(json_escape "${encryption}")",
|
||||
"flow": "$(json_escape "${flow}")"
|
||||
address: $address,
|
||||
port: $port,
|
||||
users: [
|
||||
{
|
||||
id: $uuid,
|
||||
encryption: $encryption,
|
||||
flow: $flow
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
streamSettings: {
|
||||
network: "tcp",
|
||||
security: "reality",
|
||||
realitySettings: {
|
||||
serverName: $sni,
|
||||
fingerprint: $fp,
|
||||
publicKey: $pbk,
|
||||
shortId: $sid,
|
||||
spiderX: $spx
|
||||
}
|
||||
}
|
||||
}'
|
||||
}
|
||||
|
||||
build_plain_ws_outbound() {
|
||||
jq -n \
|
||||
--arg address "${address}" \
|
||||
--argjson port "${port}" \
|
||||
--arg uuid "${uuid}" \
|
||||
--arg encryption "${encryption}" \
|
||||
--arg path "${path}" \
|
||||
--arg host "${host}" \
|
||||
'{
|
||||
tag: "proxy",
|
||||
protocol: "vless",
|
||||
settings: {
|
||||
vnext: [
|
||||
{
|
||||
address: $address,
|
||||
port: $port,
|
||||
users: [
|
||||
{
|
||||
id: $uuid,
|
||||
encryption: $encryption
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
streamSettings: {
|
||||
network: "ws",
|
||||
security: "none",
|
||||
wsSettings: {
|
||||
path: $path,
|
||||
host: $host
|
||||
}
|
||||
}
|
||||
}'
|
||||
}
|
||||
|
||||
write_config() {
|
||||
local proxy_json="$1"
|
||||
|
||||
jq -n \
|
||||
--argjson redirect_port "${XRAY_REDIRECT_PORT}" \
|
||||
--argjson proxy "${proxy_json}" \
|
||||
'{
|
||||
log: {
|
||||
loglevel: "warning"
|
||||
},
|
||||
inbounds: [
|
||||
{
|
||||
tag: "transparent",
|
||||
listen: "127.0.0.1",
|
||||
port: $redirect_port,
|
||||
protocol: "dokodemo-door",
|
||||
settings: {
|
||||
network: "tcp",
|
||||
followRedirect: true
|
||||
},
|
||||
sniffing: {
|
||||
enabled: true,
|
||||
destOverride: ["http", "tls"]
|
||||
}
|
||||
}
|
||||
],
|
||||
outbounds: [
|
||||
$proxy,
|
||||
{
|
||||
tag: "direct",
|
||||
protocol: "freedom"
|
||||
},
|
||||
{
|
||||
tag: "block",
|
||||
protocol: "blackhole"
|
||||
}
|
||||
],
|
||||
routing: {
|
||||
domainStrategy: "AsIs",
|
||||
rules: [
|
||||
{
|
||||
type: "field",
|
||||
ip: ["geoip:private"],
|
||||
outboundTag: "direct"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"streamSettings": {
|
||||
"network": "tcp",
|
||||
"security": "reality",
|
||||
"realitySettings": {
|
||||
"serverName": "$(json_escape "${sni}")",
|
||||
"fingerprint": "$(json_escape "${fp}")",
|
||||
"publicKey": "$(json_escape "${pbk}")",
|
||||
"shortId": "$(json_escape "${sid}")",
|
||||
"spiderX": "$(json_escape "${spx}")"
|
||||
}
|
||||
}
|
||||
}
|
||||
EOF
|
||||
}
|
||||
|
||||
write_plain_ws_outbound() {
|
||||
cat >>"${generated_config}" <<EOF
|
||||
{
|
||||
"tag": "proxy",
|
||||
"protocol": "vless",
|
||||
"settings": {
|
||||
"vnext": [
|
||||
{
|
||||
"address": "$(json_escape "${address}")",
|
||||
"port": ${port},
|
||||
"users": [
|
||||
{
|
||||
"id": "$(json_escape "${uuid}")",
|
||||
"encryption": "$(json_escape "${encryption}")"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"streamSettings": {
|
||||
"network": "ws",
|
||||
"security": "none",
|
||||
"wsSettings": {
|
||||
"path": "$(json_escape "${path}")",
|
||||
"host": "$(json_escape "${host}")"
|
||||
}
|
||||
}
|
||||
}
|
||||
EOF
|
||||
}' >"${tmp_config}"
|
||||
}
|
||||
|
||||
url="${1:?XRAY_URL is required}"
|
||||
generated_config="${XRAY_GENERATED_CONFIG:-/tmp/xray.generated.json}"
|
||||
generated_dir="$(dirname "${generated_config}")"
|
||||
tmp_config=""
|
||||
|
||||
mkdir -p "${generated_dir}"
|
||||
tmp_config="$(mktemp "${generated_config}.XXXXXX")"
|
||||
cleanup_tmp() {
|
||||
if [ -n "${tmp_config:-}" ] && [ -f "${tmp_config}" ]; then
|
||||
rm -f "${tmp_config}"
|
||||
fi
|
||||
}
|
||||
trap cleanup_tmp EXIT
|
||||
|
||||
if [[ "${url}" != vless://* ]]; then
|
||||
echo "Unsupported XRAY_URL: only vless:// is supported." >&2
|
||||
exit 1
|
||||
fail "Unsupported XRAY_URL: only vless:// is supported."
|
||||
fi
|
||||
|
||||
body="${url#vless://}"
|
||||
@@ -225,15 +239,13 @@ host="$(query_param "${query}" host || true)"
|
||||
path="$(query_param "${query}" path || printf '/')"
|
||||
|
||||
if [ -z "${uuid}" ] || [ -z "${address}" ] || [ -z "${port}" ]; then
|
||||
echo "Invalid XRAY_URL: missing uuid, address, or port." >&2
|
||||
exit 1
|
||||
fail "Invalid XRAY_URL: missing uuid, address, or port."
|
||||
fi
|
||||
|
||||
validate_uuid "${uuid}"
|
||||
validate_plain_token "address" "${address}"
|
||||
validate_port "${port}"
|
||||
|
||||
write_common_prefix
|
||||
validate_port "${XRAY_REDIRECT_PORT}"
|
||||
|
||||
if [ "${network}" = "tcp" ] && \
|
||||
[ "${security}" = "reality" ] && \
|
||||
@@ -249,7 +261,7 @@ if [ "${network}" = "tcp" ] && \
|
||||
validate_base64url_token "publicKey" "${pbk}"
|
||||
validate_hex_token "shortId" "${sid}"
|
||||
validate_path "spiderX" "${spx}"
|
||||
write_reality_tcp_outbound
|
||||
proxy_json="$(build_reality_tcp_outbound)"
|
||||
elif [ "${network}" = "ws" ] && \
|
||||
[ "${security}" = "none" ] && \
|
||||
[ "${encryption}" = "none" ] && \
|
||||
@@ -257,9 +269,8 @@ elif [ "${network}" = "ws" ] && \
|
||||
[ -n "${path}" ]; then
|
||||
validate_plain_token "host" "${host}"
|
||||
validate_path "path" "${path}"
|
||||
write_plain_ws_outbound
|
||||
proxy_json="$(build_plain_ws_outbound)"
|
||||
else
|
||||
rm -f "${generated_config}"
|
||||
echo "Unsupported XRAY_URL shape." >&2
|
||||
echo "Supported shapes:" >&2
|
||||
echo " 1. vless + tcp + reality + flow=xtls-rprx-vision + headerType=none" >&2
|
||||
@@ -267,5 +278,10 @@ else
|
||||
exit 1
|
||||
fi
|
||||
|
||||
write_common_suffix
|
||||
write_config "${proxy_json}"
|
||||
chown xray:xray "${tmp_config}"
|
||||
chmod 0640 "${tmp_config}"
|
||||
mv -f "${tmp_config}" "${generated_config}"
|
||||
tmp_config=""
|
||||
trap - EXIT
|
||||
printf '%s\n' "${generated_config}"
|
||||
|
||||
@@ -1,79 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
case_name="${1:?test case is required}"
|
||||
|
||||
pass() {
|
||||
printf '[PASS] %s\n' "$1"
|
||||
}
|
||||
|
||||
no() {
|
||||
printf '[NO] %s\n' "$1"
|
||||
shift || true
|
||||
if [ "$#" -gt 0 ]; then
|
||||
printf 'reason: %s\n' "$*"
|
||||
fi
|
||||
exit 1
|
||||
}
|
||||
|
||||
curl_check() {
|
||||
local label="$1"
|
||||
local url="$2"
|
||||
shift 2
|
||||
|
||||
if curl --noproxy '*' "$@" -fsSIL --connect-timeout 10 --max-time 20 "${url}" >/tmp/curl.out 2>/tmp/curl.err; then
|
||||
pass "${label}"
|
||||
return 0
|
||||
fi
|
||||
|
||||
printf '[NO] %s\n' "${label}"
|
||||
printf 'reason: '
|
||||
sed -n '1p' /tmp/curl.err || true
|
||||
return 1
|
||||
}
|
||||
|
||||
case "${case_name}" in
|
||||
ipv6-default-off)
|
||||
# PASS means an IPv6-only curl cannot complete when IPv6 is disabled by default.
|
||||
if curl --noproxy '*' -6 -fsSIL --connect-timeout 5 --max-time 10 http://google.com >/tmp/curl.out 2>/tmp/curl.err; then
|
||||
no "IPv6 request is blocked by default" "IPv6 request unexpectedly succeeded"
|
||||
fi
|
||||
pass "IPv6 request is blocked by default"
|
||||
;;
|
||||
ipv6-enabled)
|
||||
# PASS means the container policy did not disable IPv6 via sysctl.
|
||||
if [ -r /proc/sys/net/ipv6/conf/all/disable_ipv6 ]; then
|
||||
value="$(cat /proc/sys/net/ipv6/conf/all/disable_ipv6)"
|
||||
[ "${value}" = "0" ] || no "IPv6 is allowed by container policy" "disable_ipv6=${value}"
|
||||
fi
|
||||
pass "IPv6 is allowed by container policy"
|
||||
;;
|
||||
proxy-on)
|
||||
sleep 3
|
||||
# PASS means the transparent proxy NAT chain exists and redirects TCP to Xray.
|
||||
if ! iptables -t nat -S XRAY_OUTPUT >/tmp/iptables.out 2>/tmp/iptables.err; then
|
||||
no "transparent proxy iptables chain exists" "$(sed -n '1p' /tmp/iptables.err)"
|
||||
fi
|
||||
if ! grep -q -- "--to-ports ${XRAY_REDIRECT_PORT:-12345}" /tmp/iptables.out; then
|
||||
no "transparent proxy redirect rule exists" "XRAY_OUTPUT has no REDIRECT to ${XRAY_REDIRECT_PORT:-12345}"
|
||||
fi
|
||||
pass "transparent proxy redirect rule exists"
|
||||
# PASS means curl reaches Google while transparent proxy rules are active.
|
||||
curl_check "HTTP request through transparent proxy" "http://google.com" || exit 1
|
||||
;;
|
||||
proxy-off)
|
||||
# PASS means no Xray process exists when XRAY_ENABLED=0.
|
||||
if pgrep -x xray >/dev/null 2>&1; then
|
||||
no "xray is not running" "xray process exists"
|
||||
fi
|
||||
pass "xray is not running"
|
||||
# PASS means direct curl cannot reach Google when Xray is disabled.
|
||||
if curl --noproxy '*' -fsSIL --connect-timeout 3 --max-time 3 http://google.com >/tmp/curl.out 2>/tmp/curl.err; then
|
||||
no "HTTP request without xray is blocked" "direct request unexpectedly succeeded"
|
||||
fi
|
||||
pass "HTTP request without xray is blocked"
|
||||
;;
|
||||
*)
|
||||
no "known test case" "unknown test case: ${case_name}"
|
||||
;;
|
||||
esac
|
||||
+227
-39
@@ -1,52 +1,240 @@
|
||||
@echo off
|
||||
setlocal
|
||||
set "TEST_BAT_PATH=%~f0"
|
||||
powershell -NoProfile -ExecutionPolicy Bypass -Command "$path=$env:TEST_BAT_PATH; $raw=Get-Content -Raw -LiteralPath $path; $marker='# POWERSHELL'; $start=$raw.LastIndexOf($marker); if($start -lt 0){ throw 'PowerShell marker not found' }; $ps=$raw.Substring($start + $marker.Length); Set-Location -LiteralPath (Split-Path -Parent $path); Invoke-Expression $ps"
|
||||
exit /b %ERRORLEVEL%
|
||||
|
||||
REM FORCE_BUILD=1: rebuild image with --no-cache before running tests.
|
||||
REM FORCE_BUILD=0: reuse existing image; build only when the image is missing.
|
||||
set "FORCE_BUILD=0"
|
||||
# POWERSHELL
|
||||
$ErrorActionPreference = 'Stop'
|
||||
if (Get-Variable -Name PSNativeCommandUseErrorActionPreference -ErrorAction SilentlyContinue) {
|
||||
$PSNativeCommandUseErrorActionPreference = $false
|
||||
}
|
||||
|
||||
REM VERBOSE=1: print full stdout/stderr logs for every test.
|
||||
REM VERBOSE=0: print only [PASS], [NO], and reason lines; full logs stay in logs\.
|
||||
set "VERBOSE=0"
|
||||
$forceBuild = if ($env:FORCE_BUILD) { $env:FORCE_BUILD } else { '0' }
|
||||
$verbose = if ($env:VERBOSE) { $env:VERBOSE } else { '0' }
|
||||
$composeBase = @('compose', '--env-file', '.env', '-f', 'compose.yml')
|
||||
|
||||
cd /d "%~dp0"
|
||||
function Write-TaggedLine {
|
||||
param([string] $Line)
|
||||
|
||||
if not exist logs mkdir logs
|
||||
if ($Line.StartsWith('[PASS]')) {
|
||||
Write-Host $Line -ForegroundColor Green
|
||||
} elseif ($Line.StartsWith('[NO]') -or $Line.StartsWith('reason:')) {
|
||||
Write-Host $Line -ForegroundColor Red
|
||||
} elseif ($Line.StartsWith('[RUN]')) {
|
||||
Write-Host $Line -ForegroundColor Cyan
|
||||
} elseif ($Line.StartsWith('[INFO]')) {
|
||||
Write-Host $Line -ForegroundColor DarkCyan
|
||||
}
|
||||
}
|
||||
|
||||
for /f %%i in ('powershell -NoProfile -ExecutionPolicy Bypass -Command "$files = Get-ChildItem -Recurse -File scripts,rootfs,Dockerfile,compose.yml,.env; ($files | Sort-Object LastWriteTimeUtc -Descending | Select-Object -First 1).LastWriteTimeUtc.Ticks"') do set "SOURCE_VERSION=%%i"
|
||||
function Invoke-Docker {
|
||||
param([string[]] $Arguments)
|
||||
|
||||
if "%FORCE_BUILD%"=="1" (
|
||||
docker compose --env-file .env -f compose.yml build --quiet --no-cache >logs\build.out 2>logs\build.err
|
||||
if errorlevel 1 exit /b %errorlevel%
|
||||
) else (
|
||||
docker compose --env-file .env -f compose.yml build --quiet >logs\build.out 2>logs\build.err
|
||||
if errorlevel 1 (
|
||||
docker image inspect ubuntu-xray-transparent-test >nul 2>nul
|
||||
if errorlevel 1 exit /b 1
|
||||
)
|
||||
$previousErrorActionPreference = $ErrorActionPreference
|
||||
$ErrorActionPreference = 'Continue'
|
||||
try {
|
||||
$output = & docker @Arguments 2>&1
|
||||
$code = $LASTEXITCODE
|
||||
} finally {
|
||||
$ErrorActionPreference = $previousErrorActionPreference
|
||||
}
|
||||
|
||||
$lines = @(
|
||||
$output | ForEach-Object {
|
||||
if ($_ -is [System.Management.Automation.ErrorRecord]) {
|
||||
$_.Exception.Message
|
||||
} else {
|
||||
$_.ToString()
|
||||
}
|
||||
} | Where-Object {
|
||||
$_ -and $_ -ne 'System.Management.Automation.RemoteException'
|
||||
}
|
||||
)
|
||||
|
||||
[pscustomobject]@{
|
||||
Code = $code
|
||||
Lines = $lines
|
||||
}
|
||||
}
|
||||
|
||||
function Print-FilteredOutput {
|
||||
param([string[]] $Lines)
|
||||
|
||||
foreach ($line in $Lines) {
|
||||
if ($verbose -eq '1') {
|
||||
Write-Host $line
|
||||
} else {
|
||||
Write-TaggedLine $line
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function Print-FailureDiagnostics {
|
||||
param([string[]] $Lines)
|
||||
|
||||
$hasNoLine = $Lines | Where-Object { $_.StartsWith('[NO]') } | Select-Object -First 1
|
||||
if (-not $hasNoLine) {
|
||||
Write-Host '--- raw docker output ---' -ForegroundColor Yellow
|
||||
$Lines | Select-Object -Last 80 | ForEach-Object { Write-Host $_ }
|
||||
}
|
||||
}
|
||||
|
||||
function Build-Image {
|
||||
Write-Host ''
|
||||
Write-TaggedLine '[RUN] Build image'
|
||||
|
||||
$sourceFiles = Get-ChildItem -Recurse -File scripts, rootfs, Dockerfile, compose.yml, .env, .env.example
|
||||
$env:SOURCE_VERSION = ($sourceFiles | Sort-Object LastWriteTimeUtc -Descending | Select-Object -First 1).LastWriteTimeUtc.Ticks
|
||||
|
||||
$dockerArgs = $composeBase + @('build', '--quiet')
|
||||
if ($forceBuild -eq '1') {
|
||||
$dockerArgs += '--no-cache'
|
||||
}
|
||||
|
||||
$result = Invoke-Docker $dockerArgs
|
||||
if ($result.Code -ne 0) {
|
||||
Write-TaggedLine '[NO] Build image'
|
||||
$result.Lines | ForEach-Object { Write-Host $_ }
|
||||
exit $result.Code
|
||||
}
|
||||
|
||||
Write-TaggedLine '[PASS] Build image'
|
||||
if ($verbose -eq '1') {
|
||||
$result.Lines | ForEach-Object { Write-Host $_ }
|
||||
}
|
||||
}
|
||||
|
||||
function Run-Case {
|
||||
param(
|
||||
[string] $Title,
|
||||
[string] $Service,
|
||||
[string] $Script
|
||||
)
|
||||
|
||||
Write-Host ''
|
||||
Write-TaggedLine "[RUN] $Title"
|
||||
|
||||
$casePath = Join-Path (Get-Location) ('.test-case-{0}.sh' -f ([guid]::NewGuid().ToString('N')))
|
||||
Set-Content -LiteralPath $casePath -Value $Script -NoNewline -Encoding Ascii
|
||||
|
||||
try {
|
||||
$volumeArg = "${casePath}:/tmp/test-case.sh:ro"
|
||||
$dockerArgs = $composeBase + @('run', '--rm', '--no-deps', '-T', '--volume', $volumeArg, $Service, 'bash', '/tmp/test-case.sh')
|
||||
$result = Invoke-Docker $dockerArgs
|
||||
} finally {
|
||||
Remove-Item -LiteralPath $casePath -Force -ErrorAction SilentlyContinue
|
||||
}
|
||||
Print-FilteredOutput $result.Lines
|
||||
|
||||
if ($result.Code -ne 0) {
|
||||
Print-FailureDiagnostics $result.Lines
|
||||
}
|
||||
|
||||
return $result.Code
|
||||
}
|
||||
|
||||
if (-not (Test-Path -LiteralPath '.env')) {
|
||||
Write-TaggedLine '[NO] .env exists'
|
||||
Write-TaggedLine 'reason: create Ubuntu\.env from Ubuntu\.env.example and set XRAY_URL.'
|
||||
exit 1
|
||||
}
|
||||
|
||||
$tests = @(
|
||||
@{
|
||||
Title = 'IPv6 default off'
|
||||
Service = 'ipv6-default-off'
|
||||
Script = @'
|
||||
set -euo pipefail
|
||||
if curl --noproxy '*' -6 -fsSIL --connect-timeout 5 --max-time 10 http://google.com >/tmp/curl.out 2>/tmp/curl.err; then
|
||||
echo '[NO] IPv6 request is blocked by default'
|
||||
echo 'reason: IPv6 request unexpectedly succeeded'
|
||||
exit 1
|
||||
fi
|
||||
echo '[PASS] IPv6 request is blocked by default'
|
||||
'@
|
||||
}
|
||||
@{
|
||||
Title = 'IPv6 enabled'
|
||||
Service = 'ipv6-enabled'
|
||||
Script = @'
|
||||
set -euo pipefail
|
||||
if [ -r /proc/sys/net/ipv6/conf/all/disable_ipv6 ]; then
|
||||
value="$(cat /proc/sys/net/ipv6/conf/all/disable_ipv6)"
|
||||
if [ "${value}" != "0" ]; then
|
||||
echo '[NO] IPv6 is allowed by container policy'
|
||||
echo "reason: disable_ipv6=${value}"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
echo '[PASS] IPv6 is allowed by container policy'
|
||||
'@
|
||||
}
|
||||
@{
|
||||
Title = 'Proxy on'
|
||||
Service = 'proxy-on'
|
||||
Script = @'
|
||||
set -euo pipefail
|
||||
sleep 3
|
||||
|
||||
if ! iptables -t nat -S XRAY_OUTPUT >/tmp/iptables.out 2>/tmp/iptables.err; then
|
||||
echo '[NO] transparent proxy iptables chain exists'
|
||||
echo "reason: $(sed -n '1p' /tmp/iptables.err)"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! grep -q -- "--to-ports ${XRAY_REDIRECT_PORT:-12345}" /tmp/iptables.out; then
|
||||
echo '[NO] transparent proxy redirect rule exists'
|
||||
echo "reason: XRAY_OUTPUT has no REDIRECT to ${XRAY_REDIRECT_PORT:-12345}"
|
||||
exit 1
|
||||
fi
|
||||
echo '[PASS] transparent proxy redirect rule exists'
|
||||
|
||||
if curl --noproxy '*' -fsSIL --connect-timeout 10 --max-time 20 http://google.com >/tmp/curl.out 2>/tmp/curl.err; then
|
||||
echo '[PASS] HTTP request through transparent proxy'
|
||||
else
|
||||
echo '[NO] HTTP request through transparent proxy'
|
||||
echo "reason: $(sed -n '1p' /tmp/curl.err)"
|
||||
exit 1
|
||||
fi
|
||||
'@
|
||||
}
|
||||
@{
|
||||
Title = 'Proxy off'
|
||||
Service = 'proxy-off'
|
||||
Script = @'
|
||||
set -euo pipefail
|
||||
if pgrep -x xray >/dev/null 2>&1; then
|
||||
echo '[NO] xray is not running'
|
||||
echo 'reason: xray process exists'
|
||||
exit 1
|
||||
fi
|
||||
echo '[PASS] xray is not running'
|
||||
|
||||
if curl --noproxy '*' -fsSIL --connect-timeout 3 --max-time 3 http://google.com >/tmp/curl.out 2>/tmp/curl.err; then
|
||||
echo '[INFO] HTTP request without xray succeeded directly'
|
||||
exit 0
|
||||
fi
|
||||
echo '[PASS] HTTP request without xray is blocked'
|
||||
'@
|
||||
}
|
||||
)
|
||||
|
||||
docker compose --env-file .env -f compose.yml run --rm --no-deps ipv6-default-off >logs\ipv6-default-off.out 2>logs\ipv6-default-off.err
|
||||
set "IPV6_OFF_CODE=%errorlevel%"
|
||||
call :print_result logs\ipv6-default-off.out logs\ipv6-default-off.err
|
||||
Build-Image
|
||||
|
||||
docker compose --env-file .env -f compose.yml run --rm --no-deps ipv6-enabled >logs\ipv6-enabled.out 2>logs\ipv6-enabled.err
|
||||
set "IPV6_ON_CODE=%errorlevel%"
|
||||
call :print_result logs\ipv6-enabled.out logs\ipv6-enabled.err
|
||||
$failed = $false
|
||||
foreach ($test in $tests) {
|
||||
$code = Run-Case -Title $test.Title -Service $test.Service -Script $test.Script
|
||||
if ($code -ne 0) {
|
||||
$failed = $true
|
||||
}
|
||||
}
|
||||
|
||||
docker compose --env-file .env -f compose.yml run --rm --no-deps proxy-on >logs\proxy-on.out 2>logs\proxy-on.err
|
||||
set "PROXY_ON_CODE=%errorlevel%"
|
||||
call :print_result logs\proxy-on.out logs\proxy-on.err
|
||||
Write-Host ''
|
||||
if ($failed) {
|
||||
Write-TaggedLine '[NO] one or more tests failed'
|
||||
exit 1
|
||||
}
|
||||
|
||||
docker compose --env-file .env -f compose.yml run --rm --no-deps proxy-off >logs\proxy-off.out 2>logs\proxy-off.err
|
||||
set "PROXY_OFF_CODE=%errorlevel%"
|
||||
call :print_result logs\proxy-off.out logs\proxy-off.err
|
||||
|
||||
if not "%IPV6_OFF_CODE%"=="0" exit /b %IPV6_OFF_CODE%
|
||||
if not "%IPV6_ON_CODE%"=="0" exit /b %IPV6_ON_CODE%
|
||||
if not "%PROXY_ON_CODE%"=="0" exit /b %PROXY_ON_CODE%
|
||||
exit /b %PROXY_OFF_CODE%
|
||||
|
||||
:print_result
|
||||
powershell -NoProfile -ExecutionPolicy Bypass -Command "$verbose='%VERBOSE%'; $paths=@('%~1','%~2'); foreach($path in $paths){ if(!(Test-Path -LiteralPath $path)){ continue }; foreach($line in Get-Content -LiteralPath $path){ if($verbose -eq '1'){ Write-Host $line; continue }; if($line.StartsWith('[PASS]')){ Write-Host $line -ForegroundColor Green } elseif($line.StartsWith('[NO]') -or $line.StartsWith('reason:')){ Write-Host $line -ForegroundColor Red } } }"
|
||||
exit /b 0
|
||||
Write-TaggedLine '[PASS] all tests passed'
|
||||
exit 0
|
||||
|
||||
Reference in New Issue
Block a user