#!/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 # Сохраняем env-переменные до загрузки конфига (env имеет приоритет) _env_gw="${GATEWAY:-}" _env_dev="${DEV:-}" _env_local_dns="${LOCAL_DNS:-}" _env_amn_srv="${AMNEZIA_SERVER:-}" _env_ks_exc="${KILL_SWITCH_EXCEPTIONS:-}" # Загружаем сохранённый конфиг (для запуска из systemd/NM dispatcher без env) [ -f /etc/ru-bypass.conf ] && . /etc/ru-bypass.conf # ENV-переменные имеют приоритет над конфигом [ -n "$_env_gw" ] && GATEWAY="$_env_gw" [ -n "$_env_dev" ] && DEV="$_env_dev" [ -n "$_env_local_dns" ] && LOCAL_DNS="$_env_local_dns" [ -n "$_env_amn_srv" ] && AMNEZIA_SERVER="$_env_amn_srv" [ -n "$_env_ks_exc" ] && KILL_SWITCH_EXCEPTIONS="$_env_ks_exc" # Дефолты (если ни конфиг, ни env не задали значение) GATEWAY="${GATEWAY:-192.168.1.1}" DEV="${DEV:-wlp1s0}" LOCAL_DNS="${LOCAL_DNS:-}" AMNEZIA_SERVER="${AMNEZIA_SERVER:-}" KILL_SWITCH_EXCEPTIONS="${KILL_SWITCH_EXCEPTIONS:-}" # Базовые исключения, необходимые для работы корпоративных сервисов # Добавляются автоматически, даже если не указаны в конфиге _BUILTIN_EXCEPTIONS="mattermost.eltex-co.ru elph.eltex-co.ru 10.80.0.15" for _exc in $_BUILTIN_EXCEPTIONS; do case " $KILL_SWITCH_EXCEPTIONS " in *" $_exc "*) ;; *) KILL_SWITCH_EXCEPTIONS="$KILL_SWITCH_EXCEPTIONS $_exc" ;; esac done KILL_SWITCH_EXCEPTIONS="${KILL_SWITCH_EXCEPTIONS# }" # Сохраняем конфиг для будущих запусков (systemd, NM dispatcher) cat > /etc/ru-bypass.conf <<_CONF GATEWAY="$GATEWAY" DEV="$DEV" LOCAL_DNS="$LOCAL_DNS" AMNEZIA_SERVER="$AMNEZIA_SERVER" KILL_SWITCH_EXCEPTIONS="$KILL_SWITCH_EXCEPTIONS" _CONF 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 # Timer для ежесуточного обновления (однократно) BYPASS_TIMER="/etc/systemd/system/ru-bypass.timer" if [ ! -f "$BYPASS_TIMER" ]; then cat > "$BYPASS_TIMER" <<'EOF' [Unit] Description=Daily update of .ru IP routes (ru-bypass) [Timer] OnCalendar=daily Persistent=true Unit=ru-bypass.service [Install] WantedBy=timers.target EOF systemctl daemon-reload systemctl enable --now ru-bypass.timer echo "Timer ru-bypass.timer установлен (ежесуточное обновление RIPE)." 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 # --- Локальные хосты (фиксируем IP для доменов, которые должны резолвиться локально) --- # Без этого DNS через VPN может вернуть внешний IP вместо внутреннего, # и трафик пойдёт через VPN вместо прямого соединения. HOSTS_MARKER="# ru-bypass: local hosts" # Удаляем старые записи по маркеру (чтобы не копились дубли) sed -i "/$HOSTS_MARKER/d" /etc/hosts # Добавляем актуальные cat >> /etc/hosts <<_HOSTS $HOSTS_MARKER # Eltex corporate services (*.eltex.loc, mattermost, elph) 172.16.0.3 eltex.loc 172.16.5.103 intdocs.eltex.loc 172.16.5.251 red.eltex.loc 172.16.1.17 gitlab.eltex.loc 172.16.1.106 pixso.eltex.loc 172.16.1.94 mcpe-builder.eltex.loc 172.16.5.63 proxy.eltex.loc 10.80.0.16 ssw.eltex.loc 172.16.5.78 nexus.eltex.loc 172.16.1.149 cpe-worker.eltex.loc 172.16.5.22 mattermost.eltex-co.ru elph.eltex-co.ru ecss-elph-proxy.eltex-co.ru _HOSTS echo "Локальные хосты: *.eltex.loc, mattermost, elph → /etc/hosts" # --- Обновляем 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 записей" # --- Исключения для kill switch --- # AMNEZIA_SERVER — IP/домены серверов Amnezia (нужны для поднятия VPN при активном kill switch) # KILL_SWITCH_EXCEPTIONS — дополнительные IP/домены, доступные напрямую даже при kill switch ALL_EXC="${AMNEZIA_SERVER} ${KILL_SWITCH_EXCEPTIONS}" if [ -n "${ALL_EXC// }" ]; then for item in $ALL_EXC; do if echo "$item" | grep -qE "^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$"; then ips="$item" else ips=$(getent hosts "$item" 2>/dev/null | awk '{print $1}' | sort -u) fi for ip in $ips; do ipset add ru-direct "$ip" -exist 2>/dev/null || true echo "Исключение kill switch: $item → $ip (ipset)" done done fi ipset save ru-direct > /etc/ipset.conf echo "ipset сохранён в /etc/ipset.conf" # --- Добавляем маршруты --- echo "Добавляем маршруты..." rm -f /tmp/ru-routes.batch 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 # --- Маршруты для локальных сетей (*.loc, RFC1918) --- LOCAL_NETS="10.0.0.0/8 172.16.0.0/12 192.168.0.0/16" echo "Добавляем маршруты для локальных сетей (*.loc / RFC1918)..." for net in $LOCAL_NETS; do ip route replace "$net" via "$GATEWAY" dev "$DEV" 2>/dev/null done # Маршруты для исключений kill switch ALL_EXC="${AMNEZIA_SERVER} ${KILL_SWITCH_EXCEPTIONS}" if [ -n "${ALL_EXC// }" ]; then for item in $ALL_EXC; do if echo "$item" | grep -qE "^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$"; then ips="$item" else ips=$(getent hosts "$item" 2>/dev/null | awk '{print $1}' | sort -u) fi for ip in $ips; do ip route replace "$ip/32" via "$GATEWAY" dev "$DEV" 2>/dev/null echo "Маршрут для исключения: $item → $ip" done done fi # --- DNS для *.loc через LOCAL_DNS (если задан) --- if [ -n "$LOCAL_DNS" ]; then if command -v resolvectl >/dev/null 2>&1; then resolvectl dns "$DEV" "$LOCAL_DNS" 2>/dev/null && \ resolvectl domain "$DEV" "~loc" 2>/dev/null && \ echo "DNS для *.loc → $LOCAL_DNS (интерфейс $DEV)" else echo "Предупреждение: resolvectl не найден, LOCAL_DNS=$LOCAL_DNS не применён" fi fi # --- Правила в UFW before.rules (обновляются при каждом запуске) --- # Маркеры используются для идентификации правил; DEV всегда актуальный. UFW_IPSET_MARKER="ru-bypass: ipset $SETNAME" UFW_LOCAL_MARKER="ru-bypass: local-nets-bypass" echo "Обновляем правила UFW before.rules..." # Удаляем старые правила (если есть) — и в новом, и в старом формате маркеров sed -i "/# $UFW_IPSET_MARKER/d; /# \.ru bypass (ipset $SETNAME)/d" "$UFW_BEFORE" sed -i "/-A ufw-before-output -m set --match-set $SETNAME dst/d" "$UFW_BEFORE" sed -i "/# $UFW_LOCAL_MARKER/d; /# local nets bypass (local-nets-bypass)/d" "$UFW_BEFORE" sed -i "/-A ufw-before-output -d 10\.0\.0\.0\/8 -o/d" "$UFW_BEFORE" sed -i "/-A ufw-before-output -d 172\.16\.0\.0\/12 -o/d" "$UFW_BEFORE" sed -i "/-A ufw-before-output -d 192\.168\.0\.0\/16 -o/d" "$UFW_BEFORE" # Добавляем правила заново с актуальным DEV sed -i "0,/^COMMIT/{s/^COMMIT/# $UFW_IPSET_MARKER\n-A ufw-before-output -m set --match-set $SETNAME dst -o $DEV -j ACCEPT\nCOMMIT/}" "$UFW_BEFORE" sed -i "0,/^COMMIT/{s/^COMMIT/# $UFW_LOCAL_MARKER\n-A ufw-before-output -d 10.0.0.0\/8 -o $DEV -j ACCEPT\n-A ufw-before-output -d 172.16.0.0\/12 -o $DEV -j ACCEPT\n-A ufw-before-output -d 192.168.0.0\/16 -o $DEV -j ACCEPT\nCOMMIT/}" "$UFW_BEFORE" echo "UFW before.rules обновлён (ipset + локальные сети, DEV=$DEV)." # --- Исправляем MANAGE_BUILTINS (должен быть yes, иначе before.rules не вызывается) --- if grep -q '^MANAGE_BUILTINS=no' /etc/default/ufw 2>/dev/null; then sed -i 's/^MANAGE_BUILTINS=no/MANAGE_BUILTINS=yes/' /etc/default/ufw echo "UFW: MANAGE_BUILTINS исправлен (no → yes)." fi # --- Настройка UFW default deny + allow amn0 (однократно) --- ufw default deny outgoing >/dev/null 2>&1 || true ufw allow out on amn0 >/dev/null 2>&1 || true if grep -qE "$UFW_IPSET_MARKER|$UFW_LOCAL_MARKER" "$UFW_BEFORE" 2>/dev/null; then if ufw status | grep -qE "активен|active"; then ufw reload fi fi # --- Прямые правила iptables (гарантия работы даже при MANAGE_BUILTINS=no) --- echo "Добавляем прямые правила iptables..." # Правило для ipset ru-direct (RU-IP + исключения kill switch) iptables -C OUTPUT -m set --match-set "$SETNAME" dst -o "$DEV" -j ACCEPT 2>/dev/null || \ iptables -I OUTPUT 1 -m set --match-set "$SETNAME" dst -o "$DEV" -j ACCEPT # Правила для локальных сетей (RFC1918) for _net in 10.0.0.0/8 172.16.0.0/12 192.168.0.0/16; do iptables -C OUTPUT -d "$_net" -o "$DEV" -j ACCEPT 2>/dev/null || \ iptables -I OUTPUT 1 -d "$_net" -o "$DEV" -j ACCEPT done echo "iptables: прямые правила добавлены." # --- Отключаем IPv6 (утечка мимо UFW kill switch) --- # UFW работает только с IPv4 — IPv6-трафик обходит kill switch полностью. _ipv6_cnt=$(ip -6 addr show scope global 2>/dev/null | grep -c 'inet6' || true) if [ "$_ipv6_cnt" -gt 0 ]; then sysctl -w net.ipv6.conf.all.disable_ipv6=1 >/dev/null 2>&1 sysctl -w net.ipv6.conf.default.disable_ipv6=1 >/dev/null 2>&1 cat > /etc/sysctl.d/99-disable-ipv6.conf << 'SYSCTEOF' # Отключение IPv6 — требуется для защиты kill switch (UFW работает только с IPv4) net.ipv6.conf.all.disable_ipv6=1 net.ipv6.conf.default.disable_ipv6=1 SYSCTEOF systemctl restart systemd-resolved 2>/dev/null || true echo "IPv6: отключён ($_ipv6_cnt адресов) для защиты kill switch." fi echo "" echo "Готово." RU_EXAMPLE=$(dig +short ya.ru A 2>/dev/null | head -1) echo " ip route get 8.8.8.8 -> dev amn0 (через VPN)" echo " ip route get ${RU_EXAMPLE:-} -> dev $DEV (напрямую .ru)" echo " ip route get 10.10.0.1 -> dev $DEV (напрямую *.loc / RFC1918)" _log_file="${USER_HOME:-$HOME}/.config/ai-setup/setup.log" printf '%s [ru-bypass] GATEWAY=%s DEV=%s, блоков: %s\n' \ "$(date '+%Y-%m-%d %H:%M:%S')" "$GATEWAY" "$DEV" "$ENTRIES" >> "$_log_file" 2>/dev/null || true