fix(microwarp): verify WARP IP rotation

Add the verified MicroWARP packages to the localApps branch, including real SOCKS egress verification, retry and rollback behavior, and upgrade-safe replacement of legacy root rotation scripts while preserving user data.
This commit is contained in:
okxlin
2026-07-25 23:35:46 +08:00
parent ab39742ed6
commit 262c4d3d81
43 changed files with 3931 additions and 0 deletions
@@ -0,0 +1,18 @@
CONTAINER_NAME=microwarp
PANEL_APP_PORT_SOCKS5=1080
PANEL_APP_PORT_HTTP_PROXY=8080
BIND_PORT=1080
ROTATE_INTERVAL_MINUTES=0
ROTATE_MAX_ATTEMPTS=5
ROTATE_RETRY_DELAY_SECONDS=5
TEST_URL=https://cloudflare.com/cdn-cgi/trace
IP_CHECK_URL=https://cloudflare.com/cdn-cgi/trace
SOCKS_USER=
SOCKS_PASS=
ENDPOINT_IP=
GH_PROXY=
TAILSCALE_CIDR=100.64.0.0/10
MTU=1280
WARP_WGCF_CONF=
TZ=Asia/Shanghai
APP_DATA_DIR_1=./data
+277
View File
@@ -0,0 +1,277 @@
additionalProperties:
formFields:
- default: 1080
envKey: PANEL_APP_PORT_SOCKS5
labelEn: SOCKS5 Port
labelZh: SOCKS5 端口
label:
en: SOCKS5 Port
zh: SOCKS5 端口
zh-Hant: SOCKS5 連接埠
ja: SOCKS5 ポート
ko: SOCKS5 포트
ru: Порт SOCKS5
ms: Port SOCKS5
pt-br: Porta SOCKS5
required: true
rule: paramPort
type: number
edit: true
- default: 8080
envKey: PANEL_APP_PORT_HTTP_PROXY
labelEn: HTTP Proxy Port
labelZh: HTTP 代理端口
label:
en: HTTP Proxy Port
zh: HTTP 代理端口
zh-Hant: HTTP 代理連接埠
ja: HTTP プロキシポート
ko: HTTP 프록시 포트
ru: Порт HTTP-прокси
ms: Port proksi HTTP
pt-br: Porta do proxy HTTP
required: true
rule: paramPort
type: number
edit: true
- default: 1080
envKey: BIND_PORT
labelEn: Internal Bind Port
labelZh: 容器内部绑定端口
label:
en: Internal Bind Port
zh: 容器内部绑定端口
zh-Hant: 容器內部綁定連接埠
ja: コンテナ内部バインドポート
ko: 컨테이너 내부 바인드 포트
ru: Внутренний порт привязки контейнера
ms: Port bind dalaman kontena
pt-br: Porta interna de bind do contêiner
required: true
rule: paramPort
type: number
edit: true
- default: 0
envKey: ROTATE_INTERVAL_MINUTES
labelEn: Rotate Interval Minutes
labelZh: 轮换间隔(分钟)
label:
en: Rotate Interval Minutes
zh: 轮换间隔(分钟)
zh-Hant: 輪換間隔(分鐘)
ja: ローテーション間隔(分)
ko: 로테이션 간격(분)
ru: Интервал ротации (минуты)
ms: Selang putaran (minit)
pt-br: Intervalo de rotação (minutos)
required: true
type: number
edit: true
- default: 5
envKey: ROTATE_MAX_ATTEMPTS
labelEn: Rotation Attempts
labelZh: 轮换尝试次数
label:
en: Rotation Attempts
zh: 轮换尝试次数
zh-Hant: 輪換嘗試次數
ja: ローテーション試行回数
ko: 로테이션 시도 횟수
ru: Количество попыток ротации
ms: Bilangan percubaan putaran
pt-br: Número de tentativas de rotação
required: true
type: number
edit: true
- default: 5
envKey: ROTATE_RETRY_DELAY_SECONDS
labelEn: Rotation Retry Delay Seconds
labelZh: 轮换重试间隔(秒)
label:
en: Rotation Retry Delay Seconds
zh: 轮换重试间隔(秒)
zh-Hant: 輪換重試間隔(秒)
ja: ローテーション再試行間隔(秒)
ko: 로테이션 재시도 간격(초)
ru: Задержка между попытками ротации (секунды)
ms: Kelewatan percubaan semula putaran (saat)
pt-br: Atraso entre tentativas de rotação (segundos)
required: true
type: number
edit: true
- default: https://cloudflare.com/cdn-cgi/trace
envKey: TEST_URL
labelEn: Probe URL
labelZh: 探测 URL
label:
en: Probe URL
zh: 探测 URL
zh-Hant: 探測 URL
ja: プローブ URL
ko: 점검 URL
ru: URL проверки
ms: URL ujian
pt-br: URL de verificação
required: true
type: text
edit: true
- default: https://cloudflare.com/cdn-cgi/trace
envKey: IP_CHECK_URL
labelEn: Egress IP Check URL
labelZh: 出口 IP 检查 URL
label:
en: Egress IP Check URL
zh: 出口 IP 检查 URL
zh-Hant: 出口 IP 檢查 URL
ja: 出口 IP 確認 URL
ko: 출구 IP 확인 URL
ru: URL проверки исходного IP
ms: URL semakan IP keluar
pt-br: URL de verificação do IP de saída
required: true
type: text
edit: true
- default: ""
envKey: SOCKS_USER
labelEn: SOCKS Username
labelZh: SOCKS 用户名
label:
en: SOCKS Username
zh: SOCKS 用户名
zh-Hant: SOCKS 使用者名稱
ja: SOCKS ユーザー名
ko: SOCKS 사용자명
ru: Имя пользователя SOCKS
ms: Nama pengguna SOCKS
pt-br: Usuário SOCKS
required: false
type: text
edit: true
- default: ""
envKey: SOCKS_PASS
labelEn: SOCKS Password
labelZh: SOCKS 密码
label:
en: SOCKS Password
zh: SOCKS 密码
zh-Hant: SOCKS 密碼
ja: SOCKS パスワード
ko: SOCKS 비밀번호
ru: Пароль SOCKS
ms: Kata laluan SOCKS
pt-br: Senha SOCKS
required: false
type: text
edit: true
- default: ""
envKey: ENDPOINT_IP
labelEn: WARP Endpoint
labelZh: WARP 终结点
label:
en: WARP Endpoint
zh: WARP 终结点
zh-Hant: WARP 終結點
ja: WARP エンドポイント
ko: WARP 엔드포인트
ru: WARP endpoint
ms: Titik akhir WARP
pt-br: Endpoint WARP
required: false
type: text
edit: true
- default: ""
envKey: GH_PROXY
labelEn: wgcf Download Proxy
labelZh: wgcf 下载代理
label:
en: wgcf Download Proxy
zh: wgcf 下载代理
zh-Hant: wgcf 下載代理
ja: wgcf ダウンロードプロキシ
ko: wgcf 다운로드 프록시
ru: Прокси для загрузки wgcf
ms: Proksi muat turun wgcf
pt-br: Proxy de download do wgcf
required: false
type: text
edit: true
- default: 100.64.0.0/10
envKey: TAILSCALE_CIDR
labelEn: Tailscale Return Route CIDR
labelZh: Tailscale 回程路由 CIDR
label:
en: Tailscale Return Route CIDR
zh: Tailscale 回程路由 CIDR
zh-Hant: Tailscale 回程路由 CIDR
ja: Tailscale 戻りルート CIDR
ko: Tailscale 리턴 라우트 CIDR
ru: CIDR обратного маршрута Tailscale
ms: CIDR laluan balik Tailscale
pt-br: CIDR de rota de retorno do Tailscale
required: true
type: text
edit: true
- default: 1280
envKey: MTU
labelEn: WireGuard MTU
labelZh: WireGuard MTU
label:
en: WireGuard MTU
zh: WireGuard MTU
zh-Hant: WireGuard MTU
ja: WireGuard MTU
ko: WireGuard MTU
ru: MTU WireGuard
ms: MTU WireGuard
pt-br: MTU do WireGuard
required: true
type: number
edit: true
- default: ""
envKey: WARP_WGCF_CONF
labelEn: WireGuard Config Content
labelZh: WireGuard 配置内容
label:
en: WireGuard Config Content
zh: WireGuard 配置内容
zh-Hant: WireGuard 設定內容
ja: WireGuard 設定内容
ko: WireGuard 구성 내용
ru: Содержимое конфигурации WireGuard
ms: Kandungan konfigurasi WireGuard
pt-br: Conteúdo da configuração WireGuard
required: false
type: text
edit: true
- default: Asia/Shanghai
envKey: TZ
labelEn: Timezone
labelZh: 时区
label:
en: Timezone
zh: 时区
zh-Hant: 時區
ja: タイムゾーン
ko: 시간대
ru: Часовой пояс
ms: Zon waktu
pt-br: Fuso horário
required: true
type: text
edit: true
- default: ./data
envKey: APP_DATA_DIR_1
labelEn: WireGuard Data Directory
labelZh: WireGuard 数据目录
label:
en: WireGuard Data Directory
zh: WireGuard 数据目录
zh-Hant: WireGuard 資料目錄
ja: WireGuard データディレクトリ
ko: WireGuard 데이터 디렉터리
ru: Каталог данных WireGuard
ms: Direktori data WireGuard
pt-br: Diretório de dados do WireGuard
required: true
type: text
edit: true
@@ -0,0 +1,58 @@
services:
microwarp:
image: "ghcr.io/ccbkkb/microwarp:0.1.3"
container_name: ${CONTAINER_NAME}
labels:
createdBy: "Apps"
ports:
- "${PANEL_APP_PORT_SOCKS5}:${BIND_PORT}"
environment:
- TZ=${TZ}
- BIND_ADDR=0.0.0.0
- BIND_PORT=${BIND_PORT}
- ROTATE_INTERVAL_MINUTES=${ROTATE_INTERVAL_MINUTES:-0}
- ROTATE_MAX_ATTEMPTS=${ROTATE_MAX_ATTEMPTS:-5}
- ROTATE_RETRY_DELAY_SECONDS=${ROTATE_RETRY_DELAY_SECONDS:-5}
- TEST_URL=${TEST_URL}
- IP_CHECK_URL=${IP_CHECK_URL:-https://cloudflare.com/cdn-cgi/trace}
- SOCKS_USER=${SOCKS_USER:-}
- SOCKS_PASS=${SOCKS_PASS:-}
- ENDPOINT_IP=${ENDPOINT_IP:-}
- GH_PROXY=${GH_PROXY:-}
- TAILSCALE_CIDR=${TAILSCALE_CIDR:-100.64.0.0/10}
- MTU=${MTU}
- WARP_WGCF_CONF=${WARP_WGCF_CONF:-}
cap_add:
- NET_ADMIN
- SYS_MODULE
sysctls:
- net.ipv4.conf.all.src_valid_mark=1
volumes:
- "${APP_DATA_DIR_1}:/etc/wireguard"
- "./rotate.sh:/usr/local/bin/rotate.sh:ro"
restart: always
command: ["/bin/sh", "/usr/local/bin/rotate.sh"]
networks:
- 1panel-network
http-proxy:
image: "gogost/gost:latest"
container_name: ${CONTAINER_NAME}-http
labels:
createdBy: "Apps"
depends_on:
- microwarp
command:
- "-L"
- "http://0.0.0.0:8080"
- "-F"
- "socks5://microwarp:${BIND_PORT}"
ports:
- "${PANEL_APP_PORT_HTTP_PROXY}:8080"
restart: always
networks:
- 1panel-network
networks:
1panel-network:
external: true
+300
View File
@@ -0,0 +1,300 @@
#!/usr/bin/env sh
set -eu
INTERVAL_MINUTES="${ROTATE_INTERVAL_MINUTES:-0}"
ROTATE_MAX_ATTEMPTS="${ROTATE_MAX_ATTEMPTS:-5}"
ROTATE_RETRY_DELAY_SECONDS="${ROTATE_RETRY_DELAY_SECONDS:-5}"
TEST_URL="${TEST_URL:-https://cloudflare.com/cdn-cgi/trace}"
IP_CHECK_URL="${IP_CHECK_URL:-https://cloudflare.com/cdn-cgi/trace}"
SOCKS_ADDR="127.0.0.1:${BIND_PORT:-1080}"
SOCKS_USER_VALUE="${SOCKS_USER:-}"
SOCKS_PASS_VALUE="${SOCKS_PASS:-}"
WG_DIR="/etc/wireguard"
WG_CONF="${WG_DIR}/wg0.conf"
PREVIOUS_CONF="${WG_CONF}.rotation-previous"
FIXED_CONFIG="${WARP_WGCF_CONF:-}"
LOG_PREFIX="[microwarp]"
ENTRY_PID=""
CURRENT_EGRESS_IP=""
log() {
echo "${LOG_PREFIX} $*"
}
is_non_negative_int() {
case "$1" in
''|*[!0-9]*)
return 1
;;
*)
return 0
;;
esac
}
is_positive_int() {
case "$1" in
''|*[!0-9]*|0)
return 1
;;
*)
return 0
;;
esac
}
is_ipv4() {
printf '%s\n' "$1" | awk -F. '
NF == 4 {
for (i = 1; i <= 4; i++) {
if ($i !~ /^[0-9]+$/ || $i < 0 || $i > 255) exit 1
}
exit 0
}
{ exit 1 }
'
}
probe_ip() {
if ! command -v curl >/dev/null 2>&1; then
return 1
fi
curl_via_socks "$TEST_URL" >/dev/null 2>&1
}
curl_via_socks() {
url="$1"
if [ -n "$SOCKS_USER_VALUE" ] && [ -n "$SOCKS_PASS_VALUE" ]; then
curl --max-time 20 --socks5-hostname "$SOCKS_ADDR" \
--proxy-user "${SOCKS_USER_VALUE}:${SOCKS_PASS_VALUE}" "$url"
return $?
fi
curl --max-time 20 --socks5-hostname "$SOCKS_ADDR" "$url"
}
get_egress_ip() {
if ! command -v curl >/dev/null 2>&1; then
return 1
fi
trace_output="$(curl_via_socks "$IP_CHECK_URL" 2>/dev/null)" || return 1
current_ip="$(printf '%s\n' "$trace_output" |
awk -F= '$1 == "ip" { print $2; exit }' | tr -d '\r')"
is_ipv4 "$current_ip" && printf '%s\n' "$current_ip"
}
record_egress_ip() {
current_ip="$(get_egress_ip || true)"
if [ -n "$current_ip" ]; then
CURRENT_EGRESS_IP="$current_ip"
log "current WARP egress IPv4: ${CURRENT_EGRESS_IP}"
else
CURRENT_EGRESS_IP=""
log "unable to verify current WARP egress IPv4"
fi
return 0
}
inject_fixed_config() {
if [ -z "$FIXED_CONFIG" ]; then
return 0
fi
mkdir -p "$WG_DIR"
printf '%s\n' "$FIXED_CONFIG" > "$WG_CONF"
chmod 600 "$WG_CONF" || true
log "wg0.conf injected from WARP_WGCF_CONF"
}
wait_for_proxy() {
attempts=0
while [ "$attempts" -lt 30 ]; do
if [ -n "$ENTRY_PID" ] && ! kill -0 "$ENTRY_PID" 2>/dev/null; then
wait "$ENTRY_PID" || true
ENTRY_PID=""
return 1
fi
if probe_ip; then
return 0
fi
attempts=$((attempts + 1))
sleep 2
done
return 1
}
stop_runtime() {
if [ -n "$ENTRY_PID" ] && kill -0 "$ENTRY_PID" 2>/dev/null; then
kill "$ENTRY_PID" 2>/dev/null || true
wait "$ENTRY_PID" || true
fi
ENTRY_PID=""
}
teardown_for_rotation() {
stop_runtime
if ip link show wg0 >/dev/null 2>&1; then
wg-quick down wg0 >/dev/null 2>&1 || true
fi
rm -f "$WG_CONF" "$WG_DIR/wgcf-account.toml" "$WG_DIR/wgcf-profile.conf" "$WG_DIR/extra.env"
}
start_runtime() {
CURRENT_EGRESS_IP=""
inject_fixed_config
log "starting MicroWARP runtime"
/app/entrypoint.sh &
ENTRY_PID=$!
if ! wait_for_proxy; then
log "runtime did not become ready in time"
stop_runtime
return 1
fi
record_egress_ip
if [ -z "$ENTRY_PID" ] || ! kill -0 "$ENTRY_PID" 2>/dev/null; then
log "runtime exited while its egress was being checked"
stop_runtime
return 1
fi
return 0
}
save_previous_config() {
if [ -s "$WG_CONF" ]; then
previous_tmp="${PREVIOUS_CONF}.tmp.$$"
rm -f "$previous_tmp"
if ! cp -p "$WG_CONF" "$previous_tmp"; then
rm -f "$previous_tmp"
return 1
fi
if ! mv -f "$previous_tmp" "$PREVIOUS_CONF"; then
rm -f "$previous_tmp"
return 1
fi
return 0
fi
return 1
}
restore_previous_runtime() {
if [ ! -s "$PREVIOUS_CONF" ]; then
return 1
fi
log "restoring the previous working WARP configuration"
teardown_for_rotation
if ! cp -p "$PREVIOUS_CONF" "$WG_CONF"; then
log "unable to restore the previous WARP configuration"
return 1
fi
start_runtime
}
rotate_once() {
if [ -z "$ENTRY_PID" ] || ! kill -0 "$ENTRY_PID" 2>/dev/null; then
log "WARP runtime exited before the scheduled identity rebuild"
return 1
fi
record_egress_ip
old_ip="$CURRENT_EGRESS_IP"
if [ -z "$old_ip" ]; then
log "scheduled identity rebuild skipped because the current WARP egress IPv4 could not be verified"
return 0
fi
attempt=1
if ! save_previous_config; then
log "scheduled identity rebuild skipped because the working WARP configuration could not be backed up"
return 0
fi
while [ "$attempt" -le "$ROTATE_MAX_ATTEMPTS" ]; do
log "WARP identity rebuild attempt ${attempt}/${ROTATE_MAX_ATTEMPTS}"
teardown_for_rotation
if start_runtime; then
new_ip="$CURRENT_EGRESS_IP"
if [ -n "$new_ip" ] &&
[ "$new_ip" != "$old_ip" ]; then
rm -f "$PREVIOUS_CONF"
log "WARP egress IPv4 changed: ${old_ip:-unknown} -> ${new_ip}"
return 0
fi
if [ -n "$new_ip" ] && [ "$new_ip" = "$old_ip" ]; then
log "WARP egress IPv4 stayed ${new_ip}; retrying identity rebuild"
else
log "WARP egress IPv4 could not be verified; retrying identity rebuild"
fi
else
log "WARP runtime failed during identity rebuild attempt ${attempt}"
fi
if [ "$attempt" -lt "$ROTATE_MAX_ATTEMPTS" ]; then
sleep "$ROTATE_RETRY_DELAY_SECONDS"
fi
attempt=$((attempt + 1))
done
if restore_previous_runtime; then
log "no different public IPv4 was observed; previous runtime restored"
rm -f "$PREVIOUS_CONF"
return 0
fi
log "rotation failed and no working runtime is available"
return 1
}
on_signal() {
log "received stop signal"
stop_runtime
exit 0
}
main() {
interval="$INTERVAL_MINUTES"
if ! is_non_negative_int "$interval"; then
log "invalid ROTATE_INTERVAL_MINUTES=${interval}, fallback to 0"
interval=0
fi
if ! is_positive_int "$ROTATE_MAX_ATTEMPTS"; then
log "invalid ROTATE_MAX_ATTEMPTS=${ROTATE_MAX_ATTEMPTS}, fallback to 5"
ROTATE_MAX_ATTEMPTS=5
fi
if ! is_non_negative_int "$ROTATE_RETRY_DELAY_SECONDS"; then
log "invalid ROTATE_RETRY_DELAY_SECONDS=${ROTATE_RETRY_DELAY_SECONDS}, fallback to 5"
ROTATE_RETRY_DELAY_SECONDS=5
fi
if [ -n "$FIXED_CONFIG" ] && [ "$interval" -gt 0 ]; then
log "WARP_WGCF_CONF is set, scheduled rotation disabled because the config is pinned"
interval=0
fi
trap on_signal INT TERM
if ! start_runtime; then
exit 1
fi
if [ "$interval" -eq 0 ]; then
log "scheduled rotation disabled"
wait "$ENTRY_PID"
exit $?
fi
log "scheduled WARP identity rebuild enabled every ${interval} minute(s); egress verification allows ${ROTATE_MAX_ATTEMPTS} attempt(s)"
while true; do
sleep "$((interval * 60))"
log "trigger scheduled WARP identity rebuild"
if ! rotate_once; then
log "rotation failed"
exit 1
fi
done
}
main "$@"
@@ -0,0 +1,38 @@
#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR="$(CDPATH="" cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd -P)"
APP_ROOT="${APP_ROOT:-$(CDPATH="" cd -- "${SCRIPT_DIR}/.." && pwd -P)}"
ENV_FILE="${ENV_FILE:-${APP_ROOT}/.env}"
if [[ "${ENV_FILE}" != /* ]]; then
ENV_FILE="${APP_ROOT}/${ENV_FILE#./}"
fi
strip_matching_quotes() {
local value="$1"
case "${value}" in
\"*\")
value="${value#\"}"
value="${value%\"}"
;;
\'*\')
value="${value#\'}"
value="${value%\'}"
;;
esac
printf '%s\n' "${value}"
}
app_data_dir="${APP_DATA_DIR_1:-}"
if [[ -z "${app_data_dir}" && -f "${ENV_FILE}" ]]; then
app_data_dir="$(sed -n 's/^APP_DATA_DIR_1=//p' "${ENV_FILE}" | tail -n 1)"
fi
app_data_dir="$(strip_matching_quotes "${app_data_dir}")"
app_data_dir="${app_data_dir:-./data}"
if [[ "${app_data_dir}" != /* ]]; then
app_data_dir="${APP_ROOT}/${app_data_dir#./}"
fi
mkdir -p "${app_data_dir}"
+2
View File
@@ -0,0 +1,2 @@
#!/bin/bash
docker-compose down --volumes
@@ -0,0 +1,112 @@
#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR="$(CDPATH="" cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd -P)"
APP_ROOT="${APP_ROOT:-$(CDPATH="" cd -- "${SCRIPT_DIR}/.." && pwd -P)}"
ENV_FILE="${ENV_FILE:-${APP_ROOT}/.env}"
TARGET_VERSION_DIR="0.1.3-rotator-http"
if [[ "${ENV_FILE}" != /* ]]; then
ENV_FILE="${APP_ROOT}/${ENV_FILE#./}"
fi
resource_dir_for_app() {
local panel_root app_path app_key
case "${APP_ROOT}" in
*/apps/local/*)
panel_root="${APP_ROOT%%/apps/local/*}"
app_path="${APP_ROOT#"${panel_root}/apps/local/"}"
app_key="${app_path%%/*}"
[[ -n "${panel_root}" && -n "${app_key}" ]] || return 1
printf '%s/resource/apps/local/%s\n' "${panel_root}" "${app_key}"
;;
*)
return 1
;;
esac
}
find_rotate_source() {
local resource_dir candidate_upgrade candidate_rotate
if [[ -n "${ROTATE_SOURCE:-}" ]]; then
printf '%s\n' "${ROTATE_SOURCE}"
return 0
fi
if resource_dir="$(resource_dir_for_app 2>/dev/null)" && [[ -d "${resource_dir}" ]]; then
candidate_upgrade="${resource_dir}/${TARGET_VERSION_DIR}/scripts/upgrade.sh"
candidate_rotate="${resource_dir}/${TARGET_VERSION_DIR}/rotate.sh"
if [[ -f "${candidate_upgrade}" && -f "${candidate_rotate}" ]] \
&& cmp -s "${SCRIPT_DIR}/upgrade.sh" "${candidate_upgrade}" \
&& grep -q 'ROTATE_MAX_ATTEMPTS' "${candidate_rotate}"; then
printf '%s\n' "${candidate_rotate}"
return 0
fi
fi
if [[ -f "${APP_ROOT}/rotate.sh" ]] \
&& grep -q 'ROTATE_MAX_ATTEMPTS' "${APP_ROOT}/rotate.sh"; then
printf '%s\n' "${APP_ROOT}/rotate.sh"
return 0
fi
return 1
}
strip_matching_quotes() {
local value="$1"
case "${value}" in
\"*\")
value="${value#\"}"
value="${value%\"}"
;;
\'*\')
value="${value#\'}"
value="${value%\'}"
;;
esac
printf '%s\n' "${value}"
}
rotate_target="${APP_ROOT}/rotate.sh"
rotate_source="$(find_rotate_source || true)"
if [[ -z "${rotate_source}" || ! -f "${rotate_source}" ]]; then
echo "Unable to locate the upgraded rotate.sh payload; refusing to keep the old script" >&2
exit 1
fi
if [[ "${rotate_source}" != "${rotate_target}" ]] \
&& ! cmp -s "${rotate_source}" "${rotate_target}"; then
rotate_tmp="$(mktemp "${rotate_target}.upgrade.XXXXXX")"
# shellcheck disable=SC2317
cleanup_rotate_tmp() {
rm -f -- "${rotate_tmp}"
}
trap cleanup_rotate_tmp EXIT
cp -- "${rotate_source}" "${rotate_tmp}"
if [[ -e "${rotate_target}" ]]; then
chmod --reference="${rotate_target}" "${rotate_tmp}" 2>/dev/null || true
else
chmod 0644 "${rotate_tmp}"
fi
mv -f -- "${rotate_tmp}" "${rotate_target}"
trap - EXIT
echo "Updated ${rotate_target} from the target version payload"
fi
app_data_dir="${APP_DATA_DIR_1:-}"
if [[ -z "${app_data_dir}" && -f "${ENV_FILE}" ]]; then
app_data_dir="$(sed -n 's/^APP_DATA_DIR_1=//p' "${ENV_FILE}" | tail -n 1)"
fi
app_data_dir="$(strip_matching_quotes "${app_data_dir}")"
app_data_dir="${app_data_dir:-./data}"
if [[ "${app_data_dir}" != /* ]]; then
app_data_dir="${APP_ROOT}/${app_data_dir#./}"
fi
mkdir -p "${app_data_dir}"
exit 0
+17
View File
@@ -0,0 +1,17 @@
CONTAINER_NAME=microwarp
PANEL_APP_PORT_SOCKS5=1080
BIND_PORT=1080
ROTATE_INTERVAL_MINUTES=0
ROTATE_MAX_ATTEMPTS=5
ROTATE_RETRY_DELAY_SECONDS=5
TEST_URL=https://cloudflare.com/cdn-cgi/trace
IP_CHECK_URL=https://cloudflare.com/cdn-cgi/trace
SOCKS_USER=
SOCKS_PASS=
ENDPOINT_IP=
GH_PROXY=
TAILSCALE_CIDR=100.64.0.0/10
MTU=1280
WARP_WGCF_CONF=
TZ=Asia/Shanghai
APP_DATA_DIR_1=./data
+260
View File
@@ -0,0 +1,260 @@
additionalProperties:
formFields:
- default: 1080
envKey: PANEL_APP_PORT_SOCKS5
labelEn: SOCKS5 Port
labelZh: SOCKS5 端口
label:
en: SOCKS5 Port
zh: SOCKS5 端口
zh-Hant: SOCKS5 連接埠
ja: SOCKS5 ポート
ko: SOCKS5 포트
ru: Порт SOCKS5
ms: Port SOCKS5
pt-br: Porta SOCKS5
required: true
rule: paramPort
type: number
edit: true
- default: 1080
envKey: BIND_PORT
labelEn: Internal Bind Port
labelZh: 容器内部绑定端口
label:
en: Internal Bind Port
zh: 容器内部绑定端口
zh-Hant: 容器內部綁定連接埠
ja: コンテナ内部バインドポート
ko: 컨테이너 내부 바인드 포트
ru: Внутренний порт привязки контейнера
ms: Port bind dalaman kontena
pt-br: Porta interna de bind do contêiner
required: true
rule: paramPort
type: number
edit: true
- default: 0
envKey: ROTATE_INTERVAL_MINUTES
labelEn: Rotate Interval Minutes
labelZh: 轮换间隔(分钟)
label:
en: Rotate Interval Minutes
zh: 轮换间隔(分钟)
zh-Hant: 輪換間隔(分鐘)
ja: ローテーション間隔(分)
ko: 로테이션 간격(분)
ru: Интервал ротации (минуты)
ms: Selang putaran (minit)
pt-br: Intervalo de rotação (minutos)
required: true
type: number
edit: true
- default: 5
envKey: ROTATE_MAX_ATTEMPTS
labelEn: Rotation Attempts
labelZh: 轮换尝试次数
label:
en: Rotation Attempts
zh: 轮换尝试次数
zh-Hant: 輪換嘗試次數
ja: ローテーション試行回数
ko: 로테이션 시도 횟수
ru: Количество попыток ротации
ms: Bilangan percubaan putaran
pt-br: Número de tentativas de rotação
required: true
type: number
edit: true
- default: 5
envKey: ROTATE_RETRY_DELAY_SECONDS
labelEn: Rotation Retry Delay Seconds
labelZh: 轮换重试间隔(秒)
label:
en: Rotation Retry Delay Seconds
zh: 轮换重试间隔(秒)
zh-Hant: 輪換重試間隔(秒)
ja: ローテーション再試行間隔(秒)
ko: 로테이션 재시도 간격(초)
ru: Задержка между попытками ротации (секунды)
ms: Kelewatan percubaan semula putaran (saat)
pt-br: Atraso entre tentativas de rotação (segundos)
required: true
type: number
edit: true
- default: https://cloudflare.com/cdn-cgi/trace
envKey: TEST_URL
labelEn: Probe URL
labelZh: 探测 URL
label:
en: Probe URL
zh: 探测 URL
zh-Hant: 探測 URL
ja: プローブ URL
ko: 점검 URL
ru: URL проверки
ms: URL ujian
pt-br: URL de verificação
required: true
type: text
edit: true
- default: https://cloudflare.com/cdn-cgi/trace
envKey: IP_CHECK_URL
labelEn: Egress IP Check URL
labelZh: 出口 IP 检查 URL
label:
en: Egress IP Check URL
zh: 出口 IP 检查 URL
zh-Hant: 出口 IP 檢查 URL
ja: 出口 IP 確認 URL
ko: 출구 IP 확인 URL
ru: URL проверки исходного IP
ms: URL semakan IP keluar
pt-br: URL de verificação do IP de saída
required: true
type: text
edit: true
- default: ""
envKey: SOCKS_USER
labelEn: SOCKS Username
labelZh: SOCKS 用户名
label:
en: SOCKS Username
zh: SOCKS 用户名
zh-Hant: SOCKS 使用者名稱
ja: SOCKS ユーザー名
ko: SOCKS 사용자명
ru: Имя пользователя SOCKS
ms: Nama pengguna SOCKS
pt-br: Usuário SOCKS
required: false
type: text
edit: true
- default: ""
envKey: SOCKS_PASS
labelEn: SOCKS Password
labelZh: SOCKS 密码
label:
en: SOCKS Password
zh: SOCKS 密码
zh-Hant: SOCKS 密碼
ja: SOCKS パスワード
ko: SOCKS 비밀번호
ru: Пароль SOCKS
ms: Kata laluan SOCKS
pt-br: Senha SOCKS
required: false
type: text
edit: true
- default: ""
envKey: ENDPOINT_IP
labelEn: WARP Endpoint
labelZh: WARP 终结点
label:
en: WARP Endpoint
zh: WARP 终结点
zh-Hant: WARP 終結點
ja: WARP エンドポイント
ko: WARP 엔드포인트
ru: WARP endpoint
ms: Titik akhir WARP
pt-br: Endpoint WARP
required: false
type: text
edit: true
- default: ""
envKey: GH_PROXY
labelEn: wgcf Download Proxy
labelZh: wgcf 下载代理
label:
en: wgcf Download Proxy
zh: wgcf 下载代理
zh-Hant: wgcf 下載代理
ja: wgcf ダウンロードプロキシ
ko: wgcf 다운로드 프록시
ru: Прокси для загрузки wgcf
ms: Proksi muat turun wgcf
pt-br: Proxy de download do wgcf
required: false
type: text
edit: true
- default: 100.64.0.0/10
envKey: TAILSCALE_CIDR
labelEn: Tailscale Return Route CIDR
labelZh: Tailscale 回程路由 CIDR
label:
en: Tailscale Return Route CIDR
zh: Tailscale 回程路由 CIDR
zh-Hant: Tailscale 回程路由 CIDR
ja: Tailscale 戻りルート CIDR
ko: Tailscale 리턴 라우트 CIDR
ru: CIDR обратного маршрута Tailscale
ms: CIDR laluan balik Tailscale
pt-br: CIDR de rota de retorno do Tailscale
required: true
type: text
edit: true
- default: 1280
envKey: MTU
labelEn: WireGuard MTU
labelZh: WireGuard MTU
label:
en: WireGuard MTU
zh: WireGuard MTU
zh-Hant: WireGuard MTU
ja: WireGuard MTU
ko: WireGuard MTU
ru: MTU WireGuard
ms: MTU WireGuard
pt-br: MTU do WireGuard
required: true
type: number
edit: true
- default: ""
envKey: WARP_WGCF_CONF
labelEn: WireGuard Config Content
labelZh: WireGuard 配置内容
label:
en: WireGuard Config Content
zh: WireGuard 配置内容
zh-Hant: WireGuard 設定內容
ja: WireGuard 設定内容
ko: WireGuard 구성 내용
ru: Содержимое конфигурации WireGuard
ms: Kandungan konfigurasi WireGuard
pt-br: Conteúdo da configuração WireGuard
required: false
type: text
edit: true
- default: Asia/Shanghai
envKey: TZ
labelEn: Timezone
labelZh: 时区
label:
en: Timezone
zh: 时区
zh-Hant: 時區
ja: タイムゾーン
ko: 시간대
ru: Часовой пояс
ms: Zon waktu
pt-br: Fuso horário
required: true
type: text
edit: true
- default: ./data
envKey: APP_DATA_DIR_1
labelEn: WireGuard Data Directory
labelZh: WireGuard 数据目录
label:
en: WireGuard Data Directory
zh: WireGuard 数据目录
zh-Hant: WireGuard 資料目錄
ja: WireGuard データディレクトリ
ko: WireGuard 데이터 디렉터리
ru: Каталог данных WireGuard
ms: Direktori data WireGuard
pt-br: Diretório de dados do WireGuard
required: true
type: text
edit: true
@@ -0,0 +1,40 @@
services:
microwarp:
image: "ghcr.io/ccbkkb/microwarp:0.1.3"
container_name: ${CONTAINER_NAME}
labels:
createdBy: "Apps"
ports:
- "${PANEL_APP_PORT_SOCKS5}:${BIND_PORT}"
environment:
- TZ=${TZ}
- BIND_ADDR=0.0.0.0
- BIND_PORT=${BIND_PORT}
- ROTATE_INTERVAL_MINUTES=${ROTATE_INTERVAL_MINUTES:-0}
- ROTATE_MAX_ATTEMPTS=${ROTATE_MAX_ATTEMPTS:-5}
- ROTATE_RETRY_DELAY_SECONDS=${ROTATE_RETRY_DELAY_SECONDS:-5}
- TEST_URL=${TEST_URL}
- IP_CHECK_URL=${IP_CHECK_URL:-https://cloudflare.com/cdn-cgi/trace}
- SOCKS_USER=${SOCKS_USER:-}
- SOCKS_PASS=${SOCKS_PASS:-}
- ENDPOINT_IP=${ENDPOINT_IP:-}
- GH_PROXY=${GH_PROXY:-}
- TAILSCALE_CIDR=${TAILSCALE_CIDR:-100.64.0.0/10}
- MTU=${MTU}
- WARP_WGCF_CONF=${WARP_WGCF_CONF:-}
cap_add:
- NET_ADMIN
- SYS_MODULE
sysctls:
- net.ipv4.conf.all.src_valid_mark=1
volumes:
- "${APP_DATA_DIR_1}:/etc/wireguard"
- "./rotate.sh:/usr/local/bin/rotate.sh:ro"
restart: always
command: ["/bin/sh", "/usr/local/bin/rotate.sh"]
networks:
- 1panel-network
networks:
1panel-network:
external: true
+300
View File
@@ -0,0 +1,300 @@
#!/usr/bin/env sh
set -eu
INTERVAL_MINUTES="${ROTATE_INTERVAL_MINUTES:-0}"
ROTATE_MAX_ATTEMPTS="${ROTATE_MAX_ATTEMPTS:-5}"
ROTATE_RETRY_DELAY_SECONDS="${ROTATE_RETRY_DELAY_SECONDS:-5}"
TEST_URL="${TEST_URL:-https://cloudflare.com/cdn-cgi/trace}"
IP_CHECK_URL="${IP_CHECK_URL:-https://cloudflare.com/cdn-cgi/trace}"
SOCKS_ADDR="127.0.0.1:${BIND_PORT:-1080}"
SOCKS_USER_VALUE="${SOCKS_USER:-}"
SOCKS_PASS_VALUE="${SOCKS_PASS:-}"
WG_DIR="/etc/wireguard"
WG_CONF="${WG_DIR}/wg0.conf"
PREVIOUS_CONF="${WG_CONF}.rotation-previous"
FIXED_CONFIG="${WARP_WGCF_CONF:-}"
LOG_PREFIX="[microwarp]"
ENTRY_PID=""
CURRENT_EGRESS_IP=""
log() {
echo "${LOG_PREFIX} $*"
}
is_non_negative_int() {
case "$1" in
''|*[!0-9]*)
return 1
;;
*)
return 0
;;
esac
}
is_positive_int() {
case "$1" in
''|*[!0-9]*|0)
return 1
;;
*)
return 0
;;
esac
}
is_ipv4() {
printf '%s\n' "$1" | awk -F. '
NF == 4 {
for (i = 1; i <= 4; i++) {
if ($i !~ /^[0-9]+$/ || $i < 0 || $i > 255) exit 1
}
exit 0
}
{ exit 1 }
'
}
probe_ip() {
if ! command -v curl >/dev/null 2>&1; then
return 1
fi
curl_via_socks "$TEST_URL" >/dev/null 2>&1
}
curl_via_socks() {
url="$1"
if [ -n "$SOCKS_USER_VALUE" ] && [ -n "$SOCKS_PASS_VALUE" ]; then
curl --max-time 20 --socks5-hostname "$SOCKS_ADDR" \
--proxy-user "${SOCKS_USER_VALUE}:${SOCKS_PASS_VALUE}" "$url"
return $?
fi
curl --max-time 20 --socks5-hostname "$SOCKS_ADDR" "$url"
}
get_egress_ip() {
if ! command -v curl >/dev/null 2>&1; then
return 1
fi
trace_output="$(curl_via_socks "$IP_CHECK_URL" 2>/dev/null)" || return 1
current_ip="$(printf '%s\n' "$trace_output" |
awk -F= '$1 == "ip" { print $2; exit }' | tr -d '\r')"
is_ipv4 "$current_ip" && printf '%s\n' "$current_ip"
}
record_egress_ip() {
current_ip="$(get_egress_ip || true)"
if [ -n "$current_ip" ]; then
CURRENT_EGRESS_IP="$current_ip"
log "current WARP egress IPv4: ${CURRENT_EGRESS_IP}"
else
CURRENT_EGRESS_IP=""
log "unable to verify current WARP egress IPv4"
fi
return 0
}
inject_fixed_config() {
if [ -z "$FIXED_CONFIG" ]; then
return 0
fi
mkdir -p "$WG_DIR"
printf '%s\n' "$FIXED_CONFIG" > "$WG_CONF"
chmod 600 "$WG_CONF" || true
log "wg0.conf injected from WARP_WGCF_CONF"
}
wait_for_proxy() {
attempts=0
while [ "$attempts" -lt 30 ]; do
if [ -n "$ENTRY_PID" ] && ! kill -0 "$ENTRY_PID" 2>/dev/null; then
wait "$ENTRY_PID" || true
ENTRY_PID=""
return 1
fi
if probe_ip; then
return 0
fi
attempts=$((attempts + 1))
sleep 2
done
return 1
}
stop_runtime() {
if [ -n "$ENTRY_PID" ] && kill -0 "$ENTRY_PID" 2>/dev/null; then
kill "$ENTRY_PID" 2>/dev/null || true
wait "$ENTRY_PID" || true
fi
ENTRY_PID=""
}
teardown_for_rotation() {
stop_runtime
if ip link show wg0 >/dev/null 2>&1; then
wg-quick down wg0 >/dev/null 2>&1 || true
fi
rm -f "$WG_CONF" "$WG_DIR/wgcf-account.toml" "$WG_DIR/wgcf-profile.conf" "$WG_DIR/extra.env"
}
start_runtime() {
CURRENT_EGRESS_IP=""
inject_fixed_config
log "starting MicroWARP runtime"
/app/entrypoint.sh &
ENTRY_PID=$!
if ! wait_for_proxy; then
log "runtime did not become ready in time"
stop_runtime
return 1
fi
record_egress_ip
if [ -z "$ENTRY_PID" ] || ! kill -0 "$ENTRY_PID" 2>/dev/null; then
log "runtime exited while its egress was being checked"
stop_runtime
return 1
fi
return 0
}
save_previous_config() {
if [ -s "$WG_CONF" ]; then
previous_tmp="${PREVIOUS_CONF}.tmp.$$"
rm -f "$previous_tmp"
if ! cp -p "$WG_CONF" "$previous_tmp"; then
rm -f "$previous_tmp"
return 1
fi
if ! mv -f "$previous_tmp" "$PREVIOUS_CONF"; then
rm -f "$previous_tmp"
return 1
fi
return 0
fi
return 1
}
restore_previous_runtime() {
if [ ! -s "$PREVIOUS_CONF" ]; then
return 1
fi
log "restoring the previous working WARP configuration"
teardown_for_rotation
if ! cp -p "$PREVIOUS_CONF" "$WG_CONF"; then
log "unable to restore the previous WARP configuration"
return 1
fi
start_runtime
}
rotate_once() {
if [ -z "$ENTRY_PID" ] || ! kill -0 "$ENTRY_PID" 2>/dev/null; then
log "WARP runtime exited before the scheduled identity rebuild"
return 1
fi
record_egress_ip
old_ip="$CURRENT_EGRESS_IP"
if [ -z "$old_ip" ]; then
log "scheduled identity rebuild skipped because the current WARP egress IPv4 could not be verified"
return 0
fi
attempt=1
if ! save_previous_config; then
log "scheduled identity rebuild skipped because the working WARP configuration could not be backed up"
return 0
fi
while [ "$attempt" -le "$ROTATE_MAX_ATTEMPTS" ]; do
log "WARP identity rebuild attempt ${attempt}/${ROTATE_MAX_ATTEMPTS}"
teardown_for_rotation
if start_runtime; then
new_ip="$CURRENT_EGRESS_IP"
if [ -n "$new_ip" ] &&
[ "$new_ip" != "$old_ip" ]; then
rm -f "$PREVIOUS_CONF"
log "WARP egress IPv4 changed: ${old_ip:-unknown} -> ${new_ip}"
return 0
fi
if [ -n "$new_ip" ] && [ "$new_ip" = "$old_ip" ]; then
log "WARP egress IPv4 stayed ${new_ip}; retrying identity rebuild"
else
log "WARP egress IPv4 could not be verified; retrying identity rebuild"
fi
else
log "WARP runtime failed during identity rebuild attempt ${attempt}"
fi
if [ "$attempt" -lt "$ROTATE_MAX_ATTEMPTS" ]; then
sleep "$ROTATE_RETRY_DELAY_SECONDS"
fi
attempt=$((attempt + 1))
done
if restore_previous_runtime; then
log "no different public IPv4 was observed; previous runtime restored"
rm -f "$PREVIOUS_CONF"
return 0
fi
log "rotation failed and no working runtime is available"
return 1
}
on_signal() {
log "received stop signal"
stop_runtime
exit 0
}
main() {
interval="$INTERVAL_MINUTES"
if ! is_non_negative_int "$interval"; then
log "invalid ROTATE_INTERVAL_MINUTES=${interval}, fallback to 0"
interval=0
fi
if ! is_positive_int "$ROTATE_MAX_ATTEMPTS"; then
log "invalid ROTATE_MAX_ATTEMPTS=${ROTATE_MAX_ATTEMPTS}, fallback to 5"
ROTATE_MAX_ATTEMPTS=5
fi
if ! is_non_negative_int "$ROTATE_RETRY_DELAY_SECONDS"; then
log "invalid ROTATE_RETRY_DELAY_SECONDS=${ROTATE_RETRY_DELAY_SECONDS}, fallback to 5"
ROTATE_RETRY_DELAY_SECONDS=5
fi
if [ -n "$FIXED_CONFIG" ] && [ "$interval" -gt 0 ]; then
log "WARP_WGCF_CONF is set, scheduled rotation disabled because the config is pinned"
interval=0
fi
trap on_signal INT TERM
if ! start_runtime; then
exit 1
fi
if [ "$interval" -eq 0 ]; then
log "scheduled rotation disabled"
wait "$ENTRY_PID"
exit $?
fi
log "scheduled WARP identity rebuild enabled every ${interval} minute(s); egress verification allows ${ROTATE_MAX_ATTEMPTS} attempt(s)"
while true; do
sleep "$((interval * 60))"
log "trigger scheduled WARP identity rebuild"
if ! rotate_once; then
log "rotation failed"
exit 1
fi
done
}
main "$@"
@@ -0,0 +1,38 @@
#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR="$(CDPATH="" cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd -P)"
APP_ROOT="${APP_ROOT:-$(CDPATH="" cd -- "${SCRIPT_DIR}/.." && pwd -P)}"
ENV_FILE="${ENV_FILE:-${APP_ROOT}/.env}"
if [[ "${ENV_FILE}" != /* ]]; then
ENV_FILE="${APP_ROOT}/${ENV_FILE#./}"
fi
strip_matching_quotes() {
local value="$1"
case "${value}" in
\"*\")
value="${value#\"}"
value="${value%\"}"
;;
\'*\')
value="${value#\'}"
value="${value%\'}"
;;
esac
printf '%s\n' "${value}"
}
app_data_dir="${APP_DATA_DIR_1:-}"
if [[ -z "${app_data_dir}" && -f "${ENV_FILE}" ]]; then
app_data_dir="$(sed -n 's/^APP_DATA_DIR_1=//p' "${ENV_FILE}" | tail -n 1)"
fi
app_data_dir="$(strip_matching_quotes "${app_data_dir}")"
app_data_dir="${app_data_dir:-./data}"
if [[ "${app_data_dir}" != /* ]]; then
app_data_dir="${APP_ROOT}/${app_data_dir#./}"
fi
mkdir -p "${app_data_dir}"
+2
View File
@@ -0,0 +1,2 @@
#!/bin/bash
docker-compose down --volumes
@@ -0,0 +1,112 @@
#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR="$(CDPATH="" cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd -P)"
APP_ROOT="${APP_ROOT:-$(CDPATH="" cd -- "${SCRIPT_DIR}/.." && pwd -P)}"
ENV_FILE="${ENV_FILE:-${APP_ROOT}/.env}"
TARGET_VERSION_DIR="0.1.3-rotator"
if [[ "${ENV_FILE}" != /* ]]; then
ENV_FILE="${APP_ROOT}/${ENV_FILE#./}"
fi
resource_dir_for_app() {
local panel_root app_path app_key
case "${APP_ROOT}" in
*/apps/local/*)
panel_root="${APP_ROOT%%/apps/local/*}"
app_path="${APP_ROOT#"${panel_root}/apps/local/"}"
app_key="${app_path%%/*}"
[[ -n "${panel_root}" && -n "${app_key}" ]] || return 1
printf '%s/resource/apps/local/%s\n' "${panel_root}" "${app_key}"
;;
*)
return 1
;;
esac
}
find_rotate_source() {
local resource_dir candidate_upgrade candidate_rotate
if [[ -n "${ROTATE_SOURCE:-}" ]]; then
printf '%s\n' "${ROTATE_SOURCE}"
return 0
fi
if resource_dir="$(resource_dir_for_app 2>/dev/null)" && [[ -d "${resource_dir}" ]]; then
candidate_upgrade="${resource_dir}/${TARGET_VERSION_DIR}/scripts/upgrade.sh"
candidate_rotate="${resource_dir}/${TARGET_VERSION_DIR}/rotate.sh"
if [[ -f "${candidate_upgrade}" && -f "${candidate_rotate}" ]] \
&& cmp -s "${SCRIPT_DIR}/upgrade.sh" "${candidate_upgrade}" \
&& grep -q 'ROTATE_MAX_ATTEMPTS' "${candidate_rotate}"; then
printf '%s\n' "${candidate_rotate}"
return 0
fi
fi
if [[ -f "${APP_ROOT}/rotate.sh" ]] \
&& grep -q 'ROTATE_MAX_ATTEMPTS' "${APP_ROOT}/rotate.sh"; then
printf '%s\n' "${APP_ROOT}/rotate.sh"
return 0
fi
return 1
}
strip_matching_quotes() {
local value="$1"
case "${value}" in
\"*\")
value="${value#\"}"
value="${value%\"}"
;;
\'*\')
value="${value#\'}"
value="${value%\'}"
;;
esac
printf '%s\n' "${value}"
}
rotate_target="${APP_ROOT}/rotate.sh"
rotate_source="$(find_rotate_source || true)"
if [[ -z "${rotate_source}" || ! -f "${rotate_source}" ]]; then
echo "Unable to locate the upgraded rotate.sh payload; refusing to keep the old script" >&2
exit 1
fi
if [[ "${rotate_source}" != "${rotate_target}" ]] \
&& ! cmp -s "${rotate_source}" "${rotate_target}"; then
rotate_tmp="$(mktemp "${rotate_target}.upgrade.XXXXXX")"
# shellcheck disable=SC2317
cleanup_rotate_tmp() {
rm -f -- "${rotate_tmp}"
}
trap cleanup_rotate_tmp EXIT
cp -- "${rotate_source}" "${rotate_tmp}"
if [[ -e "${rotate_target}" ]]; then
chmod --reference="${rotate_target}" "${rotate_tmp}" 2>/dev/null || true
else
chmod 0644 "${rotate_tmp}"
fi
mv -f -- "${rotate_tmp}" "${rotate_target}"
trap - EXIT
echo "Updated ${rotate_target} from the target version payload"
fi
app_data_dir="${APP_DATA_DIR_1:-}"
if [[ -z "${app_data_dir}" && -f "${ENV_FILE}" ]]; then
app_data_dir="$(sed -n 's/^APP_DATA_DIR_1=//p' "${ENV_FILE}" | tail -n 1)"
fi
app_data_dir="$(strip_matching_quotes "${app_data_dir}")"
app_data_dir="${app_data_dir:-./data}"
if [[ "${app_data_dir}" != /* ]]; then
app_data_dir="${APP_ROOT}/${app_data_dir#./}"
fi
mkdir -p "${app_data_dir}"
exit 0
+107
View File
@@ -0,0 +1,107 @@
# MicroWARP1Panel v2
基于 `ghcr.io/ccbkkb/microwarp` 的轻量级 Cloudflare WARP 代理封装,统一收敛为 `microwarp` 单 key,并提供官方原版与定时轮换变体。
## 产品介绍
MicroWARP 将 Cloudflare WARP 封装为可在 1Panel 中直接部署的 SOCKS5 代理;HTTP 变体通过 GOST 额外提供 HTTP 代理端口,rotator 变体负责定时重建 WARP 身份。
## 主要功能
- 支持 SOCKS5 代理及可选用户名、密码认证
- 支持 SOCKS5-only、SOCKS5 + HTTP、定时轮换三类组合
- 轮换前后通过代理检查真实出口 IPv4,并在同 IP 或检查失败时重试
- 重试耗尽后恢复上一次可用 WireGuard 配置
## 访问说明
- SOCKS5 端口由 `PANEL_APP_PORT_SOCKS5` 配置
- HTTP 变体的 HTTP 代理端口由 `PANEL_APP_PORT_HTTP_PROXY` 配置
- 代理服务不提供 Web 管理界面;请使用对应端口和 SOCKS5/HTTP 客户端连接
- 默认不启用认证,公网部署时建议同时设置 `SOCKS_USER``SOCKS_PASS`
## 版本说明
### latest
- 官方原版
- 仅提供 SOCKS5 代理
- 不做自动 IP 轮换,适合需要稳定出口身份的场景
### http-latest
- 官方原版增强变体
- 在 SOCKS5 基础上额外提供 HTTP 代理端口
- 通过 `gogost/gost` sidecar 将 HTTP 转发到 SOCKS5
### 0.1.3-rotator
- 基于官方原版增加定时 WARP 身份重建
- 固定使用与当前 `latest` 清单一致的官方 MicroWARP `0.1.3` 镜像
- 轮换通过删除旧 `wg0.conf` 与账号缓存后重新执行上游初始化完成
- 每次轮换都会通过 SOCKS5 代理读取出口 IPv4;出口未变化或无法确认时自动重试
- 达到尝试次数仍没有新出口时恢复上一次可用配置,避免留下不可用代理
- 轮换时会有秒级短暂中断
### 0.1.3-rotator-http
-`0.1.3-rotator` 基础上额外提供 HTTP 代理端口
## 安装建议
- 只需官方 SOCKS5:安装 `latest`
- 需官方 SOCKS5 + HTTP:安装 `http-latest`
- 需要定时切换 WARP 身份:安装 `0.1.3-rotator`
- 需要定时切换并同时提供 HTTP:安装 `0.1.3-rotator-http`
## 核心参数
- `PANEL_APP_PORT_SOCKS5`SOCKS5 对外端口
- `PANEL_APP_PORT_HTTP_PROXY`:HTTP 代理端口,仅 HTTP 变体使用
- `APP_DATA_DIR_1`:WireGuard 持久化目录,保存 `wg0.conf``wgcf` 注册信息
- `SOCKS_USER` / `SOCKS_PASS`:启用 SOCKS5 认证
- `ENDPOINT_IP`:手动覆盖 WARP Endpoint
- `GH_PROXY`:辅助 `wgcf` 下载
- `TAILSCALE_CIDR`Tailscale 回程路由 CIDR,默认 `100.64.0.0/10`
- `MTU`WireGuard MTU,默认 `1280`
- `WARP_WGCF_CONF`:可选完整 WireGuard 配置内容;也可直接将 `wg0.conf` 放入持久化目录
- `ROTATE_INTERVAL_MINUTES`:轮换间隔(分钟),仅 `rotator-*` 版本使用,`0` 表示关闭
- `ROTATE_MAX_ATTEMPTS`:单次轮换最多重建身份的次数,默认 `5`
- `ROTATE_RETRY_DELAY_SECONDS`:轮换重试间隔(秒),默认 `5`
- `TEST_URL`:启动时通过 SOCKS5 探测代理的 URL
- `IP_CHECK_URL`:读取出口 IPv4 的 URL,响应需要包含 `ip=<IPv4>`,默认使用 Cloudflare trace
## 使用说明
- 默认使用外部网络 `1panel-network`
- 首次启动会自动注册 WARP 设备并生成 `/etc/wireguard/wg0.conf`
- 建议保留 `APP_DATA_DIR_1` 持久化目录,避免每次重启都重新注册
- 官方镜像内部支持通过 GitHub token 缓解 `wgcf` 版本查询的 API 限流,但本应用默认不在 1Panel 表单中暴露该参数,优先保持默认安装简洁稳定
- 如果设置了 `WARP_WGCF_CONF``rotator-*` 版本会自动关闭定时轮换,因为该配置已固定出口身份
- `rotator-*` 版本的轮换逻辑是完整重建 WARP 身份,而不是调用上游并不存在的 rotate API;会比较轮换前后的真实出口 IPv4
- 配置了 `SOCKS_USER``SOCKS_PASS` 时,轮换检查会使用相同的 SOCKS5 认证
## 升级说明
- 已有 `rotator-latest``rotator-http-latest` 安装可分别直接升级到 `0.1.3-rotator``0.1.3-rotator-http`,不需要中间版本;新版本名用于确保 1Panel 真正提供升级入口
- 旧轮换版本仍保留以兼容既有安装;新安装请选择带 `0.1.3` 前缀的版本
- 升级会保留 `APP_DATA_DIR_1` 中的 `wg0.conf`、WARP 注册信息和用户自定义环境变量;升级前仍建议使用 1Panel 备份该目录
- 1Panel 升级脚本会将当前版本的 `rotate.sh` 原子替换到已安装目录;若安装目录仍是旧脚本且无法定位新版载荷,升级会失败而不会继续运行旧轮换逻辑,重复执行则保持幂等
- 新增的 `ROTATE_MAX_ATTEMPTS``ROTATE_RETRY_DELAY_SECONDS``IP_CHECK_URL` 在旧 `.env` 缺失时使用安全默认值,不会覆盖旧的轮换间隔、端口或认证配置
- 升级后首次轮换可能产生秒级中断;若出口 IP 无法变化或无法确认,程序会恢复上一份可用配置
## Introduction
MicroWARP packages Cloudflare WARP as a 1Panel-ready SOCKS5 proxy. The HTTP variants add an HTTP proxy through GOST, while the rotator variants rebuild the WARP identity on a schedule and verify the real egress IPv4 through the proxy.
## Features
- SOCKS5 proxy with optional username/password authentication
- SOCKS5-only, SOCKS5 + HTTP, and scheduled-rotation variants
- Egress IPv4 verification before and after each rotation
- Retries for unchanged or unverifiable egress, with rollback to the last working WireGuard configuration
## Upgrade notes
- Existing `rotator-latest` and `rotator-http-latest` installations can upgrade directly to `0.1.3-rotator` and `0.1.3-rotator-http`; no intermediate version is required. The bumped package versions ensure that 1Panel exposes a real upgrade path.
- The legacy rotator versions remain for existing-install compatibility. New installations should use the `0.1.3`-prefixed variants.
- The upgrade preserves `APP_DATA_DIR_1`, including `wg0.conf`, WARP registration data, and existing user-defined environment values. Back up that directory through 1Panel before upgrading.
- The 1Panel upgrade script atomically replaces the installed `rotate.sh` with the target-version payload. If the installed script is still old and that payload cannot be located, the upgrade fails instead of continuing with the old rotation logic; reruns are idempotent.
- The new `ROTATE_MAX_ATTEMPTS`, `ROTATE_RETRY_DELAY_SECONDS`, and `IP_CHECK_URL` variables have safe Compose defaults when they are absent from an older `.env`; existing rotation interval, ports, and authentication settings are not overwritten.
- The first rotation after an upgrade may briefly interrupt the proxy. If a new or verifiable egress IP cannot be obtained, the last working configuration is restored.
+32
View File
@@ -0,0 +1,32 @@
name: MicroWARP
tags:
- 工具
title: 轻量级 Cloudflare WARP SOCKS5 代理,附带 HTTP 与定时轮换变体
type: 工具
description: 轻量级 Cloudflare WARP SOCKS5 代理,附带 HTTP 与定时轮换变体
additionalProperties:
key: microwarp
name: MicroWARP
tags:
- Tool
shortDescZh: 轻量级 Cloudflare WARP SOCKS5 代理,附带 HTTP 与定时轮换变体
shortDescEn: Lightweight Cloudflare WARP SOCKS5 proxy with official, HTTP, and rotator variants
description:
en: Lightweight Cloudflare WARP SOCKS5 proxy with official, HTTP, and rotator variants
zh: 轻量级 Cloudflare WARP SOCKS5 代理,附带 HTTP 与定时轮换变体
zh-Hant: 輕量級 Cloudflare WARP SOCKS5 代理,附帶 HTTP 與定時輪換變體
ja: 公式版、HTTP 変種、定期ローテーション変種を備えた軽量 Cloudflare WARP SOCKS5 プロキシ
ko: 공식판, HTTP 변형, 주기적 로테이터 변형을 함께 제공하는 경량 Cloudflare WARP SOCKS5 프록시
ru: Легковесный SOCKS5-прокси Cloudflare WARP с официальным, HTTP- и rotator-вариантами
ms: Proksi SOCKS5 Cloudflare WARP ringan dengan varian rasmi, HTTP dan rotator
pt-br: Proxy SOCKS5 leve do Cloudflare WARP com variantes oficial, HTTP e rotator
type: tool
crossVersionUpdate: true
limit: 0
recommend: 0
website: https://github.com/ccbkkb/MicroWARP
github: https://github.com/ccbkkb/MicroWARP
document: https://github.com/ccbkkb/MicroWARP#readme
architectures:
- amd64
- arm64
+13
View File
@@ -0,0 +1,13 @@
CONTAINER_NAME=microwarp
PANEL_APP_PORT_SOCKS5=1080
PANEL_APP_PORT_HTTP_PROXY=8080
BIND_PORT=1080
SOCKS_USER=
SOCKS_PASS=
ENDPOINT_IP=
GH_PROXY=
TAILSCALE_CIDR=100.64.0.0/10
MTU=1280
WARP_WGCF_CONF=
TZ=Asia/Shanghai
APP_DATA_DIR_1=./data
+197
View File
@@ -0,0 +1,197 @@
additionalProperties:
formFields:
- default: 1080
envKey: PANEL_APP_PORT_SOCKS5
labelEn: SOCKS5 Port
labelZh: SOCKS5 端口
label:
en: SOCKS5 Port
zh: SOCKS5 端口
zh-Hant: SOCKS5 連接埠
ja: SOCKS5 ポート
ko: SOCKS5 포트
ru: Порт SOCKS5
ms: Port SOCKS5
pt-br: Porta SOCKS5
required: true
rule: paramPort
type: number
edit: true
- default: 8080
envKey: PANEL_APP_PORT_HTTP_PROXY
labelEn: HTTP Proxy Port
labelZh: HTTP 代理端口
label:
en: HTTP Proxy Port
zh: HTTP 代理端口
zh-Hant: HTTP 代理連接埠
ja: HTTP プロキシポート
ko: HTTP 프록시 포트
ru: Порт HTTP-прокси
ms: Port proksi HTTP
pt-br: Porta do proxy HTTP
required: true
rule: paramPort
type: number
edit: true
- default: 1080
envKey: BIND_PORT
labelEn: Internal Bind Port
labelZh: 容器内部绑定端口
label:
en: Internal Bind Port
zh: 容器内部绑定端口
zh-Hant: 容器內部綁定連接埠
ja: コンテナ内部バインドポート
ko: 컨테이너 내부 바인드 포트
ru: Внутренний порт привязки контейнера
ms: Port bind dalaman kontena
pt-br: Porta interna de bind do contêiner
required: true
rule: paramPort
type: number
edit: true
- default: ""
envKey: SOCKS_USER
labelEn: SOCKS Username
labelZh: SOCKS 用户名
label:
en: SOCKS Username
zh: SOCKS 用户名
zh-Hant: SOCKS 使用者名稱
ja: SOCKS ユーザー名
ko: SOCKS 사용자명
ru: Имя пользователя SOCKS
ms: Nama pengguna SOCKS
pt-br: Usuário SOCKS
required: false
type: text
edit: true
- default: ""
envKey: SOCKS_PASS
labelEn: SOCKS Password
labelZh: SOCKS 密码
label:
en: SOCKS Password
zh: SOCKS 密码
zh-Hant: SOCKS 密碼
ja: SOCKS パスワード
ko: SOCKS 비밀번호
ru: Пароль SOCKS
ms: Kata laluan SOCKS
pt-br: Senha SOCKS
required: false
type: text
edit: true
- default: ""
envKey: ENDPOINT_IP
labelEn: WARP Endpoint
labelZh: WARP 终结点
label:
en: WARP Endpoint
zh: WARP 终结点
zh-Hant: WARP 終結點
ja: WARP エンドポイント
ko: WARP 엔드포인트
ru: WARP endpoint
ms: Titik akhir WARP
pt-br: Endpoint WARP
required: false
type: text
edit: true
- default: ""
envKey: GH_PROXY
labelEn: wgcf Download Proxy
labelZh: wgcf 下载代理
label:
en: wgcf Download Proxy
zh: wgcf 下载代理
zh-Hant: wgcf 下載代理
ja: wgcf ダウンロードプロキシ
ko: wgcf 다운로드 프록시
ru: Прокси для загрузки wgcf
ms: Proksi muat turun wgcf
pt-br: Proxy de download do wgcf
required: false
type: text
edit: true
- default: 100.64.0.0/10
envKey: TAILSCALE_CIDR
labelEn: Tailscale Return Route CIDR
labelZh: Tailscale 回程路由 CIDR
label:
en: Tailscale Return Route CIDR
zh: Tailscale 回程路由 CIDR
zh-Hant: Tailscale 回程路由 CIDR
ja: Tailscale 戻りルート CIDR
ko: Tailscale 리턴 라우트 CIDR
ru: CIDR обратного маршрута Tailscale
ms: CIDR laluan balik Tailscale
pt-br: CIDR de rota de retorno do Tailscale
required: true
type: text
edit: true
- default: 1280
envKey: MTU
labelEn: WireGuard MTU
labelZh: WireGuard MTU
label:
en: WireGuard MTU
zh: WireGuard MTU
zh-Hant: WireGuard MTU
ja: WireGuard MTU
ko: WireGuard MTU
ru: MTU WireGuard
ms: MTU WireGuard
pt-br: MTU do WireGuard
required: true
type: number
edit: true
- default: ""
envKey: WARP_WGCF_CONF
labelEn: WireGuard Config Content
labelZh: WireGuard 配置内容
label:
en: WireGuard Config Content
zh: WireGuard 配置内容
zh-Hant: WireGuard 設定內容
ja: WireGuard 設定内容
ko: WireGuard 구성 내용
ru: Содержимое конфигурации WireGuard
ms: Kandungan konfigurasi WireGuard
pt-br: Conteúdo da configuração WireGuard
required: false
type: text
edit: true
- default: Asia/Shanghai
envKey: TZ
labelEn: Timezone
labelZh: 时区
label:
en: Timezone
zh: 时区
zh-Hant: 時區
ja: タイムゾーン
ko: 시간대
ru: Часовой пояс
ms: Zon waktu
pt-br: Fuso horário
required: true
type: text
edit: true
- default: ./data
envKey: APP_DATA_DIR_1
labelEn: WireGuard Data Directory
labelZh: WireGuard 数据目录
label:
en: WireGuard Data Directory
zh: WireGuard 数据目录
zh-Hant: WireGuard 資料目錄
ja: WireGuard データディレクトリ
ko: WireGuard 데이터 디렉터리
ru: Каталог данных WireGuard
ms: Direktori data WireGuard
pt-br: Diretório de dados do WireGuard
required: true
type: text
edit: true
@@ -0,0 +1,51 @@
services:
microwarp:
image: "ghcr.io/ccbkkb/microwarp:latest"
container_name: ${CONTAINER_NAME}
labels:
createdBy: "Apps"
ports:
- "${PANEL_APP_PORT_SOCKS5}:${BIND_PORT}"
environment:
- TZ=${TZ}
- BIND_ADDR=0.0.0.0
- BIND_PORT=${BIND_PORT}
- SOCKS_USER=${SOCKS_USER:-}
- SOCKS_PASS=${SOCKS_PASS:-}
- ENDPOINT_IP=${ENDPOINT_IP:-}
- GH_PROXY=${GH_PROXY:-}
- TAILSCALE_CIDR=${TAILSCALE_CIDR:-100.64.0.0/10}
- MTU=${MTU}
- WARP_WGCF_CONF=${WARP_WGCF_CONF:-}
cap_add:
- NET_ADMIN
- SYS_MODULE
sysctls:
- net.ipv4.conf.all.src_valid_mark=1
volumes:
- "${APP_DATA_DIR_1}:/etc/wireguard"
restart: always
networks:
- 1panel-network
http-proxy:
image: "gogost/gost:latest"
container_name: ${CONTAINER_NAME}-http
labels:
createdBy: "Apps"
depends_on:
- microwarp
command:
- "-L"
- "http://0.0.0.0:8080"
- "-F"
- "socks5://microwarp:${BIND_PORT}"
ports:
- "${PANEL_APP_PORT_HTTP_PROXY}:8080"
restart: always
networks:
- 1panel-network
networks:
1panel-network:
external: true
@@ -0,0 +1,38 @@
#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR="$(CDPATH="" cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd -P)"
APP_ROOT="${APP_ROOT:-$(CDPATH="" cd -- "${SCRIPT_DIR}/.." && pwd -P)}"
ENV_FILE="${ENV_FILE:-${APP_ROOT}/.env}"
if [[ "${ENV_FILE}" != /* ]]; then
ENV_FILE="${APP_ROOT}/${ENV_FILE#./}"
fi
strip_matching_quotes() {
local value="$1"
case "${value}" in
\"*\")
value="${value#\"}"
value="${value%\"}"
;;
\'*\')
value="${value#\'}"
value="${value%\'}"
;;
esac
printf '%s\n' "${value}"
}
app_data_dir="${APP_DATA_DIR_1:-}"
if [[ -z "${app_data_dir}" && -f "${ENV_FILE}" ]]; then
app_data_dir="$(sed -n 's/^APP_DATA_DIR_1=//p' "${ENV_FILE}" | tail -n 1)"
fi
app_data_dir="$(strip_matching_quotes "${app_data_dir}")"
app_data_dir="${app_data_dir:-./data}"
if [[ "${app_data_dir}" != /* ]]; then
app_data_dir="${APP_ROOT}/${app_data_dir#./}"
fi
mkdir -p "${app_data_dir}"
+2
View File
@@ -0,0 +1,2 @@
#!/bin/bash
docker-compose down --volumes
@@ -0,0 +1,39 @@
#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR="$(CDPATH="" cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd -P)"
APP_ROOT="${APP_ROOT:-$(CDPATH="" cd -- "${SCRIPT_DIR}/.." && pwd -P)}"
ENV_FILE="${ENV_FILE:-${APP_ROOT}/.env}"
if [[ "${ENV_FILE}" != /* ]]; then
ENV_FILE="${APP_ROOT}/${ENV_FILE#./}"
fi
strip_matching_quotes() {
local value="$1"
case "${value}" in
\"*\")
value="${value#\"}"
value="${value%\"}"
;;
\'*\')
value="${value#\'}"
value="${value%\'}"
;;
esac
printf '%s\n' "${value}"
}
app_data_dir="${APP_DATA_DIR_1:-}"
if [[ -z "${app_data_dir}" && -f "${ENV_FILE}" ]]; then
app_data_dir="$(sed -n 's/^APP_DATA_DIR_1=//p' "${ENV_FILE}" | tail -n 1)"
fi
app_data_dir="$(strip_matching_quotes "${app_data_dir}")"
app_data_dir="${app_data_dir:-./data}"
if [[ "${app_data_dir}" != /* ]]; then
app_data_dir="${APP_ROOT}/${app_data_dir#./}"
fi
mkdir -p "${app_data_dir}"
exit 0
+12
View File
@@ -0,0 +1,12 @@
CONTAINER_NAME=microwarp
PANEL_APP_PORT_SOCKS5=1080
BIND_PORT=1080
SOCKS_USER=
SOCKS_PASS=
ENDPOINT_IP=
GH_PROXY=
TAILSCALE_CIDR=100.64.0.0/10
MTU=1280
WARP_WGCF_CONF=
TZ=Asia/Shanghai
APP_DATA_DIR_1=./data
+180
View File
@@ -0,0 +1,180 @@
additionalProperties:
formFields:
- default: 1080
envKey: PANEL_APP_PORT_SOCKS5
labelEn: SOCKS5 Port
labelZh: SOCKS5 端口
label:
en: SOCKS5 Port
zh: SOCKS5 端口
zh-Hant: SOCKS5 連接埠
ja: SOCKS5 ポート
ko: SOCKS5 포트
ru: Порт SOCKS5
ms: Port SOCKS5
pt-br: Porta SOCKS5
required: true
rule: paramPort
type: number
edit: true
- default: 1080
envKey: BIND_PORT
labelEn: Internal Bind Port
labelZh: 容器内部绑定端口
label:
en: Internal Bind Port
zh: 容器内部绑定端口
zh-Hant: 容器內部綁定連接埠
ja: コンテナ内部バインドポート
ko: 컨테이너 내부 바인드 포트
ru: Внутренний порт привязки контейнера
ms: Port bind dalaman kontena
pt-br: Porta interna de bind do contêiner
required: true
rule: paramPort
type: number
edit: true
- default: ""
envKey: SOCKS_USER
labelEn: SOCKS Username
labelZh: SOCKS 用户名
label:
en: SOCKS Username
zh: SOCKS 用户名
zh-Hant: SOCKS 使用者名稱
ja: SOCKS ユーザー名
ko: SOCKS 사용자명
ru: Имя пользователя SOCKS
ms: Nama pengguna SOCKS
pt-br: Usuário SOCKS
required: false
type: text
edit: true
- default: ""
envKey: SOCKS_PASS
labelEn: SOCKS Password
labelZh: SOCKS 密码
label:
en: SOCKS Password
zh: SOCKS 密码
zh-Hant: SOCKS 密碼
ja: SOCKS パスワード
ko: SOCKS 비밀번호
ru: Пароль SOCKS
ms: Kata laluan SOCKS
pt-br: Senha SOCKS
required: false
type: text
edit: true
- default: ""
envKey: ENDPOINT_IP
labelEn: WARP Endpoint
labelZh: WARP 终结点
label:
en: WARP Endpoint
zh: WARP 终结点
zh-Hant: WARP 終結點
ja: WARP エンドポイント
ko: WARP 엔드포인트
ru: WARP endpoint
ms: Titik akhir WARP
pt-br: Endpoint WARP
required: false
type: text
edit: true
- default: ""
envKey: GH_PROXY
labelEn: wgcf Download Proxy
labelZh: wgcf 下载代理
label:
en: wgcf Download Proxy
zh: wgcf 下载代理
zh-Hant: wgcf 下載代理
ja: wgcf ダウンロードプロキシ
ko: wgcf 다운로드 프록시
ru: Прокси для загрузки wgcf
ms: Proksi muat turun wgcf
pt-br: Proxy de download do wgcf
required: false
type: text
edit: true
- default: 100.64.0.0/10
envKey: TAILSCALE_CIDR
labelEn: Tailscale Return Route CIDR
labelZh: Tailscale 回程路由 CIDR
label:
en: Tailscale Return Route CIDR
zh: Tailscale 回程路由 CIDR
zh-Hant: Tailscale 回程路由 CIDR
ja: Tailscale 戻りルート CIDR
ko: Tailscale 리턴 라우트 CIDR
ru: CIDR обратного маршрута Tailscale
ms: CIDR laluan balik Tailscale
pt-br: CIDR de rota de retorno do Tailscale
required: true
type: text
edit: true
- default: 1280
envKey: MTU
labelEn: WireGuard MTU
labelZh: WireGuard MTU
label:
en: WireGuard MTU
zh: WireGuard MTU
zh-Hant: WireGuard MTU
ja: WireGuard MTU
ko: WireGuard MTU
ru: MTU WireGuard
ms: MTU WireGuard
pt-br: MTU do WireGuard
required: true
type: number
edit: true
- default: ""
envKey: WARP_WGCF_CONF
labelEn: WireGuard Config Content
labelZh: WireGuard 配置内容
label:
en: WireGuard Config Content
zh: WireGuard 配置内容
zh-Hant: WireGuard 設定內容
ja: WireGuard 設定内容
ko: WireGuard 구성 내용
ru: Содержимое конфигурации WireGuard
ms: Kandungan konfigurasi WireGuard
pt-br: Conteúdo da configuração WireGuard
required: false
type: text
edit: true
- default: Asia/Shanghai
envKey: TZ
labelEn: Timezone
labelZh: 时区
label:
en: Timezone
zh: 时区
zh-Hant: 時區
ja: タイムゾーン
ko: 시간대
ru: Часовой пояс
ms: Zon waktu
pt-br: Fuso horário
required: true
type: text
edit: true
- default: ./data
envKey: APP_DATA_DIR_1
labelEn: WireGuard Data Directory
labelZh: WireGuard 数据目录
label:
en: WireGuard Data Directory
zh: WireGuard 数据目录
zh-Hant: WireGuard 資料目錄
ja: WireGuard データディレクトリ
ko: WireGuard 데이터 디렉터리
ru: Каталог данных WireGuard
ms: Direktori data WireGuard
pt-br: Diretório de dados do WireGuard
required: true
type: text
edit: true
+33
View File
@@ -0,0 +1,33 @@
services:
microwarp:
image: "ghcr.io/ccbkkb/microwarp:latest"
container_name: ${CONTAINER_NAME}
labels:
createdBy: "Apps"
ports:
- "${PANEL_APP_PORT_SOCKS5}:${BIND_PORT}"
environment:
- TZ=${TZ}
- BIND_ADDR=0.0.0.0
- BIND_PORT=${BIND_PORT}
- SOCKS_USER=${SOCKS_USER:-}
- SOCKS_PASS=${SOCKS_PASS:-}
- ENDPOINT_IP=${ENDPOINT_IP:-}
- GH_PROXY=${GH_PROXY:-}
- TAILSCALE_CIDR=${TAILSCALE_CIDR:-100.64.0.0/10}
- MTU=${MTU}
- WARP_WGCF_CONF=${WARP_WGCF_CONF:-}
cap_add:
- NET_ADMIN
- SYS_MODULE
sysctls:
- net.ipv4.conf.all.src_valid_mark=1
volumes:
- "${APP_DATA_DIR_1}:/etc/wireguard"
restart: always
networks:
- 1panel-network
networks:
1panel-network:
external: true
+38
View File
@@ -0,0 +1,38 @@
#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR="$(CDPATH="" cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd -P)"
APP_ROOT="${APP_ROOT:-$(CDPATH="" cd -- "${SCRIPT_DIR}/.." && pwd -P)}"
ENV_FILE="${ENV_FILE:-${APP_ROOT}/.env}"
if [[ "${ENV_FILE}" != /* ]]; then
ENV_FILE="${APP_ROOT}/${ENV_FILE#./}"
fi
strip_matching_quotes() {
local value="$1"
case "${value}" in
\"*\")
value="${value#\"}"
value="${value%\"}"
;;
\'*\')
value="${value#\'}"
value="${value%\'}"
;;
esac
printf '%s\n' "${value}"
}
app_data_dir="${APP_DATA_DIR_1:-}"
if [[ -z "${app_data_dir}" && -f "${ENV_FILE}" ]]; then
app_data_dir="$(sed -n 's/^APP_DATA_DIR_1=//p' "${ENV_FILE}" | tail -n 1)"
fi
app_data_dir="$(strip_matching_quotes "${app_data_dir}")"
app_data_dir="${app_data_dir:-./data}"
if [[ "${app_data_dir}" != /* ]]; then
app_data_dir="${APP_ROOT}/${app_data_dir#./}"
fi
mkdir -p "${app_data_dir}"
+2
View File
@@ -0,0 +1,2 @@
#!/bin/bash
docker-compose down --volumes
+39
View File
@@ -0,0 +1,39 @@
#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR="$(CDPATH="" cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd -P)"
APP_ROOT="${APP_ROOT:-$(CDPATH="" cd -- "${SCRIPT_DIR}/.." && pwd -P)}"
ENV_FILE="${ENV_FILE:-${APP_ROOT}/.env}"
if [[ "${ENV_FILE}" != /* ]]; then
ENV_FILE="${APP_ROOT}/${ENV_FILE#./}"
fi
strip_matching_quotes() {
local value="$1"
case "${value}" in
\"*\")
value="${value#\"}"
value="${value%\"}"
;;
\'*\')
value="${value#\'}"
value="${value%\'}"
;;
esac
printf '%s\n' "${value}"
}
app_data_dir="${APP_DATA_DIR_1:-}"
if [[ -z "${app_data_dir}" && -f "${ENV_FILE}" ]]; then
app_data_dir="$(sed -n 's/^APP_DATA_DIR_1=//p' "${ENV_FILE}" | tail -n 1)"
fi
app_data_dir="$(strip_matching_quotes "${app_data_dir}")"
app_data_dir="${app_data_dir:-./data}"
if [[ "${app_data_dir}" != /* ]]; then
app_data_dir="${APP_ROOT}/${app_data_dir#./}"
fi
mkdir -p "${app_data_dir}"
exit 0
Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

@@ -0,0 +1,18 @@
CONTAINER_NAME=microwarp
PANEL_APP_PORT_SOCKS5=1080
PANEL_APP_PORT_HTTP_PROXY=8080
BIND_PORT=1080
ROTATE_INTERVAL_MINUTES=0
ROTATE_MAX_ATTEMPTS=5
ROTATE_RETRY_DELAY_SECONDS=5
TEST_URL=https://cloudflare.com/cdn-cgi/trace
IP_CHECK_URL=https://cloudflare.com/cdn-cgi/trace
SOCKS_USER=
SOCKS_PASS=
ENDPOINT_IP=
GH_PROXY=
TAILSCALE_CIDR=100.64.0.0/10
MTU=1280
WARP_WGCF_CONF=
TZ=Asia/Shanghai
APP_DATA_DIR_1=./data
+277
View File
@@ -0,0 +1,277 @@
additionalProperties:
formFields:
- default: 1080
envKey: PANEL_APP_PORT_SOCKS5
labelEn: SOCKS5 Port
labelZh: SOCKS5 端口
label:
en: SOCKS5 Port
zh: SOCKS5 端口
zh-Hant: SOCKS5 連接埠
ja: SOCKS5 ポート
ko: SOCKS5 포트
ru: Порт SOCKS5
ms: Port SOCKS5
pt-br: Porta SOCKS5
required: true
rule: paramPort
type: number
edit: true
- default: 8080
envKey: PANEL_APP_PORT_HTTP_PROXY
labelEn: HTTP Proxy Port
labelZh: HTTP 代理端口
label:
en: HTTP Proxy Port
zh: HTTP 代理端口
zh-Hant: HTTP 代理連接埠
ja: HTTP プロキシポート
ko: HTTP 프록시 포트
ru: Порт HTTP-прокси
ms: Port proksi HTTP
pt-br: Porta do proxy HTTP
required: true
rule: paramPort
type: number
edit: true
- default: 1080
envKey: BIND_PORT
labelEn: Internal Bind Port
labelZh: 容器内部绑定端口
label:
en: Internal Bind Port
zh: 容器内部绑定端口
zh-Hant: 容器內部綁定連接埠
ja: コンテナ内部バインドポート
ko: 컨테이너 내부 바인드 포트
ru: Внутренний порт привязки контейнера
ms: Port bind dalaman kontena
pt-br: Porta interna de bind do contêiner
required: true
rule: paramPort
type: number
edit: true
- default: 0
envKey: ROTATE_INTERVAL_MINUTES
labelEn: Rotate Interval Minutes
labelZh: 轮换间隔(分钟)
label:
en: Rotate Interval Minutes
zh: 轮换间隔(分钟)
zh-Hant: 輪換間隔(分鐘)
ja: ローテーション間隔(分)
ko: 로테이션 간격(분)
ru: Интервал ротации (минуты)
ms: Selang putaran (minit)
pt-br: Intervalo de rotação (minutos)
required: true
type: number
edit: true
- default: 5
envKey: ROTATE_MAX_ATTEMPTS
labelEn: Rotation Attempts
labelZh: 轮换尝试次数
label:
en: Rotation Attempts
zh: 轮换尝试次数
zh-Hant: 輪換嘗試次數
ja: ローテーション試行回数
ko: 로테이션 시도 횟수
ru: Количество попыток ротации
ms: Bilangan percubaan putaran
pt-br: Número de tentativas de rotação
required: true
type: number
edit: true
- default: 5
envKey: ROTATE_RETRY_DELAY_SECONDS
labelEn: Rotation Retry Delay Seconds
labelZh: 轮换重试间隔(秒)
label:
en: Rotation Retry Delay Seconds
zh: 轮换重试间隔(秒)
zh-Hant: 輪換重試間隔(秒)
ja: ローテーション再試行間隔(秒)
ko: 로테이션 재시도 간격(초)
ru: Задержка между попытками ротации (секунды)
ms: Kelewatan percubaan semula putaran (saat)
pt-br: Atraso entre tentativas de rotação (segundos)
required: true
type: number
edit: true
- default: https://cloudflare.com/cdn-cgi/trace
envKey: TEST_URL
labelEn: Probe URL
labelZh: 探测 URL
label:
en: Probe URL
zh: 探测 URL
zh-Hant: 探測 URL
ja: プローブ URL
ko: 점검 URL
ru: URL проверки
ms: URL ujian
pt-br: URL de verificação
required: true
type: text
edit: true
- default: https://cloudflare.com/cdn-cgi/trace
envKey: IP_CHECK_URL
labelEn: Egress IP Check URL
labelZh: 出口 IP 检查 URL
label:
en: Egress IP Check URL
zh: 出口 IP 检查 URL
zh-Hant: 出口 IP 檢查 URL
ja: 出口 IP 確認 URL
ko: 출구 IP 확인 URL
ru: URL проверки исходного IP
ms: URL semakan IP keluar
pt-br: URL de verificação do IP de saída
required: true
type: text
edit: true
- default: ""
envKey: SOCKS_USER
labelEn: SOCKS Username
labelZh: SOCKS 用户名
label:
en: SOCKS Username
zh: SOCKS 用户名
zh-Hant: SOCKS 使用者名稱
ja: SOCKS ユーザー名
ko: SOCKS 사용자명
ru: Имя пользователя SOCKS
ms: Nama pengguna SOCKS
pt-br: Usuário SOCKS
required: false
type: text
edit: true
- default: ""
envKey: SOCKS_PASS
labelEn: SOCKS Password
labelZh: SOCKS 密码
label:
en: SOCKS Password
zh: SOCKS 密码
zh-Hant: SOCKS 密碼
ja: SOCKS パスワード
ko: SOCKS 비밀번호
ru: Пароль SOCKS
ms: Kata laluan SOCKS
pt-br: Senha SOCKS
required: false
type: text
edit: true
- default: ""
envKey: ENDPOINT_IP
labelEn: WARP Endpoint
labelZh: WARP 终结点
label:
en: WARP Endpoint
zh: WARP 终结点
zh-Hant: WARP 終結點
ja: WARP エンドポイント
ko: WARP 엔드포인트
ru: WARP endpoint
ms: Titik akhir WARP
pt-br: Endpoint WARP
required: false
type: text
edit: true
- default: ""
envKey: GH_PROXY
labelEn: wgcf Download Proxy
labelZh: wgcf 下载代理
label:
en: wgcf Download Proxy
zh: wgcf 下载代理
zh-Hant: wgcf 下載代理
ja: wgcf ダウンロードプロキシ
ko: wgcf 다운로드 프록시
ru: Прокси для загрузки wgcf
ms: Proksi muat turun wgcf
pt-br: Proxy de download do wgcf
required: false
type: text
edit: true
- default: 100.64.0.0/10
envKey: TAILSCALE_CIDR
labelEn: Tailscale Return Route CIDR
labelZh: Tailscale 回程路由 CIDR
label:
en: Tailscale Return Route CIDR
zh: Tailscale 回程路由 CIDR
zh-Hant: Tailscale 回程路由 CIDR
ja: Tailscale 戻りルート CIDR
ko: Tailscale 리턴 라우트 CIDR
ru: CIDR обратного маршрута Tailscale
ms: CIDR laluan balik Tailscale
pt-br: CIDR de rota de retorno do Tailscale
required: true
type: text
edit: true
- default: 1280
envKey: MTU
labelEn: WireGuard MTU
labelZh: WireGuard MTU
label:
en: WireGuard MTU
zh: WireGuard MTU
zh-Hant: WireGuard MTU
ja: WireGuard MTU
ko: WireGuard MTU
ru: MTU WireGuard
ms: MTU WireGuard
pt-br: MTU do WireGuard
required: true
type: number
edit: true
- default: ""
envKey: WARP_WGCF_CONF
labelEn: WireGuard Config Content
labelZh: WireGuard 配置内容
label:
en: WireGuard Config Content
zh: WireGuard 配置内容
zh-Hant: WireGuard 設定內容
ja: WireGuard 設定内容
ko: WireGuard 구성 내용
ru: Содержимое конфигурации WireGuard
ms: Kandungan konfigurasi WireGuard
pt-br: Conteúdo da configuração WireGuard
required: false
type: text
edit: true
- default: Asia/Shanghai
envKey: TZ
labelEn: Timezone
labelZh: 时区
label:
en: Timezone
zh: 时区
zh-Hant: 時區
ja: タイムゾーン
ko: 시간대
ru: Часовой пояс
ms: Zon waktu
pt-br: Fuso horário
required: true
type: text
edit: true
- default: ./data
envKey: APP_DATA_DIR_1
labelEn: WireGuard Data Directory
labelZh: WireGuard 数据目录
label:
en: WireGuard Data Directory
zh: WireGuard 数据目录
zh-Hant: WireGuard 資料目錄
ja: WireGuard データディレクトリ
ko: WireGuard 데이터 디렉터리
ru: Каталог данных WireGuard
ms: Direktori data WireGuard
pt-br: Diretório de dados do WireGuard
required: true
type: text
edit: true
@@ -0,0 +1,58 @@
services:
microwarp:
image: "ghcr.io/ccbkkb/microwarp:0.1.3"
container_name: ${CONTAINER_NAME}
labels:
createdBy: "Apps"
ports:
- "${PANEL_APP_PORT_SOCKS5}:${BIND_PORT}"
environment:
- TZ=${TZ}
- BIND_ADDR=0.0.0.0
- BIND_PORT=${BIND_PORT}
- ROTATE_INTERVAL_MINUTES=${ROTATE_INTERVAL_MINUTES:-0}
- ROTATE_MAX_ATTEMPTS=${ROTATE_MAX_ATTEMPTS:-5}
- ROTATE_RETRY_DELAY_SECONDS=${ROTATE_RETRY_DELAY_SECONDS:-5}
- TEST_URL=${TEST_URL}
- IP_CHECK_URL=${IP_CHECK_URL:-https://cloudflare.com/cdn-cgi/trace}
- SOCKS_USER=${SOCKS_USER:-}
- SOCKS_PASS=${SOCKS_PASS:-}
- ENDPOINT_IP=${ENDPOINT_IP:-}
- GH_PROXY=${GH_PROXY:-}
- TAILSCALE_CIDR=${TAILSCALE_CIDR:-100.64.0.0/10}
- MTU=${MTU}
- WARP_WGCF_CONF=${WARP_WGCF_CONF:-}
cap_add:
- NET_ADMIN
- SYS_MODULE
sysctls:
- net.ipv4.conf.all.src_valid_mark=1
volumes:
- "${APP_DATA_DIR_1}:/etc/wireguard"
- "./rotate.sh:/usr/local/bin/rotate.sh:ro"
restart: always
command: ["/bin/sh", "/usr/local/bin/rotate.sh"]
networks:
- 1panel-network
http-proxy:
image: "gogost/gost:latest"
container_name: ${CONTAINER_NAME}-http
labels:
createdBy: "Apps"
depends_on:
- microwarp
command:
- "-L"
- "http://0.0.0.0:8080"
- "-F"
- "socks5://microwarp:${BIND_PORT}"
ports:
- "${PANEL_APP_PORT_HTTP_PROXY}:8080"
restart: always
networks:
- 1panel-network
networks:
1panel-network:
external: true
@@ -0,0 +1,300 @@
#!/usr/bin/env sh
set -eu
INTERVAL_MINUTES="${ROTATE_INTERVAL_MINUTES:-0}"
ROTATE_MAX_ATTEMPTS="${ROTATE_MAX_ATTEMPTS:-5}"
ROTATE_RETRY_DELAY_SECONDS="${ROTATE_RETRY_DELAY_SECONDS:-5}"
TEST_URL="${TEST_URL:-https://cloudflare.com/cdn-cgi/trace}"
IP_CHECK_URL="${IP_CHECK_URL:-https://cloudflare.com/cdn-cgi/trace}"
SOCKS_ADDR="127.0.0.1:${BIND_PORT:-1080}"
SOCKS_USER_VALUE="${SOCKS_USER:-}"
SOCKS_PASS_VALUE="${SOCKS_PASS:-}"
WG_DIR="/etc/wireguard"
WG_CONF="${WG_DIR}/wg0.conf"
PREVIOUS_CONF="${WG_CONF}.rotation-previous"
FIXED_CONFIG="${WARP_WGCF_CONF:-}"
LOG_PREFIX="[microwarp]"
ENTRY_PID=""
CURRENT_EGRESS_IP=""
log() {
echo "${LOG_PREFIX} $*"
}
is_non_negative_int() {
case "$1" in
''|*[!0-9]*)
return 1
;;
*)
return 0
;;
esac
}
is_positive_int() {
case "$1" in
''|*[!0-9]*|0)
return 1
;;
*)
return 0
;;
esac
}
is_ipv4() {
printf '%s\n' "$1" | awk -F. '
NF == 4 {
for (i = 1; i <= 4; i++) {
if ($i !~ /^[0-9]+$/ || $i < 0 || $i > 255) exit 1
}
exit 0
}
{ exit 1 }
'
}
probe_ip() {
if ! command -v curl >/dev/null 2>&1; then
return 1
fi
curl_via_socks "$TEST_URL" >/dev/null 2>&1
}
curl_via_socks() {
url="$1"
if [ -n "$SOCKS_USER_VALUE" ] && [ -n "$SOCKS_PASS_VALUE" ]; then
curl --max-time 20 --socks5-hostname "$SOCKS_ADDR" \
--proxy-user "${SOCKS_USER_VALUE}:${SOCKS_PASS_VALUE}" "$url"
return $?
fi
curl --max-time 20 --socks5-hostname "$SOCKS_ADDR" "$url"
}
get_egress_ip() {
if ! command -v curl >/dev/null 2>&1; then
return 1
fi
trace_output="$(curl_via_socks "$IP_CHECK_URL" 2>/dev/null)" || return 1
current_ip="$(printf '%s\n' "$trace_output" |
awk -F= '$1 == "ip" { print $2; exit }' | tr -d '\r')"
is_ipv4 "$current_ip" && printf '%s\n' "$current_ip"
}
record_egress_ip() {
current_ip="$(get_egress_ip || true)"
if [ -n "$current_ip" ]; then
CURRENT_EGRESS_IP="$current_ip"
log "current WARP egress IPv4: ${CURRENT_EGRESS_IP}"
else
CURRENT_EGRESS_IP=""
log "unable to verify current WARP egress IPv4"
fi
return 0
}
inject_fixed_config() {
if [ -z "$FIXED_CONFIG" ]; then
return 0
fi
mkdir -p "$WG_DIR"
printf '%s\n' "$FIXED_CONFIG" > "$WG_CONF"
chmod 600 "$WG_CONF" || true
log "wg0.conf injected from WARP_WGCF_CONF"
}
wait_for_proxy() {
attempts=0
while [ "$attempts" -lt 30 ]; do
if [ -n "$ENTRY_PID" ] && ! kill -0 "$ENTRY_PID" 2>/dev/null; then
wait "$ENTRY_PID" || true
ENTRY_PID=""
return 1
fi
if probe_ip; then
return 0
fi
attempts=$((attempts + 1))
sleep 2
done
return 1
}
stop_runtime() {
if [ -n "$ENTRY_PID" ] && kill -0 "$ENTRY_PID" 2>/dev/null; then
kill "$ENTRY_PID" 2>/dev/null || true
wait "$ENTRY_PID" || true
fi
ENTRY_PID=""
}
teardown_for_rotation() {
stop_runtime
if ip link show wg0 >/dev/null 2>&1; then
wg-quick down wg0 >/dev/null 2>&1 || true
fi
rm -f "$WG_CONF" "$WG_DIR/wgcf-account.toml" "$WG_DIR/wgcf-profile.conf" "$WG_DIR/extra.env"
}
start_runtime() {
CURRENT_EGRESS_IP=""
inject_fixed_config
log "starting MicroWARP runtime"
/app/entrypoint.sh &
ENTRY_PID=$!
if ! wait_for_proxy; then
log "runtime did not become ready in time"
stop_runtime
return 1
fi
record_egress_ip
if [ -z "$ENTRY_PID" ] || ! kill -0 "$ENTRY_PID" 2>/dev/null; then
log "runtime exited while its egress was being checked"
stop_runtime
return 1
fi
return 0
}
save_previous_config() {
if [ -s "$WG_CONF" ]; then
previous_tmp="${PREVIOUS_CONF}.tmp.$$"
rm -f "$previous_tmp"
if ! cp -p "$WG_CONF" "$previous_tmp"; then
rm -f "$previous_tmp"
return 1
fi
if ! mv -f "$previous_tmp" "$PREVIOUS_CONF"; then
rm -f "$previous_tmp"
return 1
fi
return 0
fi
return 1
}
restore_previous_runtime() {
if [ ! -s "$PREVIOUS_CONF" ]; then
return 1
fi
log "restoring the previous working WARP configuration"
teardown_for_rotation
if ! cp -p "$PREVIOUS_CONF" "$WG_CONF"; then
log "unable to restore the previous WARP configuration"
return 1
fi
start_runtime
}
rotate_once() {
if [ -z "$ENTRY_PID" ] || ! kill -0 "$ENTRY_PID" 2>/dev/null; then
log "WARP runtime exited before the scheduled identity rebuild"
return 1
fi
record_egress_ip
old_ip="$CURRENT_EGRESS_IP"
if [ -z "$old_ip" ]; then
log "scheduled identity rebuild skipped because the current WARP egress IPv4 could not be verified"
return 0
fi
attempt=1
if ! save_previous_config; then
log "scheduled identity rebuild skipped because the working WARP configuration could not be backed up"
return 0
fi
while [ "$attempt" -le "$ROTATE_MAX_ATTEMPTS" ]; do
log "WARP identity rebuild attempt ${attempt}/${ROTATE_MAX_ATTEMPTS}"
teardown_for_rotation
if start_runtime; then
new_ip="$CURRENT_EGRESS_IP"
if [ -n "$new_ip" ] &&
[ "$new_ip" != "$old_ip" ]; then
rm -f "$PREVIOUS_CONF"
log "WARP egress IPv4 changed: ${old_ip:-unknown} -> ${new_ip}"
return 0
fi
if [ -n "$new_ip" ] && [ "$new_ip" = "$old_ip" ]; then
log "WARP egress IPv4 stayed ${new_ip}; retrying identity rebuild"
else
log "WARP egress IPv4 could not be verified; retrying identity rebuild"
fi
else
log "WARP runtime failed during identity rebuild attempt ${attempt}"
fi
if [ "$attempt" -lt "$ROTATE_MAX_ATTEMPTS" ]; then
sleep "$ROTATE_RETRY_DELAY_SECONDS"
fi
attempt=$((attempt + 1))
done
if restore_previous_runtime; then
log "no different public IPv4 was observed; previous runtime restored"
rm -f "$PREVIOUS_CONF"
return 0
fi
log "rotation failed and no working runtime is available"
return 1
}
on_signal() {
log "received stop signal"
stop_runtime
exit 0
}
main() {
interval="$INTERVAL_MINUTES"
if ! is_non_negative_int "$interval"; then
log "invalid ROTATE_INTERVAL_MINUTES=${interval}, fallback to 0"
interval=0
fi
if ! is_positive_int "$ROTATE_MAX_ATTEMPTS"; then
log "invalid ROTATE_MAX_ATTEMPTS=${ROTATE_MAX_ATTEMPTS}, fallback to 5"
ROTATE_MAX_ATTEMPTS=5
fi
if ! is_non_negative_int "$ROTATE_RETRY_DELAY_SECONDS"; then
log "invalid ROTATE_RETRY_DELAY_SECONDS=${ROTATE_RETRY_DELAY_SECONDS}, fallback to 5"
ROTATE_RETRY_DELAY_SECONDS=5
fi
if [ -n "$FIXED_CONFIG" ] && [ "$interval" -gt 0 ]; then
log "WARP_WGCF_CONF is set, scheduled rotation disabled because the config is pinned"
interval=0
fi
trap on_signal INT TERM
if ! start_runtime; then
exit 1
fi
if [ "$interval" -eq 0 ]; then
log "scheduled rotation disabled"
wait "$ENTRY_PID"
exit $?
fi
log "scheduled WARP identity rebuild enabled every ${interval} minute(s); egress verification allows ${ROTATE_MAX_ATTEMPTS} attempt(s)"
while true; do
sleep "$((interval * 60))"
log "trigger scheduled WARP identity rebuild"
if ! rotate_once; then
log "rotation failed"
exit 1
fi
done
}
main "$@"
@@ -0,0 +1,38 @@
#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR="$(CDPATH="" cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd -P)"
APP_ROOT="${APP_ROOT:-$(CDPATH="" cd -- "${SCRIPT_DIR}/.." && pwd -P)}"
ENV_FILE="${ENV_FILE:-${APP_ROOT}/.env}"
if [[ "${ENV_FILE}" != /* ]]; then
ENV_FILE="${APP_ROOT}/${ENV_FILE#./}"
fi
strip_matching_quotes() {
local value="$1"
case "${value}" in
\"*\")
value="${value#\"}"
value="${value%\"}"
;;
\'*\')
value="${value#\'}"
value="${value%\'}"
;;
esac
printf '%s\n' "${value}"
}
app_data_dir="${APP_DATA_DIR_1:-}"
if [[ -z "${app_data_dir}" && -f "${ENV_FILE}" ]]; then
app_data_dir="$(sed -n 's/^APP_DATA_DIR_1=//p' "${ENV_FILE}" | tail -n 1)"
fi
app_data_dir="$(strip_matching_quotes "${app_data_dir}")"
app_data_dir="${app_data_dir:-./data}"
if [[ "${app_data_dir}" != /* ]]; then
app_data_dir="${APP_ROOT}/${app_data_dir#./}"
fi
mkdir -p "${app_data_dir}"
+2
View File
@@ -0,0 +1,2 @@
#!/bin/bash
docker-compose down --volumes
@@ -0,0 +1,112 @@
#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR="$(CDPATH="" cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd -P)"
APP_ROOT="${APP_ROOT:-$(CDPATH="" cd -- "${SCRIPT_DIR}/.." && pwd -P)}"
ENV_FILE="${ENV_FILE:-${APP_ROOT}/.env}"
TARGET_VERSION_DIR="0.1.3-rotator-http"
if [[ "${ENV_FILE}" != /* ]]; then
ENV_FILE="${APP_ROOT}/${ENV_FILE#./}"
fi
resource_dir_for_app() {
local panel_root app_path app_key
case "${APP_ROOT}" in
*/apps/local/*)
panel_root="${APP_ROOT%%/apps/local/*}"
app_path="${APP_ROOT#"${panel_root}/apps/local/"}"
app_key="${app_path%%/*}"
[[ -n "${panel_root}" && -n "${app_key}" ]] || return 1
printf '%s/resource/apps/local/%s\n' "${panel_root}" "${app_key}"
;;
*)
return 1
;;
esac
}
find_rotate_source() {
local resource_dir candidate_upgrade candidate_rotate
if [[ -n "${ROTATE_SOURCE:-}" ]]; then
printf '%s\n' "${ROTATE_SOURCE}"
return 0
fi
if resource_dir="$(resource_dir_for_app 2>/dev/null)" && [[ -d "${resource_dir}" ]]; then
candidate_upgrade="${resource_dir}/${TARGET_VERSION_DIR}/scripts/upgrade.sh"
candidate_rotate="${resource_dir}/${TARGET_VERSION_DIR}/rotate.sh"
if [[ -f "${candidate_upgrade}" && -f "${candidate_rotate}" ]] \
&& cmp -s "${SCRIPT_DIR}/upgrade.sh" "${candidate_upgrade}" \
&& grep -q 'ROTATE_MAX_ATTEMPTS' "${candidate_rotate}"; then
printf '%s\n' "${candidate_rotate}"
return 0
fi
fi
if [[ -f "${APP_ROOT}/rotate.sh" ]] \
&& grep -q 'ROTATE_MAX_ATTEMPTS' "${APP_ROOT}/rotate.sh"; then
printf '%s\n' "${APP_ROOT}/rotate.sh"
return 0
fi
return 1
}
strip_matching_quotes() {
local value="$1"
case "${value}" in
\"*\")
value="${value#\"}"
value="${value%\"}"
;;
\'*\')
value="${value#\'}"
value="${value%\'}"
;;
esac
printf '%s\n' "${value}"
}
rotate_target="${APP_ROOT}/rotate.sh"
rotate_source="$(find_rotate_source || true)"
if [[ -z "${rotate_source}" || ! -f "${rotate_source}" ]]; then
echo "Unable to locate the upgraded rotate.sh payload; refusing to keep the old script" >&2
exit 1
fi
if [[ "${rotate_source}" != "${rotate_target}" ]] \
&& ! cmp -s "${rotate_source}" "${rotate_target}"; then
rotate_tmp="$(mktemp "${rotate_target}.upgrade.XXXXXX")"
# shellcheck disable=SC2317
cleanup_rotate_tmp() {
rm -f -- "${rotate_tmp}"
}
trap cleanup_rotate_tmp EXIT
cp -- "${rotate_source}" "${rotate_tmp}"
if [[ -e "${rotate_target}" ]]; then
chmod --reference="${rotate_target}" "${rotate_tmp}" 2>/dev/null || true
else
chmod 0644 "${rotate_tmp}"
fi
mv -f -- "${rotate_tmp}" "${rotate_target}"
trap - EXIT
echo "Updated ${rotate_target} from the target version payload"
fi
app_data_dir="${APP_DATA_DIR_1:-}"
if [[ -z "${app_data_dir}" && -f "${ENV_FILE}" ]]; then
app_data_dir="$(sed -n 's/^APP_DATA_DIR_1=//p' "${ENV_FILE}" | tail -n 1)"
fi
app_data_dir="$(strip_matching_quotes "${app_data_dir}")"
app_data_dir="${app_data_dir:-./data}"
if [[ "${app_data_dir}" != /* ]]; then
app_data_dir="${APP_ROOT}/${app_data_dir#./}"
fi
mkdir -p "${app_data_dir}"
exit 0
+17
View File
@@ -0,0 +1,17 @@
CONTAINER_NAME=microwarp
PANEL_APP_PORT_SOCKS5=1080
BIND_PORT=1080
ROTATE_INTERVAL_MINUTES=0
ROTATE_MAX_ATTEMPTS=5
ROTATE_RETRY_DELAY_SECONDS=5
TEST_URL=https://cloudflare.com/cdn-cgi/trace
IP_CHECK_URL=https://cloudflare.com/cdn-cgi/trace
SOCKS_USER=
SOCKS_PASS=
ENDPOINT_IP=
GH_PROXY=
TAILSCALE_CIDR=100.64.0.0/10
MTU=1280
WARP_WGCF_CONF=
TZ=Asia/Shanghai
APP_DATA_DIR_1=./data
+260
View File
@@ -0,0 +1,260 @@
additionalProperties:
formFields:
- default: 1080
envKey: PANEL_APP_PORT_SOCKS5
labelEn: SOCKS5 Port
labelZh: SOCKS5 端口
label:
en: SOCKS5 Port
zh: SOCKS5 端口
zh-Hant: SOCKS5 連接埠
ja: SOCKS5 ポート
ko: SOCKS5 포트
ru: Порт SOCKS5
ms: Port SOCKS5
pt-br: Porta SOCKS5
required: true
rule: paramPort
type: number
edit: true
- default: 1080
envKey: BIND_PORT
labelEn: Internal Bind Port
labelZh: 容器内部绑定端口
label:
en: Internal Bind Port
zh: 容器内部绑定端口
zh-Hant: 容器內部綁定連接埠
ja: コンテナ内部バインドポート
ko: 컨테이너 내부 바인드 포트
ru: Внутренний порт привязки контейнера
ms: Port bind dalaman kontena
pt-br: Porta interna de bind do contêiner
required: true
rule: paramPort
type: number
edit: true
- default: 0
envKey: ROTATE_INTERVAL_MINUTES
labelEn: Rotate Interval Minutes
labelZh: 轮换间隔(分钟)
label:
en: Rotate Interval Minutes
zh: 轮换间隔(分钟)
zh-Hant: 輪換間隔(分鐘)
ja: ローテーション間隔(分)
ko: 로테이션 간격(분)
ru: Интервал ротации (минуты)
ms: Selang putaran (minit)
pt-br: Intervalo de rotação (minutos)
required: true
type: number
edit: true
- default: 5
envKey: ROTATE_MAX_ATTEMPTS
labelEn: Rotation Attempts
labelZh: 轮换尝试次数
label:
en: Rotation Attempts
zh: 轮换尝试次数
zh-Hant: 輪換嘗試次數
ja: ローテーション試行回数
ko: 로테이션 시도 횟수
ru: Количество попыток ротации
ms: Bilangan percubaan putaran
pt-br: Número de tentativas de rotação
required: true
type: number
edit: true
- default: 5
envKey: ROTATE_RETRY_DELAY_SECONDS
labelEn: Rotation Retry Delay Seconds
labelZh: 轮换重试间隔(秒)
label:
en: Rotation Retry Delay Seconds
zh: 轮换重试间隔(秒)
zh-Hant: 輪換重試間隔(秒)
ja: ローテーション再試行間隔(秒)
ko: 로테이션 재시도 간격(초)
ru: Задержка между попытками ротации (секунды)
ms: Kelewatan percubaan semula putaran (saat)
pt-br: Atraso entre tentativas de rotação (segundos)
required: true
type: number
edit: true
- default: https://cloudflare.com/cdn-cgi/trace
envKey: TEST_URL
labelEn: Probe URL
labelZh: 探测 URL
label:
en: Probe URL
zh: 探测 URL
zh-Hant: 探測 URL
ja: プローブ URL
ko: 점검 URL
ru: URL проверки
ms: URL ujian
pt-br: URL de verificação
required: true
type: text
edit: true
- default: https://cloudflare.com/cdn-cgi/trace
envKey: IP_CHECK_URL
labelEn: Egress IP Check URL
labelZh: 出口 IP 检查 URL
label:
en: Egress IP Check URL
zh: 出口 IP 检查 URL
zh-Hant: 出口 IP 檢查 URL
ja: 出口 IP 確認 URL
ko: 출구 IP 확인 URL
ru: URL проверки исходного IP
ms: URL semakan IP keluar
pt-br: URL de verificação do IP de saída
required: true
type: text
edit: true
- default: ""
envKey: SOCKS_USER
labelEn: SOCKS Username
labelZh: SOCKS 用户名
label:
en: SOCKS Username
zh: SOCKS 用户名
zh-Hant: SOCKS 使用者名稱
ja: SOCKS ユーザー名
ko: SOCKS 사용자명
ru: Имя пользователя SOCKS
ms: Nama pengguna SOCKS
pt-br: Usuário SOCKS
required: false
type: text
edit: true
- default: ""
envKey: SOCKS_PASS
labelEn: SOCKS Password
labelZh: SOCKS 密码
label:
en: SOCKS Password
zh: SOCKS 密码
zh-Hant: SOCKS 密碼
ja: SOCKS パスワード
ko: SOCKS 비밀번호
ru: Пароль SOCKS
ms: Kata laluan SOCKS
pt-br: Senha SOCKS
required: false
type: text
edit: true
- default: ""
envKey: ENDPOINT_IP
labelEn: WARP Endpoint
labelZh: WARP 终结点
label:
en: WARP Endpoint
zh: WARP 终结点
zh-Hant: WARP 終結點
ja: WARP エンドポイント
ko: WARP 엔드포인트
ru: WARP endpoint
ms: Titik akhir WARP
pt-br: Endpoint WARP
required: false
type: text
edit: true
- default: ""
envKey: GH_PROXY
labelEn: wgcf Download Proxy
labelZh: wgcf 下载代理
label:
en: wgcf Download Proxy
zh: wgcf 下载代理
zh-Hant: wgcf 下載代理
ja: wgcf ダウンロードプロキシ
ko: wgcf 다운로드 프록시
ru: Прокси для загрузки wgcf
ms: Proksi muat turun wgcf
pt-br: Proxy de download do wgcf
required: false
type: text
edit: true
- default: 100.64.0.0/10
envKey: TAILSCALE_CIDR
labelEn: Tailscale Return Route CIDR
labelZh: Tailscale 回程路由 CIDR
label:
en: Tailscale Return Route CIDR
zh: Tailscale 回程路由 CIDR
zh-Hant: Tailscale 回程路由 CIDR
ja: Tailscale 戻りルート CIDR
ko: Tailscale 리턴 라우트 CIDR
ru: CIDR обратного маршрута Tailscale
ms: CIDR laluan balik Tailscale
pt-br: CIDR de rota de retorno do Tailscale
required: true
type: text
edit: true
- default: 1280
envKey: MTU
labelEn: WireGuard MTU
labelZh: WireGuard MTU
label:
en: WireGuard MTU
zh: WireGuard MTU
zh-Hant: WireGuard MTU
ja: WireGuard MTU
ko: WireGuard MTU
ru: MTU WireGuard
ms: MTU WireGuard
pt-br: MTU do WireGuard
required: true
type: number
edit: true
- default: ""
envKey: WARP_WGCF_CONF
labelEn: WireGuard Config Content
labelZh: WireGuard 配置内容
label:
en: WireGuard Config Content
zh: WireGuard 配置内容
zh-Hant: WireGuard 設定內容
ja: WireGuard 設定内容
ko: WireGuard 구성 내용
ru: Содержимое конфигурации WireGuard
ms: Kandungan konfigurasi WireGuard
pt-br: Conteúdo da configuração WireGuard
required: false
type: text
edit: true
- default: Asia/Shanghai
envKey: TZ
labelEn: Timezone
labelZh: 时区
label:
en: Timezone
zh: 时区
zh-Hant: 時區
ja: タイムゾーン
ko: 시간대
ru: Часовой пояс
ms: Zon waktu
pt-br: Fuso horário
required: true
type: text
edit: true
- default: ./data
envKey: APP_DATA_DIR_1
labelEn: WireGuard Data Directory
labelZh: WireGuard 数据目录
label:
en: WireGuard Data Directory
zh: WireGuard 数据目录
zh-Hant: WireGuard 資料目錄
ja: WireGuard データディレクトリ
ko: WireGuard 데이터 디렉터리
ru: Каталог данных WireGuard
ms: Direktori data WireGuard
pt-br: Diretório de dados do WireGuard
required: true
type: text
edit: true
@@ -0,0 +1,40 @@
services:
microwarp:
image: "ghcr.io/ccbkkb/microwarp:0.1.3"
container_name: ${CONTAINER_NAME}
labels:
createdBy: "Apps"
ports:
- "${PANEL_APP_PORT_SOCKS5}:${BIND_PORT}"
environment:
- TZ=${TZ}
- BIND_ADDR=0.0.0.0
- BIND_PORT=${BIND_PORT}
- ROTATE_INTERVAL_MINUTES=${ROTATE_INTERVAL_MINUTES:-0}
- ROTATE_MAX_ATTEMPTS=${ROTATE_MAX_ATTEMPTS:-5}
- ROTATE_RETRY_DELAY_SECONDS=${ROTATE_RETRY_DELAY_SECONDS:-5}
- TEST_URL=${TEST_URL}
- IP_CHECK_URL=${IP_CHECK_URL:-https://cloudflare.com/cdn-cgi/trace}
- SOCKS_USER=${SOCKS_USER:-}
- SOCKS_PASS=${SOCKS_PASS:-}
- ENDPOINT_IP=${ENDPOINT_IP:-}
- GH_PROXY=${GH_PROXY:-}
- TAILSCALE_CIDR=${TAILSCALE_CIDR:-100.64.0.0/10}
- MTU=${MTU}
- WARP_WGCF_CONF=${WARP_WGCF_CONF:-}
cap_add:
- NET_ADMIN
- SYS_MODULE
sysctls:
- net.ipv4.conf.all.src_valid_mark=1
volumes:
- "${APP_DATA_DIR_1}:/etc/wireguard"
- "./rotate.sh:/usr/local/bin/rotate.sh:ro"
restart: always
command: ["/bin/sh", "/usr/local/bin/rotate.sh"]
networks:
- 1panel-network
networks:
1panel-network:
external: true
+300
View File
@@ -0,0 +1,300 @@
#!/usr/bin/env sh
set -eu
INTERVAL_MINUTES="${ROTATE_INTERVAL_MINUTES:-0}"
ROTATE_MAX_ATTEMPTS="${ROTATE_MAX_ATTEMPTS:-5}"
ROTATE_RETRY_DELAY_SECONDS="${ROTATE_RETRY_DELAY_SECONDS:-5}"
TEST_URL="${TEST_URL:-https://cloudflare.com/cdn-cgi/trace}"
IP_CHECK_URL="${IP_CHECK_URL:-https://cloudflare.com/cdn-cgi/trace}"
SOCKS_ADDR="127.0.0.1:${BIND_PORT:-1080}"
SOCKS_USER_VALUE="${SOCKS_USER:-}"
SOCKS_PASS_VALUE="${SOCKS_PASS:-}"
WG_DIR="/etc/wireguard"
WG_CONF="${WG_DIR}/wg0.conf"
PREVIOUS_CONF="${WG_CONF}.rotation-previous"
FIXED_CONFIG="${WARP_WGCF_CONF:-}"
LOG_PREFIX="[microwarp]"
ENTRY_PID=""
CURRENT_EGRESS_IP=""
log() {
echo "${LOG_PREFIX} $*"
}
is_non_negative_int() {
case "$1" in
''|*[!0-9]*)
return 1
;;
*)
return 0
;;
esac
}
is_positive_int() {
case "$1" in
''|*[!0-9]*|0)
return 1
;;
*)
return 0
;;
esac
}
is_ipv4() {
printf '%s\n' "$1" | awk -F. '
NF == 4 {
for (i = 1; i <= 4; i++) {
if ($i !~ /^[0-9]+$/ || $i < 0 || $i > 255) exit 1
}
exit 0
}
{ exit 1 }
'
}
probe_ip() {
if ! command -v curl >/dev/null 2>&1; then
return 1
fi
curl_via_socks "$TEST_URL" >/dev/null 2>&1
}
curl_via_socks() {
url="$1"
if [ -n "$SOCKS_USER_VALUE" ] && [ -n "$SOCKS_PASS_VALUE" ]; then
curl --max-time 20 --socks5-hostname "$SOCKS_ADDR" \
--proxy-user "${SOCKS_USER_VALUE}:${SOCKS_PASS_VALUE}" "$url"
return $?
fi
curl --max-time 20 --socks5-hostname "$SOCKS_ADDR" "$url"
}
get_egress_ip() {
if ! command -v curl >/dev/null 2>&1; then
return 1
fi
trace_output="$(curl_via_socks "$IP_CHECK_URL" 2>/dev/null)" || return 1
current_ip="$(printf '%s\n' "$trace_output" |
awk -F= '$1 == "ip" { print $2; exit }' | tr -d '\r')"
is_ipv4 "$current_ip" && printf '%s\n' "$current_ip"
}
record_egress_ip() {
current_ip="$(get_egress_ip || true)"
if [ -n "$current_ip" ]; then
CURRENT_EGRESS_IP="$current_ip"
log "current WARP egress IPv4: ${CURRENT_EGRESS_IP}"
else
CURRENT_EGRESS_IP=""
log "unable to verify current WARP egress IPv4"
fi
return 0
}
inject_fixed_config() {
if [ -z "$FIXED_CONFIG" ]; then
return 0
fi
mkdir -p "$WG_DIR"
printf '%s\n' "$FIXED_CONFIG" > "$WG_CONF"
chmod 600 "$WG_CONF" || true
log "wg0.conf injected from WARP_WGCF_CONF"
}
wait_for_proxy() {
attempts=0
while [ "$attempts" -lt 30 ]; do
if [ -n "$ENTRY_PID" ] && ! kill -0 "$ENTRY_PID" 2>/dev/null; then
wait "$ENTRY_PID" || true
ENTRY_PID=""
return 1
fi
if probe_ip; then
return 0
fi
attempts=$((attempts + 1))
sleep 2
done
return 1
}
stop_runtime() {
if [ -n "$ENTRY_PID" ] && kill -0 "$ENTRY_PID" 2>/dev/null; then
kill "$ENTRY_PID" 2>/dev/null || true
wait "$ENTRY_PID" || true
fi
ENTRY_PID=""
}
teardown_for_rotation() {
stop_runtime
if ip link show wg0 >/dev/null 2>&1; then
wg-quick down wg0 >/dev/null 2>&1 || true
fi
rm -f "$WG_CONF" "$WG_DIR/wgcf-account.toml" "$WG_DIR/wgcf-profile.conf" "$WG_DIR/extra.env"
}
start_runtime() {
CURRENT_EGRESS_IP=""
inject_fixed_config
log "starting MicroWARP runtime"
/app/entrypoint.sh &
ENTRY_PID=$!
if ! wait_for_proxy; then
log "runtime did not become ready in time"
stop_runtime
return 1
fi
record_egress_ip
if [ -z "$ENTRY_PID" ] || ! kill -0 "$ENTRY_PID" 2>/dev/null; then
log "runtime exited while its egress was being checked"
stop_runtime
return 1
fi
return 0
}
save_previous_config() {
if [ -s "$WG_CONF" ]; then
previous_tmp="${PREVIOUS_CONF}.tmp.$$"
rm -f "$previous_tmp"
if ! cp -p "$WG_CONF" "$previous_tmp"; then
rm -f "$previous_tmp"
return 1
fi
if ! mv -f "$previous_tmp" "$PREVIOUS_CONF"; then
rm -f "$previous_tmp"
return 1
fi
return 0
fi
return 1
}
restore_previous_runtime() {
if [ ! -s "$PREVIOUS_CONF" ]; then
return 1
fi
log "restoring the previous working WARP configuration"
teardown_for_rotation
if ! cp -p "$PREVIOUS_CONF" "$WG_CONF"; then
log "unable to restore the previous WARP configuration"
return 1
fi
start_runtime
}
rotate_once() {
if [ -z "$ENTRY_PID" ] || ! kill -0 "$ENTRY_PID" 2>/dev/null; then
log "WARP runtime exited before the scheduled identity rebuild"
return 1
fi
record_egress_ip
old_ip="$CURRENT_EGRESS_IP"
if [ -z "$old_ip" ]; then
log "scheduled identity rebuild skipped because the current WARP egress IPv4 could not be verified"
return 0
fi
attempt=1
if ! save_previous_config; then
log "scheduled identity rebuild skipped because the working WARP configuration could not be backed up"
return 0
fi
while [ "$attempt" -le "$ROTATE_MAX_ATTEMPTS" ]; do
log "WARP identity rebuild attempt ${attempt}/${ROTATE_MAX_ATTEMPTS}"
teardown_for_rotation
if start_runtime; then
new_ip="$CURRENT_EGRESS_IP"
if [ -n "$new_ip" ] &&
[ "$new_ip" != "$old_ip" ]; then
rm -f "$PREVIOUS_CONF"
log "WARP egress IPv4 changed: ${old_ip:-unknown} -> ${new_ip}"
return 0
fi
if [ -n "$new_ip" ] && [ "$new_ip" = "$old_ip" ]; then
log "WARP egress IPv4 stayed ${new_ip}; retrying identity rebuild"
else
log "WARP egress IPv4 could not be verified; retrying identity rebuild"
fi
else
log "WARP runtime failed during identity rebuild attempt ${attempt}"
fi
if [ "$attempt" -lt "$ROTATE_MAX_ATTEMPTS" ]; then
sleep "$ROTATE_RETRY_DELAY_SECONDS"
fi
attempt=$((attempt + 1))
done
if restore_previous_runtime; then
log "no different public IPv4 was observed; previous runtime restored"
rm -f "$PREVIOUS_CONF"
return 0
fi
log "rotation failed and no working runtime is available"
return 1
}
on_signal() {
log "received stop signal"
stop_runtime
exit 0
}
main() {
interval="$INTERVAL_MINUTES"
if ! is_non_negative_int "$interval"; then
log "invalid ROTATE_INTERVAL_MINUTES=${interval}, fallback to 0"
interval=0
fi
if ! is_positive_int "$ROTATE_MAX_ATTEMPTS"; then
log "invalid ROTATE_MAX_ATTEMPTS=${ROTATE_MAX_ATTEMPTS}, fallback to 5"
ROTATE_MAX_ATTEMPTS=5
fi
if ! is_non_negative_int "$ROTATE_RETRY_DELAY_SECONDS"; then
log "invalid ROTATE_RETRY_DELAY_SECONDS=${ROTATE_RETRY_DELAY_SECONDS}, fallback to 5"
ROTATE_RETRY_DELAY_SECONDS=5
fi
if [ -n "$FIXED_CONFIG" ] && [ "$interval" -gt 0 ]; then
log "WARP_WGCF_CONF is set, scheduled rotation disabled because the config is pinned"
interval=0
fi
trap on_signal INT TERM
if ! start_runtime; then
exit 1
fi
if [ "$interval" -eq 0 ]; then
log "scheduled rotation disabled"
wait "$ENTRY_PID"
exit $?
fi
log "scheduled WARP identity rebuild enabled every ${interval} minute(s); egress verification allows ${ROTATE_MAX_ATTEMPTS} attempt(s)"
while true; do
sleep "$((interval * 60))"
log "trigger scheduled WARP identity rebuild"
if ! rotate_once; then
log "rotation failed"
exit 1
fi
done
}
main "$@"
@@ -0,0 +1,38 @@
#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR="$(CDPATH="" cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd -P)"
APP_ROOT="${APP_ROOT:-$(CDPATH="" cd -- "${SCRIPT_DIR}/.." && pwd -P)}"
ENV_FILE="${ENV_FILE:-${APP_ROOT}/.env}"
if [[ "${ENV_FILE}" != /* ]]; then
ENV_FILE="${APP_ROOT}/${ENV_FILE#./}"
fi
strip_matching_quotes() {
local value="$1"
case "${value}" in
\"*\")
value="${value#\"}"
value="${value%\"}"
;;
\'*\')
value="${value#\'}"
value="${value%\'}"
;;
esac
printf '%s\n' "${value}"
}
app_data_dir="${APP_DATA_DIR_1:-}"
if [[ -z "${app_data_dir}" && -f "${ENV_FILE}" ]]; then
app_data_dir="$(sed -n 's/^APP_DATA_DIR_1=//p' "${ENV_FILE}" | tail -n 1)"
fi
app_data_dir="$(strip_matching_quotes "${app_data_dir}")"
app_data_dir="${app_data_dir:-./data}"
if [[ "${app_data_dir}" != /* ]]; then
app_data_dir="${APP_ROOT}/${app_data_dir#./}"
fi
mkdir -p "${app_data_dir}"
+2
View File
@@ -0,0 +1,2 @@
#!/bin/bash
docker-compose down --volumes
@@ -0,0 +1,112 @@
#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR="$(CDPATH="" cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd -P)"
APP_ROOT="${APP_ROOT:-$(CDPATH="" cd -- "${SCRIPT_DIR}/.." && pwd -P)}"
ENV_FILE="${ENV_FILE:-${APP_ROOT}/.env}"
TARGET_VERSION_DIR="0.1.3-rotator"
if [[ "${ENV_FILE}" != /* ]]; then
ENV_FILE="${APP_ROOT}/${ENV_FILE#./}"
fi
resource_dir_for_app() {
local panel_root app_path app_key
case "${APP_ROOT}" in
*/apps/local/*)
panel_root="${APP_ROOT%%/apps/local/*}"
app_path="${APP_ROOT#"${panel_root}/apps/local/"}"
app_key="${app_path%%/*}"
[[ -n "${panel_root}" && -n "${app_key}" ]] || return 1
printf '%s/resource/apps/local/%s\n' "${panel_root}" "${app_key}"
;;
*)
return 1
;;
esac
}
find_rotate_source() {
local resource_dir candidate_upgrade candidate_rotate
if [[ -n "${ROTATE_SOURCE:-}" ]]; then
printf '%s\n' "${ROTATE_SOURCE}"
return 0
fi
if resource_dir="$(resource_dir_for_app 2>/dev/null)" && [[ -d "${resource_dir}" ]]; then
candidate_upgrade="${resource_dir}/${TARGET_VERSION_DIR}/scripts/upgrade.sh"
candidate_rotate="${resource_dir}/${TARGET_VERSION_DIR}/rotate.sh"
if [[ -f "${candidate_upgrade}" && -f "${candidate_rotate}" ]] \
&& cmp -s "${SCRIPT_DIR}/upgrade.sh" "${candidate_upgrade}" \
&& grep -q 'ROTATE_MAX_ATTEMPTS' "${candidate_rotate}"; then
printf '%s\n' "${candidate_rotate}"
return 0
fi
fi
if [[ -f "${APP_ROOT}/rotate.sh" ]] \
&& grep -q 'ROTATE_MAX_ATTEMPTS' "${APP_ROOT}/rotate.sh"; then
printf '%s\n' "${APP_ROOT}/rotate.sh"
return 0
fi
return 1
}
strip_matching_quotes() {
local value="$1"
case "${value}" in
\"*\")
value="${value#\"}"
value="${value%\"}"
;;
\'*\')
value="${value#\'}"
value="${value%\'}"
;;
esac
printf '%s\n' "${value}"
}
rotate_target="${APP_ROOT}/rotate.sh"
rotate_source="$(find_rotate_source || true)"
if [[ -z "${rotate_source}" || ! -f "${rotate_source}" ]]; then
echo "Unable to locate the upgraded rotate.sh payload; refusing to keep the old script" >&2
exit 1
fi
if [[ "${rotate_source}" != "${rotate_target}" ]] \
&& ! cmp -s "${rotate_source}" "${rotate_target}"; then
rotate_tmp="$(mktemp "${rotate_target}.upgrade.XXXXXX")"
# shellcheck disable=SC2317
cleanup_rotate_tmp() {
rm -f -- "${rotate_tmp}"
}
trap cleanup_rotate_tmp EXIT
cp -- "${rotate_source}" "${rotate_tmp}"
if [[ -e "${rotate_target}" ]]; then
chmod --reference="${rotate_target}" "${rotate_tmp}" 2>/dev/null || true
else
chmod 0644 "${rotate_tmp}"
fi
mv -f -- "${rotate_tmp}" "${rotate_target}"
trap - EXIT
echo "Updated ${rotate_target} from the target version payload"
fi
app_data_dir="${APP_DATA_DIR_1:-}"
if [[ -z "${app_data_dir}" && -f "${ENV_FILE}" ]]; then
app_data_dir="$(sed -n 's/^APP_DATA_DIR_1=//p' "${ENV_FILE}" | tail -n 1)"
fi
app_data_dir="$(strip_matching_quotes "${app_data_dir}")"
app_data_dir="${app_data_dir:-./data}"
if [[ "${app_data_dir}" != /* ]]; then
app_data_dir="${APP_ROOT}/${app_data_dir#./}"
fi
mkdir -p "${app_data_dir}"
exit 0