Или как скрестить ежа (маршрутизацию на основании адресов) и ужа (встроенный механизм политик интернет-подключений).
Достаточно очевидным решением представляется подсесть на существующий механизм (работающий за счет xmarks/connmarks), помечая нужным нам соединения так, чтоб их уже встроенный механизм маршрутизации отправлял в нужную сторону с учетом приоритетов, балансировки и прочих сетевых кунштюков.
Для определения адреса в нужный ipset будем использовать AdGuard Home.
Конфигурация будет простым шелл-скриптом с набором переменных, который будем в нужном месте включать в рабочие скрипты.
Правила будут иметь вид:
назначение источник протокол порты имя_политики
Если перед каким-то ключом стоит "!" - значит, в итоговых правилах будет отрицание этого условия ("все, кроме Х"). Значение ключа "-" означает, что по этому ключу в правиле выбор осуществляться не будет ("все"). Порядок применения - в порядке перечисления в конфигурации, сработает последнее примененное правило.
# cat /opt/etc/config/pbr
BASE_URL="http://192.168.1.100/config/" # здесь у нас хранилище конфигураций
FILTER_DIR="/tmp/filter.lists/"
TIMEOUT=4200
DST_IPSETS="vpn1 vpn1.list
vpn2 vpn2.list
vpn vpn.list
isp isp.list
tor tor.list"
DST_FIXED_IPSETS="blocked"
SRC_MACSETS="trusted trusted.list
devices devices.list"
RULES="vpn trusted tcp 21,22,25,80,81,443,587,993,995,5222,5269 ANY_VPN
vpn1 trusted tcp 21,22,25,80,81,443,587,993,995,5222,5269 VPN1
vpn1 devices tcp 80,443 VPN1
vpn2 trusted tcp 21,22,25,80,81,443,587,993,995,5222,5269 VPN2
vpn2 !trusted tcp 80,443 VPN2
blocked trusted - - ANY_VPN
blocked devices tcp 80,443 ANY_VPN
tor trusted - - TOR
tor devices tcp 80,443 TOR
isp - - - ISP"
Скрипт для создания ipsets и файла настроек для AdGuard:
# cat /opt/etc/init.d/S84ipset
#!/bin/sh
. /opt/etc/config/pbr
get_dst_set() {
SET_N=$1
LIST_N=$2
curl $BASE_URL/$LIST_N 2>/dev/null | grep . | grep -v '^#' | cut -f 1 | sort | uniq > $FILTER_DIR/${LIST_N}.new
if ! diff -q $FILTER_DIR/${LIST_N}.new $FILTER_DIR/$LIST_N; then
cp $FILTER_DIR/${LIST_N}.new $FILTER_DIR/$LIST_N
cat $FILTER_DIR/${LIST_N} | while read DOMAIN; do
echo "$DOMAIN/$SET_N,${SET_N}6"
done > $FILTER_DIR/${SET_N}.ipset
fi
rm $FILTER_DIR/${LIST_N}.new
}
get_src_set() {
SET_N=$1
LIST_N=$2
curl $BASE_URL/$LIST_N 2>/dev/null | grep . | grep -v '^#' | cut -f 1 | sort | uniq > $FILTER_DIR/${SET_N}.mac
}
fill_src_ipsets() {
curl http://127.0.0.1:79/rci/show/ip/neighbour 2>/dev/null | jq -c -r '.[]' | while read NEIGHBOUR; do
MAC_ADDR=$(echo $NEIGHBOUR | jq '.mac' | tr -d '"')
echo "$SRC_MACSETS" | while read SET_NAME FILE_NAME; do
if grep -q $MAC_ADDR $FILTER_DIR/${SET_NAME}.mac; then
FAMILY=$(echo $NEIGHBOUR | jq -c '."address-family"' | tr -d '"')
if [ "$FAMILY" == "ipv6" ]; then
ADDR_LIST=$(echo $NEIGHBOUR | jq -c '.addresses.address')
echo $ADDR_LIST | jq -c -r '.[]' | jq -c '.address' |tr -d '"' | while read ADDRESS; do
ipset -q add ${SET_NAME}6 $ADDRESS
done
else
ADDRESS=$(echo $NEIGHBOUR | jq -c '.address' | tr -d '"')
ipset -q add $SET_NAME $ADDRESS
fi
fi
done
done
}
fill_ipset_files() {
mkdir -p $FILTER_DIR
touch $FILTER_DIR/adguard.ipsets
echo "$DST_IPSETS" | while read SET_NAME FILE_NAME; do
get_dst_set $SET_NAME $FILE_NAME
done
cat $FILTER_DIR/*.ipset > $FILTER_DIR/adguard.ipsets
echo "$SRC_MACSETS" | while read SET_NAME FILE_NAME; do
get_src_set $SET_NAME $FILE_NAME
done
fill_src_ipsets
}
start() {
echo "$DST_IPSETS" | while read SET_NAME FILE_NAME; do
ipset -q create $SET_NAME hash:net timeout $TIMEOUT
ipset -q create ${SET_NAME}6 hash:net timeout $TIMEOUT family ipv6
done
echo "$DST_FIXED_IPSETS" | while read SET_NAME; do
ipset -q create $SET_NAME hash:net timeout 0
ipset -q create ${SET_NAME}6 hash:net timeout 0 family ipv6
done
echo "$SRC_MACSETS" | while read SET_NAME FILE_NAME; do
ipset -q create $SET_NAME hash:ip timeout 0
ipset -q create ${SET_NAME}6 hash:ip timeout 0 family ipv6
done
fill_ipset_files
}
reload() {
fill_ipset_files
/opt/etc/init.d/S99adguardhome restart
}
case "$1" in
start)
start
;;
restart)
start
;;
reload)
reload
;;
esac
Не забываем в конфигурации AdGuard сослаться на ipset-file: /tmp/filter.lists/adguard.ipsets
Хук для обработки сообщений о появлении нового устройства в сети, проверки его принадлежности к одному из наборов mac-адресов и добавления адреса в ipset.
# cat /opt/etc/ndm/neighbour.d/macset
#!/bin/sh
. /opt/etc/config/pbr
MAC=$(curl http://127.0.0.1:79/rci/show/ip/neighbour 2>/dev/null | jq --arg num "$id" '.[$num].mac' | tr -d '"')
echo "${SRC_MACSETS}" | while read SET_NAME FILE_NAME; do
if grep -q $MAC $FILTER_DIR/${SET_NAME}.mac; then
ADDR=$(curl http://127.0.0.1:79/rci/show/ip/neighbour 2>/dev/null | jq -c --arg num "$id" '.[$num]')
FAMILY=$(echo $ADDR | jq -c '."address-family"' | tr -d '"')
if [ "$FAMILY" == "ipv6" ]; then
ADDR_LIST=$(echo $ADDR | jq -c '.addresses.address')
echo $ADDR_LIST | jq -c -r '.[]' | jq -c '.address' |tr -d '"' | while read ADDRESS; do
if [ "$ACTION" == "del" ]; then
ipset -q del ${SET_NAME}6 $ADDRESS
else
ipset -q add ${SET_NAME}6 $ADDRESS
fi
done
fi
if [ "$FAMILY" == "ipv4" ]; then
ADDRESS=$(echo $ADDR | jq -c '.address' | tr -d '"')
if [ "$ACTION" == "del" ]; then
ipset -q del $SET_NAME $ADDRESS
else
ipset -q add $SET_NAME $ADDRESS
fi
fi
fi
done
exit 0
Ну и главный хук - для подвешивания меток на соединения.
# cat /opt/etc/ndm/netfilter.d/050-pbr.sh
#!/bin/sh
[ "$table" != "mangle" ] && exit;
[ ! -z "$(ipset --quiet list vpn)" ] || exit 0
. /opt/etc/config/pbr
# HACK!!!!
insmod /lib/modules/4.9-ndm-5/xt_multiport.ko
check_bang() {
VAR=$1
PREFIX1=$2
PREFIX2=$3
POSTFIX=$4
SETPOSTFIX=$5
if [ "$VAR" != "-" ]; then
if [ "${VAR:0:1}" != "!" ]; then
echo $PREFIX1 $PREFIX2 $VAR$SETPOSTFIX $POSTFIX
else
echo $PREFIX1 ! $PREFIX2 ${VAR:1}$SETPOSTFIX $POSTFIX
fi
else
echo ""
fi
}
process_rule() {
DSTSET=$1
SRCSET=$2
PROTO=$3
PORTS=$4
POLICY=$5
CONNMARK=$(curl http://127.0.0.1:79/rci/show/ip/policy 2>/dev/null | jq '.[] | select( .description == "'$POLICY'") | .mark' | tr -d '"')
RULE=""
if [ "$type" != "ip6tables" ]; then
DSTSET=$(check_bang $DSTSET "-m set" "--match-set" "dst" "")
SRCSET=$(check_bang $SRCSET "-m set" "--match-set" "src" "")
else
DSTSET=$(check_bang $DSTSET "-m set" "--match-set" "dst" "6")
SRCSET=$(check_bang $SRCSET "-m set" "--match-set" "src" "6")
fi
PROTO=$(check_bang $PROTO "" "-p" "" "")
PORTS=$(check_bang $PORTS "-m multiport" "--dports" "" "")
if [ "$type" != "ip6tables" ]; then
iptables -w -t mangle -A PBRP -m conntrack --ctstate NEW $PROTO $PORTS $DSTSET $SRCSET -j CONNMARK --set-mark 0x$CONNMARK
iptables -w -t mangle -A PBRO -m conntrack --ctstate NEW $PROTO $PORTS $DSTSET -j CONNMARK --set-mark 0x$CONNMARK
else
ip6tables -w -t mangle -A PBRP -m conntrack --ctstate NEW $PROTO $PORTS $DSTSET $SRCSET -j CONNMARK --set-mark 0x$CONNMARK
ip6tables -w -t mangle -A PBRO -m conntrack --ctstate NEW $PROTO $PORTS $DSTSET -j CONNMARK --set-mark 0x$CONNMARK
fi
}
process_rules() {
if [ "$type" != "ip6tables" ]; then
iptables -w -t mangle -N PBRP 2>/dev/null
iptables -w -t mangle -N PBRO 2>/dev/null
iptables -w -t mangle -F PBRP
iptables -w -t mangle -F PBRO
echo "$RULES" | while read -r RULE; do
process_rule $RULE
done
iptables -w -t mangle -A PBRP -j CONNMARK --restore-mark
iptables -w -t mangle -A PBRO -j CONNMARK --restore-mark
iptables -w -t mangle -A PBRP -j RETURN
iptables -w -t mangle -A PBRO -j RETURN
iptables -w -t mangle -D PREROUTING -j PBRP 2>/dev/null
iptables -w -t mangle -A PREROUTING -j PBRP
iptables -w -t mangle -D OUTPUT -j PBRO 2>/dev/null
iptables -w -t mangle -A OUTPUT -j PBRO
else
ip6tables -w -t mangle -N PBRP 2>/dev/null
ip6tables -w -t mangle -N PBRO 2>/dev/null
ip6tables -w -t mangle -F PBRP
ip6tables -w -t mangle -F PBRO
echo "$RULES" | while read -r RULE; do
process_rule $RULE
done
ip6tables -w -t mangle -A PBRP -j CONNMARK --restore-mark
ip6tables -w -t mangle -A PBRO -j CONNMARK --restore-mark
ip6tables -w -t mangle -A PBRP -j RETURN
ip6tables -w -t mangle -A PBRO -j RETURN
ip6tables -w -t mangle -D PREROUTING -j PBRP 2>/dev/null
ip6tables -w -t mangle -A PREROUTING -j PBRP
ip6tables -w -t mangle -D OUTPUT -j PBRO 2>/dev/null
ip6tables -w -t mangle -A OUTPUT -j PBRO
fi
}
process_rules
exit 0