From cc5724e409c8d971eb524e6b581f96018b37e9e3 Mon Sep 17 00:00:00 2001 From: Matteo Di Lorenzi Date: Tue, 27 Jan 2026 15:05:37 +0100 Subject: [PATCH 1/5] fix(ns-scan): block network scan for large network (/19 or lower) --- packages/ns-api/files/ns.scan | 30 ++++++++++++++++++++++++++---- 1 file changed, 26 insertions(+), 4 deletions(-) diff --git a/packages/ns-api/files/ns.scan b/packages/ns-api/files/ns.scan index a617bb203..b1bf06e13 100755 --- a/packages/ns-api/files/ns.scan +++ b/packages/ns-api/files/ns.scan @@ -12,6 +12,7 @@ import sys import json import socket import subprocess +import ipaddress from euci import EUci from nethsec import utils @@ -29,12 +30,33 @@ def list_interfaces(): # skip loopback, bond devices, wans, aliases, ipsec, tun, tap, wg if re.match(r'^(loopback|tun|tap|ipsec|wg)', i) or i in wans or interfaces[i].get('device', '').startswith('@'): continue - ret.append({"interface": i, "device": interfaces[i].get('device', '')}) + netmask = u.get('network', i, 'netmask') or "" + netmask_cidr = netmask_to_cidr_notation(netmask) + ret.append({"interface": i, "device": interfaces[i].get('device', ''), "netmask": netmask_cidr}) return {"interfaces": ret} -def scan(device): +def netmask_to_cidr_notation(netmask): + """Convert a netmask to CIDR notation (ex. 255.255.0.0 -> 16)""" + if not netmask: + return None + try: + return ipaddress.IPv4Network(f'0.0.0.0/{netmask}').prefixlen + except: + return None + +def scan(device, interface): ret = [] + u = EUci() + + if interface: + netmask = u.get('network', interface, 'netmask') + netmask_cidr = netmask_to_cidr_notation(netmask) + + # block arp-scan if the subnet is /19 or smaller + if netmask_cidr is not None and netmask_cidr < 20: + return utils.validation_error("subnet_too_large_for_scan") + # scan the network using arpscan and add the results to the return value try: p = subprocess.run(["arp-scan", "-I", device, "-l", "-x", "--macfile=/usr/share/arp-scan/mac-vendor.txt", "--ouifile=/usr/share/arp-scan/ieee-oui.txt"], capture_output=True, check=True, text=True) @@ -55,11 +77,11 @@ def scan(device): cmd = sys.argv[1] if cmd == 'list': - print(json.dumps({"list-interfaces": {}, "scan": {"device": "eth0"}})) + print(json.dumps({"list-interfaces": {}, "scan": {"device": "eth0", "interface": "green"}})) else: action = sys.argv[2] if action == "list-interfaces": print(json.dumps(list_interfaces())) elif action == "scan": args = json.loads(sys.stdin.read()) - print(json.dumps(scan(args['device']))) + print(json.dumps(scan(args['device'], args['interface']))) From 9f4d8c335448bef67b7b1e4ba5f7767746fb0d93 Mon Sep 17 00:00:00 2001 From: Matteo Di Lorenzi Date: Thu, 29 Jan 2026 09:48:01 +0100 Subject: [PATCH 2/5] fix(ns-scan): simplify scan function by removing interface parameter --- packages/ns-api/files/ns.scan | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/packages/ns-api/files/ns.scan b/packages/ns-api/files/ns.scan index b1bf06e13..b722e0471 100755 --- a/packages/ns-api/files/ns.scan +++ b/packages/ns-api/files/ns.scan @@ -45,17 +45,16 @@ def netmask_to_cidr_notation(netmask): except: return None -def scan(device, interface): +def scan(device): ret = [] u = EUci() - if interface: - netmask = u.get('network', interface, 'netmask') - netmask_cidr = netmask_to_cidr_notation(netmask) - - # block arp-scan if the subnet is /19 or smaller - if netmask_cidr is not None and netmask_cidr < 20: - return utils.validation_error("subnet_too_large_for_scan") + netmask = u.get('network', utils.get_interface_from_device(u, device), 'netmask') + netmask_cidr = netmask_to_cidr_notation(netmask) + + # block arp-scan if the subnet is /19 or smaller + if netmask_cidr is not None and netmask_cidr < 20: + return utils.validation_error("subnet_too_large_for_scan") # scan the network using arpscan and add the results to the return value try: @@ -77,11 +76,11 @@ def scan(device, interface): cmd = sys.argv[1] if cmd == 'list': - print(json.dumps({"list-interfaces": {}, "scan": {"device": "eth0", "interface": "green"}})) + print(json.dumps({"list-interfaces": {}, "scan": {"device": "eth0"}})) else: action = sys.argv[2] if action == "list-interfaces": print(json.dumps(list_interfaces())) elif action == "scan": args = json.loads(sys.stdin.read()) - print(json.dumps(scan(args['device'], args['interface']))) + print(json.dumps(scan(args['device']))) From d19ee318f146cda3b7df8c8f499ca55b20ad5c1f Mon Sep 17 00:00:00 2001 From: Matteo Di Lorenzi Date: Thu, 29 Jan 2026 15:37:10 +0100 Subject: [PATCH 3/5] fix(ns-scan): handle interfaces without netmask in list_interfaces and scan functions --- packages/ns-api/files/ns.scan | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/packages/ns-api/files/ns.scan b/packages/ns-api/files/ns.scan index b722e0471..bb5c6e608 100755 --- a/packages/ns-api/files/ns.scan +++ b/packages/ns-api/files/ns.scan @@ -30,8 +30,12 @@ def list_interfaces(): # skip loopback, bond devices, wans, aliases, ipsec, tun, tap, wg if re.match(r'^(loopback|tun|tap|ipsec|wg)', i) or i in wans or interfaces[i].get('device', '').startswith('@'): continue - netmask = u.get('network', i, 'netmask') or "" - netmask_cidr = netmask_to_cidr_notation(netmask) + try: + netmask = u.get('network', i, 'netmask') or "" + netmask_cidr = netmask_to_cidr_notation(netmask) + except: + # manage interfaces without netmask (ex. OpenVPN tunnels) + netmask_cidr = None ret.append({"interface": i, "device": interfaces[i].get('device', ''), "netmask": netmask_cidr}) return {"interfaces": ret} @@ -49,8 +53,13 @@ def scan(device): ret = [] u = EUci() - netmask = u.get('network', utils.get_interface_from_device(u, device), 'netmask') - netmask_cidr = netmask_to_cidr_notation(netmask) + try: + interface = utils.get_interface_from_device(u, device) + netmask = u.get('network', interface, 'netmask') + netmask_cidr = netmask_to_cidr_notation(netmask) + except: + # manage interfaces without netmask (ex. OpenVPN tunnels) + netmask_cidr = None # block arp-scan if the subnet is /19 or smaller if netmask_cidr is not None and netmask_cidr < 20: From 622ac452de8f4afd9505b37673cbad3dbb6fc3d5 Mon Sep 17 00:00:00 2001 From: Matteo Di Lorenzi Date: Fri, 30 Jan 2026 14:55:02 +0100 Subject: [PATCH 4/5] fix(ns-scan): replace netmask conversion function with direct CIDR calculation --- packages/ns-api/files/ns.scan | 13 ++----------- 1 file changed, 2 insertions(+), 11 deletions(-) diff --git a/packages/ns-api/files/ns.scan b/packages/ns-api/files/ns.scan index bb5c6e608..1df799d4c 100755 --- a/packages/ns-api/files/ns.scan +++ b/packages/ns-api/files/ns.scan @@ -32,7 +32,7 @@ def list_interfaces(): continue try: netmask = u.get('network', i, 'netmask') or "" - netmask_cidr = netmask_to_cidr_notation(netmask) + netmask_cidr = ipaddress.IPv4Network(f'0.0.0.0/{netmask}').prefixlen except: # manage interfaces without netmask (ex. OpenVPN tunnels) netmask_cidr = None @@ -40,15 +40,6 @@ def list_interfaces(): return {"interfaces": ret} -def netmask_to_cidr_notation(netmask): - """Convert a netmask to CIDR notation (ex. 255.255.0.0 -> 16)""" - if not netmask: - return None - try: - return ipaddress.IPv4Network(f'0.0.0.0/{netmask}').prefixlen - except: - return None - def scan(device): ret = [] u = EUci() @@ -56,7 +47,7 @@ def scan(device): try: interface = utils.get_interface_from_device(u, device) netmask = u.get('network', interface, 'netmask') - netmask_cidr = netmask_to_cidr_notation(netmask) + netmask_cidr = ipaddress.IPv4Network(f'0.0.0.0/{netmask}').prefixlen except: # manage interfaces without netmask (ex. OpenVPN tunnels) netmask_cidr = None From 3af2b382a2367b565b719f8aabceb7c35e4a85a2 Mon Sep 17 00:00:00 2001 From: Matteo Di Lorenzi Date: Tue, 3 Feb 2026 14:23:26 +0100 Subject: [PATCH 5/5] fix(ns-scan): use ip address command to check all networks netmasks --- packages/ns-api/files/ns.scan | 59 +++++++++++++++++++++-------------- 1 file changed, 36 insertions(+), 23 deletions(-) diff --git a/packages/ns-api/files/ns.scan b/packages/ns-api/files/ns.scan index 1df799d4c..27278bf63 100755 --- a/packages/ns-api/files/ns.scan +++ b/packages/ns-api/files/ns.scan @@ -12,14 +12,41 @@ import sys import json import socket import subprocess -import ipaddress from euci import EUci from nethsec import utils +### Utils + +def get_devices_info(): + """Get all network devices information from ip address command""" + try: + p = subprocess.run(["/sbin/ip", "-j", "address"], + check=True, text=True, capture_output=True) + return json.loads(p.stdout) + except Exception: + return [] + +def is_scan_enabled(device_name, devices_info): + """Determine if scan should be enabled based on netmask""" + for device in devices_info: + if device.get('ifname') == device_name: + addr_info = device.get('addr_info', []) + for addr in addr_info: + if addr.get('family') == 'inet': + prefixlen = addr.get('prefixlen', 0) + # enable scan for networks with netmask larger than /19 + if prefixlen > 19: + return True + return False + return False + +### APIs + def list_interfaces(): ret = [] u = EUci() wans = [] + devices_info = get_devices_info() interfaces = utils.get_all_by_type(u, 'network', 'interface') for device in utils.get_all_wan_devices(u): iname = utils.get_interface_from_device(u, device) @@ -30,32 +57,18 @@ def list_interfaces(): # skip loopback, bond devices, wans, aliases, ipsec, tun, tap, wg if re.match(r'^(loopback|tun|tap|ipsec|wg)', i) or i in wans or interfaces[i].get('device', '').startswith('@'): continue - try: - netmask = u.get('network', i, 'netmask') or "" - netmask_cidr = ipaddress.IPv4Network(f'0.0.0.0/{netmask}').prefixlen - except: - # manage interfaces without netmask (ex. OpenVPN tunnels) - netmask_cidr = None - ret.append({"interface": i, "device": interfaces[i].get('device', ''), "netmask": netmask_cidr}) + device_name = interfaces[i].get('device', '') + scan_enabled = is_scan_enabled(device_name, devices_info) + ret.append({ + "interface": i, + "device": device_name, + "scan_enabled": scan_enabled + }) return {"interfaces": ret} def scan(device): ret = [] - u = EUci() - - try: - interface = utils.get_interface_from_device(u, device) - netmask = u.get('network', interface, 'netmask') - netmask_cidr = ipaddress.IPv4Network(f'0.0.0.0/{netmask}').prefixlen - except: - # manage interfaces without netmask (ex. OpenVPN tunnels) - netmask_cidr = None - - # block arp-scan if the subnet is /19 or smaller - if netmask_cidr is not None and netmask_cidr < 20: - return utils.validation_error("subnet_too_large_for_scan") - # scan the network using arpscan and add the results to the return value try: p = subprocess.run(["arp-scan", "-I", device, "-l", "-x", "--macfile=/usr/share/arp-scan/mac-vendor.txt", "--ouifile=/usr/share/arp-scan/ieee-oui.txt"], capture_output=True, check=True, text=True) @@ -83,4 +96,4 @@ else: print(json.dumps(list_interfaces())) elif action == "scan": args = json.loads(sys.stdin.read()) - print(json.dumps(scan(args['device']))) + print(json.dumps(scan(args['device']))) \ No newline at end of file