Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add support for ipv6 #39

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
*.pyc
qemuc
.vscode/
147 changes: 141 additions & 6 deletions hooks
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ CONFIG_SCHEMA_FILENAME = os.getenv('CONFIG_SCHEMA_FILENAME') or os.path.join(
CONFIG_PATH, "hooks.schema.json")
IPTABLES_BINARY = os.getenv('IPTABLES_BINARY') or subprocess.check_output([
"which", "iptables"]).strip()
IP6TABLES_BINARY = os.getenv('IP6TABLES_BINARY') or subprocess.check_output([
"which", "ip6tables"]).strip()


# Allow comments in json, copied from https://github.com/getify/JSON.minify
Expand Down Expand Up @@ -95,26 +97,62 @@ def json_minify(string, strip_space=True):
new_str.append(' ' * len(val))

new_str.append(string[index:])
return ''.join(new_str)
new_str = ''.join(new_str)
return new_str


def host_ip():
def host_ip(devname=None):
"""Returns the default route interface IP (if any).

In other words, the public IP used to access the virtualization host. It
is used as default public IP for guest forwarding rules should they not
specify a different public IP to forward from.
"""
if not hasattr(host_ip, "_host_ip"):
cmd = "ip route | grep default | cut -d' ' -f5 | head -n1"
default_route_interface = subprocess.check_output(
cmd, shell=True).decode().strip()
if not devname:
cmd = "ip -6 route | grep default | cut -d' ' -f5 | head -n1"
default_route_interface = subprocess.check_output(
cmd, shell=True).decode().strip()
else:
default_route_interface = devname
cmd = "ip addr show {0} | grep -E 'inet .*{0}' | cut -d' ' -f6 | cut -d'/' -f1".format(
default_route_interface)
host_ip._host_ip = subprocess.check_output(
cmd, shell=True).decode().strip()
return host_ip._host_ip.split('\n')[0]

def host_ipv6(devname=None):
"""Returns the default route interface IPv6 (if any).

Modified from host_ip() to return the IPv6 address of the default route

*** THIS FUNCTION IS BUGGY ***
`ip -6 route | grep default | cut -d' ' -f5 | head -n1` do no always return the correct device name
for example, `ip -6 route` may return:
```
...
default proto ra metric 100 pref medium
nexthop via fe80::9e3a:9aff:fe97:6882 dev enp6s0 weight 1
nexthop via fe80::6aed:34ff:fe13:a2c9 dev enp6s0 weight 1
```
in this case, the correct device name is `enp6s0`, but the command above will return `100`
and the function will return the wrong empty IPv6 address.

So add devname parameter to specify the device name to get the IPv6 address
"""
if not hasattr(host_ipv6, "_host_ipv6"):
if not devname:
cmd = "ip -6 route | grep default | cut -d' ' -f5 | head -n1"
default_route_interface = subprocess.check_output(
cmd, shell=True).decode().strip()
else:
default_route_interface = devname
cmd = "ip addr show {0} | grep -E 'inet6 .*global' | cut -d' ' -f6 | cut -d'/' -f1".format(
default_route_interface)
host_ipv6._host_ipv6 = subprocess.check_output(
cmd, shell=True).decode().strip()
return host_ipv6._host_ipv6.split('\n')[0]


def config(validate=True):
"""Returns the hook configuration.
Expand Down Expand Up @@ -148,12 +186,23 @@ def create_chain(table, name):
subprocess.call([IPTABLES_BINARY, "-t", table, "-N", name])


def create_chain_v6(table, name):
""" Creates the named chain. """
subprocess.call([IP6TABLES_BINARY, "-t", table, "-N", name])


def delete_chain(table, name):
""" Flushes and deletes the named chain. """
subprocess.call([IPTABLES_BINARY, "-t", table, "-F", name])
subprocess.call([IPTABLES_BINARY, "-t", table, "-X", name])


def delete_chain_v6(table, name):
""" Flushes and deletes the named chain. """
subprocess.call([IP6TABLES_BINARY, "-t", table, "-F", name])
subprocess.call([IP6TABLES_BINARY, "-t", table, "-X", name])


def populate_chains(dnat_chain, snat_chain, fwd_chain, public_ip, private_ip, domain, source_ip=None):
""" Fills the two custom chains with the port mappings. """
port_map = domain["port_map"]
Expand Down Expand Up @@ -194,6 +243,46 @@ def populate_chains(dnat_chain, snat_chain, fwd_chain, public_ip, private_ip, do
"-d", private_ip, "--dport", ports_range, "-j", "ACCEPT"] + interface)


def pupulate_chains_v6(dnat_chain, snat_chain, fwd_chain, public_ip, private_ip, domain, source_ip=None):
""" Fills the two custom chains with the port mappings. """
port_map = domain["port_map"]
for protocol in port_map:
for ports in port_map[protocol]:
# a single integer 80 is equivalent to [80, 80]
public_port, private_port = ports if isinstance(ports, list) else [
ports, ports]
dest = "[{0}]:{1}".format(private_ip, str(private_port))
subprocess.call([IP6TABLES_BINARY, "-t", "nat", "-A", dnat_chain, "-p", protocol,
"-d", public_ip, "--dport", str(public_port), "-j", "DNAT", "--to", dest] +
(["-s", source_ip] if source_ip else []))
subprocess.call([IP6TABLES_BINARY, "-t", "nat", "-A", snat_chain, "-p", protocol,
"-s", private_ip, "--dport", str(private_port), "-j", "SNAT", "--to-source", public_ip])
subprocess.call([IP6TABLES_BINARY, "-t", "nat", "-A", snat_chain, "-p", protocol,
"-s", private_ip, "-d", private_ip, "--dport", str(public_port), "-j", "MASQUERADE"])
interface = ["-o", domain["interface"]
] if "interface" in domain else []
subprocess.call([IP6TABLES_BINARY, "-t", "filter", "-A", fwd_chain, "-p", protocol,
"-d", private_ip, "--dport", str(private_port), "-j", "ACCEPT"] + interface)

# Iterate over all port ranges
if "port_range" in domain:
for port_range in domain["port_range"]:
ports_range = (str(port_range["init_port"]) + ":" +
str(port_range["init_port"] + port_range["ports_num"] - 1))
dest = "[{0}]:{1}".format(
private_ip, ports_range.replace(":", "-", 1))
subprocess.call([IP6TABLES_BINARY, "-t", "nat", "-A", dnat_chain, "-p", port_range["protocol"],
"-d", public_ip, "--dport", ports_range, "-j", "DNAT", "--to", dest])
subprocess.call([IP6TABLES_BINARY, "-t", "nat", "-A", snat_chain, "-p", port_range["protocol"],
"-s", private_ip, "--dport", ports_range, "-j", "SNAT", "--to-source", public_ip])
subprocess.call([IP6TABLES_BINARY, "-t", "nat", "-A", snat_chain, "-p", port_range["protocol"],
"-s", private_ip, "-d", private_ip, "--dport", ports_range, "-j", "MASQUERADE"])
interface = ["-o", domain["interface"]
] if "interface" in domain else []
subprocess.call([IP6TABLES_BINARY, "-t", "filter", "-A", fwd_chain, "-p", port_range["protocol"],
"-d", private_ip, "--dport", ports_range, "-j", "ACCEPT"] + interface)


def insert_chains(action, dnat_chain, snat_chain, fwd_chain, public_ip, private_ip):
""" inserts (action='-I') or removes (action='-D') the custom chains."""
subprocess.call([IPTABLES_BINARY, "-t", "nat", action,
Expand All @@ -210,6 +299,20 @@ def insert_chains(action, dnat_chain, snat_chain, fwd_chain, public_ip, private_

# the snat_chain doesn't work unless we turn off filtering bridged packets
# https://wiki.libvirt.org/page/Net.bridge.bridge-nf-call_and_sysctl.conf


def insert_chains_v6(action, dnat_chain, snat_chain, fwd_chain, public_ip, private_ip):
""" inserts (action='-I') or removes (action='-D') the custom chains."""
subprocess.call([IP6TABLES_BINARY, "-t", "nat", action,
"OUTPUT", "-d", public_ip, "-j", dnat_chain])
subprocess.call([IP6TABLES_BINARY, "-t", "nat", action,
"PREROUTING", "-d", public_ip, "-j", dnat_chain])

subprocess.call([IP6TABLES_BINARY, "-t", "nat", action, "POSTROUTING",
"-s", private_ip, "-d", private_ip, "-j", snat_chain])
subprocess.call([IP6TABLES_BINARY, "-t", "filter", action,
"FORWARD", "-d", private_ip, "-j", fwd_chain])



def disable_bridge_filtering():
Expand All @@ -230,6 +333,19 @@ def start_forwarding(dnat_chain, snat_chain, fwd_chain, public_ip, private_ip, d

insert_chains("-I", dnat_chain, snat_chain,
fwd_chain, public_ip, private_ip)


def start_forwarding_v6(dnat_chain, snat_chain, fwd_chain, public_ip, private_ip, domain, source_ip=None):
""" sets up iptables port-forwarding rules based on the port_map. """
disable_bridge_filtering()
create_chain_v6("nat", dnat_chain)
create_chain_v6("nat", snat_chain)
create_chain_v6("filter", fwd_chain)
pupulate_chains_v6(dnat_chain, snat_chain, fwd_chain,
public_ip, private_ip, domain, source_ip)

insert_chains_v6("-I", dnat_chain, snat_chain,
fwd_chain, public_ip, private_ip)


def stop_forwarding(dnat_chain, snat_chain, fwd_chain, public_ip, private_ip):
Expand All @@ -241,6 +357,15 @@ def stop_forwarding(dnat_chain, snat_chain, fwd_chain, public_ip, private_ip):
delete_chain("filter", fwd_chain)


def stop_forwarding_v6(dnat_chain, snat_chain, fwd_chain, public_ip, private_ip):
""" tears down the iptables port-forwarding rules. """
insert_chains_v6("-D", dnat_chain, snat_chain,
fwd_chain, public_ip, private_ip)
delete_chain_v6("nat", dnat_chain)
delete_chain_v6("nat", snat_chain)
delete_chain_v6("filter", fwd_chain)


def substitute_domain_name(domain_name, index_str):
# handle the 28 char limit of iptables
# we need to take the possible 5 chars of "DNAT-" and
Expand All @@ -252,16 +377,26 @@ def handle_domain(action, domain, vir_domain):
dnat_chain = "DNAT-{0}".format(vir_domain)
snat_chain = "SNAT-{0}".format(vir_domain)
fwd_chain = "FWD-{0}".format(vir_domain)
public_ip = domain.get("public_ip", host_ip())
public_ip = domain.get("public_ip", host_ip(devname=domain.get("public_interface")))
private_ip = domain["private_ip"]
source_ip = domain.get("source_ip")

public_ipv6 = domain.get("public_ipv6", host_ipv6(devname=domain.get("public_interface")))
private_ipv6 = domain["private_ipv6"]
source_ipv6 = domain.get("source_ipv6")

if action in ["stopped", "reconnect"]:
stop_forwarding(dnat_chain, snat_chain,
fwd_chain, public_ip, private_ip)
if private_ipv6:
stop_forwarding_v6(dnat_chain, snat_chain,
fwd_chain, public_ipv6, private_ipv6)
if action in ["start", "reconnect"]:
start_forwarding(dnat_chain, snat_chain, fwd_chain,
public_ip, private_ip, domain, source_ip)
if private_ipv6:
start_forwarding_v6(dnat_chain, snat_chain, fwd_chain,
public_ipv6, private_ipv6, domain, source_ipv6)


if __name__ == "__main__":
Expand Down
21 changes: 21 additions & 0 deletions hooks.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,15 +12,27 @@
"interface": {
"type": "string"
},
"public_interface": {
"type": "string"
},
"private_ip": {
"type": "string"
},
"private_ipv6": {
"type": "string"
},
"public_ip": {
"type": "string"
},
"public_ipv6": {
"type": "string"
},
"source_ip": {
"type": "string"
},
"source_ipv6": {
"type": "string"
},
"port_map": {
"type": "object",
"patternProperties": {
Expand Down Expand Up @@ -109,12 +121,21 @@
"private_ip": {
"type": "string"
},
"private_ipv6": {
"type": "string"
},
"public_ip": {
"type": "string"
},
"public_ipv6": {
"type": "string"
},
"source_ip": {
"type": "string"
},
"source_ipv6": {
"type": "string"
},
"port_map": {
"type": "object",
"patternProperties": {
Expand Down