📜 ⬆️ ⬇️

Script for setting up MultiHomed linux router

Not being a full-fledged system administrator, I often encounter the need to configure the gateway. While the external interface was one - just changed the relatively universal script to bash, compiled on the Internet and lartc.com. When there were options with 2 Internet providers - I was honored to write a script, with settings grouped for ease of change.
The script can:

Description


It is arranged in such a way that the main python script, according to the settings in itself, forms the bash script, which runs it. Such a freak turned out for a reason, sufficiently weighty, that it could not be ignored, but not sufficiently adequate, to voice it. Download the script with the settings you can link .
And here his device is described.

Settings


The python script msr_ip.py starts with the required introductory lines:
#! / usr / bin / env python
# - * - coding: utf-8 - * -
')
# settings are here


Next comes a stream of settings, first a description of the interfaces:
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"
}
]

The interface has the following properties, which are actually dictionary elements:
dev is the name of the interface, the only required element;
gw - gateway;
table - the number or name of the routing table, if the name is specified, then it should already be in the / etc / iproute2 / rt_tables file. Required for external interfaces;
mark - label for packages, used if you add your own rules with its use;
role - role, can be external, local or none. If external - the interface is considered external, respectively, it is NATit and does not accept new connections from the outside. If local - the interface is local: NAT and can create a new connection to the outside;
weight is the weight for route-based outbound load balancing.
If the external interface is not raised, it will not be listed among the default routes.
Now the ports that need to be opened and / or forward are described .:
Ports = [
{
"proto" : "tcp" ,
"tPort" : 22 ,
"sHost" : None ,
"nPort" : None ,
"dHost" : None
}
,
{ # let's solve the whole protocol right away
"proto" : "icmp" ,
"tPort" : None ,
"sHost" : None ,
"nPort" : None ,
"dHost" : None
}
,
{ # forwarding RDP from internet to LAN on 192.168.0.3
"proto" : "tcp" ,
"tPort" : 3389 ,
"sHost" : None ,
"nPort" : 3389 ,
"dHost" : "192.168.0.3"
}
,
{ # forwarding RDP from Internet with port changed to 3390 to 192.168.0.2
"proto" : "tcp" ,
"tPort" : 3390 ,
"sHost" : None ,
"nPort" : 3389 ,
"dHost" : "192.168.0.2"
}
,
{ # web control from only one remote host
"proto" : "tcp" ,
"tPort" : 80 ,
"sHost" : "9.8.7.6" ,
"nPort" : None ,
"dHost" : None
}
,
{ # openVPN for all
"proto" : "tcp" ,
"tPort" : 1194 ,
"sHost" : None ,
"nPort" : None ,
"dHost" : None
}
]

From the examples everything should be clear, but for the order I will describe:
proto - protocol;
tPort - external port that opens;
sHost - restriction, if necessary, to the source address;
nPort - new port when forwarding;
dHost - the host to which the port is being forwarded.

Now the rules are used to bind nodes to external interfaces:
rules = [
{ # provider statistics through his gateway
"to" : "213.79.2.30" , # stats
"lookup" : "velnet"
} ,
{ # VMs on VM table
"from" : "192.168.0.150" ,
"lookup" : "VM"
} ,
{ # VMs on VM table
"from" : "192.168.0.151" ,
"lookup" : "VM"
}
]

The rules from the example use few properties, but you can apply more, I tried to cover everything that is supported by the ip rule command.
cmd - command, the default is add = add rule;
priority - priority, if not specified, then auto-numbered from 1 in the order of mention;
fwmark - packet label;
from - source;
to - destination;
lookup - table;

Now the routes:
routes = [
{ # Network behind VPN client
"to" : "192.168.8.0" ,
"dev" : "tun0"
} ,
{ # VM virtualok table through one provider
"to" : "default" ,
"via" : "5.6.7.8" ,
"dev" : "ppp0" ,
"table" : "VM"
}
]

to - destination;
via - through which gateway;
dev - through which interface;
table - for which table.

Finally, the last portion of the settings
ext_sure_ip = "8.8.8.8"
customcommands = "" "
"" "
cmdfile = "/root/msr_ip.run"


ext_sure_ip - For external interfaces, which address should be pinged for a health check, at the moment this does not affect anything;
customcommands - commands that must be executed after all the settings;
cmdfile - The file in which the resulting script is written.

Script body


Now only the script itself is going on and there will be no more settings, which is what the Perf line of the fragment happily reports:
# == 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, drop, and all masses; 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 thrown incomming iface ================================== "" " )
#for iface in ifaces:
# if if ["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 ) )



Launch


One of the options, by me and the currently used script call when changing interfaces. To do this, write a call script:
 
  root @ Gate: / root # cat ./msr_on_if_changed  
  #! / bin / sh 
  / usr / bin / python /root/msr_ip.py 
  exit 0 
 

Links to the script are installed in the network interface configuration directories. The main thing is to make the name without an extension, otherwise the link will be ignored by the initialization system:
 ln -s /root/msr_ip.py / etc / network / if-down / msr_ip
 ln -s /root/msr_ip.py / etc / network / if-up / msr_ip
 ln -s /root/msr_ip.py /etc/ppp/ip-up.d/msr_ip
 ln -s /root/msr_ip.py /etc/ppp/ip-down.d/msr_ip

Total


The script has been working for 3 months and so far there have been no failures due to its fault, I hope that this will happen.

Source: https://habr.com/ru/post/138705/


All Articles