Add wireguard-nat-nftables python script
This commit is contained in:
		
					parent
					
						
							
								667b1c256b
							
						
					
				
			
			
				commit
				
					
						299d04142f
					
				
			
		
					 6 changed files with 152 additions and 4 deletions
				
			
		|  | @ -4,5 +4,6 @@ | ||||||
|     ./configuration.nix |     ./configuration.nix | ||||||
|     ./nginx.nix |     ./nginx.nix | ||||||
|     ./containers/uptime-kuma |     ./containers/uptime-kuma | ||||||
|  |     ./services.nix | ||||||
|   ]; |   ]; | ||||||
| } | } | ||||||
|  |  | ||||||
							
								
								
									
										30
									
								
								config/hosts/valkyrie/services.nix
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										30
									
								
								config/hosts/valkyrie/services.nix
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,30 @@ | ||||||
|  | { pkgs, ... }: | ||||||
|  | let | ||||||
|  |   wireguard-nat-nftables = import ../../../pkgs/wireguard-nat-nftables pkgs; | ||||||
|  |   config = pkgs.writeText "wireguard-nat-nftables-config" (builtins.toJSON { | ||||||
|  |     interface = "ens3"; | ||||||
|  |     wg_interface = "wg0"; | ||||||
|  |     pubkey_port_mapping = { | ||||||
|  |       "SJ8xCRb4hWm5EnXoV4FnwgbiaxmY2wI+xzfk+3HXERg=" = [ 51827 51829 ]; | ||||||
|  |       "BbNeBTe6HwQuHPK+ZQXWYRZJJMPdS0h81n07omYyRl4=" = [ 51828 51830 ]; | ||||||
|  |       "u9h+D8XZ62ABnetBRKnf6tjs+tJwM8fQ4d6ipOCLFyE=" = [ 51821 51824 ]; | ||||||
|  |     }; | ||||||
|  |   }); | ||||||
|  | in | ||||||
|  | { | ||||||
|  |   systemd.services.wireguard-nat-nftables = { | ||||||
|  |     description = "A python script to update nftable dnat rules based on WireGuard peer IPs"; | ||||||
|  |     requires = [ "wireguard-wg0.service" ]; | ||||||
|  |     after = [ "wireguard-wg0.service" ]; | ||||||
|  | 
 | ||||||
|  |     script = '' | ||||||
|  |       ${wireguard-nat-nftables}/bin/wireguard-nat-nftables.py ${config} | ||||||
|  |     ''; | ||||||
|  | 
 | ||||||
|  |     serviceConfig = { | ||||||
|  |       Type = "simple"; | ||||||
|  |       User = "root"; | ||||||
|  |       Group = "root"; | ||||||
|  |     }; | ||||||
|  |   }; | ||||||
|  | } | ||||||
|  | @ -9,7 +9,8 @@ | ||||||
|     simple-nixos-mailserver.url = "gitlab:simple-nixos-mailserver/nixos-mailserver/nixos-23.05"; |     simple-nixos-mailserver.url = "gitlab:simple-nixos-mailserver/nixos-mailserver/nixos-23.05"; | ||||||
|   }; |   }; | ||||||
| 
 | 
 | ||||||
|   outputs = { self, nixpkgs, nixpkgs-unstable, nixos-generators, simple-nixos-mailserver, ... }@inputs: let |   outputs = { self, nixpkgs, nixpkgs-unstable, nixos-generators, simple-nixos-mailserver, ... }@inputs: | ||||||
|  |   let | ||||||
|     hosts = import ./hosts.nix inputs; |     hosts = import ./hosts.nix inputs; | ||||||
|     helper = import ./helper.nix inputs; |     helper = import ./helper.nix inputs; | ||||||
|   in { |   in { | ||||||
|  | @ -32,9 +33,9 @@ | ||||||
|     } // builtins.mapAttrs (helper.generateColmenaHost) hosts; |     } // builtins.mapAttrs (helper.generateColmenaHost) hosts; | ||||||
| 
 | 
 | ||||||
|     hydraJobs = { |     hydraJobs = { | ||||||
|       nixConfigurations = builtins.mapAttrs ( |       nixConfigurations = builtins.mapAttrs (host: helper.generateNixConfiguration host { | ||||||
|         host: helper.generateNixConfiguration host { inherit nixpkgs-unstable hosts simple-nixos-mailserver; } |         inherit nixpkgs-unstable hosts simple-nixos-mailserver; | ||||||
|       ) hosts; |       }) hosts; | ||||||
|     }; |     }; | ||||||
| 
 | 
 | ||||||
|     # Generate a base VM image for Proxmox with `nix build .#base-proxmox` |     # Generate a base VM image for Proxmox with `nix build .#base-proxmox` | ||||||
|  |  | ||||||
							
								
								
									
										17
									
								
								pkgs/wireguard-nat-nftables/default.nix
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								pkgs/wireguard-nat-nftables/default.nix
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,17 @@ | ||||||
|  | { pkgs, ... }: | ||||||
|  | let | ||||||
|  |   nftablesWithPythonOverlay = final: prev: { | ||||||
|  |     nftables = (prev.nftables.override { withPython = true; }); | ||||||
|  |   }; | ||||||
|  |   pkgs-overlay = pkgs.extend nftablesWithPythonOverlay; | ||||||
|  | in  | ||||||
|  | pkgs-overlay.python310Packages.buildPythonApplication { | ||||||
|  |   pname = "wireguard-nat-nftables"; | ||||||
|  |   version = "0.0.1"; | ||||||
|  | 
 | ||||||
|  |   propagatedBuildInputs = with pkgs-overlay; [ | ||||||
|  |     python310Packages.nftables | ||||||
|  |   ]; | ||||||
|  | 
 | ||||||
|  |   src = ./src; | ||||||
|  | } | ||||||
							
								
								
									
										7
									
								
								pkgs/wireguard-nat-nftables/src/setup.py
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								pkgs/wireguard-nat-nftables/src/setup.py
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,7 @@ | ||||||
|  | from distutils.core import setup | ||||||
|  | 
 | ||||||
|  | setup( | ||||||
|  |     name='wireguard-nat-nftables', | ||||||
|  |     version='0.0.1', | ||||||
|  |     scripts=['wireguard-nat-nftables.py'] | ||||||
|  | ) | ||||||
							
								
								
									
										92
									
								
								pkgs/wireguard-nat-nftables/src/wireguard-nat-nftables.py
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										92
									
								
								pkgs/wireguard-nat-nftables/src/wireguard-nat-nftables.py
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,92 @@ | ||||||
|  | #!/usr/bin/env python3 | ||||||
|  | 
 | ||||||
|  | import nftables | ||||||
|  | import json | ||||||
|  | import subprocess | ||||||
|  | import time | ||||||
|  | import sys | ||||||
|  | 
 | ||||||
|  | def main(): | ||||||
|  |     f = open(sys.argv[1], "r") | ||||||
|  |     config = json.loads(f.read()) | ||||||
|  |     f.close() | ||||||
|  | 
 | ||||||
|  |     interface = config["interface"] | ||||||
|  |     wg_interface = config["wg_interface"] | ||||||
|  |     pubkey_port_mapping = config["pubkey_port_mapping"] | ||||||
|  | 
 | ||||||
|  |     nft = nftables.Nftables() | ||||||
|  |     nft.set_json_output(True) | ||||||
|  |     nft.set_handle_output(True) | ||||||
|  | 
 | ||||||
|  |     # add nat table rules for dnat and snat masquerade | ||||||
|  |     nft.cmd("add table nat") | ||||||
|  |     nft.cmd("add chain nat prerouting { type nat hook prerouting priority -100; }") | ||||||
|  |     nft.cmd("add chain nat postrouting { type nat hook postrouting priority 100; }") | ||||||
|  |      | ||||||
|  |     # load current nftables rules | ||||||
|  |     rc, output, error = nft.cmd("list ruleset") | ||||||
|  |     if error: | ||||||
|  |         print(error, file=sys.stderr) | ||||||
|  |     nftables_output = json.loads(output) | ||||||
|  | 
 | ||||||
|  |     add_masquerade = True | ||||||
|  |     for item in nftables_output["nftables"]: | ||||||
|  |         if ("rule" in item  | ||||||
|  |             and item["rule"]["family"] == "ip" | ||||||
|  |             and item["rule"]["table"] == "nat" | ||||||
|  |             and item["rule"]["chain"] == "postrouting" | ||||||
|  |             and "masquerade" in item["rule"]["expr"][0] | ||||||
|  |         ): | ||||||
|  |             add_masquerade = False | ||||||
|  |             break | ||||||
|  |     if add_masquerade: | ||||||
|  |         nft.cmd("add rule nat postrouting masquerade") | ||||||
|  | 
 | ||||||
|  |     while True: | ||||||
|  |         # list WireGuard peer endpoint addresses of WireGuard VPN connection | ||||||
|  |         process = subprocess.Popen(["wg", "show", wg_interface, "endpoints"], stdout=subprocess.PIPE, stderr=subprocess.PIPE) | ||||||
|  |         stdout, stderr = process.communicate() | ||||||
|  |         lines = stdout.decode().split("\n")[:-1] | ||||||
|  |         if stderr: | ||||||
|  |             print("{}: {}".format(wg_interface, stderr.decode()), file=sys.stderr) | ||||||
|  |         else: | ||||||
|  |             # map destination port to IP | ||||||
|  |             port_ip_mapping = {} | ||||||
|  |             for line in lines: | ||||||
|  |                 pubkey = line.split("\t")[0] | ||||||
|  |                 ip = line.split("\t")[1].split(":")[0] # probably only works for IPv4 | ||||||
|  |                 for port in pubkey_port_mapping[pubkey]: | ||||||
|  |                     port_ip_mapping[port] = ip | ||||||
|  | 
 | ||||||
|  |             # load current nftables rules | ||||||
|  |             rc, output, error = nft.cmd("list ruleset") | ||||||
|  |             if error: | ||||||
|  |                 print(error, file=sys.stderr) | ||||||
|  |             nftables_output = json.loads(output) | ||||||
|  | 
 | ||||||
|  |             # update existing nftable dnat rules, if the remote IP mismatches | ||||||
|  |             for item in nftables_output["nftables"]: | ||||||
|  |                 if "rule" in item and item["rule"]["family"] == "ip" and item["rule"]["table"] == "nat" and item["rule"]["chain"] == "prerouting": | ||||||
|  |                     handle = item["rule"]["handle"] | ||||||
|  |                     ip = item["rule"]["expr"][2]["dnat"]["addr"] | ||||||
|  |                     port = item["rule"]["expr"][1]["match"]["right"] | ||||||
|  |                     if not ip == port_ip_mapping[port]: | ||||||
|  |                         rc, output, error = nft.cmd("replace rule nat prerouting handle {} iif {} udp dport {} dnat to {}".format(handle, interface, port, port_ip_mapping[port])) | ||||||
|  |                         if error: | ||||||
|  |                             eprint(error) | ||||||
|  |                         else: | ||||||
|  |                             print("Changed dnat address from {} to {} for UDP port {}".format(ip, port_ip_mapping[port], port)) | ||||||
|  |                     port_ip_mapping.pop(port) | ||||||
|  | 
 | ||||||
|  |             # loop through all remaining ports and add needed dnat rules | ||||||
|  |             for port in port_ip_mapping: | ||||||
|  |                 rc, output, error = nft.cmd("add rule nat prerouting iif {} udp dport {} dnat to {}".format(interface, port, port_ip_mapping[port])) | ||||||
|  |                 if error: | ||||||
|  |                     print(error, file=sys.stderr) | ||||||
|  |                 else: | ||||||
|  |                     print("Added dnat rule from UDP port {} to address {}".format(port, port_ip_mapping[port])) | ||||||
|  |         time.sleep(10) | ||||||
|  | 
 | ||||||
|  | if __name__ == "__main__": | ||||||
|  |     main() | ||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue