Cкрипт для настройки MultiHomed linux router

в 9:24, , рубрики: linux, Linux для всех, python, script, метки: , , ,

Не являясь полноценным системным администратором, тем не менее часто сталкиваюсь с необходимостью настроить шлюз, пока внешний интерфейс был один просто изменял относительно универсальный скрипт на bash, собранный по интернетам и lartc.com. Когда появились варианты с 2мя интернет провайдерами — сподобился написать скрипт с настройками в одном месте.

Описание

Устроено это так, что основной скрипт на python по настройкам в самом себе формирует bash скрипт, который и запускает. Такой выкрутас получился по причине, достаточно веской, что бы её нельзя было проигнорировать, но недостаточно адекватной, что бы её озвучивать. Скачать скрипт с настройками можно по ссылке.
А здесь описывается его устройство.

Настройки

Python скрипт msr_ip.py начинается с положенных вводных строк:

#!/usr/bin/env python
# -*- coding: utf-8 -*-
 
# settings are here

Далее идёт поток настроек, сначала описание интерфейсов:

ifaces=[
    {
        "dev"  : "eth1",
        "ip"   : "192.168.0.1",
        "role" : "local"
    },
    {
        "dev"  : "eth2"
    },
    {
        "dev"  : "eth3",
        "ip"   : "1.2.3.4",
        "gw"   : "1.2.3.1",
        "table""NewComPort",
        "mark" : "0x2",
        "role" : "external",
        "weight"7
    },
    {
        "dev"  : "ppp0",
        "gw"   : "5.6.7.1",
        "table""velnet",
        "mark" : "0x1",
        "role" : "external",
        "weight"3
    },
    {
        "dev"  : "tun0",
        "ip"   : "192.168.1.1",
        "table""VPN",
        "mark" : "0x4",
        "role" : "local"
    }
]

У интерфейса возможны следующие свойства, которые на самом деле элементы словаря:
dev — имя интерфейса, единственный обязательный элемент;
gw — шлюз;
table — номер или имя таблицы маршрутизации, если указывается имя, то оно уже должно быть в файле /etc/iproute2/rt_tables. Обязательно указывается для внешний интерфейсов;
mark — метка для пакетов, используется если будете добавлять свои правила с её использованием;
role — роль, может быть external, local или никакая. Если external — интерфейс считается внешним соответственно он NATит и не принимает новые соединения снаружи. Если local — интерфейс локальный: NATится и может создавать новый соединения наружу;
weight — вес для основанной на маршрутах балансировки исходящей нагрузки.
Если внешний интерфейс не поднят, он не будет указан среди маршрутов по умолчанию.
Теперь описываются порты, которые надо открыть и/или пробросить.:

Ports = [
        {
        "proto":"tcp",
        "tPort"22,
        "sHost":None,
        "nPort":None,
        "dHost":None
        }
        ,
        { # разрешим сразу целый протокол
        "proto":"icmp",
        "tPort":None,
        "sHost":None,
        "nPort":None,
        "dHost":None
        }
        ,
        { # проброс RDP с интернетного в локалку на 192.168.0.3
        "proto":"tcp",
        "tPort":3389,
        "sHost":None,
        "nPort":3389,
        "dHost":"192.168.0.3"
        }
        ,
        {  # проброс RDP с интернетного с изменённым на 3390 портом в локалку на 192.168.0.2
        "proto":"tcp",
        "tPort":3390,
        "sHost":None,
        "nPort":3389,
        "dHost":"192.168.0.2"
        }
        ,
        { #  веб управление только с одного удалённого хоста
        "proto":"tcp",
        "tPort":80,
        "sHost":"9.8.7.6",
        "nPort":None,
        "dHost":None
        }
        ,
        { # openVPN для всех
        "proto":"tcp",
        "tPort":1194,
        "sHost":None,
        "nPort":None,
        "dHost":None
        }
]

Из примеров должно быть всё понятно, но для порядка опишу:
proto — протокол;
tPort — внешний порт, который открывается;
sHost — ограничение, если необходимо, на адрес источника;
nPort — новый порт при пробросе;
dHost — хост на который пробрасывется порт.

Теперь правила, используются для привязки узлов к внешним интерфейсам:

rules=[
    { # статистика провайдера через его шлюз
    "to"     :"213.79.2.30"# stats
    "lookup" :"velnet"
    },
    { # виртуальные машины по таблице VM
    "from"   :"192.168.0.150",
    "lookup" :"VM"
    },
    { # виртуальные машины по таблице VM
    "from"   :"192.168.0.151",
    "lookup" :"VM"
    }
]

Правила из примера используют мало свойств, но применять можно больше, я старался охватить все, которые поддерживает команда ip rule.
cmd — команда, по умолчанию это add=добавить правило;
priority — приоритет, если не указан, то автонумеруется от 1 в порядке упоминания;
fwmark — метка пакетов;
from — источник;
to — назначение;
lookup — таблица;

Теперь маршруты:

routes=[
    { # Сеть за VPN клиентом
    "to"      : "192.168.8.0",
    "dev"     : "tun0"
    },
    { # Таблица виртуалок VM через одного провайдера
    "to"      : "default",
    "via"     : "5.6.7.8",
    "dev"     : "ppp0",
    "table"   : "VM"
    }
]
 

to — назначение;
via — через какой шлюз;
dev — через какой интерфейс;
table — для какой таблицы.

Наконец, последняя порция настроек

ext_sure_ip="8.8.8.8"
customcommands="""
"""

cmdfile="/root/msr_ip.run"
 

ext_sure_ip — Для внешних интерфейсов какой адрес пингануть для проверки работоспособности, в настоящий момент это ни на что не влияет;
customcommands — команды, которые надо выполнить после всех настроек;
cmdfile — Файл в который запишется получившийся скрипт.

Тело скрипта

Теперь идёт только сам скрипт и никаких настроек больше не будет, о чём радостно и рапортует перфая строчка фрагмента:

#== no more configuration settings below this line =======================
 
import os
import syslog
import subprocess
 
syslog.syslog("Started")
 
fields=("dev","ip","gw","table","mark","works","state","role","type","weight")
fout = open(cmdfile,'w')
priority=1
 
#== add system's rules
rules=rules+[
            { "priority":100  ,"from":"all","lookup":"local"},
            { "priority":32766,"from":"all","lookup":"main"},
            { "priority":32767,"from":"all","lookup":"default"}
        ]
 
 
def isup(dev):
    x=subprocess.call(["ifconfig",dev], stdin=None, stdout=open('/dev/null''w'), stderr=open('/dev/null''w'), shell=False)
    if (x==0):
        r="up"
    else:
        r="down"
    return r
 
def works(dev):
#    global ext_sure_ip
    x=subprocess.call(["ping","-I" + dev,"-c 3","-w 5",ext_sure_ip], stdin=None, stdout=open('/dev/null''w'), stderr=open('/dev/null''w'), shell=False)
    if (x==0):
        r=True
    else:
        r=False
    return r
 
def addmissingfields(iface):
    for fld in fields:
        if not (fld in iface):
            iface[fld]=None
 
def openportcmd(port):
    r="n$IPTABLES -t filter -A INPUT "
    if port["sHost"]:
        r=r+" -s {0}".format(port["sHost"])
    if port["proto"]:
        r=r+" -p {0}".format(port["proto"])
    if port["tPort"]:
        r=r+" --dport {0}".format(port["tPort"])
    r=r+" -j ACCEPT"
    return r
 
def forwardportcmd(port):
    if port["dHost"]:
    # allow$IPTABLES        -A FORWARD                -p tcp --dport 3389 -j ACCEPT
        r="n$IPTABLES -A FORWARD "
        if port["proto"]:
            r=r+" -p {0}".format(port["proto"])
        if port["nPort"]:
            r=r+" --dport {0}".format(port["nPort"])
        r=r+" -j ACCEPT"
 
        # forward
        r=r+"n$IPTABLES -t nat -A PREROUTING "
        if port["sHost"]:
            r=r+" -s {0}".format(port["sHost"])
        if port["proto"]:
            r=r+" -p {0}".format(port["proto"])
        if port["tPort"]:
            r=r+" --dport {0}".format(port["tPort"])
        r=r+" -j DNAT --to-destination {0}".format(port["dHost"])
        if port["nPort"]:
            r=r+":{0}".format(port["nPort"])
        return r
    else:
        return ""
 
def rulecmd(rule):
    global priority
    priority=priority+1
    r="nip rule"
    if ("cmd" in rule) and (rule["cmd"]):
        r=r+" {0}".rule["cmd"]
    else:
        r=r+" add"
 
    if ("priority" in rule) and (rule["priority"]):
        r=r+" priority {0}".format(rule["priority"])
    else:
        r=r+" priority {0}".format(priority)
 
    if ("fwmark" in rule) and (rule["fwmark"]):
        r=r+" fwmark {0}/{0}".format(rule["fwmark"])
 
    if ("from" in rule) and (rule["from"]):
        r=r+" from {0}".format(rule["from"])
 
    if ("to" in rule) and (rule["to"]):
        r=r+" to {0}".format(rule["to"])
 
    if ("lookup" in rule) and (rule["lookup"]):
        r=r+" lookup {0}".format(rule["lookup"])
 
    return r
 
def routecmd(route):
    r="nip route add"
    if ("to" in route) and (route["to"]):
        r=r+" {0}".format(route["to"])
#    if ("netmask" in route) and (route["netmask"]):
#        r=r+"/{0}".format(route["netmask"])
    if ("via" in route) and (route["via"]):
        r=r+" via {0}".format(route["via"])
    if ("dev" in route) and (route["dev"]):
        r=r+" dev {0}".format(route["dev"])
    if ("table" in route) and (route["table"]):
        r=r+" table {0}".format(route["table"])
    return r
 
portion="""#!/bin/sh
 
#== set variables ===========================================
IPTABLES=/sbin/iptables
"""

fout.write(portion)
 
 
fout.write("n#== interfaces: ")
 
for iface in ifaces:
    addmissingfields(iface)
 
    iface["state"]=isup(iface["dev"])
 
    if (iface["role"]=="external"):
        iface["works"]=works(iface["dev"])
    descr="{3} {0} is {1} and works={2} ".format(iface["dev"],iface["state"],iface["works"],iface["role"])
    fout.write("n#  "+descr)
    syslog.syslog(descr)
 
portion="""
#== Disallow forwarding during script running ========================================
echo 0 > /proc/sys/net/ipv4/ip_forward
#== Drop all rules at script running ========================================
ip rule flush
 
#== delete all existing policies ===============================
$IPTABLES -F
$IPTABLES -t nat -F
$IPTABLES -t mangle -F
$IPTABLES -X
$IPTABLES -Z
 
#== Set default policies ====================================
$IPTABLES -P INPUT   DROP
$IPTABLES -P OUTPUT  ACCEPT
$IPTABLES -P FORWARD DROP
 
#== DROP bad packets ========================================
$IPTABLES -t filter  -A INPUT  -m state --state  INVALID -j DROP
$IPTABLES -t filter  -A OUTPUT -m state --state  INVALID -j DROP
$IPTABLES -t filter  -A OUTPUT -m state --state  INVALID -j DROP
 
#== Only SYN packets establish NEW connections ==============
$IPTABLES -t filter -A INPUT   -p tcp ! --syn -m state --state NEW -j DROP
$IPTABLES -t filter -A OUTPUT  -p tcp ! --syn -m state --state NEW -j DROP
$IPTABLES -t filter -A FORWARD -p tcp ! --syn -m state --state NEW -j DROP
 
#== secure lo iface =========================================
$IPTABLES -t filter  -A FORWARD -i lo -j DROP
$IPTABLES -t filter  -A FORWARD -o lo -j DROP
$IPTABLES -t filter  -A OUTPUT  -o lo -s 127.0.0.1/255.0.0.0 -j ACCEPT
$IPTABLES -t filter  -A INPUT   -i lo -d 127.0.0.1/255.0.0.0 -j ACCEPT
$IPTABLES -t filter  -A OUTPUT  -o lo                        -j DROP
$IPTABLES -t filter  -A INPUT   -i lo                        -j DROP
 
"""

 
fout.write(portion)
 
fout.write("""
 
#== Open & forward ports ================================"""
)
for port in Ports:
    fout.write(openportcmd(port))
    fout.write(forwardportcmd(port))
 
fout.write("""
 
#== between all if: allow all loc-loc, allow new loc 2 ext, drop new from ext, masquerade to ext"""
)
for iface in ifaces:
    fout.write("n#==== for {0}".format(iface["dev"]))
    for oface in ifaces:
        if (iface!=oface):
            if (iface["role"]=="local") and (oface["role"]=="local"):
                fout.write("n$IPTABLES -t filter -A FORWARD -i {0} -o {1} -j ACCEPT".format(iface["dev"],oface["dev"]))
            if (iface["role"]=="local") and (oface["role"]=="external"):
                fout.write("n$IPTABLES -t filter -A FORWARD -i {0} -o {1} -m state --state NEW -j ACCEPT".format(iface["dev"],oface["dev"]))
 
    if iface["role"]=="local":
        action="ACCEPT"
    else:
        action="DROP"
    fout.write("n$IPTABLES -t filter -A INPUT -i {0} -m state --state NEW -j {1}".format(iface["dev"],action))
 
    fout.write("n$IPTABLES -t nat -A POSTROUTING -o {0} -j MASQUERADE".format(iface["dev"]))
 
 
fout.write("""
 
#== Allow established to forward, others will be dropped by policy
$IPTABLES -t filter -A FORWARD -m state --state ESTABLISHED,RELATED -j ACCEPT
$IPTABLES -t filter -A INPUT   -m state --state ESTABLISHED,RELATED -j ACCEPT
 
"""
)
 
 
fout.write("""
#== For some ifaces set tables, marks & rules ==================================================="""
)
for iface in ifaces:
 
    if iface["table"] and iface["gw"] and iface["table"]:
        fout.write("nip route add default via {0} dev {1} table {2}".format(iface["gw"],iface["dev"],iface["table"]))
 
    if iface["mark"]:
        fout.write(rulecmd({
            "fwmark":iface["mark"],
            "lookup":iface["table"]
            }))
 
 
fout.write("""
 
#== Answers must be sent throught incomming iface  =================================="""
)
#for iface in ifaces:
#    if iface["mark"]:
#   fout.write("n$IPTABLES -t mangle -A INPUT -i {0} -j CONNMARK --set-mark {1}".format(iface["dev"],iface["mark"]))
#fout.write("n$IPTABLES -t mangle -A OUTPUT -j CONNMARK --restore-markn")
for iface in ifaces:
    if iface["table"] and iface["gw"]:
        fout.write(rulecmd({
            "from":iface["gw"],
            "lookup":iface["table"]
        }))
 
fout.write("""
 
#== Rules from rules' list =================================="""
)
for rule in rules:
    fout.write(rulecmd(rule))
 
fout.write("""
 
#== Routes from routes' list =================================="""
)
for route in routes:
    fout.write(routecmd(route))
 
fout.write("""
 
#== External ifaces interleaving (between all UP ext)==================================="""
)
portion="nip route replace default scope global"
for iface in ifaces:
    if iface["dev"] and iface["gw"] and iface["weight"] and (iface["role"]=="external") and (iface["state"]=="up"):
        portion=portion + " nexthop via {0} dev {1} weight {2}".format(iface["gw"],iface["dev"],iface["weight"])
fout.write(portion)
 
fout.write("""
 
#== Allow forwarding ========================================================================
echo 1 > /proc/sys/net/ipv4/ip_forward
"""
)
 
fout.write("""
 
#== some handmade custom commands ==============================================================
{0}
"""
.format(customcommands))
 
 
fout.close()
 
syslog.syslog("Finished")
syslog.syslog("Script started")
x=subprocess.call(["/bin/sh","{0}".format(cmdfile)], stdin=None, stdout=open('/dev/null''w'), stderr=open('/dev/null''w'), shell=False)
syslog.syslog("Script finished with result {0}".format(x))

Запуск

Один из вариантов, мной и применяемый в настоящий момент, вызов скрипта при изменении интерфейсов. Для написан скрипт вызова:

root@Gate:/root# cat ./msr_on_if_changed 
#!/bin/sh
/usr/bin/python /root/msr_ip.py
exit 0

И ссылки на него установлены в каталогах:

/etc/network/if-down
/etc/network/if-up
/etc/ppp/ip-up.d
/etc/ppp/ip-down.d

Итого

Скрипт работает уже 3 месяца и пока что сбоев по его вине не было, надеюсь что так и будет.

Автор: 4dmonster

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js