Skip to content

Commit 1d09d05

Browse files
committed
feat(poJun-proxy): 添加 PoJun Worker 支持,生成代理节点 bundle 和相关配置
1 parent 48a7093 commit 1d09d05

8 files changed

Lines changed: 675 additions & 6 deletions

File tree

aliyun/proxy/README.md

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,9 @@
22

33
## 场景说明
44

5-
该场景会在阿里云批量部署多台基于 shadowsocks-libev 的代理节点,默认创建 10 台抢占式 ECS,并通过 redc 插件链生成 Clash 配置。适合快速拿到一组可批量接入的代理出口。
5+
该场景会在阿里云批量部署多台基于 shadowsocks-libev 的代理节点,默认创建 10 台抢占式 ECS,并通过 redc 插件链生成 Clash 配置和供 PoJun Worker 消费的代理节点 bundle。适合快速拿到一组可批量接入的代理出口。
66

7-
部署完成后,你会得到节点公网 IP 列表、批量 SSH 连接命令,以及本地生成的 Clash 配置文件;如果已经配置 R2 上传能力,还可以把配置文件自动上传到 Cloudflare R2 后再分发使用。
7+
部署完成后,你会得到节点公网 IP 列表、批量 SSH 连接命令、本地生成的 Clash 配置文件,以及场景目录下自包含的 `pojun-proxy/bundle.json`;如果已经配置 R2 上传能力,还可以把 Clash 配置文件自动上传到 Cloudflare R2 后再分发使用。
88

99
## 前置条件
1010

@@ -21,6 +21,7 @@ redc pull aliyun/proxy
2121
redc run aliyun/proxy
2222
redc run aliyun/proxy -e node=20
2323
redc status [uuid]
24+
redc status [uuid] -o json | jq '.plugin_outputs'
2425
redc stop [uuid]
2526
```
2627

@@ -54,6 +55,8 @@ redc run aliyun/proxy \
5455
- `ssh_user`:默认 SSH 用户名,当前为 `root`
5556
- `ecs_password` / `ssh_password`:所有节点共用的 SSH 登录密码。
5657
- Clash 配置文件:由 `redc-plugin-clash-config` 根据 `port``password``filename` 生成;如果配置了 R2 上传,还会由上传插件进一步分发。
58+
- `pojun-proxy/bundle.json`:由 `redc-plugin-pojun-proxy` 生成的单个自包含文件,包含稳定代理池 ID、节点数、来源 Case、revision 和 Shadowsocks 节点数组,供后续 PoJun Worker 代理池适配使用。
59+
- `plugin_outputs``redc status [uuid] -o json` 中的独立对象,包含 `pojun_proxy_bundle_file``pojun_proxy_pool_id``pojun_proxy_node_count``pojun_proxy_revision`
5760

5861
## 常见问题
5962

@@ -70,6 +73,7 @@ redc run aliyun/proxy \
7073
- 模板在不同区域使用硬编码的实例规格与可用区:北京默认 `ecs.n1.tiny` / `cn-beijing-f`,东京默认 `ecs.t5-lc1m1.small` / `ap-northeast-1b`
7174
- `main.tf` 中已启用 `spot_strategy = "SpotWithPriceLimit"`,因此节点稳定性和最终节点数量可能受市场价格与库存影响。
7275
- 默认安全组会放开全部 TCP 和 UDP 入站流量,部署后请按你的场景自行收紧。
76+
- macOS/Linux 上 `pojun-proxy` 目录权限为 `0700``bundle.json` 权限为 `0600`;Windows 使用当前用户 Case 目录继承的 ACL。该文件包含代理密码,不要上传到公开存储、提交到 Git 或粘贴到日志中。
7377

7478
## 附录
7579

aliyun/proxy/README_EN.md

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,9 @@
22

33
## Scene Description
44

5-
This scenario deploys multiple Shadowsocks-based proxy nodes on Aliyun, creates 10 spot ECS instances by default, and uses the redc plugin chain to generate a Clash configuration. It is suitable for quickly obtaining a batch of proxy exits that can be consumed together.
5+
This scenario deploys multiple Shadowsocks-based proxy nodes on Aliyun, creates 10 spot ECS instances by default, and uses the redc plugin chain to generate a Clash configuration and a proxy-node bundle for PoJun Workers. It is suitable for quickly obtaining a batch of proxy exits that can be consumed together.
66

7-
After deployment, you get the node public IP list, batch SSH commands, and a locally generated Clash configuration file. If R2 upload is configured, the config can also be uploaded automatically to Cloudflare R2 for distribution.
7+
After deployment, you get the node public IP list, batch SSH commands, a locally generated Clash configuration file, and a self-contained `pojun-proxy/bundle.json` under the case directory. If R2 upload is configured, the Clash config can also be uploaded automatically to Cloudflare R2 for distribution.
88

99
## Prerequisites
1010

@@ -21,6 +21,7 @@ redc pull aliyun/proxy
2121
redc run aliyun/proxy
2222
redc run aliyun/proxy -e node=20
2323
redc status [uuid]
24+
redc status [uuid] -o json | jq '.plugin_outputs'
2425
redc stop [uuid]
2526
```
2627

@@ -54,6 +55,8 @@ redc run aliyun/proxy \
5455
- `ssh_user`: Default SSH username, currently `root`.
5556
- `ecs_password` / `ssh_password`: Shared SSH login password for all nodes.
5657
- Clash configuration file: Generated by `redc-plugin-clash-config` from `port`, `password`, and `filename`; if R2 upload is configured, the upload plugin can distribute it further.
58+
- `pojun-proxy/bundle.json`: A single self-contained file generated by `redc-plugin-pojun-proxy`; it records the stable pool ID, node count, source case, revision, and Shadowsocks nodes for the later PoJun Worker pool integration.
59+
- `plugin_outputs`: A separate object in `redc status [uuid] -o json`, containing `pojun_proxy_bundle_file`, `pojun_proxy_pool_id`, `pojun_proxy_node_count`, and `pojun_proxy_revision`.
5760

5861
## FAQ
5962

@@ -70,6 +73,7 @@ redc run aliyun/proxy \
7073
- The template uses hard-coded instance mappings per region: Beijing uses `ecs.n1.tiny` in `cn-beijing-f`, and Tokyo uses `ecs.t5-lc1m1.small` in `ap-northeast-1b`.
7174
- `main.tf` enables `spot_strategy = "SpotWithPriceLimit"`, so stability and final node count can change with market price and capacity.
7275
- The default security group opens all inbound TCP and UDP traffic. Tighten the exposure after deployment if the nodes are not meant to remain fully exposed.
76+
- On macOS/Linux, the `pojun-proxy` directory is mode `0700` and `bundle.json` is mode `0600`; on Windows it inherits the current user's case-directory ACL. The bundle contains proxy passwords; do not upload it to public storage, commit it to Git, or paste it into logs.
7377

7478
## Appendix
7579

aliyun/proxy/case.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,10 @@
22
"name": "proxy",
33
"arch": "x86_64",
44
"user": "r0fus0d",
5-
"version": "1.2.9",
5+
"version": "1.4.0",
66
"description": "阿里云多节点代理场景,默认支持 10 台 spot ecs.n1.tiny 节点,shadowsocks-libev 搭建并可上传 clash/QuantumultX 配置到 Cloudflare R2",
77
"description_en": "Aliyun multi-node proxy scenario with 10 spot ecs.n1.tiny nodes by default, shadowsocks-libev setup and supports uploading clash/QuantumultX configuration to Cloudflare R2",
8-
"redc_plugins": "redc-plugin-clash-config,redc-plugin-quantumultx-config,redc-plugin-upload-r2",
8+
"redc_plugins": "redc-plugin-clash-config,redc-plugin-quantumultx-config,redc-plugin-pojun-proxy,redc-plugin-upload-r2",
99
"template": "preset",
1010
"tags": ["proxy"]
1111
}
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
# redc-plugin-pojun-proxy
2+
3+
在代理场景部署成功后,为 PoJun Worker 生成私有、可校验的 Shadowsocks 代理节点 bundle。
4+
5+
## 生成内容
6+
7+
插件在场景目录下固定生成:
8+
9+
```text
10+
pojun-proxy/
11+
└── bundle.json
12+
```
13+
14+
- `bundle.json`:单个自包含文件,包含 schema 版本、稳定代理池 ID、来源 Case、规范化节点 revision 和 Mihomo/Clash 兼容的 Shadowsocks 节点数组;不包含本地监听、DNS、规则、控制端口或代理组。
15+
16+
在 macOS/Linux 上目录权限为 `0700``bundle.json` 权限为 `0600`;Windows 使用当前用户 Case 目录继承的 ACL。该文件含代理密码,不应上传到公开存储、提交到 Git 或粘贴到日志中。
17+
18+
## 依赖
19+
20+
- redc `3.3.8` 或更高版本
21+
- 无额外命令行依赖;插件使用 redc 的 Go Template 引擎,可在 Windows、macOS 和 Linux 运行
22+
23+
## 安装
24+
25+
从插件市场安装,或在 redc-template 源码目录中安装本地版本:
26+
27+
```bash
28+
redc plugin install ./plugins/redc-plugin-pojun-proxy
29+
```
30+
31+
`aliyun/proxy` 模板已在 `case.json` 中声明该插件。场景在 Terraform apply 成功后会通过跨平台 `.tmpl` hook 自动生成 bundle。
32+
33+
## 配置
34+
35+
| 参数 | 类型 | 必填 | 说明 |
36+
|------|------|------|------|
37+
| `pool_id` | string || 稳定代理池 ID;留空时使用 `redc-<case-id>-aliyun-proxy`。仅允许字母、数字、`.``_``:``-`,最长 128 字符。 |
38+
39+
节点地址来自 Terraform output `ecs_ip`,兼容 `public_ip`;端口和密码按以下优先级读取:
40+
41+
1. 当前 Case 运行参数 `REDC_CASE_VARS`
42+
2. 场景目录的 `terraform.tfvars`
43+
44+
加密算法固定为 `chacha20-ietf-poly1305`,与 `aliyun/proxy` 服务端配置保持一致。
45+
46+
## 输出
47+
48+
插件通过 redc 的结构化输出协议提供以下字段:
49+
50+
| 字段 | 说明 |
51+
|------|------|
52+
| `pojun_proxy_bundle_file` | `bundle.json` 的绝对路径。 |
53+
| `pojun_proxy_pool_id` | 稳定代理池 ID。 |
54+
| `pojun_proxy_node_count` | bundle 中的节点数量。 |
55+
| `pojun_proxy_revision` | 规范化 `nodes` 数组的 SHA-256,也是后续同步的修订标识。 |
56+
57+
```bash
58+
redc status <case-id> -o json | jq '.plugin_outputs'
59+
```
60+
61+
插件会拒绝空节点、重复节点、非法 IPv4、空密码、非法端口和非法 pool ID。校验失败时不会替换上一次成功生成的 bundle。
Lines changed: 170 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,170 @@
1+
#!/usr/bin/env bash
2+
set -euo pipefail
3+
4+
fail() {
5+
echo "[pojun-proxy] ERROR: $*" >&2
6+
exit 1
7+
}
8+
9+
read_tfvar() {
10+
local key="$1"
11+
local tfvars="$REDC_CASE_PATH/terraform.tfvars"
12+
if [[ -f "$tfvars" ]]; then
13+
sed -n -E "s/^[[:space:]]*${key}[[:space:]]*=[[:space:]]*\"([^\"]*)\"[[:space:]]*$/\\1/p" "$tfvars" | head -n 1
14+
fi
15+
}
16+
17+
has_case_var() {
18+
local key="$1"
19+
[[ -n "$REDC_CASE_VARS" ]] && jq -e --arg key "$key" 'has($key)' <<<"$REDC_CASE_VARS" >/dev/null 2>&1
20+
}
21+
22+
read_case_var() {
23+
local key="$1"
24+
jq -er --arg key "$key" '.[$key] | if type == "string" or type == "number" then tostring else empty end' <<<"$REDC_CASE_VARS"
25+
}
26+
27+
sha256_file() {
28+
if command -v sha256sum >/dev/null 2>&1; then
29+
sha256sum "$1" | awk '{print $1}'
30+
elif command -v shasum >/dev/null 2>&1; then
31+
shasum -a 256 "$1" | awk '{print $1}'
32+
else
33+
fail "sha256sum or shasum is required"
34+
fi
35+
}
36+
37+
sync_path() {
38+
local path="$1"
39+
if sync -f "$path" 2>/dev/null; then
40+
return
41+
fi
42+
sync
43+
}
44+
45+
is_valid_ipv4() {
46+
local address="$1"
47+
local octets octet
48+
[[ "$address" =~ ^([0-9]{1,3}\.){3}[0-9]{1,3}$ ]] || return 1
49+
IFS='.' read -r -a octets <<<"$address"
50+
[[ ${#octets[@]} -eq 4 ]] || return 1
51+
for octet in "${octets[@]}"; do
52+
((10#$octet >= 0 && 10#$octet <= 255)) || return 1
53+
done
54+
}
55+
56+
command -v jq >/dev/null 2>&1 || fail "jq is required"
57+
58+
REDC_CASE_ID=${REDC_CASE_ID:-}
59+
REDC_CASE_NAME=${REDC_CASE_NAME:-}
60+
REDC_CASE_PATH=${REDC_CASE_PATH:-}
61+
REDC_CASE_TEMPLATE=${REDC_CASE_TEMPLATE:-}
62+
REDC_CASE_VARS=${REDC_CASE_VARS:-}
63+
REDC_OUTPUT_JSON=${REDC_OUTPUT_JSON:-}
64+
65+
[[ -n "$REDC_CASE_PATH" ]] || fail "REDC_CASE_PATH is required"
66+
[[ -d "$REDC_CASE_PATH" ]] || fail "REDC_CASE_PATH is not a directory"
67+
[[ -n "$REDC_CASE_ID" ]] || fail "REDC_CASE_ID is required"
68+
[[ -n "$REDC_OUTPUT_JSON" ]] || fail "REDC_OUTPUT_JSON is required"
69+
70+
if has_case_var port; then
71+
PORT=$(read_case_var port)
72+
else
73+
PORT=$(read_tfvar port)
74+
fi
75+
if has_case_var password; then
76+
PASSWORD=$(read_case_var password)
77+
else
78+
PASSWORD=$(read_tfvar password)
79+
fi
80+
CIPHER=chacha20-ietf-poly1305
81+
POOL_ID=${REDC_PLUGIN_CONFIG_POOL_ID:-redc-${REDC_CASE_ID}-aliyun-proxy}
82+
83+
[[ -n "$PORT" ]] || fail "Shadowsocks port is required"
84+
[[ -n "$PASSWORD" ]] || fail "Shadowsocks password is required"
85+
[[ "$POOL_ID" =~ ^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$ ]] || fail "pool ID contains unsupported characters or is too long"
86+
[[ "$PORT" =~ ^[0-9]+$ ]] || fail "Shadowsocks port must be numeric"
87+
((10#$PORT >= 1 && 10#$PORT <= 65535)) || fail "Shadowsocks port must be between 1 and 65535"
88+
89+
IPS_JSON=$(jq -ce '
90+
(.ecs_ip.value // .public_ip.value // empty) as $value
91+
| if ($value | type) == "array" then $value
92+
elif ($value | type) == "string" then [$value]
93+
else empty
94+
end
95+
' <<<"$REDC_OUTPUT_JSON") || fail "ecs_ip or public_ip output is required"
96+
NODE_COUNT=$(jq -r 'length' <<<"$IPS_JSON")
97+
[[ "$NODE_COUNT" -gt 0 ]] || fail "at least one proxy node is required"
98+
UNIQUE_NODE_COUNT=$(jq -r 'unique | length' <<<"$IPS_JSON")
99+
[[ "$UNIQUE_NODE_COUNT" -eq "$NODE_COUNT" ]] || fail "duplicate proxy nodes are not allowed"
100+
while IFS= read -r IP; do
101+
is_valid_ipv4 "$IP" || fail "proxy node address is not a valid IPv4 literal"
102+
done < <(jq -r '.[]' <<<"$IPS_JSON")
103+
104+
umask 077
105+
BUNDLE_DIR="$REDC_CASE_PATH/pojun-proxy"
106+
[[ ! -L "$BUNDLE_DIR" ]] || fail "bundle directory must not be a symlink"
107+
mkdir -p "$BUNDLE_DIR"
108+
chmod 0700 "$BUNDLE_DIR"
109+
110+
BUNDLE_FILE="$BUNDLE_DIR/bundle.json"
111+
NODES_TMP=$(mktemp "$BUNDLE_DIR/.nodes.json.tmp.XXXXXX")
112+
BUNDLE_TMP=$(mktemp "$BUNDLE_DIR/.bundle.json.tmp.XXXXXX")
113+
cleanup() {
114+
rm -f "$NODES_TMP" "$BUNDLE_TMP"
115+
}
116+
trap cleanup EXIT HUP INT TERM
117+
118+
NODES_JSON=$(jq -cn \
119+
--argjson ips "$IPS_JSON" \
120+
--argjson port "$PORT" \
121+
--arg cipher "$CIPHER" \
122+
--arg password "$PASSWORD" \
123+
'$ips | to_entries | map({
124+
cipher: $cipher,
125+
name: ("redc-node-" + (("000" + ((.key + 1) | tostring))[-3:])),
126+
password: $password,
127+
port: $port,
128+
server: .value,
129+
type: "ss"
130+
})')
131+
printf '%s' "$NODES_JSON" >"$NODES_TMP"
132+
chmod 0600 "$NODES_TMP"
133+
134+
REVISION=$(sha256_file "$NODES_TMP")
135+
GENERATED_AT=$(date -u '+%Y-%m-%dT%H:%M:%SZ')
136+
jq -n \
137+
--arg pool_id "$POOL_ID" \
138+
--arg generated_at "$GENERATED_AT" \
139+
--arg revision "$REVISION" \
140+
--arg case_id "$REDC_CASE_ID" \
141+
--arg case_name "$REDC_CASE_NAME" \
142+
--arg template "$REDC_CASE_TEMPLATE" \
143+
--argjson node_count "$NODE_COUNT" \
144+
--slurpfile nodes "$NODES_TMP" \
145+
'{
146+
schema_version: 1,
147+
pool_id: $pool_id,
148+
revision: $revision,
149+
generated_at: $generated_at,
150+
node_count: $node_count,
151+
nodes: $nodes[0],
152+
source: {
153+
kind: "redc_case",
154+
case_id: $case_id,
155+
case_name: $case_name,
156+
template: $template
157+
}
158+
}' >"$BUNDLE_TMP"
159+
chmod 0600 "$BUNDLE_TMP"
160+
161+
sync_path "$BUNDLE_TMP"
162+
mv -f "$BUNDLE_TMP" "$BUNDLE_FILE"
163+
sync_path "$BUNDLE_DIR"
164+
trap - EXIT HUP INT TERM
165+
rm -f "$NODES_TMP"
166+
167+
echo "REDC_OUTPUT:pojun_proxy_bundle_file=$BUNDLE_FILE"
168+
echo "REDC_OUTPUT:pojun_proxy_pool_id=$POOL_ID"
169+
echo "REDC_OUTPUT:pojun_proxy_node_count=$NODE_COUNT"
170+
echo "REDC_OUTPUT:pojun_proxy_revision=$REVISION"
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
{{- $poolID := .Config.pool_id | default (printf "redc-%s-aliyun-proxy" .CaseID) -}}
2+
{{- $bundle := writePojunProxyBundle $poolID .Vars.port .Vars.password -}}
3+
{{- setOutput "pojun_proxy_bundle_file" (index $bundle "bundle_file") -}}
4+
{{- setOutput "pojun_proxy_pool_id" (index $bundle "pool_id") -}}
5+
{{- setOutput "pojun_proxy_node_count" (index $bundle "node_count") -}}
6+
{{- setOutput "pojun_proxy_revision" (index $bundle "revision") -}}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
{
2+
"name": "redc-plugin-pojun-proxy",
3+
"version": "1.2.0",
4+
"description": "为 PoJun Worker 生成安全、可校验的 Shadowsocks 代理节点 Bundle",
5+
"description_en": "Generate a private, verifiable Shadowsocks proxy bundle for PoJun Workers",
6+
"author": "wgpsec",
7+
"homepage": "https://github.com/wgpsec/redc-template",
8+
"category": "proxy",
9+
"tags": ["pojun", "proxy", "shadowsocks", "mihomo"],
10+
"min_redc_version": "3.3.8",
11+
"capabilities": {
12+
"hooks": {
13+
"post-apply": {
14+
"type": "template",
15+
"template": "hooks/post-apply.tmpl",
16+
"output": ""
17+
}
18+
}
19+
},
20+
"config_schema": {
21+
"pool_id": {
22+
"type": "string",
23+
"required": false,
24+
"description": "稳定代理池 ID;留空时根据 RedC Case ID 自动生成",
25+
"default": ""
26+
}
27+
}
28+
}

0 commit comments

Comments
 (0)