mirror of
https://github.com/andrey271192/kaskad.git
synced 2026-09-20 13:49:56 +00:00
Initial release of Kaskad
Каскадная маршрутизация русских сайтов через свой набор RU-серверов с failover, Telegram-ботом и веб-интерфейсом. - bin/ — failover-script, route/domain helpers, ams/ru bootstrap - bot/ — Telegram-бот (Python, long-poll) - webui/ — Flask single-page dashboard - docs/ — установка, архитектура, API бота и WebUI - examples/ — шаблоны конфигов
This commit is contained in:
25
bin/add-ams-helper.sh
Executable file
25
bin/add-ams-helper.sh
Executable file
@@ -0,0 +1,25 @@
|
||||
#!/bin/bash
|
||||
# add-ams-helper.sh <bot_pubkey_b64>
|
||||
# Запускается на НОВОМ ам. сервере. Только устанавливает базу — основные файлы scp-ом.
|
||||
set -e
|
||||
exec 2>&1
|
||||
|
||||
BOT_KEY_B64=$1
|
||||
DEBIAN_FRONTEND=noninteractive apt-get update -qq >/dev/null 2>&1 || true
|
||||
DEBIAN_FRONTEND=noninteractive apt-get install -y wireguard iptables-persistent curl python3 >/dev/null 2>&1
|
||||
|
||||
mkdir -p /etc/wireguard /usr/local/bin /var/lib/ru-failover
|
||||
mkdir -p /root/.ssh && chmod 700 /root/.ssh
|
||||
BOT_KEY=$(echo "$BOT_KEY_B64" | base64 -d)
|
||||
grep -qF "$BOT_KEY" /root/.ssh/authorized_keys 2>/dev/null || echo "$BOT_KEY" >> /root/.ssh/authorized_keys
|
||||
chmod 600 /root/.ssh/authorized_keys
|
||||
|
||||
sysctl -w net.ipv4.ip_forward=1 >/dev/null
|
||||
grep -q '^net.ipv4.ip_forward=1' /etc/sysctl.conf || echo 'net.ipv4.ip_forward=1' >> /etc/sysctl.conf
|
||||
|
||||
PUBLIC_IP=$(curl -s --max-time 5 ifconfig.me 2>/dev/null || echo unknown)
|
||||
IFACE=$(ip route | awk '/^default/{print $5; exit}')
|
||||
echo "----RESULT----"
|
||||
echo "PUBLIC_IP=$PUBLIC_IP"
|
||||
echo "IFACE=$IFACE"
|
||||
echo "----END----"
|
||||
66
bin/add-ru-helper.sh
Executable file
66
bin/add-ru-helper.sh
Executable file
@@ -0,0 +1,66 @@
|
||||
#!/bin/bash
|
||||
# add-ru-helper.sh <bot_pubkey_b64> <listen_port> <peer_pk> <peer_ip> ...
|
||||
set -e
|
||||
exec 2>&1
|
||||
|
||||
BOT_KEY_B64=$1; shift
|
||||
LISTEN_PORT=$1; shift
|
||||
declare -a PEERS_PK PEERS_IP
|
||||
while [ $# -ge 2 ]; do
|
||||
PEERS_PK+=("$1"); shift
|
||||
PEERS_IP+=("$1"); shift
|
||||
done
|
||||
|
||||
DEBIAN_FRONTEND=noninteractive apt-get update -qq >/dev/null 2>&1 || true
|
||||
DEBIAN_FRONTEND=noninteractive apt-get install -y wireguard iptables-persistent curl >/dev/null 2>&1
|
||||
|
||||
mkdir -p /etc/wireguard
|
||||
cd /etc/wireguard
|
||||
if [ ! -f ru_private.key ]; then
|
||||
umask 077
|
||||
wg genkey | tee ru_private.key | wg pubkey > ru_public.key
|
||||
fi
|
||||
PRIVKEY=$(cat ru_private.key)
|
||||
PUBKEY=$(cat ru_public.key)
|
||||
IFACE=$(ip route | awk '/^default/{print $5; exit}')
|
||||
PUBLIC_IP=$(curl -s --max-time 5 ifconfig.me 2>/dev/null || echo unknown)
|
||||
|
||||
CONF=/etc/wireguard/wg_ru.conf
|
||||
{
|
||||
echo "[Interface]"
|
||||
echo "Address = 10.0.0.1/24"
|
||||
echo "PrivateKey = $PRIVKEY"
|
||||
echo "ListenPort = $LISTEN_PORT"
|
||||
echo "PostUp = iptables -t nat -A POSTROUTING -o $IFACE -j MASQUERADE; iptables -I FORWARD 1 -i wg_ru -j ACCEPT; iptables -I FORWARD 1 -o wg_ru -j ACCEPT; iptables -I INPUT -i wg_ru -j ACCEPT"
|
||||
echo "PostDown = iptables -t nat -D POSTROUTING -o $IFACE -j MASQUERADE; iptables -D FORWARD -i wg_ru -j ACCEPT; iptables -D FORWARD -o wg_ru -j ACCEPT; iptables -D INPUT -i wg_ru -j ACCEPT"
|
||||
for i in "${!PEERS_PK[@]}"; do
|
||||
echo
|
||||
echo "[Peer]"
|
||||
echo "PublicKey = ${PEERS_PK[$i]}"
|
||||
echo "AllowedIPs = ${PEERS_IP[$i]}/32"
|
||||
done
|
||||
} > "$CONF"
|
||||
chmod 600 "$CONF"
|
||||
|
||||
sysctl -w net.ipv4.ip_forward=1 >/dev/null
|
||||
grep -q '^net.ipv4.ip_forward=1' /etc/sysctl.conf || echo 'net.ipv4.ip_forward=1' >> /etc/sysctl.conf
|
||||
|
||||
iptables -C INPUT -p udp --dport "$LISTEN_PORT" -j ACCEPT 2>/dev/null \
|
||||
|| iptables -I INPUT -p udp --dport "$LISTEN_PORT" -j ACCEPT
|
||||
netfilter-persistent save >/dev/null 2>&1 || iptables-save > /etc/iptables/rules.v4 2>/dev/null
|
||||
|
||||
wg-quick down wg_ru 2>/dev/null || true
|
||||
wg-quick up wg_ru
|
||||
systemctl enable wg-quick@wg_ru >/dev/null 2>&1
|
||||
|
||||
# bot pubkey в root authorized_keys для будущего управления
|
||||
mkdir -p /root/.ssh && chmod 700 /root/.ssh
|
||||
BOT_KEY=$(echo "$BOT_KEY_B64" | base64 -d)
|
||||
grep -qF "$BOT_KEY" /root/.ssh/authorized_keys 2>/dev/null || echo "$BOT_KEY" >> /root/.ssh/authorized_keys
|
||||
chmod 600 /root/.ssh/authorized_keys
|
||||
|
||||
echo "----RESULT----"
|
||||
echo "PUBKEY=$PUBKEY"
|
||||
echo "IFACE=$IFACE"
|
||||
echo "PUBLIC_IP=$PUBLIC_IP"
|
||||
echo "----END----"
|
||||
117
bin/ru-domains.py
Executable file
117
bin/ru-domains.py
Executable file
@@ -0,0 +1,117 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Управление доменами для маршрутизации через ru-туннель."""
|
||||
import json, subprocess, sys
|
||||
from pathlib import Path
|
||||
|
||||
JSON = Path("/etc/wireguard/ru-domains.json")
|
||||
ROUTES = "/usr/local/bin/ru-routes.sh"
|
||||
|
||||
def load():
|
||||
if not JSON.exists():
|
||||
JSON.write_text("{}")
|
||||
JSON.chmod(0o600)
|
||||
return json.loads(JSON.read_text())
|
||||
|
||||
def save(d):
|
||||
JSON.write_text(json.dumps(d, indent=2, ensure_ascii=False))
|
||||
JSON.chmod(0o600)
|
||||
|
||||
def resolve(dom):
|
||||
try:
|
||||
r = subprocess.run(["dig","+short","+time=3","+tries=2","A", dom],
|
||||
capture_output=True, text=True, timeout=10)
|
||||
except Exception:
|
||||
return set()
|
||||
out = set()
|
||||
for line in r.stdout.split("\n"):
|
||||
line = line.strip()
|
||||
if line and line.count(".") == 3 and line.replace(".","").isdigit():
|
||||
out.add(line)
|
||||
return out
|
||||
|
||||
def routes_call(verb, ips):
|
||||
if not ips: return
|
||||
args = [ROUTES, verb] + [ip + "/32" for ip in ips]
|
||||
subprocess.run(args, capture_output=True)
|
||||
|
||||
def cmd_list():
|
||||
d = load()
|
||||
if not d: return "(пусто)"
|
||||
lines = []
|
||||
for dom, ips in sorted(d.items()):
|
||||
lines.append(f"{dom}: {len(ips)} IP")
|
||||
return "\n".join(lines)
|
||||
|
||||
def cmd_show(dom):
|
||||
d = load()
|
||||
if dom not in d: return f"{dom}: нет в списке"
|
||||
return f"{dom}:\n" + "\n".join(d[dom])
|
||||
|
||||
def cmd_add(domains):
|
||||
d = load()
|
||||
results = []
|
||||
for dom in domains:
|
||||
ips = resolve(dom)
|
||||
if not ips:
|
||||
results.append(f"❌ {dom}: не резолвится")
|
||||
continue
|
||||
old = set(d.get(dom, []))
|
||||
new = old | ips
|
||||
d[dom] = sorted(new)
|
||||
added = ips - old
|
||||
routes_call("add", sorted(added))
|
||||
results.append(f"✅ {dom}: {len(ips)} IP (+{len(added)} новых)")
|
||||
save(d)
|
||||
return "\n".join(results)
|
||||
|
||||
def cmd_remove(domains):
|
||||
d = load()
|
||||
results = []
|
||||
for dom in domains:
|
||||
if dom not in d:
|
||||
results.append(f"❌ {dom}: нет в списке")
|
||||
continue
|
||||
ips_to_check = set(d[dom])
|
||||
del d[dom]
|
||||
still_owned = set()
|
||||
for v in d.values(): still_owned.update(v)
|
||||
to_remove = ips_to_check - still_owned
|
||||
routes_call("remove", sorted(to_remove))
|
||||
results.append(f"✅ {dom}: убрано {len(to_remove)} IP из extra")
|
||||
save(d)
|
||||
return "\n".join(results)
|
||||
|
||||
def cmd_refresh():
|
||||
d = load()
|
||||
total_add = 0; total_rm = 0; failed = []
|
||||
for dom in list(d.keys()):
|
||||
new_ips = resolve(dom)
|
||||
if not new_ips:
|
||||
failed.append(dom); continue
|
||||
old = set(d[dom])
|
||||
added = new_ips - old
|
||||
removed = old - new_ips
|
||||
d[dom] = sorted(new_ips)
|
||||
# после обновления — пересобрать карту владельцев
|
||||
owners = set()
|
||||
for v in d.values(): owners.update(v)
|
||||
to_rm = sorted(removed - owners)
|
||||
routes_call("add", sorted(added))
|
||||
routes_call("remove", to_rm)
|
||||
total_add += len(added)
|
||||
total_rm += len(to_rm)
|
||||
save(d)
|
||||
msg = f"обновлено: +{total_add} / -{total_rm} IP"
|
||||
if failed: msg += f"\nне резолвятся: {', '.join(failed)}"
|
||||
return msg
|
||||
|
||||
if __name__ == "__main__":
|
||||
if len(sys.argv) < 2:
|
||||
print("usage: ru-domains {list|show DOM|add DOM...|remove DOM...|refresh}"); sys.exit(1)
|
||||
cmd = sys.argv[1]; args = sys.argv[2:]
|
||||
if cmd == "list": print(cmd_list())
|
||||
elif cmd == "show": print(cmd_show(args[0]) if args else "show DOM")
|
||||
elif cmd == "add": print(cmd_add(args))
|
||||
elif cmd in ("remove","del"): print(cmd_remove(args))
|
||||
elif cmd == "refresh": print(cmd_refresh())
|
||||
else: print(f"unknown: {cmd}"); sys.exit(1)
|
||||
138
bin/ru-failover.py
Executable file
138
bin/ru-failover.py
Executable file
@@ -0,0 +1,138 @@
|
||||
#!/usr/bin/env python3
|
||||
"""N-серверный failover с приоритетами. Конфиг — /etc/wireguard/ru-servers.json."""
|
||||
import json, os, re, socket, subprocess, time
|
||||
from pathlib import Path
|
||||
from urllib import parse, request, error as urlerror
|
||||
|
||||
CONF = Path("/etc/wireguard/ru.conf")
|
||||
SERVERS = Path("/etc/wireguard/ru-servers.json")
|
||||
STATE_DIR = Path("/var/lib/ru-failover")
|
||||
NOTIFY_ENV = Path("/etc/wireguard/notify.env")
|
||||
|
||||
HS_THRESHOLD = 180
|
||||
COOLDOWN = 300
|
||||
FAIL_BACKOFF = 1800
|
||||
TEST_TIMEOUT = 60
|
||||
|
||||
STATE_DIR.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
def state(name, val=None):
|
||||
p = STATE_DIR / name
|
||||
if val is None:
|
||||
try: return int((p.read_text().strip() or "0"))
|
||||
except Exception: return 0
|
||||
p.write_text(str(val))
|
||||
|
||||
def load_servers():
|
||||
return sorted(json.loads(SERVERS.read_text())["servers"], key=lambda s: s["priority"])
|
||||
|
||||
def cur_endpoint():
|
||||
for line in CONF.read_text().splitlines():
|
||||
if line.lstrip().startswith("Endpoint"):
|
||||
return line.split("=", 1)[1].strip()
|
||||
return ""
|
||||
|
||||
def hs_age(now):
|
||||
try:
|
||||
out = subprocess.run(["wg","show","ru","latest-handshakes"],
|
||||
capture_output=True, text=True, timeout=5).stdout.strip()
|
||||
if not out: return 999999
|
||||
hs = int(out.split()[1])
|
||||
return 999999 if hs == 0 else now - hs
|
||||
except Exception:
|
||||
return 999999
|
||||
|
||||
def probe(host, port, timeout=3):
|
||||
try:
|
||||
with socket.create_connection((host, int(port)), timeout=timeout):
|
||||
return True
|
||||
except (socket.error, OSError, ValueError):
|
||||
return False
|
||||
|
||||
def apply_server(s):
|
||||
t = CONF.read_text()
|
||||
t = re.sub(r'(?m)^[ \t]*PublicKey *=.*$', f'PublicKey = {s["pubkey"]}', t, count=1)
|
||||
t = re.sub(r'(?m)^[ \t]*Endpoint *=.*$', f'Endpoint = {s["endpoint"]}', t, count=1)
|
||||
CONF.write_text(t)
|
||||
subprocess.run(["bash","-c","wg syncconf ru <(wg-quick strip ru)"], check=False)
|
||||
subprocess.run(["/usr/local/bin/ru-routes.sh","apply"], capture_output=True)
|
||||
state("last_switch", int(time.time()))
|
||||
|
||||
def find_by_endpoint(servers, ep):
|
||||
for s in servers:
|
||||
if s["endpoint"] == ep: return s
|
||||
return None
|
||||
|
||||
def notify(msg):
|
||||
subprocess.run(["logger","-t","ru-failover", msg], check=False)
|
||||
if not NOTIFY_ENV.exists(): return
|
||||
env = {}
|
||||
for line in NOTIFY_ENV.read_text().splitlines():
|
||||
if "=" in line:
|
||||
k, v = line.split("=", 1)
|
||||
env[k.strip()] = v.strip().strip('"').strip("'")
|
||||
token = env.get("TG_BOT_TOKEN", "")
|
||||
chat = env.get("TG_CHAT_ID", "")
|
||||
if not (token and chat): return
|
||||
try:
|
||||
host = subprocess.check_output(["hostname"], text=True).strip()
|
||||
body = parse.urlencode({"chat_id": chat, "text": f"[ru-failover @ {host}] {msg}"}).encode()
|
||||
request.urlopen(f"https://api.telegram.org/bot{token}/sendMessage", data=body, timeout=5).read()
|
||||
except (urlerror.URLError, OSError):
|
||||
pass
|
||||
|
||||
def main():
|
||||
servers = load_servers()
|
||||
if not servers: return
|
||||
now = int(time.time())
|
||||
cur = find_by_endpoint(servers, cur_endpoint())
|
||||
if not cur:
|
||||
# Текущий endpoint не в списке — выставить highest-priority alive
|
||||
for s in servers:
|
||||
if probe(s["host"], s["probe_port"]):
|
||||
apply_server(s)
|
||||
notify(f"endpoint {cur_endpoint()} не найден в списке — выставил {s['label']}")
|
||||
return
|
||||
return
|
||||
|
||||
age = hs_age(now)
|
||||
last_switch = state("last_switch")
|
||||
test_started = state("test_started")
|
||||
last_fail = state("last_fail")
|
||||
|
||||
# Phase 1: failback test
|
||||
if test_started:
|
||||
elapsed = now - test_started
|
||||
if age < 60:
|
||||
state("test_started", 0); state("last_fail", 0)
|
||||
notify(f"failback на {cur['label']} успешен за {elapsed}s")
|
||||
elif elapsed > TEST_TIMEOUT:
|
||||
others = [s for s in servers if s["id"] != cur["id"] and probe(s["host"], s["probe_port"])]
|
||||
if others:
|
||||
apply_server(others[0])
|
||||
notify(f"failback на {cur['label']} провалился — откат на {others[0]['label']}")
|
||||
state("test_started", 0); state("last_fail", now)
|
||||
return
|
||||
|
||||
since_switch = now - last_switch
|
||||
since_fail = now - last_fail
|
||||
|
||||
# Failover: текущий мёртв
|
||||
if age > HS_THRESHOLD and not probe(cur["host"], cur["probe_port"]) and since_switch > COOLDOWN:
|
||||
others = [s for s in servers if s["id"] != cur["id"] and probe(s["host"], s["probe_port"])]
|
||||
if others:
|
||||
t = others[0]
|
||||
apply_server(t)
|
||||
notify(f"{cur['label']} упал (hs {age}s, probe fail) — переключился на {t['label']}")
|
||||
return
|
||||
|
||||
# Failback: есть live сервер с приоритетом выше
|
||||
higher = [s for s in servers if s["priority"] < cur["priority"] and probe(s["host"], s["probe_port"])]
|
||||
if higher and since_switch > COOLDOWN and since_fail > FAIL_BACKOFF:
|
||||
t = higher[0]
|
||||
apply_server(t)
|
||||
state("test_started", now)
|
||||
notify(f"{t['label']} поднялся — пробую failback (тест {TEST_TIMEOUT}s)")
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
120
bin/ru-routes.sh
Executable file
120
bin/ru-routes.sh
Executable file
@@ -0,0 +1,120 @@
|
||||
#!/bin/bash
|
||||
# /usr/local/bin/ru-routes.sh — управление extra-маршрутами через ru-туннель
|
||||
set -u
|
||||
LIST=/etc/wireguard/ru-extra.list
|
||||
BASE=/etc/wireguard/ru-base.aips
|
||||
CONF=/etc/wireguard/ru.conf
|
||||
XRAY_IF="${XRAY_IF:-amn0}"
|
||||
MARK=100
|
||||
|
||||
touch "$LIST"; chmod 600 "$LIST"
|
||||
|
||||
valid_cidr() {
|
||||
[[ "$1" =~ ^([0-9]{1,3}\.){3}[0-9]{1,3}(/[0-9]{1,2})?$ ]] || return 1
|
||||
IFS=/ read -r ip mask <<< "$1"
|
||||
IFS=. read -r a b c d <<< "$ip"
|
||||
for o in $a $b $c $d; do (( o < 0 || o > 255 )) && return 1; done
|
||||
if [[ -n "${mask:-}" ]]; then (( mask < 0 || mask > 32 )) && return 1; fi
|
||||
return 0
|
||||
}
|
||||
normalize() { local n="$1"; [[ "$n" == */* ]] || n="$n/32"; echo "$n"; }
|
||||
|
||||
peer_pk() { awk '/^\[Peer\]/{p=1} p && /^PublicKey *= /{sub(/^PublicKey *= */,""); print; exit}' "$CONF"; }
|
||||
current_aips() { awk '/^\[Peer\]/{p=1} p && /^AllowedIPs *= /{sub(/^AllowedIPs *= */,""); print; exit}' "$CONF" | tr -d ' '; }
|
||||
|
||||
ensure_base() {
|
||||
if [ ! -f "$BASE" ]; then
|
||||
current_aips > "$BASE"
|
||||
chmod 600 "$BASE"
|
||||
fi
|
||||
}
|
||||
|
||||
sync_wg() {
|
||||
ensure_base
|
||||
local pk base extra all
|
||||
pk=$(peer_pk)
|
||||
base=$(cat "$BASE")
|
||||
extra=$(grep -vE '^[[:space:]]*(#|$)' "$LIST" | tr -d ' ' | tr '\n' ',' | sed 's/,$//')
|
||||
if [ -n "$extra" ]; then all="$base,$extra"; else all="$base"; fi
|
||||
python3 - "$CONF" "$all" <<'PY'
|
||||
import sys, re, pathlib
|
||||
p = pathlib.Path(sys.argv[1])
|
||||
t = p.read_text()
|
||||
t = re.sub(r'^AllowedIPs *= .*$', 'AllowedIPs = ' + sys.argv[2], t, count=1, flags=re.M)
|
||||
p.write_text(t)
|
||||
PY
|
||||
wg set ru peer "$pk" allowed-ips "$all" 2>&1
|
||||
}
|
||||
|
||||
apply_route() {
|
||||
local net="$1"
|
||||
ip route replace "$net" dev ru 2>/dev/null || true
|
||||
iptables -t mangle -C PREROUTING -i "$XRAY_IF" -d "$net" -j MARK --set-mark $MARK 2>/dev/null \
|
||||
|| iptables -t mangle -A PREROUTING -i "$XRAY_IF" -d "$net" -j MARK --set-mark $MARK 2>/dev/null || true
|
||||
}
|
||||
remove_route() {
|
||||
local net="$1"
|
||||
ip route del "$net" dev ru 2>/dev/null || true
|
||||
iptables -t mangle -D PREROUTING -i "$XRAY_IF" -d "$net" -j MARK --set-mark $MARK 2>/dev/null || true
|
||||
}
|
||||
|
||||
cmd_list() { if [ -s "$LIST" ]; then cat "$LIST"; else echo "(пусто)"; fi; }
|
||||
|
||||
cmd_add() {
|
||||
local added=0 skipped=0 invalid=()
|
||||
for raw in "$@"; do
|
||||
if ! valid_cidr "$raw"; then invalid+=("$raw"); continue; fi
|
||||
local net; net=$(normalize "$raw")
|
||||
if grep -qxF "$net" "$LIST"; then ((skipped++)); else echo "$net" >> "$LIST"; ((added++)); fi
|
||||
apply_route "$net"
|
||||
done
|
||||
sync_wg >/dev/null
|
||||
printf "added=%d skipped=%d invalid=%d" "$added" "$skipped" "${#invalid[@]}"
|
||||
((${#invalid[@]})) && printf " (%s)" "${invalid[*]}"
|
||||
echo
|
||||
}
|
||||
|
||||
cmd_remove() {
|
||||
local removed=0 missing=0
|
||||
for raw in "$@"; do
|
||||
valid_cidr "$raw" || continue
|
||||
local net; net=$(normalize "$raw")
|
||||
if grep -qxF "$net" "$LIST"; then
|
||||
grep -vxF "$net" "$LIST" > "$LIST.tmp"; mv "$LIST.tmp" "$LIST"
|
||||
((removed++))
|
||||
else ((missing++)); fi
|
||||
remove_route "$net"
|
||||
done
|
||||
sync_wg >/dev/null
|
||||
echo "removed=$removed missing=$missing"
|
||||
}
|
||||
|
||||
cmd_clear() {
|
||||
local n=0
|
||||
while IFS= read -r net; do
|
||||
[[ -z "$net" || "$net" == \#* ]] && continue
|
||||
remove_route "$net"; ((n++))
|
||||
done < "$LIST"
|
||||
: > "$LIST"
|
||||
sync_wg >/dev/null
|
||||
echo "cleared=$n"
|
||||
}
|
||||
|
||||
cmd_apply() {
|
||||
local n=0
|
||||
while IFS= read -r net; do
|
||||
[[ -z "$net" || "$net" == \#* ]] && continue
|
||||
apply_route "$net"; ((n++))
|
||||
done < "$LIST"
|
||||
sync_wg >/dev/null
|
||||
echo "applied=$n"
|
||||
}
|
||||
|
||||
case "${1:-}" in
|
||||
list) cmd_list ;;
|
||||
add) shift; cmd_add "$@" ;;
|
||||
remove|del) shift; cmd_remove "$@" ;;
|
||||
clear) cmd_clear ;;
|
||||
apply|post-up) cmd_apply ;;
|
||||
*) echo "usage: $0 {list|add NET...|remove NET...|clear|apply}"; exit 1 ;;
|
||||
esac
|
||||
25
bin/ru-set.sh
Executable file
25
bin/ru-set.sh
Executable file
@@ -0,0 +1,25 @@
|
||||
#!/bin/bash
|
||||
set -u
|
||||
ID="${1:-}"
|
||||
[ -z "$ID" ] && { echo "usage: $0 <server_id>"; exit 1; }
|
||||
DATA=$(python3 -c "
|
||||
import json, sys
|
||||
try:
|
||||
data = json.load(open('/etc/wireguard/ru-servers.json'))
|
||||
except Exception as e:
|
||||
print('ERR: '+str(e), file=sys.stderr); sys.exit(2)
|
||||
for s in data['servers']:
|
||||
if s['id'] == sys.argv[1]:
|
||||
print(s['pubkey'] + '|' + s['endpoint'] + '|' + s['label']); sys.exit(0)
|
||||
sys.exit(1)
|
||||
" "$ID")
|
||||
[ -z "$DATA" ] && { echo "unknown server: $ID"; exit 1; }
|
||||
PK="${DATA%%|*}"; rest="${DATA#*|}"; EP="${rest%%|*}"; LABEL="${rest#*|}"
|
||||
sed -i "s|^PublicKey = .*|PublicKey = $PK|" /etc/wireguard/ru.conf
|
||||
sed -i "s|^Endpoint = .*|Endpoint = $EP|" /etc/wireguard/ru.conf
|
||||
wg syncconf ru <(wg-quick strip ru)
|
||||
/usr/local/bin/ru-routes.sh apply >/dev/null 2>&1 || true
|
||||
mkdir -p /var/lib/ru-failover
|
||||
date +%s > /var/lib/ru-failover/last_switch
|
||||
echo 0 > /var/lib/ru-failover/test_started
|
||||
echo "OK: ru → $LABEL ($EP)"
|
||||
Reference in New Issue
Block a user