Не являясь полноценным системным администратором, тем не менее часто сталкиваюсь с необходимостью настроить шлюз, пока внешний интерфейс был один просто изменял относительно универсальный скрипт на 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