Skip to content

Instantly share code, notes, and snippets.

@kived
Created February 13, 2015 19:09
Show Gist options
  • Select an option

  • Save kived/9ee4ad36a16881d4b25e to your computer and use it in GitHub Desktop.

Select an option

Save kived/9ee4ad36a16881d4b25e to your computer and use it in GitHub Desktop.
LXC NAT tool
#!/usr/bin/env python3
'''
lxc-nat.py: Simple Python script to create port-forwarding rules based on a static table.
Configuration
=============
Create /etc/lxc/lxc-nat.json with rules such as the following:
::
{
"config": {
"default_interface": "eth0"
},
"forwards": [
{ "source": { "ip": "10.0.0.1", "port": 80 },
"destination": { "name": "www", "port": 80 } },
{ "source": { "port": 3306 },
"destination": { "name": "mysql_server" } },
{ "proto": "udp",
"source": { "interface": "eth1", "port": 53 },
"destination": { "name": "dns_server" } }
]
}
`config` section
----------------
This section only contains a single setting: `default_interface`. Useful for
machines which do not have an `eth0` device.
`forwards` section
------------------
The bulk of the configuration. This section is an array of port forward
definitions. Each definition may contain the following settings:
- proto: protocol to forward (defaults to 'tcp').
- source: the source to forward from. `port` is the only required value.
- ip: source IP address (if empty, uses interface instead)
- interface: source interface (defaults to `default_interface`)
- port: source port
- destination: the destination to forward to. `name` is the only required
value.
- name: destination container name
- port: destination port (defaults to source port)
'''
import os
import argparse
import subprocess
from pprint import pformat
try:
from cjson import decode as loads
except ImportError:
from json import loads
parser = argparse.ArgumentParser(description='Simple Python script to create port-forwarding rules based on a static table')
parser.add_argument('-f', '--flush', action='store_true', help='flush tables without adding new rules')
parser.add_argument('-n', '--noop', action='store_true', help='simulate commands without running them')
parser.add_argument('-c', '--config', nargs=1, action='store', default='/etc/lxc/lxc-nat.json', help='config file (default: /etc/lxc/lxc-nat.json)')
parser.add_argument('-v', '--verbose', action='store_true', help='show detailed output')
parser.add_argument('-R', '--ignoreroot', action='store_true', help='do not check for root access')
args = parser.parse_args()
forwards = []
containers = {}
if not args.ignoreroot and not os.geteuid() == 0:
print('This script requires root privileges to modify iptables.')
raise SystemExit
lxc_cmd = 'lxc-ls -f -F name,ipv4'
lxc_output = subprocess.check_output(lxc_cmd.split()).decode('utf8')
if args.verbose:
print('LXC container list:')
print('\n'.join('\t' + ln for ln in lxc_output.splitlines()))
for line in lxc_output.splitlines()[2:]:
cols = line.split()
containers[cols[0]] = cols[1]
with open(args.config, 'r') as f:
config = loads(f.read())
script_config = config.get('config', {})
default_interface = script_config.get('default_interface', 'eth0')
def print_forward(forward):
print('Port forward:')
print('\n'.join('\t' + ln for ln in pformat(forward).splitlines()))
def name_forward(forward):
proto = forward.get('proto', 'tcp')
src = forward.get('source', {})
dest = forward.get('destination', {})
sname = src.get('ip', None)
if not sname:
sname = src.get('interface', default_interface)
sport = src.get('port')
dname = dest.get('name')
dport = dest.get('port', sport)
return '{}/{}:{} -> {}:{}'.format(proto, sname, sport, dname, dport)
for forward in config['forwards']:
fw_src = forward.get('source', {})
fw_dest = forward.get('destination', {})
fw_proto = forward.get('proto', 'tcp')
src_ip = fw_src.get('ip', None)
src_iface = fw_src.get('interface', None)
src_port = fw_src.get('port', None)
dest_name = fw_dest.get('name', None)
dest_port = fw_dest.get('port', None)
if not src_iface and not src_ip:
src_iface = default_interface
if not dest_port:
dest_port = src_port
if not src_port:
if args.verbose:
print_forward(forward)
raise RuntimeError('Port forward definition is missing a source port!')
if not dest_name:
if args.verbose:
print_forward(forward)
raise RuntimeError('Port forward definition is missing a destination container name!')
if dest_name not in containers:
if args.verbose:
print_forward(forward)
print('WARNING: Port forward definition refers to non-existant container "{}"'.format(dest_name))
continue
cmd = ['iptables -t nat -A lxc-nat']
if src_iface:
cmd.append('-i ' + src_iface)
if src_ip:
cmd.append('-d ' + src_ip)
cmd.append('-p ' + fw_proto)
cmd.append('--dport {}'.format(src_port))
cmd.append('-j DNAT --to {}:{}'.format(containers[dest_name], dest_port))
forwards.append((name_forward(forward), ' '.join(cmd)))
def runcmd(cmd):
print('\t' + cmd)
if not args.noop:
subprocess.check_call(cmd.split())
try:
subprocess.check_call('iptables -t nat -L lxc-nat'.split(), stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
except Exception:
print('No existing chain to flush')
else:
print('Flush existing chain...')
runcmd('iptables -t nat -F lxc-nat')
runcmd('iptables -t nat -D PREROUTING -j lxc-nat')
runcmd('iptables -t nat -X lxc-nat')
if not args.flush:
print('Create chain...')
runcmd('iptables -t nat -N lxc-nat')
runcmd('iptables -t nat -A PREROUTING -j lxc-nat')
for forward in forwards:
print('Forward: ' + forward[0])
runcmd(forward[1])
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment