IPv6-трафик обходит UFW (который работает только с IPv4) — kill switch не защищает от утечек по IPv6. Изменения: - ru-bypass.sh: молча отключает IPv6 (sysctl + /etc/sysctl.d) при каждом запуске, если есть глобальные IPv6-адреса - ks-on.sh: тоже отключает IPv6, но без интерактивного вопроса (раньше спрашивал «Отключить IPv6? [Y/n]») - ai-setup.sh (direct mode): не восстанавливает IPv6, если UFW kill switch активен (раньше безусловно включал обратно, из-за чего после каждого запуска setup.sh IPv6 снова утекал) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
398 lines
16 KiB
Bash
398 lines
16 KiB
Bash
#!/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" <<EOF
|
||
[Unit]
|
||
Description=Restore ru-direct ipset before UFW starts
|
||
DefaultDependencies=no
|
||
Before=ufw.service network.target
|
||
After=local-fs.target
|
||
|
||
[Service]
|
||
Type=oneshot
|
||
ExecStart=/sbin/ipset restore -exist -file $IPSET_SAVE
|
||
RemainAfterExit=yes
|
||
|
||
[Install]
|
||
WantedBy=multi-user.target
|
||
EOF
|
||
systemctl daemon-reload
|
||
systemctl enable ru-ipset-restore.service
|
||
echo "Сервис ru-ipset-restore установлен (стартует до UFW)."
|
||
fi
|
||
|
||
# Основной сервис обновления маршрутов (однократно)
|
||
BYPASS_SVC="/etc/systemd/system/ru-bypass.service"
|
||
if [ ! -f "$BYPASS_SVC" ]; then
|
||
cat > "$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:-<ya.ru ip>} -> 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
|