Last active
January 22, 2018 17:08
-
-
Save ekimekim/23f75b15687b5a6a900744227dc4da4c to your computer and use it in GitHub Desktop.
run_in_netns.py: A script for running a command in a new network namespace with NATting
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import functools | |
import itertools | |
import sys | |
from collections import namedtuple | |
import argh | |
from ipaddress import IPv4Network | |
import easycmd | |
pair = namedtuple('pair', ['outer', 'inner']) | |
class Conf(object): | |
def __init__(self, name, network, nat_interface, forwards): | |
self.name = name | |
self.network = IPv4Network(network.decode('utf-8')) | |
self.nat_interface = nat_interface | |
self.forwards = [ | |
forward.split(':') if ':' in forward else ('tcp', forward) | |
for forward in forwards.split(',') | |
] if forwards else [] | |
@property | |
def netns(self): | |
return self.name | |
@property | |
def bridge(self): | |
return self.name | |
@property | |
def veths(self): | |
return pair(*list('{}{}'.format(self.name, i) for i in range(2))) | |
@property | |
def addrs(self): | |
return pair(*list(itertools.islice(self.network.hosts(), 2))) | |
@property | |
def addrs_with_prefix(self): | |
def _addprefix(addr): | |
return '{}/{}'.format(addr, self.network.prefixlen) | |
return pair(*map(_addprefix, self.addrs)) | |
def cmd(*args, **kwargs): | |
return easycmd.cmd(args, **kwargs) | |
sudo = functools.partial(cmd, 'sudo') | |
iptables = functools.partial(sudo, 'iptables') | |
brctl = functools.partial(sudo, 'brctl') | |
ip = functools.partial(sudo, 'ip') | |
ns_exec = functools.partial(ip, 'netns', 'exec') | |
def iptables_rules(conf): | |
forward_rules = [ | |
('nat', [ | |
'PREROUTING', | |
'-i', conf.bridge, | |
'-d', conf.addrs.outer, | |
'-p', proto, | |
'--dport', port, | |
'-j', 'DNAT', | |
'--to-destination', '127.0.0.1', | |
]) | |
for proto, port in conf.forwards | |
] | |
return [ | |
# NAT packets from inside ns to external network | |
('nat', [ | |
'POSTROUTING', | |
'-s', conf.addrs.inner, | |
'-j', 'MASQUERADE', | |
]), | |
# Localhost forwards | |
] + forward_rules + [ | |
# Allow packets to/from ns | |
('filter', ['FORWARD', '-i', conf.bridge, '-j', 'ACCEPT']), | |
('filter', ['FORWARD', '-o', conf.bridge, '-j', 'ACCEPT']), | |
] | |
def cleanup(conf): | |
no_error = {'success': 'nosignal'} | |
# Remove iptables rules | |
for table, rule in iptables_rules(conf)[::-1]: | |
iptables('-t', table, '-D', *rule, **no_error) | |
# Bring down and destroy the bridge | |
ip('link', 'set', conf.bridge, 'down', **no_error) | |
brctl('delbr', conf.bridge, **no_error) | |
# Destroy the netns. This also destroys the inner veth, which then destroys the outer veth. | |
ip('netns', 'delete', conf.netns, **no_error) | |
def setup(conf): | |
# Create the netns | |
ip('netns', 'add', conf.netns) | |
# Create the veths, and put the inner one inside the netns | |
ip('link', 'add', conf.veths.outer, 'type', 'veth', 'peer', 'name', conf.veths.inner) | |
ip('link', 'set', conf.veths.inner, 'netns', conf.netns) | |
# Create bridge and put outer veth onto it | |
brctl('addbr', conf.bridge) | |
brctl('addif', conf.bridge, conf.veths.outer) | |
# Set addresses for bridge and inner veth, and set them up | |
ip('addr', 'add', conf.addrs_with_prefix.outer, 'dev', conf.bridge) | |
ns_exec(conf.netns, 'ip', 'addr', 'add', conf.addrs_with_prefix.inner, 'dev', conf.veths.inner) | |
ip('link', 'set', conf.bridge, 'up') | |
ns_exec(conf.netns, 'ip', 'link', 'set', conf.veths.inner, 'up') | |
# We can now talk between the host ns (on the bridge interface) and the new ns. | |
# Now we need to set up NAT and forwards. | |
# We use -I to insert at the beginning because otherwise docker's default rules rather rudely | |
# interfere and cause issues. | |
for table, rule in iptables_rules(conf): | |
iptables('-t', table, '-I', *rule) | |
# In addition, we set up NAT inside the network namespace to transparently forward 'localhost' | |
# packets to outside the namespace (where they are then forwarded to the main ns's localhost) | |
for proto, port in conf.forwards: | |
ns_exec(conf.netns, 'iptables', '-t', 'nat', '-I', 'OUTPUT', | |
'-d', '127.0.0.1', | |
'-p', proto, | |
'--dport', port, | |
'-j', 'DNAT', '--to-destination', conf.addrs.outer, | |
) | |
# If we are doing any forwards, the route_localnet setting must be set on the bridge | |
# to allow DNAT to localhost addresses. | |
if conf.forwards: | |
sudo('tee', '/proc/sys/net/ipv4/conf/{}/route_localnet'.format(conf.bridge), stdin="1") | |
# Finally, set the default route | |
ns_exec(conf.netns, 'ip', 'route', 'add', 'default', 'via', conf.addrs.outer) | |
@argh.arg('--network', default='172.18.0.0/30') | |
@argh.arg('--nat-interface', default='eno1') | |
@argh.arg('--forwards', default='') | |
def main(name, command, *args, **kwargs): | |
"""Run COMMAND inside a network namespace called NAME, | |
with NATted access to the external network via NAT_INTERFACE. | |
NETWORK is the address range used to give the new network namespace an address. | |
It must have space for at least two addresses. | |
The namespace is automatically cleaned up on process exit, but if you need to manually | |
do a cleanup you can run it with the command 'clean'. | |
You can selectively grant access to services running on localhost to the network namespace | |
using the FORWARDS option, which should be a comma-seperated list of either tcp ports, or PROTO:PORT pairs. | |
eg. to forward DNS (UDP and TCP) as well as tcp port 8080, you would do: | |
--forwards=53,udp:53,8080 | |
""" | |
conf = Conf(name, **kwargs) | |
cleanup(conf) | |
if command == 'clean': | |
return | |
setup(conf) | |
ns_exec(conf.netns, command, *args, success='any', stdin=sys.stdin, stdout=sys.stdout, stderr=sys.stderr) | |
cleanup(conf) | |
if __name__ == '__main__': | |
argh.dispatch_command(main) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment