diff --git a/.gitignore b/.gitignore index a597a27..0b2ead0 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ *.pyc qemuc +.vscode/ \ No newline at end of file diff --git a/hooks b/hooks index b81dd3e..b417bd0 100755 --- a/hooks +++ b/hooks @@ -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 @@ -95,10 +97,11 @@ 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 @@ -106,15 +109,50 @@ def host_ip(): 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. @@ -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"] @@ -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, @@ -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(): @@ -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): @@ -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 @@ -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__": diff --git a/hooks.schema.json b/hooks.schema.json index 4c46d94..f9732be 100644 --- a/hooks.schema.json +++ b/hooks.schema.json @@ -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": { @@ -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": {