From 4598bca979c834166c33390c6624398f5fc30c50 Mon Sep 17 00:00:00 2001 From: Giacomo Sanchietti Date: Tue, 5 Nov 2024 16:47:53 +0100 Subject: [PATCH 01/10] ns-api: add ns.wireguard --- packages/ns-api/Makefile | 2 + packages/ns-api/files/ns.wireguard | 283 ++++++++++++++++++++++++ packages/ns-api/files/ns.wireguard.json | 13 ++ 3 files changed, 298 insertions(+) create mode 100644 packages/ns-api/files/ns.wireguard create mode 100644 packages/ns-api/files/ns.wireguard.json diff --git a/packages/ns-api/Makefile b/packages/ns-api/Makefile index 7ee4d9f2f..0a688e065 100644 --- a/packages/ns-api/Makefile +++ b/packages/ns-api/Makefile @@ -152,6 +152,8 @@ define Package/ns-api/install $(INSTALL_DATA) ./files/ns.conntrack.json $(1)/usr/share/rpcd/acl.d/ $(INSTALL_BIN) ./files/ns.scan $(1)/usr/libexec/rpcd/ $(INSTALL_DATA) ./files/ns.scan.json $(1)/usr/share/rpcd/acl.d/ + $(INSTALL_BIN) ./files/ns.wireguard $(1)/usr/libexec/rpcd/ + $(INSTALL_DATA) ./files/ns.wireguard.json $(1)/usr/share/rpcd/acl.d/ $(INSTALL_BIN) ./files/ns.objects $(1)/usr/libexec/rpcd/ $(INSTALL_DATA) ./files/ns.objects.json $(1)/usr/share/rpcd/acl.d/ $(INSTALL_BIN) ./files/ns.snort $(1)/usr/libexec/rpcd/ diff --git a/packages/ns-api/files/ns.wireguard b/packages/ns-api/files/ns.wireguard new file mode 100644 index 000000000..4cf51de66 --- /dev/null +++ b/packages/ns-api/files/ns.wireguard @@ -0,0 +1,283 @@ +#!/usr/bin/python3 + +# +# Copyright (C) 2024 Nethesis S.r.l. +# SPDX-License-Identifier: GPL-2.0-only +# + +import sys +import json +import subprocess +from euci import EUci +from nethsec import utils, firewall, ovpn +import ipaddress +import base64 + +## Utils + +def generate_wireguard_keys(): + private_key = subprocess.run(["wg", "genkey"], capture_output=True, text=True).stdout.strip() + public_key = subprocess.run(["wg", "pubkey"], input=private_key, capture_output=True, text=True).stdout.strip() + return private_key, public_key + +def get_wireguard_interface(): + u = EUci() + interfaces = utils.get_all_by_type(u, "network", "interface") + for i in interfaces: + interface = interfaces[i] + if interface.get("proto") == "wireguard": + return i + return None + +def set_wireguard_interface(u, name, interface, private_key, listen_port, network, public_endpoint, routes): + u.set("network", interface, "interface") + u.set("network", interface, "proto", "wireguard") + u.set("network", interface, "private_key", private_key) + u.set("network", interface, "listen_port", listen_port) + # calculate the first IP for network + net = ipaddress.ip_network(network, strict=False) + first_ip = str(list(net.hosts())[0]) + u.set("network", interface, "addresses", [first_ip]) + u.set("network", interface, "ns_network", network) + u.set("network", interface, "ns_public_endpoint", public_endpoint) + u.set("network", interface, "ns_routes", routes) + u.set("network", interface, "ns_name", name) + u.save("network") + +def remove_wireguard_interface(u, interface): + u.delete("network", interface) + u.save("network") + +def set_wireguard_peer(u, interface, account, route_all_traffic, client_to_client): + peer_section = f"{interface}_{account}_peer" + if u.get("network", peer_section, default=None) is None: + # First time configuration + u.set("network", peer_section, "wireguard_%s" % interface) + private_key, public_key = generate_wireguard_keys() + u.set("network", peer_section, "public_key", public_key) + u.set("network", peer_section, "private_key", private_key) + + # calculate next available IP + vpn_addr = u.get("network", interface, "ns_network") + net = ipaddress.ip_network(vpn_addr, strict=False) + used_ips = set() + used_ips.add(str(list(net.hosts())[0])) # first host is reserved for the server + for p in utils.get_all_by_type(u, "network", f"wireguard_{interface}"): + peer_ip = u.get("network", p, "allowed_ips", default="") + if peer_ip: + used_ips.add(peer_ip) + for ip in net.hosts(): + if str(ip) not in used_ips: + ipaddr = str(ip) + break + if not ipaddr: + return utils.validation_error("ipaddr", "no_available_ip", account) + u.set("network", peer_section, "allowed_ips", [ipaddr]) + u.set("network", peer_section, "persistent_keepalive", 25) + u.set("network", peer_section, "description", account) + u.set("network", peer_section, "ns_link", f"wireguard/{interface}") + # automatically create route for the peer + u.set("network", peer_section, "route_allowed_ips", '1') + # Update configuration + u.set("network", peer_section, "ns_route_all_traffic", '1' if route_all_traffic else '0') + u.set("network", peer_section, "ns_client_to_client", '1' if client_to_client else '0') + u.save("network") + return {"section": peer_section} + +def remove_wireguard_peer(u, interface, account): + peer_section = f"{interface}_{account}_peer" + u.delete("network", peer_section) + u.save("network") + +## APIs + +def list_instances(): + u = EUci() + ret = [] + interfaces = utils.get_all_by_type(u, "network", "interface") + for i in interfaces: + interface = interfaces[i] + if interface.get("proto") == "wireguard": + ret.append(i) + return {"instances": ret} + +def get_instance_defaults(): + u = EUci() + ret = {} + next_instance = len(list_instances()['instances']) + 1 + if next_instance == 1: + listen_port = 51820 + else: + listen_port = 51820 + next_instance - 1 + interface = f'wg{next_instance}' + ret["listen_port"] = listen_port + ret["interface"] = interface + # search for a free network + used_networks = [] + interfaces = utils.get_all_by_type(u, "network", "interface") + for i in interfaces: + interface = interfaces[i] + if interface.get("proto") == "wireguard": + addr = u.get("network", i, "addresses", default="") + if addr: + net = ipaddress.IPv4Network(addr, strict=False) + used_networks.append(str(net)) + network = ovpn.random_network() + while network in used_networks: + network = ovpn.get_random_network() + ret["network"] = network + ret["routes"] = ovpn.get_local_networks(u) + try: + ret["public_endpoint"] = ovpn.get_public_addresses(u)[0] + except: + ret["public_endpoint"] = "" + return ret + +def set_instance(args): + u = EUci() + # check if the interface already exists + if u.get("network", args['instance'], default=None) is None: + # First time configuration + firewall.add_service(u, f'WireGuard{args["instance"]}', args['listen_port'], ['udp'], link=f"wireguard/{args['instance']}") + zone = f"{args['instance']}vpn" + firewall.add_trusted_zone(u, zone, link=f"wireguard/{args['instance']}") + firewall.add_device_to_zone(u, args['instance'], zone) + private_key, public_key = generate_wireguard_keys() + else: + private_key = u.get("network", args['instance'], "private_key") + public_key = subprocess.run(["wg", "pubkey"], input=private_key, capture_output=True, text=True).stdout.strip() + set_wireguard_interface(u, + args['name'], + args['instance'], + private_key, + args['listen_port'], + args['network'], + args['public_endpoint'], + args['routes'] + ) + + return {"public_key": public_key} + +def remove_instance(instance): + u = EUci() + remove_wireguard_interface(u, instance) + firewall.remove_device_from_zone(u, instance, f"{instance}vpn") + firewall.delete_linked_sections(u, f"wireguard/{instance}") + return {"result": "success"} + +def get_configuration(instance): + u = EUci() + ret = u.get_all("network", instance) + ret['ns_client_to_client'] = ret.get('ns_client_to_client', '0') == '1' + ret['ns_route_all_traffic'] = ret.get('ns_route_all_traffic', '0') == '1' + return ret + +def set_peer(args): + u = EUci() + ret = set_wireguard_peer(u, args["instance"], args["account"], args["route_all_traffic"], args["client_to_client"]) + return ret + +def remove_peer(args): + u = EUci() + interface = args["instance"] + account = args["account"] + remove_wireguard_peer(u, interface, account) + return {"result": "success"} + +def download_peer_config(args): + u = EUci() + interface = args["instance"] + account = args["account"] + peer_section = f"{interface}_{account}_peer" + data = u.get_all("network", peer_section) + private_key = data["private_key"] + server_private_key = u.get("network", interface, "private_key") + server_public_key = subprocess.run(["wg", "pubkey"], input=server_private_key, capture_output=True, text=True).stdout.strip() + peer_ip = ','.join(list(data["allowed_ips"])) + persistent_keepalive = data["persistent_keepalive"] + server_port = u.get("network", interface, "listen_port") + public_endpoint = u.get("network", interface, "ns_public_endpoint") + allowed_ips = [] + # push custom routes + try: + routes = list(u.get_all("network", interface, "ns_routes")) + except: + routes = [] + if routes: + allowed_ips += routes + + # force all traffic through the tunnel + if data.get("ns_route_all_traffic", '0') == '1': + allowed_ips.append("0.0.0.0/0") + allowed_ips.append("::/0") + else: + # push route for client to client communication + if data.get("ns_client_to_client", '0') == '1': + allowed_ips.append(u.get("network", interface, "ns_network")) + else: + allowed_ips.append(u.get("network", interface, "addresses")) + name = u.get("network", interface, "ns_name") + config = f""" +# Account: {account} for {name} +[Interface] +PrivateKey = {private_key} +Address = {peer_ip} + +[Peer] +PublicKey = {server_public_key} +AllowedIPs = {",".join(allowed_ips)} +Endpoint = {public_endpoint}:{server_port} +PersistentKeepalive = {persistent_keepalive} + """ + + qrcode = subprocess.run(["qrencode", "-t", "ANSIUTF8"], input=config, capture_output=True, text=True).stdout + # encode qrcode in base64 + qrcode = base64.b64encode(qrcode.encode()).decode() + return {"config": config.strip(), "qrcode": qrcode} + +cmd = sys.argv[1] + +if cmd == 'list': + print(json.dumps({ + "get-configuration": {"instance": "wg1"}, + "list-instances": {}, + "set-instance": { + "name": "wg1", + "instance": "wg1", + "listen_port": 51820, + "network": "192.168.231.0/24", + "public_endpoint": "wg.server.org", + "routes": ["192.168.100.0/24"] + }, + "remove-instance": {"instance": "wg1"}, + "set-peer": {"instance": "wg1", "account": "user1", "route_all_traffic": False, "client_to_client": False}, + "remove-peer": {"instance": "wg1", "account": "user1"}, + "download-peer-config": {"instance": "wg1", "account": "user1"} + })) +else: + action = sys.argv[2] + + if action == "set-peer": + args = json.loads(sys.stdin.read()) + ret = set_peer(args) + elif action == "remove-peer": + args = json.loads(sys.stdin.read()) + ret = remove_peer(args) + elif action == "remove-instance": + args = json.loads(sys.stdin.read()) + ret = remove_instance(args["instance"]) + elif action == "get-configuration": + args = json.loads(sys.stdin.read()) + ret = get_configuration(args["instance"]) + elif action == "download-peer-config": + args = json.loads(sys.stdin.read()) + ret = download_peer_config(args) + elif action == "list-instances": + ret = list_instances() + elif action == "set-instance": + args = json.loads(sys.stdin.read()) + ret = set_instance(args) + elif action == "get-instance-defaults": + ret = get_instance_defaults() + + print(json.dumps(ret)) \ No newline at end of file diff --git a/packages/ns-api/files/ns.wireguard.json b/packages/ns-api/files/ns.wireguard.json new file mode 100644 index 000000000..433d7b09c --- /dev/null +++ b/packages/ns-api/files/ns.wireguard.json @@ -0,0 +1,13 @@ +{ + "wireguard-manager": { + "description": "Read and write Wireguard configuration", + "write": {}, + "read": { + "ubus": { + "ns.wireguard": [ + "*" + ] + } + } + } +} From 3b1ebf45de9d86cd9871f0bded207b142573fef2 Mon Sep 17 00:00:00 2001 From: Giacomo Sanchietti Date: Wed, 13 Nov 2024 11:52:23 +0100 Subject: [PATCH 02/10] feat(ns-api): wireguard, add extra peer routes If ns_routes field is set inside a peer, it allows to create a net2net tunnel --- packages/ns-api/files/ns.wireguard | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/packages/ns-api/files/ns.wireguard b/packages/ns-api/files/ns.wireguard index 4cf51de66..51a38647d 100644 --- a/packages/ns-api/files/ns.wireguard +++ b/packages/ns-api/files/ns.wireguard @@ -48,7 +48,7 @@ def remove_wireguard_interface(u, interface): u.delete("network", interface) u.save("network") -def set_wireguard_peer(u, interface, account, route_all_traffic, client_to_client): +def set_wireguard_peer(u, interface, account, route_all_traffic, client_to_client, ns_routes): peer_section = f"{interface}_{account}_peer" if u.get("network", peer_section, default=None) is None: # First time configuration @@ -72,15 +72,25 @@ def set_wireguard_peer(u, interface, account, route_all_traffic, client_to_clien break if not ipaddr: return utils.validation_error("ipaddr", "no_available_ip", account) - u.set("network", peer_section, "allowed_ips", [ipaddr]) + # save peer ip address to custom field, allowed_ips will be calculated later + u.set("network", peer_section, "ns_ip", ipaddr) u.set("network", peer_section, "persistent_keepalive", 25) u.set("network", peer_section, "description", account) u.set("network", peer_section, "ns_link", f"wireguard/{interface}") # automatically create route for the peer u.set("network", peer_section, "route_allowed_ips", '1') + # Update configuration u.set("network", peer_section, "ns_route_all_traffic", '1' if route_all_traffic else '0') u.set("network", peer_section, "ns_client_to_client", '1' if client_to_client else '0') + u.set("network", peer_section, "ns_routes", ns_routes) + # Set allowed_ips: the IP of the peer must be the first one + allowed_ips = [u.get("network", peer_section, "ns_ip")] + if ns_routes: + # add all ns_routes to allowed_ip: smake sure the server can reach the peer and the newtorks behind it + allowed_ips += ns_routes + u.set("network", peer_section, "allowed_ips", allowed_ips) + u.save("network") return {"section": peer_section} @@ -174,7 +184,7 @@ def get_configuration(instance): def set_peer(args): u = EUci() - ret = set_wireguard_peer(u, args["instance"], args["account"], args["route_all_traffic"], args["client_to_client"]) + ret = set_wireguard_peer(u, args["instance"], args["account"], args["route_all_traffic"], args["client_to_client"], args["ns_routes"]) return ret def remove_peer(args): @@ -250,7 +260,7 @@ if cmd == 'list': "routes": ["192.168.100.0/24"] }, "remove-instance": {"instance": "wg1"}, - "set-peer": {"instance": "wg1", "account": "user1", "route_all_traffic": False, "client_to_client": False}, + "set-peer": {"instance": "wg1", "account": "user1", "route_all_traffic": False, "client_to_client": False, "ns_routes": []}, "remove-peer": {"instance": "wg1", "account": "user1"}, "download-peer-config": {"instance": "wg1", "account": "user1"} })) From 17eee9fba29ebbf683a732c5c1affff287994fa8 Mon Sep 17 00:00:00 2001 From: Giacomo Sanchietti Date: Wed, 13 Nov 2024 12:37:50 +0100 Subject: [PATCH 03/10] feat(ns-api): wireguard, add enable/disable flag Allow to enable and disable server instances and peers. --- packages/ns-api/files/ns.wireguard | 31 +++++++++++++++++++++++++----- 1 file changed, 26 insertions(+), 5 deletions(-) diff --git a/packages/ns-api/files/ns.wireguard b/packages/ns-api/files/ns.wireguard index 51a38647d..930f4f21b 100644 --- a/packages/ns-api/files/ns.wireguard +++ b/packages/ns-api/files/ns.wireguard @@ -29,7 +29,7 @@ def get_wireguard_interface(): return i return None -def set_wireguard_interface(u, name, interface, private_key, listen_port, network, public_endpoint, routes): +def set_wireguard_interface(u, name, enabled, interface, private_key, listen_port, network, public_endpoint, routes): u.set("network", interface, "interface") u.set("network", interface, "proto", "wireguard") u.set("network", interface, "private_key", private_key) @@ -42,13 +42,17 @@ def set_wireguard_interface(u, name, interface, private_key, listen_port, networ u.set("network", interface, "ns_public_endpoint", public_endpoint) u.set("network", interface, "ns_routes", routes) u.set("network", interface, "ns_name", name) + if enabled: + u.set("network", interface, "disabled", '0') + else: + u.set("network", interface, "disabled", '1') u.save("network") def remove_wireguard_interface(u, interface): u.delete("network", interface) u.save("network") -def set_wireguard_peer(u, interface, account, route_all_traffic, client_to_client, ns_routes): +def set_wireguard_peer(u, enabled, interface, account, route_all_traffic, client_to_client, ns_routes): peer_section = f"{interface}_{account}_peer" if u.get("network", peer_section, default=None) is None: # First time configuration @@ -81,13 +85,17 @@ def set_wireguard_peer(u, interface, account, route_all_traffic, client_to_clien u.set("network", peer_section, "route_allowed_ips", '1') # Update configuration + if enabled: + u.set("network", peer_section, "disabled", '0') + else: + u.set("network", peer_section, "disabled", '1') u.set("network", peer_section, "ns_route_all_traffic", '1' if route_all_traffic else '0') u.set("network", peer_section, "ns_client_to_client", '1' if client_to_client else '0') u.set("network", peer_section, "ns_routes", ns_routes) # Set allowed_ips: the IP of the peer must be the first one allowed_ips = [u.get("network", peer_section, "ns_ip")] if ns_routes: - # add all ns_routes to allowed_ip: smake sure the server can reach the peer and the newtorks behind it + # add all ns_routes to allowed_ip: make sure the server can reach the peer and the newtorks behind it allowed_ips += ns_routes u.set("network", peer_section, "allowed_ips", allowed_ips) @@ -158,6 +166,7 @@ def set_instance(args): public_key = subprocess.run(["wg", "pubkey"], input=private_key, capture_output=True, text=True).stdout.strip() set_wireguard_interface(u, args['name'], + args['enabled'], args['instance'], private_key, args['listen_port'], @@ -180,11 +189,15 @@ def get_configuration(instance): ret = u.get_all("network", instance) ret['ns_client_to_client'] = ret.get('ns_client_to_client', '0') == '1' ret['ns_route_all_traffic'] = ret.get('ns_route_all_traffic', '0') == '1' + if ret.get('disabled', '0') == '1': + ret['enabled'] = False + else: + ret['enabled'] = True return ret def set_peer(args): u = EUci() - ret = set_wireguard_peer(u, args["instance"], args["account"], args["route_all_traffic"], args["client_to_client"], args["ns_routes"]) + ret = set_wireguard_peer(u, args["enabled"], args["instance"], args["account"], args["route_all_traffic"], args["client_to_client"], args["ns_routes"]) return ret def remove_peer(args): @@ -253,6 +266,7 @@ if cmd == 'list': "list-instances": {}, "set-instance": { "name": "wg1", + "enabled": True, "instance": "wg1", "listen_port": 51820, "network": "192.168.231.0/24", @@ -260,7 +274,14 @@ if cmd == 'list': "routes": ["192.168.100.0/24"] }, "remove-instance": {"instance": "wg1"}, - "set-peer": {"instance": "wg1", "account": "user1", "route_all_traffic": False, "client_to_client": False, "ns_routes": []}, + "set-peer": { + "enabled": True, + "instance": "wg1", + "account": "user1", + "route_all_traffic": False, + "client_to_client": False, + "ns_routes": [] + }, "remove-peer": {"instance": "wg1", "account": "user1"}, "download-peer-config": {"instance": "wg1", "account": "user1"} })) From a7bc5cc7e9b6b3dbad4fa6d5b7d299109dca3b2e Mon Sep 17 00:00:00 2001 From: Giacomo Sanchietti Date: Wed, 13 Nov 2024 14:49:22 +0100 Subject: [PATCH 04/10] feat(ns-api): wireguard, add custom dns Also add ns_user_db field to connect an instance to a user db --- packages/ns-api/files/ns.wireguard | 23 ++++++++++++++++++++--- 1 file changed, 20 insertions(+), 3 deletions(-) diff --git a/packages/ns-api/files/ns.wireguard b/packages/ns-api/files/ns.wireguard index 930f4f21b..0571a15ac 100644 --- a/packages/ns-api/files/ns.wireguard +++ b/packages/ns-api/files/ns.wireguard @@ -29,7 +29,7 @@ def get_wireguard_interface(): return i return None -def set_wireguard_interface(u, name, enabled, interface, private_key, listen_port, network, public_endpoint, routes): +def set_wireguard_interface(u, name, enabled, interface, private_key, listen_port, network, public_endpoint, routes, dns, user_db = None): u.set("network", interface, "interface") u.set("network", interface, "proto", "wireguard") u.set("network", interface, "private_key", private_key) @@ -38,6 +38,7 @@ def set_wireguard_interface(u, name, enabled, interface, private_key, listen_por net = ipaddress.ip_network(network, strict=False) first_ip = str(list(net.hosts())[0]) u.set("network", interface, "addresses", [first_ip]) + u.set("network", interface, "ns_dns", dns) # do no use official dns field, we do not want to modify resolv.conf u.set("network", interface, "ns_network", network) u.set("network", interface, "ns_public_endpoint", public_endpoint) u.set("network", interface, "ns_routes", routes) @@ -46,6 +47,13 @@ def set_wireguard_interface(u, name, enabled, interface, private_key, listen_por u.set("network", interface, "disabled", '0') else: u.set("network", interface, "disabled", '1') + if user_db: + u.set("network", interface, "ns_user_db", user_db) + else: + try: + u.delete("network", interface, "ns_user_db") + except: + pass u.save("network") def remove_wireguard_interface(u, interface): @@ -172,7 +180,9 @@ def set_instance(args): args['listen_port'], args['network'], args['public_endpoint'], - args['routes'] + args['routes'], + args['dns'], + args.get('user_db', None) ) return {"public_key": public_key} @@ -233,7 +243,11 @@ def download_peer_config(args): if data.get("ns_route_all_traffic", '0') == '1': allowed_ips.append("0.0.0.0/0") allowed_ips.append("::/0") + # set also DNS, if any + ns_dns = list(u.get_all("network", interface, "ns_dns")) + dns_config = f"DNS={','.join(ns_dns)}" else: + dns_config = "" # push route for client to client communication if data.get("ns_client_to_client", '0') == '1': allowed_ips.append(u.get("network", interface, "ns_network")) @@ -245,6 +259,7 @@ def download_peer_config(args): [Interface] PrivateKey = {private_key} Address = {peer_ip} +{dns_config} [Peer] PublicKey = {server_public_key} @@ -271,7 +286,9 @@ if cmd == 'list': "listen_port": 51820, "network": "192.168.231.0/24", "public_endpoint": "wg.server.org", - "routes": ["192.168.100.0/24"] + "routes": ["192.168.100.0/24"], + "dns": ["1.1.1.1"], + "user_db": "local" }, "remove-instance": {"instance": "wg1"}, "set-peer": { From f646c129b1554988163538c815bb8537e2820ab1 Mon Sep 17 00:00:00 2001 From: Giacomo Sanchietti Date: Wed, 13 Nov 2024 15:09:35 +0100 Subject: [PATCH 05/10] feat(ns-api): wireguard, add preshared key Support automatic creation of preshared key for peers --- packages/ns-api/files/ns.wireguard | 29 +++++++++++++++++++++++++---- 1 file changed, 25 insertions(+), 4 deletions(-) diff --git a/packages/ns-api/files/ns.wireguard b/packages/ns-api/files/ns.wireguard index 0571a15ac..07c57f240 100644 --- a/packages/ns-api/files/ns.wireguard +++ b/packages/ns-api/files/ns.wireguard @@ -60,7 +60,7 @@ def remove_wireguard_interface(u, interface): u.delete("network", interface) u.save("network") -def set_wireguard_peer(u, enabled, interface, account, route_all_traffic, client_to_client, ns_routes): +def set_wireguard_peer(u, enabled, interface, account, route_all_traffic, client_to_client, ns_routes, preshared_key=False): peer_section = f"{interface}_{account}_peer" if u.get("network", peer_section, default=None) is None: # First time configuration @@ -107,6 +107,17 @@ def set_wireguard_peer(u, enabled, interface, account, route_all_traffic, client allowed_ips += ns_routes u.set("network", peer_section, "allowed_ips", allowed_ips) + if preshared_key: + cur_key = u.get("network", peer_section, "preshared_key", default=None) + if not cur_key: + psk = subprocess.run(["wg", "genpsk"], capture_output=True, text=True).stdout.strip() + u.set("network", peer_section, "preshared_key", psk) + else: + try: + u.delete("network", peer_section, "preshared_key") + except: + pass + u.save("network") return {"section": peer_section} @@ -207,7 +218,7 @@ def get_configuration(instance): def set_peer(args): u = EUci() - ret = set_wireguard_peer(u, args["enabled"], args["instance"], args["account"], args["route_all_traffic"], args["client_to_client"], args["ns_routes"]) + ret = set_wireguard_peer(u, args["enabled"], args["instance"], args["account"], args["route_all_traffic"], args["client_to_client"], args["ns_routes"], args.get("preshared_key", False)) return ret def remove_peer(args): @@ -247,12 +258,20 @@ def download_peer_config(args): ns_dns = list(u.get_all("network", interface, "ns_dns")) dns_config = f"DNS={','.join(ns_dns)}" else: - dns_config = "" + dns_config = "# Custom DNS disabled" # push route for client to client communication if data.get("ns_client_to_client", '0') == '1': allowed_ips.append(u.get("network", interface, "ns_network")) else: allowed_ips.append(u.get("network", interface, "addresses")) + + # Pre-shared key + if data.get("preshared_key", None): + psk = u.get("network", peer_section, "preshared_key") + psk = f"PreSharedKey = {psk}" + else: + psk = "# PreSharedKey disabled" + name = u.get("network", interface, "ns_name") config = f""" # Account: {account} for {name} @@ -263,6 +282,7 @@ Address = {peer_ip} [Peer] PublicKey = {server_public_key} +{psk} AllowedIPs = {",".join(allowed_ips)} Endpoint = {public_endpoint}:{server_port} PersistentKeepalive = {persistent_keepalive} @@ -297,7 +317,8 @@ if cmd == 'list': "account": "user1", "route_all_traffic": False, "client_to_client": False, - "ns_routes": [] + "ns_routes": [], + "preshared_key": True }, "remove-peer": {"instance": "wg1", "account": "user1"}, "download-peer-config": {"instance": "wg1", "account": "user1"} From d76e423ecfa3a32f8bc035d3421b50529ae5b1ea Mon Sep 17 00:00:00 2001 From: Giacomo Sanchietti Date: Wed, 13 Nov 2024 16:27:23 +0100 Subject: [PATCH 06/10] feat(ns-api): wireguard, support existing users Allow to use existing user as wireguard peer. Make sure to not conflict with OpenVPN config. --- packages/ns-api/files/ns.wireguard | 72 ++++++++++++++++++++++++++++-- 1 file changed, 69 insertions(+), 3 deletions(-) diff --git a/packages/ns-api/files/ns.wireguard b/packages/ns-api/files/ns.wireguard index 07c57f240..36eb1b5a0 100644 --- a/packages/ns-api/files/ns.wireguard +++ b/packages/ns-api/files/ns.wireguard @@ -9,7 +9,7 @@ import sys import json import subprocess from euci import EUci -from nethsec import utils, firewall, ovpn +from nethsec import utils, firewall, ovpn, users import ipaddress import base64 @@ -29,6 +29,17 @@ def get_wireguard_interface(): return i return None +def get_user_extra_config(u, user_id): + try: + details = u.get_all("users", user_id) + extra_config = {} + for opt in details: + if opt.startswith("openvpn_") or opt.startswith("wireguard_"): + extra_config[opt] = details[opt] + return extra_config + except: + return {} + def set_wireguard_interface(u, name, enabled, interface, private_key, listen_port, network, public_endpoint, routes, dns, user_db = None): u.set("network", interface, "interface") u.set("network", interface, "proto", "wireguard") @@ -61,7 +72,41 @@ def remove_wireguard_interface(u, interface): u.save("network") def set_wireguard_peer(u, enabled, interface, account, route_all_traffic, client_to_client, ns_routes, preshared_key=False): + # check if the parent instance exists + if u.get("network", interface, default=None) is None: + return utils.validation_error("instance", "instance_not_found", interface) + peer_section = f"{interface}_{account}_peer" + + # check if the parent instance is has ns_user_db set + user_db = u.get("network", interface, "ns_user_db", default=None) + if user_db: + # the account must be in the user_db + user_list = users.list_users(u, user_db) + # user list example: [{'local': True, 'database': 'main', 'name': 'giacomo', 'description': 'Giacomo Rossi', 'admin': False, 'id': 'ns_2edf63a8'}] + user = next((user for user in user_list if user['name'] == account), None) + if not user: + return utils.validation_error("account", "account_not_found", account) + user_id = user.get('id', None) + + wg_config = {"wireguard_peer": peer_section} + if u.get("users", user_db) == "ldap": + print(user) + # remote user + if user_id is not None: # id is None if the user is not found + extra_config = get_user_extra_config(u, user_id) + extra_config.update(wg_config) + users.edit_remote_user(u, account, user_db, extra_fields=extra_config) + else: + users.add_remote_user(u, account, user_db, extra_fields=wg_config) + else: + # local user + extra_config = get_user_extra_config(u, user_id) + extra_config.update(wg_config) + users.edit_local_user(u, account, extra_fields=extra_config) + + else: + user_id = None if u.get("network", peer_section, default=None) is None: # First time configuration u.set("network", peer_section, "wireguard_%s" % interface) @@ -92,6 +137,9 @@ def set_wireguard_peer(u, enabled, interface, account, route_all_traffic, client # automatically create route for the peer u.set("network", peer_section, "route_allowed_ips", '1') + if user_id: + u.set("users", user_id, "wireguard_peer", peer_section) + # Update configuration if enabled: u.set("network", peer_section, "disabled", '0') @@ -125,6 +173,14 @@ def remove_wireguard_peer(u, interface, account): peer_section = f"{interface}_{account}_peer" u.delete("network", peer_section) u.save("network") + # check if parent instance has ns_user_db set: cleanup user db if needed + user_db = u.get("network", interface, "ns_user_db", default=None) + if user_db: + users = utils.get_all_by_type(u, "users", "user") + for user in users: + if u.get("users", user, "wireguard_peer", default=None) == peer_section: + u.delete("users", user, "wireguard_peer") + u.save("users") ## APIs @@ -148,7 +204,7 @@ def get_instance_defaults(): listen_port = 51820 + next_instance - 1 interface = f'wg{next_instance}' ret["listen_port"] = listen_port - ret["interface"] = interface + ret["instance"] = interface # search for a free network used_networks = [] interfaces = utils.get_all_by_type(u, "network", "interface") @@ -200,9 +256,19 @@ def set_instance(args): def remove_instance(instance): u = EUci() + user_db = u.get("network", instance, "ns_user_db", default=None) remove_wireguard_interface(u, instance) firewall.remove_device_from_zone(u, instance, f"{instance}vpn") firewall.delete_linked_sections(u, f"wireguard/{instance}") + if user_db: + # remove wireguard_peer from all users + users = utils.get_all_by_type(u, "users", "user") + for user in users: + peer = u.get("users", user, "wireguard_peer", default="") + if peer.startswith(f"{instance}_"): + u.delete("users", user, "wireguard_peer") + u.save("users") + return {"result": "success"} def get_configuration(instance): @@ -308,7 +374,7 @@ if cmd == 'list': "public_endpoint": "wg.server.org", "routes": ["192.168.100.0/24"], "dns": ["1.1.1.1"], - "user_db": "local" + "user_db": "main" }, "remove-instance": {"instance": "wg1"}, "set-peer": { From 4744bfad2040889f3f397a28bbcd57e178892a04 Mon Sep 17 00:00:00 2001 From: Giacomo Sanchietti Date: Mon, 18 Nov 2024 16:15:00 +0100 Subject: [PATCH 07/10] feat(conf): add kernel dynamic debug Enable debug for wireguard. Usage example: echo module wireguard +p > /sys/kernel/debug/dynamic_debug/control --- config/debugtools.conf | 2 ++ 1 file changed, 2 insertions(+) diff --git a/config/debugtools.conf b/config/debugtools.conf index 484cb91ef..683963345 100644 --- a/config/debugtools.conf +++ b/config/debugtools.conf @@ -29,3 +29,5 @@ CONFIG_PACKAGE_bind-libs=y CONFIG_PACKAGE_libuv=y CONFIG_PACKAGE_bind-dig=y CONFIG_BIND_ENABLE_DOH=y +# kernel dynamic debugging for wireguard +CONFIG_KERNEL_DYNAMIC_DEBUG=y From 6a95f03ec1abf18a51055723acc6e75c51c0a51a Mon Sep 17 00:00:00 2001 From: Giacomo Sanchietti Date: Wed, 20 Nov 2024 14:10:32 +0100 Subject: [PATCH 08/10] fix(ns-api): ovpnrw, preserve wireguard fields --- packages/ns-api/files/ns.ovpnrw | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/packages/ns-api/files/ns.ovpnrw b/packages/ns-api/files/ns.ovpnrw index 32ec2e466..4ac348707 100755 --- a/packages/ns-api/files/ns.ovpnrw +++ b/packages/ns-api/files/ns.ovpnrw @@ -195,6 +195,17 @@ def instance2number(input_string): numbers = re.findall(r'\d+', input_string) return int(numbers[0]) if numbers else None +def get_user_extra_config(u, user_id): + try: + details = u.get_all("users", user_id) + extra_config = {} + for opt in details: + if opt.startswith("openvpn_") or opt.startswith("wireguard_"): + extra_config[opt] = details[opt] + return extra_config + except: + return {} + ## APIs def list_bridges(): @@ -648,9 +659,8 @@ def add_user(args): if user_id == None: return utils.validation_error("username", "user_not_in_db", args["username"]) - for user in db_users: - if os.path.exists(f"/etc/openvpn/{ovpninstance}/pki/issued/{args['username']}.crt"): - return utils.validation_error("username", "user_certificate_already_exists", args["username"]) + if os.path.exists(f"/etc/openvpn/{ovpninstance}/pki/issued/{args['username']}.crt"): + return utils.validation_error("username", "user_certificate_already_exists", args["username"]) if "ipaddr" in args and args["ipaddr"]: valid, error = is_valid_ip(ovpninstance, args["ipaddr"]) if not valid: @@ -663,7 +673,9 @@ def add_user(args): print(e, file=sys.stderr) return utils.validation_error("username", "user_add_failed", args["username"]) - ovpn_config={"openvpn_enabled": args.get("enabled", "1"), "openvpn_ipaddr": args.get("ipaddr", ""), "openvpn_2fa": generate_2fa_secret()} + extra_fields = get_user_extra_config(u, user_id) + ovpn_config = {"openvpn_enabled": args.get("enabled", "1"), "openvpn_ipaddr": args.get("ipaddr", ""), "openvpn_2fa": generate_2fa_secret()} + ovpn_config.update(extra_fields) if u.get("users", db) == "ldap": # remote user From 567a83dc02cb310a1d4b6a843d8f02b1ede58e6c Mon Sep 17 00:00:00 2001 From: Giacomo Sanchietti Date: Thu, 21 Nov 2024 15:54:49 +0100 Subject: [PATCH 09/10] feat(wireguard): add API to import a config file The API can be used to setup site-to-site tunnels --- packages/ns-api/README.md | 145 ++++++++++++++++++++ packages/ns-api/files/ns.wireguard | 209 +++++++++++++++++++++++------ 2 files changed, 312 insertions(+), 42 deletions(-) diff --git a/packages/ns-api/README.md b/packages/ns-api/README.md index 861d1d32f..792e1c359 100644 --- a/packages/ns-api/README.md +++ b/packages/ns-api/README.md @@ -7788,3 +7788,148 @@ If `set_home_net` is `true`, the API will set the `HOME_NET` variable for the Sn If `include_vpn` is `true`, the API will include the VPN networks in the `HOME_NET` variable. The `ns_policy` can be `balanced`, `security` or `connectivity` or `max-detect`. The `ns_disabled_rules` is a list of SIDs (integer) of rules to be disabled. + +## ns.wireguard + +Configure WireGuard VPN both in Road Warrior and site-to-site mode. + +### list-instances + +List all WireGuard instances: +``` +api-cli ns.wireguard list-instances +``` + +Response example: +```json +{"instances": ["wg1", "wg2"]} +``` + +### get-instance-defaults + +Generate defaults for a new WireGuard instance: +``` +api-cli ns.wireguard get-instance-defaults +``` + +Response example: +```json +{"listen_port": 51821, "instance": "wg2", "network": "10.210.112.0/24", "routes": ["192.168.100.0/24"], "public_endpoint": "185.96.1.1"} +``` + +### get-configuration + +Return current instance configuration: +``` +api-cli ns.wireguard get-configuration --data '{"instance": "wg1"}' +``` + +Response example: +```json +{"proto": "wireguard", "private_key": "oBwTyCkOgUz29UEvuJZstuAjB87SH4x26MVLxAj152M=", "listen_port": "51820", "addresses": ["10.103.1.1"], "ns_network": "10.103.1.0/24", "ns_public_endpoint": "192.168.122.49", "ns_routes": ["192.168.100.0/24"], "ns_name": "wg1", "disabled": "0", "ns_client_to_client": false, "ns_route_all_traffic": false, "enabled": true} +``` + +### set-instance + +Create a new instance or configure an existing one: +``` +api-cli ns.wireguard set-instance --data '{"listen_port": 51820, "name": "wg1", "instance": "wg1", "enabled": true, "network": "10.103.1.0/24", "routes": ["192.168.100.0/24"], "public_endpoint": "192.168.122.49", "dns": [], "user_db": ""}' +``` + +Response example: +```json +{"result": "success"} +``` + +Parameters: +- `listen_port`: the port where the WireGuard server listens +- `name`: the name of the instance, it must be unique and it's the name of the interface on the system, it must be a valid interface name and start with `wg` +- `enabled`: `true` to enable the instance, `false` to disable it +- `network`: the network of the WireGuard instance, this is the network where the clients will be connected +- `routes`: the routes that the clients will receive when connected, this parameter is used during the client configuration creation +- `public_endpoint`: the public endpoint of the WireGuard server, it can be an IP address or a domain name, it's used during the client configuration creation +- `dns`: the DNS servers that the clients will receive when connected, it's used during the client configuration creation; this option is honored nly if the peer + has the `ns_route_all_traffic` option set to `1` +- `user_db`: the user database to use for authentication; if empty, the instance will not be connected to an existing user db and the WireGuard peer will be + indipendent; if the user db is set, each new peer must be have a user with the same name in the user db + +### remove-instance + +Remove an existing instance and all associated peers: +``` +api-cli ns.wireguard remove-instance --data '{"instance": "wg1"}' +``` + +Response example: +```json +{"result": "success"} +``` + +### set-peer + +Create or configure a peer. + +Example to create a Road Warrior peer: +``` +api-cli ns.wireguard set-peer --data '{"instance": "wg1", "account": "user1", "enabled": true, "route_all_traffic": false, "client_to_client": false, "ns_routes": [], "preshared_key": true}' +``` + +Example to create a Site-to-Site peer: +``` +api-cli ns.wireguard set-peer --data '{"instance": "wg1", "account": "site1", "enabled": true, "route_all_traffic": true, "client_to_client": true, "ns_routes": ["192.168.100.0/24"], "preshared_key": true}' +``` + +Response example: +```json +{"result": "success"} +``` + +Parameters: +- `instance`: the name of the WireGuard instance, the instance must exist +- `account`: the name of the peer, it must be unique for the instance; if the instance is connected to a user db, the account must be the name of an existing user +- `enabled`: `true` to enable the peer, `false` to disable it +- `route_all_traffic`: `true` to route all the traffic of the peer through the WireGuard tunnel, `false` to route only the traffic for the `ns_routes` through the tunnel; if this option iset the `dns` option in the instance configuration will be honored +- `client_to_client`: `true` to allow the peer to communicate with other peers connected to the same instance, `false` to disallow it; it must be set to `true` + if the `route_all_traffic` is set to `true` if the client is not a Road Warrior user but another firewall for a site-to-site connection +- `ns_routes`: the routes that the peer will receive when connected, this parameter is used during the client configuration creation +- `preshared_key`: `true` to generate a new preshared key for the peer, `false` to not use it + +### remove-peer + +Remove an existing peer: +``` +api-cli ns.wireguard remove-peer --data '{"instance": "wg1", "account": "user1"}' +``` + +Response example: +```json +{"result": "success"} +``` + +### download-peer-config + +Download the configuration of a peer: +``` +api-cli ns.wireguard download-peer-config --data '{"instance": "wg1", "account": "user1"}' +``` + +Response example: +```json +{"config": "# Account: user1 for wg1\n[Interface]\nPrivateKey = 4OoVRqKW0Tur511IL6ttX6iz/EnxrbKzUcAX89bUxlU=\nAddress = 10.103.1.2\n# Custom DNS disabled\n\n[Peer]\nPublicKey = gm1cTae6ub4QGvQcknrb3FbN46x1tbaXJjOQbwX/siM=\nPreSharedKey = /3EbK9a8DW3D7vn0SFp3oK2XSoem05DpG4IxEZ4qoyU=\nAllowedIPs = 192.168.100.0/24,10.103.1.0/24\nEndpoint = 192.168.122.49:51820\nPersistentKeepalive = 25", "qrcode": "G1s0MDszNzs..."} +``` + +Output parameters: +- `config`: the configuration of the peer, it's in clear text; remember to encode it in base64 before importing into another firewall +- `qrcode`: the QR code of the configuration, it's a base64 encoded image; it can be used to import the configuration into a mobile app + +### import-configuration + +Import a WireGuard configuration: +``` +api-cli ns.wireguard import-configuration --data '{"config": "base64encodedconfig"}' +``` + +Response example: +```json +{"result": "success"} +``` diff --git a/packages/ns-api/files/ns.wireguard b/packages/ns-api/files/ns.wireguard index 36eb1b5a0..18f8a6176 100644 --- a/packages/ns-api/files/ns.wireguard +++ b/packages/ns-api/files/ns.wireguard @@ -12,6 +12,7 @@ from euci import EUci from nethsec import utils, firewall, ovpn, users import ipaddress import base64 +import configparser ## Utils @@ -40,17 +41,30 @@ def get_user_extra_config(u, user_id): except: return {} -def set_wireguard_interface(u, name, enabled, interface, private_key, listen_port, network, public_endpoint, routes, dns, user_db = None): +def set_wireguard_interface(u, name, enabled, interface, private_key, listen_port, network, public_endpoint, routes, dns, user_db = None, isimport = False): u.set("network", interface, "interface") u.set("network", interface, "proto", "wireguard") u.set("network", interface, "private_key", private_key) u.set("network", interface, "listen_port", listen_port) - # calculate the first IP for network - net = ipaddress.ip_network(network, strict=False) - first_ip = str(list(net.hosts())[0]) - u.set("network", interface, "addresses", [first_ip]) + + if isimport: + # honor passed configuration + if ',' in network: + # imported configuration with multiple networks + network = network.split(',') + u.set("network", interface, "addresses", network) + if len(network) > 1: + u.set("network", interface, "ns_network", network[:1]) + else: + u.set("network", interface, "ns_network", "") + else: + # calculate the first IP for network + net = ipaddress.ip_network(network, strict=False) + first_ip = str(list(net.hosts())[0]) + u.set("network", interface, "addresses", [first_ip]) + u.set("network", interface, "ns_network", network) + u.set("network", interface, "ns_dns", dns) # do no use official dns field, we do not want to modify resolv.conf - u.set("network", interface, "ns_network", network) u.set("network", interface, "ns_public_endpoint", public_endpoint) u.set("network", interface, "ns_routes", routes) u.set("network", interface, "ns_name", name) @@ -71,7 +85,7 @@ def remove_wireguard_interface(u, interface): u.delete("network", interface) u.save("network") -def set_wireguard_peer(u, enabled, interface, account, route_all_traffic, client_to_client, ns_routes, preshared_key=False): +def set_wireguard_peer(u, enabled, interface, account, route_all_traffic, client_to_client, ns_routes, preshared_key=None, ipaddr=None, endpoint=None, public_key=None): # check if the parent instance exists if u.get("network", interface, default=None) is None: return utils.validation_error("instance", "instance_not_found", interface) @@ -91,7 +105,6 @@ def set_wireguard_peer(u, enabled, interface, account, route_all_traffic, client wg_config = {"wireguard_peer": peer_section} if u.get("users", user_db) == "ldap": - print(user) # remote user if user_id is not None: # id is None if the user is not found extra_config = get_user_extra_config(u, user_id) @@ -110,30 +123,44 @@ def set_wireguard_peer(u, enabled, interface, account, route_all_traffic, client if u.get("network", peer_section, default=None) is None: # First time configuration u.set("network", peer_section, "wireguard_%s" % interface) - private_key, public_key = generate_wireguard_keys() - u.set("network", peer_section, "public_key", public_key) - u.set("network", peer_section, "private_key", private_key) - - # calculate next available IP - vpn_addr = u.get("network", interface, "ns_network") - net = ipaddress.ip_network(vpn_addr, strict=False) - used_ips = set() - used_ips.add(str(list(net.hosts())[0])) # first host is reserved for the server - for p in utils.get_all_by_type(u, "network", f"wireguard_{interface}"): - peer_ip = u.get("network", p, "allowed_ips", default="") - if peer_ip: - used_ips.add(peer_ip) - for ip in net.hosts(): - if str(ip) not in used_ips: - ipaddr = str(ip) - break + if public_key: + # import existing peer + u.set("network", peer_section, "public_key", public_key) + else: + # generate new keys + private_key, public_key = generate_wireguard_keys() + u.set("network", peer_section, "public_key", public_key) + u.set("network", peer_section, "private_key", private_key) + if not ipaddr: - return utils.validation_error("ipaddr", "no_available_ip", account) + # calculate next available IP, skip for imported peers + vpn_addr = u.get("network", interface, "ns_network") + net = ipaddress.ip_network(vpn_addr, strict=False) + used_ips = set() + used_ips.add(str(list(net.hosts())[0])) # first host is reserved for the server + for p in utils.get_all_by_type(u, "network", f"wireguard_{interface}"): + peer_ip = u.get("network", p, "allowed_ips", default="") + if peer_ip: + used_ips.add(peer_ip) + for ip in net.hosts(): + if str(ip) not in used_ips: + ipaddr = str(ip) + break + if not ipaddr: + return utils.validation_error("ipaddr", "no_available_ip", account) + # save peer ip address to custom field, allowed_ips will be calculated later - u.set("network", peer_section, "ns_ip", ipaddr) + try: + ip_list = ipaddr.split(",") + if len(ip_list) > 1: + ns_routes += ip_list[1:] + except: + ip_list = [ipaddr] + + u.set("network", peer_section, "ns_ip", ip_list[0]) u.set("network", peer_section, "persistent_keepalive", 25) u.set("network", peer_section, "description", account) - u.set("network", peer_section, "ns_link", f"wireguard/{interface}") + u.set("network", peer_section, "ns_link", f"network/{interface}") # automatically create route for the peer u.set("network", peer_section, "route_allowed_ips", '1') @@ -156,16 +183,25 @@ def set_wireguard_peer(u, enabled, interface, account, route_all_traffic, client u.set("network", peer_section, "allowed_ips", allowed_ips) if preshared_key: - cur_key = u.get("network", peer_section, "preshared_key", default=None) - if not cur_key: - psk = subprocess.run(["wg", "genpsk"], capture_output=True, text=True).stdout.strip() - u.set("network", peer_section, "preshared_key", psk) + u.set("network", peer_section, "preshared_key", preshared_key) else: try: u.delete("network", peer_section, "preshared_key") except: pass + if endpoint: + host, port = endpoint.split(":") + u.set("network", peer_section, "endpoint_host", host) + u.set("network", peer_section, "endpoint_port", port) + + else: + try: + u.delete("network", peer_section, "endpoint_host") + u.delete("network", peer_section, "endpoint_port") + except: + pass + u.save("network") return {"section": peer_section} @@ -231,11 +267,15 @@ def set_instance(args): # check if the interface already exists if u.get("network", args['instance'], default=None) is None: # First time configuration - firewall.add_service(u, f'WireGuard{args["instance"]}', args['listen_port'], ['udp'], link=f"wireguard/{args['instance']}") + firewall.add_service(u, f'WireGuard{args["instance"]}', args['listen_port'], ['udp'], link=f"network/{args['instance']}") zone = f"{args['instance']}vpn" - firewall.add_trusted_zone(u, zone, link=f"wireguard/{args['instance']}") + firewall.add_trusted_zone(u, zone, link=f"network/{args['instance']}") firewall.add_device_to_zone(u, args['instance'], zone) - private_key, public_key = generate_wireguard_keys() + if not args.get('private_key'): + private_key, public_key = generate_wireguard_keys() + else: + private_key = args['private_key'] + public_key = subprocess.run(["wg", "pubkey"], input=private_key, capture_output=True, text=True).stdout.strip() else: private_key = u.get("network", args['instance'], "private_key") public_key = subprocess.run(["wg", "pubkey"], input=private_key, capture_output=True, text=True).stdout.strip() @@ -249,7 +289,8 @@ def set_instance(args): args['public_endpoint'], args['routes'], args['dns'], - args.get('user_db', None) + args.get('user_db', None), + args.get('isimport', False) ) return {"public_key": public_key} @@ -259,7 +300,7 @@ def remove_instance(instance): user_db = u.get("network", instance, "ns_user_db", default=None) remove_wireguard_interface(u, instance) firewall.remove_device_from_zone(u, instance, f"{instance}vpn") - firewall.delete_linked_sections(u, f"wireguard/{instance}") + firewall.delete_linked_sections(u, f"network/{instance}") if user_db: # remove wireguard_peer from all users users = utils.get_all_by_type(u, "users", "user") @@ -284,7 +325,40 @@ def get_configuration(instance): def set_peer(args): u = EUci() - ret = set_wireguard_peer(u, args["enabled"], args["instance"], args["account"], args["route_all_traffic"], args["client_to_client"], args["ns_routes"], args.get("preshared_key", False)) + + # create the preshared key if needed + if args['preshared_key']: + peer_section = f"{args['instance']}_{args['account']}_peer" + cur_key = u.get("network", peer_section, "preshared_key", default=None) + if not cur_key: + psk = subprocess.run(["wg", "genpsk"], capture_output=True, text=True).stdout.strip() + else: + psk = cur_key + + ret = set_wireguard_peer(u, + args["enabled"], + args["instance"], + args["account"], + args["route_all_traffic"], + args["client_to_client"], + args["ns_routes"], + psk) + return ret + +def import_peer(args): + u = EUci() + ret = set_wireguard_peer(u, + args["enabled"], + args["instance"], + args["account"], + args["route_all_traffic"], + args["client_to_client"], + args["ns_routes"], + args["preshared_key"], + args.get("ipaddr", None), + args.get("endpoint", None), + args.get("public_key", None) + ) return ret def remove_peer(args): @@ -303,7 +377,6 @@ def download_peer_config(args): private_key = data["private_key"] server_private_key = u.get("network", interface, "private_key") server_public_key = subprocess.run(["wg", "pubkey"], input=server_private_key, capture_output=True, text=True).stdout.strip() - peer_ip = ','.join(list(data["allowed_ips"])) persistent_keepalive = data["persistent_keepalive"] server_port = u.get("network", interface, "listen_port") public_endpoint = u.get("network", interface, "ns_public_endpoint") @@ -330,7 +403,7 @@ def download_peer_config(args): allowed_ips.append(u.get("network", interface, "ns_network")) else: allowed_ips.append(u.get("network", interface, "addresses")) - + # Pre-shared key if data.get("preshared_key", None): psk = u.get("network", peer_section, "preshared_key") @@ -343,7 +416,7 @@ def download_peer_config(args): # Account: {account} for {name} [Interface] PrivateKey = {private_key} -Address = {peer_ip} +Address = {data.get("ns_ip")} {dns_config} [Peer] @@ -359,6 +432,54 @@ PersistentKeepalive = {persistent_keepalive} qrcode = base64.b64encode(qrcode.encode()).decode() return {"config": config.strip(), "qrcode": qrcode} +def import_configuration(args): + u = EUci() + config = args["config"] + try: + data = base64.b64decode(config).decode() + except: + return utils.validation_error("config", "invalid_config", config) + + config_parser = configparser.ConfigParser(allow_no_value=True) + config_parser.read_string(data) + + # Import is like a set-instance plus a set-peer for the remote server + # Steps: + # 1. create the instance + # 2. create the peer for the remote server + defaults = get_instance_defaults() + #{"listen_port": 51821, "instance": "wg2", "network": "10.50.98.0/24", "routes": ["192.168.100.0/24"], "public_endpoint": ""} + # add to defaults PrivateKey, Address, DNS + defaults["private_key"] = config_parser["Interface"]["PrivateKey"] + defaults["dns"] = config_parser["Interface"].get("DNS", "").split(',') + + defaults["user_db"] = "" + defaults["name"] = f"imported_{defaults['instance']}" + defaults["enabled"] = True + defaults["routes"] = [] # FIXME + # Address can be a single IP, or an IP,network1,network2, .... + defaults["network"] = config_parser["Interface"]["Address"] + defaults["isimport"] = True + + set_instance(defaults) + + import_peer({ + "enabled": True, + "instance": defaults["instance"], + "account": "imported", + "route_all_traffic": False, + "client_to_client": True, + "ns_routes": [], + "preshared_key": config_parser["Peer"].get("PreSharedKey", None), + "ipaddr": config_parser["Peer"]["AllowedIPs"], + "endpoint": config_parser["Peer"]["Endpoint"], + "public_key": config_parser["Peer"]["PublicKey"] + }) + + return {"result": "success"} + + + cmd = sys.argv[1] if cmd == 'list': @@ -387,7 +508,8 @@ if cmd == 'list': "preshared_key": True }, "remove-peer": {"instance": "wg1", "account": "user1"}, - "download-peer-config": {"instance": "wg1", "account": "user1"} + "download-peer-config": {"instance": "wg1", "account": "user1"}, + "import-configuration": {"config": "base64encodedconfig"} })) else: action = sys.argv[2] @@ -407,6 +529,9 @@ else: elif action == "download-peer-config": args = json.loads(sys.stdin.read()) ret = download_peer_config(args) + elif action == "import-configuration": + args = json.loads(sys.stdin.read()) + ret = import_configuration(args) elif action == "list-instances": ret = list_instances() elif action == "set-instance": From 9f4630d517bb019110724d8b98cbee2313db2496 Mon Sep 17 00:00:00 2001 From: Giacomo Sanchietti Date: Thu, 28 Nov 2024 16:54:26 +0100 Subject: [PATCH 10/10] fix(ns-api): improve readme Co-authored-by: Filippo Carletti --- packages/ns-api/README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/ns-api/README.md b/packages/ns-api/README.md index 792e1c359..8ae166c6d 100644 --- a/packages/ns-api/README.md +++ b/packages/ns-api/README.md @@ -7848,7 +7848,7 @@ Parameters: - `network`: the network of the WireGuard instance, this is the network where the clients will be connected - `routes`: the routes that the clients will receive when connected, this parameter is used during the client configuration creation - `public_endpoint`: the public endpoint of the WireGuard server, it can be an IP address or a domain name, it's used during the client configuration creation -- `dns`: the DNS servers that the clients will receive when connected, it's used during the client configuration creation; this option is honored nly if the peer +- `dns`: the DNS servers that the clients will receive when connected, it's used during the client configuration creation; this option is honored only if the peer has the `ns_route_all_traffic` option set to `1` - `user_db`: the user database to use for authentication; if empty, the instance will not be connected to an existing user db and the WireGuard peer will be indipendent; if the user db is set, each new peer must be have a user with the same name in the user db @@ -7888,9 +7888,9 @@ Parameters: - `instance`: the name of the WireGuard instance, the instance must exist - `account`: the name of the peer, it must be unique for the instance; if the instance is connected to a user db, the account must be the name of an existing user - `enabled`: `true` to enable the peer, `false` to disable it -- `route_all_traffic`: `true` to route all the traffic of the peer through the WireGuard tunnel, `false` to route only the traffic for the `ns_routes` through the tunnel; if this option iset the `dns` option in the instance configuration will be honored +- `route_all_traffic`: `true` to route all the traffic of the peer through the WireGuard tunnel, `false` to route only the traffic for the `ns_routes` through the tunnel; if this option is set the `dns` option in the instance configuration will be honored - `client_to_client`: `true` to allow the peer to communicate with other peers connected to the same instance, `false` to disallow it; it must be set to `true` - if the `route_all_traffic` is set to `true` if the client is not a Road Warrior user but another firewall for a site-to-site connection + if the `route_all_traffic` is set to `true` when the client is not a Road Warrior user but another firewall for a site-to-site connection - `ns_routes`: the routes that the peer will receive when connected, this parameter is used during the client configuration creation - `preshared_key`: `true` to generate a new preshared key for the peer, `false` to not use it @@ -7919,7 +7919,7 @@ Response example: ``` Output parameters: -- `config`: the configuration of the peer, it's in clear text; remember to encode it in base64 before importing into another firewall +- `config`: the configuration of the peer, it's in clear text; remember to encode it to base64 before importing it into another firewall - `qrcode`: the QR code of the configuration, it's a base64 encoded image; it can be used to import the configuration into a mobile app ### import-configuration