diff --git a/README.md b/README.md index 8207854..3b5d4fc 100644 --- a/README.md +++ b/README.md @@ -42,7 +42,8 @@ home-configs/ │ └── SKILL.md ├── network/ │ ├── ks-off.sh # временно отключить UFW kill switch -│ └── ks-on.sh # восстановить UFW kill switch +│ ├── ks-on.sh # восстановить UFW kill switch +│ └── ru-bypass.sh # .ru трафик напрямую (bypass Amnezia), всё остальное через VPN ├── vless/ │ └── servers.conf # список VLESS-серверов для прокси ├── proxychains/ diff --git a/home-configs/network/ru-bypass.sh b/home-configs/network/ru-bypass.sh new file mode 100644 index 0000000..b9529ac --- /dev/null +++ b/home-configs/network/ru-bypass.sh @@ -0,0 +1,182 @@ +#!/bin/bash +# ru-bypass.sh — .ru трафик напрямую мимо Amnezia, всё остальное через VPN + kill switch +# +# Принцип: ipset + более специфичные маршруты имеют приоритет над amn0. +# Kill switch (UFW) остаётся активным — не-.ru трафик при отвале Amnezia блокируется. +# +# Первый запуск устанавливает ipset, два systemd сервиса и NetworkManager dispatcher: +# - ru-ipset-restore.service запускается ДО UFW, восстанавливает ipset из файла +# - ru-bypass.service запускается после network-online, обновляет RIPE-список и маршруты +# Каждый запуск обновляет список .ru IP-блоков из RIPE (кэш 24ч). +# +# Использование: sudo bash ru-bypass.sh + +GATEWAY="192.168.1.1" +DEV="wlp1s0" +SETNAME="ru-direct" +CACHE="/var/cache/ru-delegations.txt" +IPSET_SAVE="/etc/ipset.conf" +RIPE_URL="https://ftp.ripe.net/pub/stats/ripencc/delegated-ripencc-latest" +SCRIPT_DEST="/usr/local/bin/ru-bypass.sh" +UFW_BEFORE="/etc/ufw/before.rules" + +if [ "$(id -u)" != "0" ]; then + echo "Запускай от root: sudo bash $0" + exit 1 +fi + +# --- Первичная настройка (однократно) --- + +if ! command -v ipset >/dev/null 2>&1; then + echo "Устанавливаем ipset..." + apt-get install -y ipset +fi + +# Копируем скрипт в /usr/local/bin (нужно для systemd + NM dispatcher) +SELF=$(realpath "$0") +if [ "$SELF" != "$SCRIPT_DEST" ]; then + cp "$SELF" "$SCRIPT_DEST" + chmod +x "$SCRIPT_DEST" + echo "Скрипт скопирован в $SCRIPT_DEST" +fi + +# Сервис восстановления ipset ДО старта UFW (однократно) +RESTORE_SVC="/etc/systemd/system/ru-ipset-restore.service" +if [ ! -f "$RESTORE_SVC" ]; then + cat > "$RESTORE_SVC" < "$BYPASS_SVC" <<'EOF' +[Unit] +Description=Route .ru IP blocks directly (bypass Amnezia VPN) +After=network-online.target ru-ipset-restore.service +Wants=network-online.target + +[Service] +Type=oneshot +ExecStart=/usr/local/bin/ru-bypass.sh +RemainAfterExit=yes + +[Install] +WantedBy=multi-user.target +EOF + systemctl daemon-reload + systemctl enable ru-bypass.service + echo "Сервис ru-bypass установлен." +fi + +# NetworkManager dispatcher — авто-перезапуск когда amn0 поднимается (однократно) +NM_DISPATCHER="/etc/NetworkManager/dispatcher.d/99-ru-bypass" +if [ ! -f "$NM_DISPATCHER" ]; then + cat > "$NM_DISPATCHER" <<'EOF' +#!/bin/bash +[ "$1" = "amn0" ] && [ "$2" = "up" ] && exec /usr/local/bin/ru-bypass.sh +EOF + chmod +x "$NM_DISPATCHER" + echo "NetworkManager dispatcher установлен." +fi + +# --- Обновляем RIPE-список (кэш 24ч) --- + +if [ ! -f "$CACHE" ] || [ $(( $(date +%s) - $(stat -c %Y "$CACHE" 2>/dev/null || echo 0) )) -gt 86400 ]; then + echo "Обновляем список .ru IP-блоков из RIPE..." + if curl -fsS -o "$CACHE.tmp" "$RIPE_URL"; then + mv "$CACHE.tmp" "$CACHE" + else + echo "Предупреждение: не удалось скачать RIPE-список" + if [ ! -f "$CACHE" ]; then exit 1; fi + echo "Используем старый кэш от $(date -r "$CACHE")" + fi +fi + +# --- Создаём/обновляем ipset --- + +echo "Обновляем ipset $SETNAME..." +# create -exist: не падает если уже есть (UFW на него ссылается, destroy ломает цепочку) +ipset create "$SETNAME" hash:net -exist +# flush: очищаем записи, но сохраняем сам set (iptables-правило остаётся валидным) +ipset flush "$SETNAME" + +python3 -c " +import ipaddress + +entries = 0 +with open('$CACHE') as f_in: + for line in f_in: + parts = line.strip().split('|') + if len(parts) < 5 or parts[1] != 'RU' or parts[2] != 'ipv4': + continue + ip_str, count = parts[3], int(parts[4]) + first = ipaddress.IPv4Address(ip_str) + last = first + count - 1 + for net in ipaddress.summarize_address_range(first, last): + print(f'add $SETNAME {net}') + entries += 1 +import sys; print(f'# entries: {entries}', file=sys.stderr) +" 2>/tmp/ru-ipset-count.txt | ipset restore -exist -quiet + +ENTRIES=$(ipset list "$SETNAME" 2>/dev/null | grep -c '/') +echo "ipset обновлён: $ENTRIES записей" + +# Сохраняем ipset на диск — ru-ipset-restore.service восстановит его до UFW при перезагрузке +ipset save "$SETNAME" > "$IPSET_SAVE" +echo "ipset сохранён в $IPSET_SAVE" + +# --- Добавляем маршруты --- + +echo "Добавляем маршруты..." +python3 -c " +import ipaddress + +with open('$CACHE') as f, open('/tmp/ru-routes.batch', 'w') as out: + count = 0 + for line in f: + parts = line.strip().split('|') + if len(parts) < 5 or parts[1] != 'RU' or parts[2] != 'ipv4': + continue + ip_str, n = parts[3], int(parts[4]) + first = ipaddress.IPv4Address(ip_str) + last = first + n - 1 + for net in ipaddress.summarize_address_range(first, last): + out.write(f'route replace {net} via $GATEWAY dev $DEV\n') + count += 1 + print(f'Маршрутов: {count}') +" +ip -force -batch /tmp/ru-routes.batch 2>/dev/null + +# --- Правило в UFW before.rules (однократно, после создания ipset) --- + +UFW_MARKER="match-set $SETNAME" +if ! grep -q "$UFW_MARKER" "$UFW_BEFORE" 2>/dev/null; then + echo "Добавляем правило в UFW before.rules..." + sed -i "0,/^COMMIT/{s/^COMMIT/# .ru bypass (ipset $SETNAME)\n-A ufw-before-output -m set --match-set $SETNAME dst -o $DEV -j ACCEPT\nCOMMIT/}" "$UFW_BEFORE" + if ufw status | grep -qE "активен|active"; then + ufw reload + fi + echo "UFW обновлён." +fi + +echo "" +echo "Готово." +echo " ip route get 8.8.8.8 -> dev amn0 (через VPN)" +echo " ip route get 95.173.136.1 -> dev $DEV (напрямую)" diff --git a/tests/test_network.sh b/tests/test_network.sh new file mode 100644 index 0000000..400f9df --- /dev/null +++ b/tests/test_network.sh @@ -0,0 +1,82 @@ +#!/bin/bash +# Тесты сетевой настройки: Amnezia + ru-bypass + kill switch +# Запускать без sudo (проверяет что доступно обычному пользователю) + +RED='\033[0;31m' +GRN='\033[0;32m' +YEL='\033[0;33m' +CLR='\033[0m' + +pass=0 fail=0 + +check() { + local desc="$1" expected="$2" + local actual + actual=$(eval "$3" 2>&1) + if echo "$actual" | grep -qE "$expected"; then + echo -e "${GRN}✓${CLR} $desc" + pass=$((pass+1)) + else + echo -e "${RED}✗${CLR} $desc" + echo " ожидалось: $expected" + echo " получено: $(echo "$actual" | head -3)" + fail=$((fail+1)) + fi +} + +echo "=== 1. Проверка окружения ===" +check "Amnezia интерфейс (amn0) существует" "amn0" "ip link show amn0 2>/dev/null" +check "wlp1s0 wifi интерфейс" "wlp1s0" "ip link show wlp1s0 2>/dev/null" + +IPSET_INFO=$(sudo ipset list ru-direct 2>/dev/null) +if [ -n "$IPSET_INFO" ]; then + echo -e "${GRN}✓${CLR} ipset ru-direct существует" + IPSET_COUNT=$(echo "$IPSET_INFO" | grep -c '/') + if [ "$IPSET_COUNT" -gt 100 ]; then + echo -e "${GRN}✓${CLR} ipset не пуст ($IPSET_COUNT блоков)" + else + echo -e "${RED}✗${CLR} ipset слишком мал ($IPSET_COUNT блоков)" + fi + RU_IP=$(echo "$IPSET_INFO" | grep -oE '[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+' | head -1) +else + echo -e "${YEL}?${CLR} ipset — проверь с sudo" + RU_IP=$(dig +short ya.ru A | head -1) +fi +echo "" +echo "=== 2. Маршрутизация .ru vs не-.ru ===" +check ".ru IP ($RU_IP) → НЕ через amn0" "wl[pi]" "ip route get $RU_IP 2>/dev/null" +check "8.8.8.8 → через amn0" "amn0" "ip route get 8.8.8.8 2>/dev/null" +check "1.1.1.1 → через amn0" "amn0" "ip route get 1.1.1.1 2>/dev/null" + +echo "" +echo "=== 3. DNS резолвинг ===" +check "ozon.ru резолвится" "185\.73\." "dig +short ozon.ru A 2>/dev/null" +check "google.com резолвится" "[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+" "dig +short google.com A 2>/dev/null | head -1" +check "gosuslugi.ru резолвится" "[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+" "dig +short gosuslugi.ru A 2>/dev/null | head -1" + +echo "" +echo "=== 4. Связность (Amnezia вкл) ===" +check "google.com отвечает (VPN)" "HTTP" "curl -sI --max-time 5 https://google.com 2>&1 | head -1" +check "ozon.ru отвечает (прямо)" "HTTP" "curl -sI --max-time 5 https://ozon.ru 2>&1 | head -1" +check "gosuslugi.ru отвечает (прямо)" "HTTP" "curl -sI --max-time 5 https://gosuslugi.ru 2>&1 | head -1" + +echo "" +echo "=== 5. Инфраструктура (нужен sudo) ===" +UFW_STATUS=$(sudo ufw status 2>/dev/null) +if echo "$UFW_STATUS" | grep -qE "активен|active"; then + echo -e "${GRN}✓${CLR} UFW активен" +else + echo -e "${YEL}?${CLR} UFW — запусти с sudo: sudo ufw status" +fi +check "ru-bypass сервис есть" "ru-bypass" "systemctl list-unit-files 2>/dev/null | grep ru-bypass || echo 'OK (проверить с sudo)'" +check "NM dispatcher есть" "99-ru-bypass" "ls -la /etc/NetworkManager/dispatcher.d/99-ru-bypass 2>/dev/null" + +echo "" +echo "=== 6. Краевые случаи ===" +check "api.anthropic.com → amn0" "amn0" "ip route get $(dig +short api.anthropic.com A | head -1) 2>/dev/null" +check "ya.ru → НЕ через amn0 (прямо)" "wl[pi]" "ip route get $(dig +short ya.ru A | head -1) 2>/dev/null" + +echo "" +echo "=========================================" +echo -e "Итого: ${GRN}$pass пройдено${CLR}, ${RED}$fail провалено${CLR}" +[ "$fail" -eq 0 ] && echo -e "${GRN}ВСЁ ОК${CLR}" || echo -e "${RED}ЕСТЬ ПРОБЛЕМЫ${CLR}"