-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathmmw.sh
More file actions
executable file
·419 lines (380 loc) · 15.2 KB
/
Copy pathmmw.sh
File metadata and controls
executable file
·419 lines (380 loc) · 15.2 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
#!/usr/bin/env bash
# MultiMicroWARP v1.0 — N independent Cloudflare WARP exits as local SOCKS5
# proxies, running ccbkkb/MicroWARP containers on Podman.
#
# Commands: run | probe | status | pool | logs | wipe | stop | restart
# install | uninstall (quadlet/systemd persistence)
set -euo pipefail
_SCRIPT_SRC="${BASH_SOURCE[0]}"
while [[ -L "$_SCRIPT_SRC" ]]; do
_link="$(readlink "$_SCRIPT_SRC")"
if [[ "$_link" == /* ]]; then
_SCRIPT_SRC="$_link"
else
_SCRIPT_SRC="$(cd "$(dirname "$_SCRIPT_SRC")" && pwd)/$_link"
fi
done
ROOT="$(cd "$(dirname "$_SCRIPT_SRC")/.." && pwd)"
cd "$ROOT"
unset _SCRIPT_SRC _link
# shellcheck source=scripts/lib.sh
source "$ROOT/scripts/lib.sh"
usage() {
cat <<EOF
MultiMicroWARP v${MMW_VERSION} — Usage: ./mmw <command> [options]
Commands:
run [-n N] Start N exits, then recreate bad nodes (FAIL, warp!=on,
duplicate exit IP) until all are unique — or the host's
distinct-exit ceiling is reached. Writes ${POOL_FILE}.
probe [-n N] Wipe identities, start N fresh nodes, single probe pass →
report this host's egress-IP ceiling (no recreate loop).
status Show fleet (label-discovered, no -n needed) + probe all.
pool [-o FILE] Export SOCKS pool file from the discovered fleet.
logs <n|name> [N] podman logs for a node (index or warp-nN), last N lines.
wipe <n...|all> Remove node container(s) + identity volume(s) → the next
'run' registers a brand-new WARP identity.
stop Remove all managed containers (identities are kept).
restart stop, then run.
install [-n N] Install the fleet as quadlet systemd user units
(survives reboots; identities reused if volumes exist).
uninstall [--purge] Remove quadlet units; --purge also drops identities.
Node count / ports / image:
-n, --count N Nodes (default: ${COUNT})
-p, --base-port PORT First host port (default: ${BASE_PORT})
--bind ADDR Host bind address (default: ${BIND})
--image IMAGE MicroWARP image (default: ${IMAGE})
MicroWARP tuning (forwarded to every node):
--protocol wireguard|masque TUNNEL_PROTOCOL
--endpoint "ip[:port] ..." ENDPOINT_IP rotation across nodes
(bare IP gets port ${ENDPOINT_PORT}; re-rotated each recreate round)
--socks-user U --socks-pass P SOCKS5 auth on every exit
--mtu BYTES / --keepalive SEC MTU / WireGuard KEEPALIVE
--ipv6 / --no-ipv6 ENABLE_IPV6=1 / 0
--gh-proxy URL GH_PROXY for the wgcf download
--set KEY=VAL Any other MicroWARP env (repeatable)
Recreate-loop tuning:
--max-rounds N (default: ${MAX_ROUNDS})
--recreate-batch N (default: ${RECREATE_BATCH})
--recreate-wait SEC (default: ${RECREATE_WAIT})
--initial-wait SEC (default: ${INITIAL_WAIT})
Pool export:
-o, --out FILE (default: ${POOL_FILE})
-H, --host HOST Host used in pool URLs (default: bind/127.0.0.1)
--scheme socks5h|socks5 (default: ${POOL_SCHEME})
--stdout Print pool to stdout instead of a file
Everything is also configurable via ./mmw.env (see mmw.env.example).
Env overrides: COUNT BASE_PORT BIND IMAGE ENDPOINTS MAX_ROUNDS ...
EOF
}
# ---------------------------------------------------------------------------
# commands
# ---------------------------------------------------------------------------
_preflight() {
require_podman
}
_fleet_specs_from_discovery() {
local e idx port bind
for e in ${FLEET[@]+"${FLEET[@]}"}; do
IFS='|' read -r _ idx port bind _ <<< "$e"
printf '%s:%s:%s\n' "$idx" "$port" "${bind:-127.0.0.1}"
done
}
cmd_run() {
_preflight
guard_no_quadlet
fleet_trim_to "$COUNT"
local wait="$INITIAL_WAIT"
if (( COUNT > 15 )) && (( wait < COUNT + 20 )); then wait=$((COUNT + 20)); fi
echo "Starting ${COUNT} node(s) (image=${IMAGE}, ${BIND}:${BASE_PORT}..$((BASE_PORT + COUNT - 1)))"
fleet_pull
local i
for ((i = 1; i <= COUNT; i++)); do node_up "$i"; done
echo "Waiting ${wait}s for first WARP registration..."
sleep "$wait"
local -a specs
mapfile -t specs < <(specs_for_range 1 "$COUNT")
local round=1 stall=0 prev_unique=-1 line port status ip warp
while (( round <= MAX_ROUNDS )); do
echo
echo "======== round ${round} / ${MAX_ROUNDS} ========"
probe_fleet "${specs[@]}"
if (( FAIL_COUNT == 0 && UNIQUE_COUNT == COUNT && WARP_ON_COUNT == COUNT )); then
local -a ports=()
mapfile -t ports < <(printf '%s\n' "${specs[@]}" | cut -d: -f2)
pool_write_ports "$(pool_host_from_bind "$BIND")" "$POOL_SCHEME" "$POOL_FILE" "${ports[@]}"
echo
echo "READY: ${COUNT} unique WARP exits."
[[ "$POOL_FILE" == /* ]] || echo "Pool: ${ROOT}/${POOL_FILE}"
echo
for line in "${RESULT_LINES[@]}"; do
IFS='|' read -r port status ip warp <<< "$line"
printf ' %s → %s\n' "socks5h://$(pool_host_from_bind "$BIND"):${port}" "$ip"
done
echo
echo "Persist across reboots: ./mmw stop && ./mmw install -n ${COUNT}"
return 0
fi
if ((${#BAD_IDXS[@]} == 0)); then
die "unique=${UNIQUE_COUNT} but no bad nodes listed — bug in classifier"
fi
# ceiling detection: healthy stack, unique count stopped growing →
# Cloudflare is not handing out more distinct egress IPs; stop spinning
if [[ "$UNIQUE_COUNT" == "$prev_unique" && "$FAIL_COUNT" -eq 0 ]]; then
stall=$((stall + 1))
else
stall=0
fi
prev_unique="$UNIQUE_COUNT"
if (( stall >= 2 )); then
echo "Ceiling reached: unique stayed ${UNIQUE_COUNT}/${COUNT} for ${stall} healthy rounds — stopping."
break
fi
(( round == MAX_ROUNDS )) && break
recreate_nodes "${BAD_IDXS[@]}"
round=$((round + 1))
done
echo
echo "STOPPED after ${round}/${MAX_ROUNDS} rounds: unique=${UNIQUE_COUNT} / ${COUNT} (want all different + warp=on)."
echo "This host's WARP egress ceiling appears to be ~${UNIQUE_COUNT} distinct IP(s) (Cloudflare NAT)."
echo "Tips:"
echo " ./mmw probe -n ${COUNT} # quick ceiling read with fresh accounts"
echo " MAX_ROUNDS=12 RECREATE_WAIT=90 ./mmw run -n ${COUNT} # be more patient"
echo " ./mmw run -n ${COUNT} --endpoint \"162.159.192.1 188.114.96.3 188.114.97.2\""
echo " Want N>${UNIQUE_COUNT} distinct? Run nodes on multiple hosts (each host = its own egress pool)."
return 1
}
cmd_probe() {
_preflight
guard_no_quadlet
fleet_trim_to "$COUNT"
echo "Wiping identities for a fresh ceiling read (${COUNT} brand-new registrations)..."
local i
for ((i = 1; i <= COUNT; i++)); do node_wipe "$i"; done
echo "Starting ${COUNT} fresh node(s) (image=${IMAGE})"
fleet_pull
for ((i = 1; i <= COUNT; i++)); do node_up "$i"; done
echo "Waiting ${INITIAL_WAIT}s for first WARP registration..."
sleep "$INITIAL_WAIT"
local -a specs
mapfile -t specs < <(specs_for_range 1 "$COUNT")
probe_fleet "${specs[@]}"
echo
if (( FAIL_COUNT == 0 && UNIQUE_COUNT == COUNT )); then
echo "CEILING READ: this host produced ${UNIQUE_COUNT}/${COUNT} distinct exits."
elif (( FAIL_COUNT == 0 )); then
echo "CEILING READ: healthy but only ${UNIQUE_COUNT}/${COUNT} distinct — Cloudflare NAT on this network caps distinct exits."
else
echo "CEILING READ: ${FAIL_COUNT} failed, ${UNIQUE_COUNT}/${COUNT} distinct."
fi
echo "Stack is left running. ./mmw status | ./mmw stop"
(( FAIL_COUNT == 0 && UNIQUE_COUNT == COUNT ))
}
cmd_status() {
require_podman
fleet_discover
((${#FLEET[@]} > 0)) || die "no mmw-managed containers found (start with: ./mmw run)"
"$PODMAN_BIN" ps -a --filter label=mmw.managed=true \
--format 'table {{.Names}}\t{{.Status}}\t{{.Ports}}'
echo
local -a specs
mapfile -t specs < <(_fleet_specs_from_discovery)
probe_fleet "${specs[@]}"
if (( FAIL_COUNT == 0 && UNIQUE_COUNT == ${#specs[@]} && WARP_ON_COUNT == ${#specs[@]} )); then
echo "STATUS: healthy (all unique + warp=on)"
return 0
fi
echo "STATUS: not fully unique / not all warp=on"
return 1
}
cmd_stop() {
require_podman
guard_no_quadlet
fleet_discover
((${#FLEET[@]} > 0)) || { echo "Nothing to stop — no managed containers."; return 0; }
fleet_rm_all
echo "Stopped. Identity volumes were kept ('./mmw wipe all' to reset identities)."
}
cmd_restart() { cmd_stop || true; cmd_run; }
cmd_wipe() {
require_podman
guard_no_quadlet
((${#POS[@]} > 0)) || die "usage: ./mmw wipe <n...|all>"
local idx
if [[ "${POS[0]}" == "all" ]]; then
local -a vols=()
mapfile -t vols < <("$PODMAN_BIN" volume ls --filter label=mmw.managed=true --format '{{.Name}}' 2>/dev/null || true)
fleet_discover
local e idx v count=0
for e in ${FLEET[@]+"${FLEET[@]}"}; do
IFS='|' read -r _ idx _ _ _ <<< "$e"
node_wipe "$idx"
count=$((count + 1))
done
# orphaned identity volumes (e.g. after a plain 'stop') count too
for v in ${vols[@]+"${vols[@]}"}; do
"$PODMAN_BIN" volume rm -f "$v" >/dev/null 2>&1 || true
done
if (( count == 0 && ${#vols[@]} == 0 )); then
echo "Nothing to wipe — no managed containers or identity volumes."
return 0
fi
echo "Wiped ${count} container(s) and $((count > ${#vols[@]} ? count : ${#vols[@]})) identity volume(s) — next run registers fresh identities."
return 0
fi
for idx in "${POS[@]}"; do
[[ "$idx" =~ ^[0-9]+$ ]] || die "invalid node index: $idx"
node_wipe "$idx"
echo "Wiped $(node_name "$idx") — next run registers a fresh identity."
done
}
cmd_pool() {
require_podman
fleet_discover
((${#FLEET[@]} > 0)) || die "no fleet discovered — nothing to export"
local host="$POOL_HOST" e idx port p
local -a ports=()
for e in "${FLEET[@]}"; do
IFS='|' read -r _ idx port _ _ <<< "$e"
ports+=("$port")
done
if (( POOL_STDOUT )); then
for p in "${ports[@]}"; do printf '%s://%s:%s\n' "$POOL_SCHEME" "$host" "$p"; done
else
pool_write_ports "$host" "$POOL_SCHEME" "$POOL_OUT" "${ports[@]}"
fi
}
cmd_logs() {
require_podman
((${#POS[@]} >= 1)) || die "usage: ./mmw logs <n|name> [lines]"
local target="${POS[0]}" lines="${POS[1]:-80}"
[[ "$target" =~ ^[0-9]+$ ]] && target="$(node_name "$target")"
node_container_exists "$target" || die "no such container: ${target}"
exec "$PODMAN_BIN" logs "--tail=${lines}" "$target"
}
cmd_install() {
require_quadlet
fleet_discover
((${#FLEET[@]} == 0)) || die "managed containers exist — run './mmw stop' first (identities are kept and reused)"
[[ -z "$(quadlet_units_list)" ]] || die "quadlet units already exist — run './mmw uninstall' first"
if ! linger_enabled; then
echo "WARNING: linger is off — units stop when you log out. Enable once with:"
echo " sudo loginctl enable-linger ${USER}"
fi
mkdir -p "$UNIT_DIR"
local i unit svc
local -a svcs=()
for ((i = 1; i <= COUNT; i++)); do
volume_ensure "$(node_vol "$i")" # pre-create with mmw label so wipe/purge can find it
unit="$(quadlet_unit_path "$i")"
quadlet_unit_text "$i" > "$unit"
chmod 600 "$unit"
svcs+=("mmw-$(node_name "$i").service")
echo " wrote ${unit}"
done
systemctl --user daemon-reload
# generated (quadlet) units cannot be `enable`d directly — the generator
# honors [Install] WantedBy and creates the symlinks at daemon-reload time
systemctl --user start "${svcs[@]}"
echo
echo "Installed ${COUNT} node(s) as systemd user units."
echo " status: ./mmw status (or systemctl --user list-units 'mmw-warp-*')"
echo " per node: systemctl --user status mmw-$(node_name 1)"
echo " remove: ./mmw uninstall [--purge]"
}
cmd_uninstall() {
require_quadlet
local unit stem cname idx count=0
local -a vols=()
while IFS= read -r unit; do
[[ -n "$unit" ]] || continue
stem="$(basename "$unit" .container)"
systemctl --user stop "${stem}.service" >/dev/null 2>&1 || true
if [[ "$PURGE" -eq 1 ]]; then
cname="$(grep -m1 '^ContainerName=' "$unit" | cut -d= -f2-)"
if [[ -n "$cname" && "$cname" == "${NAME_PREFIX}"* ]]; then
idx="${cname#"$NAME_PREFIX"}"
[[ "$idx" =~ ^[0-9]+$ ]] && vols+=("$(node_vol "$idx")")
fi
fi
rm -f "$unit"
count=$((count + 1))
echo " removed ${stem}.service"
done < <(quadlet_units_list)
(( count > 0 )) || { echo "No quadlet units installed."; return 0; }
systemctl --user daemon-reload
systemctl --user reset-failed >/dev/null 2>&1 || true
# catch any leftovers (should not happen: quadlet services own their containers)
fleet_discover
fleet_rm_all >/dev/null 2>&1 || true
if [[ "$PURGE" -eq 1 ]]; then
local v
for v in "${vols[@]}"; do "$PODMAN_BIN" volume rm -f "$v" >/dev/null 2>&1 || true; done
echo "Purged ${#vols[@]} identity volume(s)."
fi
echo "Uninstalled ${count} unit(s)$( [[ "$PURGE" -eq 1 ]] || echo ' — identity volumes kept' )."
}
# ---------------------------------------------------------------------------
# arg parsing
# ---------------------------------------------------------------------------
CMD=""
POS=()
POOL_OUT="$POOL_FILE"
POOL_HOST=""
POOL_STDOUT=0
PURGE=0
while [[ $# -gt 0 ]]; do
case "$1" in
run|probe|status|pool|logs|wipe|stop|restart|install|uninstall)
CMD="$1"; shift ;;
-n|--count) COUNT="$2"; shift 2 ;;
-p|--base-port) BASE_PORT="$2"; shift 2 ;;
--bind) BIND="$2"; shift 2 ;;
--image) IMAGE="$2"; shift 2 ;;
--protocol) TUNNEL_PROTOCOL="$2"; shift 2 ;;
--endpoint|--endpoints) ENDPOINTS="$2"; shift 2 ;;
--socks-user) SOCKS_USER="$2"; shift 2 ;;
--socks-pass) SOCKS_PASS="$2"; shift 2 ;;
--mtu) MTU="$2"; shift 2 ;;
--keepalive) KEEPALIVE="$2"; shift 2 ;;
--ipv6) ENABLE_IPV6=1; shift ;;
--no-ipv6) ENABLE_IPV6=0; shift ;;
--gh-proxy) GH_PROXY="$2"; shift 2 ;;
--set)
[[ "$2" =~ ^[A-Za-z_][A-Za-z0-9_]*= ]] || die "--set expects KEY=VAL (got: $2)"
EXTRA_ENV+=("$2"); shift 2 ;;
--max-rounds) MAX_ROUNDS="$2"; shift 2 ;;
--recreate-batch) RECREATE_BATCH="$2"; shift 2 ;;
--recreate-wait) RECREATE_WAIT="$2"; shift 2 ;;
--initial-wait) INITIAL_WAIT="$2"; shift 2 ;;
-o|--out) POOL_OUT="$2"; shift 2 ;;
-H|--host) POOL_HOST="$2"; shift 2 ;;
--scheme) POOL_SCHEME="$2"; shift 2 ;;
--stdout) POOL_STDOUT=1; shift ;;
--purge) PURGE=1; shift ;;
-h|--help) usage; exit 0 ;;
--) shift; while [[ $# -gt 0 ]]; do POS+=("$1"); shift; done ;;
-*) echo "Unknown option: $1" >&2; usage >&2; exit 2 ;;
*) [[ -z "$CMD" ]] && { CMD="$1"; shift; } || { POS+=("$1"); shift; } ;;
esac
done
[[ -n "$CMD" ]] || { usage; exit 2; }
[[ "$COUNT" =~ ^[0-9]+$ ]] && (( COUNT >= 1 && COUNT <= 64 )) || die "count must be 1..64"
[[ "$BASE_PORT" =~ ^[0-9]+$ ]] && (( BASE_PORT >= 1024 && BASE_PORT <= 65000 )) || die "base-port must be 1024..65000"
if [[ -n "$POOL_SCHEME" ]] && [[ "$POOL_SCHEME" != socks5h && "$POOL_SCHEME" != socks5 ]]; then
die "--scheme must be socks5h or socks5"
fi
if [[ -z "$POOL_HOST" ]]; then
POOL_HOST="$(pool_host_from_bind "$BIND")"
fi
case "$CMD" in
run) cmd_run ;;
probe) cmd_probe ;;
status) cmd_status ;;
pool) cmd_pool ;;
logs) cmd_logs ;;
wipe) cmd_wipe ;;
stop) cmd_stop ;;
restart) cmd_restart ;;
install) cmd_install ;;
uninstall) cmd_uninstall ;;
esac