diff --git a/packages/ns-api/README.md b/packages/ns-api/README.md index 37b7068a8..79a0d39a0 100644 --- a/packages/ns-api/README.md +++ b/packages/ns-api/README.md @@ -8025,16 +8025,17 @@ Response example: { "enabled": true, "ns_policy": "balanced", - "oinkcode": "123456789" + "oinkcode": "123456789", + "home_net": ["192.168.1.0/24", "10.0.0.0/8"] } ``` ### save-settings -Set `snort` configuration +Set `snort` configuration. The `home_net` field is a list of IPv4 CIDRs representing the protected networks. ```bash -api-cli ns.snort save-settings --data '{"enabled": true, "ns_policy": "balanced", "oinkcode": "123456789"}' +api-cli ns.snort save-settings --data '{"enabled": true, "ns_policy": "balanced", "oinkcode": "123456789", "home_net": ["192.168.1.0/24"]}' ``` ### check-oinkcode @@ -8077,13 +8078,11 @@ Response example: { "bypasses": [ { - "direction": "src", "protocol": "ipv4", "ip": "192.168.100.23", "description": "Description" }, { - "direction": "dst", "protocol": "ipv6", "ip": "2001:db8::1", "description": "Another description" @@ -8094,10 +8093,10 @@ Response example: ### create-bypass -Create a new bypass rule for Snort IDS. +Create a new bypass rule for Snort IDS. Each bypass applies to both source and destination traffic. ```bash -api-cli ns.snort create-bypass --data '{"protocol": "ipv4", "ip": "192.168.100.23", "direction": "src", "description": "Description"}' +api-cli ns.snort create-bypass --data '{"protocol": "ipv4", "ip": "192.168.100.23", "description": "Description"}' ``` Response example: @@ -8113,7 +8112,7 @@ Response example: Delete an existing bypass rule for Snort IDS. ```bash -api-cli ns.snort delete-bypass --data '{"protocol": "ipv4", "ip": "192.168.100.23", "direction": "src"}' +api-cli ns.snort delete-bypass --data '{"protocol": "ipv4", "ip": "192.168.100.23"}' ``` Response example: diff --git a/packages/ns-api/files/ns.snort b/packages/ns-api/files/ns.snort index 7b8a49527..008982663 100755 --- a/packages/ns-api/files/ns.snort +++ b/packages/ns-api/files/ns.snort @@ -87,7 +87,7 @@ def get_snort_homenet(uci, include_vpn=False): addr = ipaddress.IPv4Network(f"{ip}/{netmask}", strict=False) snort_homenet.add(str(addr)) - return ' '.join(list(snort_homenet)) + return list(snort_homenet) def add_download_cron_job(): # add download rules cron job: every night at 2:30 plus random 30 minutes @@ -111,7 +111,7 @@ def remove_download_cron_job(): f.write(line) -def __setup(enabled, set_home_net=False, include_vpn=False, ns_policy='balanced'): +def __setup(enabled, ns_policy='balanced'): uci = EUci() # first setup @@ -133,9 +133,6 @@ def __setup(enabled, set_home_net=False, include_vpn=False, ns_policy='balanced' uci.set('snort', 'nfq', 'queue_count', str(cpu_count)) uci.set('snort', 'nfq', 'thread_count', str(cpu_count)) - if set_home_net: - uci.set('snort', 'snort', 'home_net', get_snort_homenet(uci, include_vpn)) - uci.set('snort', 'snort', 'ns_policy', ns_policy) if enabled: @@ -150,17 +147,13 @@ def __setup(enabled, set_home_net=False, include_vpn=False, ns_policy='balanced' cmd = sys.argv[1] -def validate_request(request): +def validate_bypass_request(request): if 'protocol' not in request or request['protocol'] == '': raise ValidationError('protocol', 'required') if request['protocol'] not in ['ipv4', 'ipv6']: raise ValidationError('protocol', 'invalid') if 'ip' not in request or request['ip'] == '': raise ValidationError('ip', 'required') - if 'direction' not in request or request['direction'] == '': - raise ValidationError('direction', 'required') - if request['direction'] not in ['src', 'dst']: - raise ValidationError('direction', 'invalid') def __save_settings(): @@ -173,18 +166,38 @@ def __save_settings(): raise ValidationError('ns_policy', 'required') if data['ns_policy'] not in ['connectivity', 'balanced', 'security']: raise ValidationError('ns_policy', 'invalid') + + # Validate home_net: list of IPv4 CIDRs, it can't be empty + home_net = data.get('home_net', []) + if not home_net or not isinstance(home_net, list): + raise ValidationError('home_net', 'required') + for cidr in home_net: + try: + ipaddress.IPv4Network(cidr, strict=False) + except (ipaddress.AddressValueError, ipaddress.NetmaskValueError, ValueError): + raise ValidationError('home_net', 'invalid_cidr', cidr) + __setup(data.get('enabled'), ns_policy=data.get('ns_policy')) e_uci = EUci() e_uci.set('snort', 'snort', 'oinkcode', data.get('oinkcode', '')) + e_uci.set('snort', 'snort', 'home_net', ' '.join(home_net)) e_uci.save('snort') def __settings(): e_uci = EUci() + try: + home_net = e_uci.get('snort', 'snort', 'home_net', default='').split(' ') + except: + home_net = [] + if not home_net: + home_net = get_snort_homenet(e_uci, include_vpn=False) + return { "enabled": e_uci.get('snort', 'snort', 'enabled', dtype=bool, default=False), "ns_policy": e_uci.get('snort', 'snort', 'ns_policy', default='connectivity'), "oinkcode": e_uci.get('snort', 'snort', 'oinkcode', default=''), + "home_net": home_net, } @@ -207,39 +220,23 @@ def __check_oinkcode(): def __list_bypasses(): e_uci = EUci() - bypasses_src_v4 = e_uci.get('snort', 'nfq', 'bypass_src_v4', list=True, default=[]) - bypasses_dst_v4 = e_uci.get('snort', 'nfq', 'bypass_dst_v4', list=True, default=[]) - bypasses_src_v6 = e_uci.get('snort', 'nfq', 'bypass_src_v6', list=True, default=[]) - bypasses_dst_v6 = e_uci.get('snort', 'nfq', 'bypass_dst_v6', list=True, default=[]) - # for each bypass, we need to give direction, protocol and ip, also it can contain a comma after the value for the description + bypasses_v4 = e_uci.get('snort', 'nfq', 'bypass_v4', list=True, default=[]) + bypasses_v6 = e_uci.get('snort', 'nfq', 'bypass_v6', list=True, default=[]) + # for each bypass, we need to give protocol and ip, also it can contain a comma after the value for the description bypasses = [] - for bypass in bypasses_src_v4: - bypasses.append({ - "direction": "src", - "protocol": "ipv4", - "ip": bypass.split(',')[0], - "description": bypass.split(',')[1] if ',' in bypass else "" - }) - for bypass in bypasses_dst_v4: + for bypass in bypasses_v4: + parts = bypass.split(',', 1) bypasses.append({ - "direction": "dst", "protocol": "ipv4", - "ip": bypass.split(',')[0], - "description": bypass.split(',')[1] if ',' in bypass else "" - }) - for bypass in bypasses_src_v6: - bypasses.append({ - "direction": "src", - "protocol": "ipv6", - "ip": bypass.split(',')[0], - "description": bypass.split(',')[1] if ',' in bypass else "" + "ip": parts[0], + "description": parts[1] if len(parts) > 1 else "" }) - for bypass in bypasses_dst_v6: + for bypass in bypasses_v6: + parts = bypass.split(',', 1) bypasses.append({ - "direction": "dst", "protocol": "ipv6", - "ip": bypass.split(',')[0], - "description": bypass.split(',')[1] if ',' in bypass else "" + "ip": parts[0], + "description": parts[1] if len(parts) > 1 else "" }) return bypasses @@ -247,7 +244,7 @@ def __list_bypasses(): def __create_bypass(): request = json.load(sys.stdin) - validate_request(request) + validate_bypass_request(request) not_ip = False not_cidr = False @@ -275,57 +272,33 @@ def __create_bypass(): raise ValidationError('ip', 'invalid_ip_address_or_cidr') e_uci = EUci() - if request['direction'] == 'src': - if request['protocol'] == 'ipv4': - bypasses = list(e_uci.get('snort', 'nfq', 'bypass_src_v4', list=True, default=[])) - if any(request['ip'] in bypass for bypass in bypasses): - raise ValidationError('ip', 'ip_already_used') - bypasses.append(f"{request['ip']},{request.get('description', '')}") - e_uci.set('snort', 'nfq', 'bypass_src_v4', bypasses) - else: - bypasses = list(e_uci.get('snort', 'nfq', 'bypass_src_v6', list=True, default=[])) - if any(request['ip'] in bypass for bypass in bypasses): - raise ValidationError('ip', 'ip_already_used') - bypasses.append(f"{request['ip']},{request.get('description', '')}") - e_uci.set('snort', 'nfq', 'bypass_src_v6', bypasses) + if request['protocol'] == 'ipv4': + bypasses = list(e_uci.get('snort', 'nfq', 'bypass_v4', list=True, default=[])) + if any(request['ip'] in bypass for bypass in bypasses): + raise ValidationError('ip', 'ip_already_used') + bypasses.append(f"{request['ip']},{request.get('description', '')}") + e_uci.set('snort', 'nfq', 'bypass_v4', bypasses) else: - if request['protocol'] == 'ipv4': - bypasses = list(e_uci.get('snort', 'nfq', 'bypass_dst_v4', list=True, default=[])) - if any(request['ip'] in bypass for bypass in bypasses): - raise ValidationError('ip', 'ip_already_used') - bypasses.append(f"{request['ip']},{request.get('description', '')}") - e_uci.set('snort', 'nfq', 'bypass_dst_v4', bypasses) - else: - bypasses = list(e_uci.get('snort', 'nfq', 'bypass_dst_v6', list=True, default=[])) - if any(request['ip'] in bypass for bypass in bypasses): - raise ValidationError('ip', 'ip_already_used') - bypasses.append(f"{request['ip']},{request.get('description', '')}") - e_uci.set('snort', 'nfq', 'bypass_dst_v6', bypasses) + bypasses = list(e_uci.get('snort', 'nfq', 'bypass_v6', list=True, default=[])) + if any(request['ip'] in bypass for bypass in bypasses): + raise ValidationError('ip', 'ip_already_used') + bypasses.append(f"{request['ip']},{request.get('description', '')}") + e_uci.set('snort', 'nfq', 'bypass_v6', bypasses) e_uci.save('snort') def __delete_bypass(): request = json.load(sys.stdin) - validate_request(request) + validate_bypass_request(request) e_uci = EUci() - if request['direction'] == 'src': - if request['protocol'] == 'ipv4': - bypasses = list(e_uci.get('snort', 'nfq', 'bypass_src_v4', list=True, default=[])) - bypasses = [bypass for bypass in bypasses if bypass.split(',')[0] != request['ip']] - e_uci.set('snort', 'nfq', 'bypass_src_v4', bypasses) - else: - bypasses = list(e_uci.get('snort', 'nfq', 'bypass_src_v6', list=True, default=[])) - bypasses = [bypass for bypass in bypasses if bypass.split(',')[0] != request['ip']] - e_uci.set('snort', 'nfq', 'bypass_src_v6', bypasses) + if request['protocol'] == 'ipv4': + bypasses = list(e_uci.get('snort', 'nfq', 'bypass_v4', list=True, default=[])) + bypasses = [bypass for bypass in bypasses if bypass.split(',')[0] != request['ip']] + e_uci.set('snort', 'nfq', 'bypass_v4', bypasses) else: - if request['protocol'] == 'ipv4': - bypasses = list(e_uci.get('snort', 'nfq', 'bypass_dst_v4', list=True, default=[])) - bypasses = [bypass for bypass in bypasses if bypass.split(',')[0] != request['ip']] - e_uci.set('snort', 'nfq', 'bypass_dst_v4', bypasses) - else: - bypasses = list(e_uci.get('snort', 'nfq', 'bypass_dst_v6', list=True, default=[])) - bypasses = [bypass for bypass in bypasses if bypass.split(',')[0] != request['ip']] - e_uci.set('snort', 'nfq', 'bypass_dst_v6', bypasses) + bypasses = list(e_uci.get('snort', 'nfq', 'bypass_v6', list=True, default=[])) + bypasses = [bypass for bypass in bypasses if bypass.split(',')[0] != request['ip']] + e_uci.set('snort', 'nfq', 'bypass_v6', bypasses) e_uci.save('snort') @@ -487,12 +460,13 @@ if cmd == 'list': "enabled": True, "ns_policy": "balanced", "oinkcode": "1234567890", + "home_net": ["192.168.1.0/24"], }, - "save-settings": {"enabled": True, "ns_policy": "balanced", "oinkcode": "1234567890"}, + "save-settings": {"enabled": True, "ns_policy": "balanced", "oinkcode": "1234567890", "home_net": ["192.168.1.0/24"]}, "check-oinkcode": {}, "list-bypasses": {}, - "create-bypass": {"protocol": "ipv4", "ip": "*.*.*.*", "direction": "src", "description": "Description"}, - "delete-bypass": {"protocol": "ipv4", "ip": "*.*.*.*", "direction": "src"}, + "create-bypass": {"protocol": "ipv4", "ip": "*.*.*.*", "description": "Description"}, + "delete-bypass": {"protocol": "ipv4", "ip": "*.*.*.*"}, "list-disabled-rules": {}, "disable-rule": {"gid": 1, "sid": 100000, "description": "Description"}, "enable-rule": {"gid": 1, "sid": 100000}, diff --git a/packages/snort3/Makefile b/packages/snort3/Makefile index 690c1305c..37f142a57 100644 --- a/packages/snort3/Makefile +++ b/packages/snort3/Makefile @@ -96,9 +96,15 @@ define Package/snort3/install ./files/ns-snort-rules \ $(1)/usr/bin/ + $(INSTALL_DIR) $(1)/usr/libexec $(INSTALL_BIN) \ - ./files/ns-bypass-config \ - $(1)/usr/bin/ + ./files/ns-snort-bypass-config \ + $(1)/usr/libexec/ + + $(INSTALL_DIR) $(1)/etc/uci-defaults + $(INSTALL_BIN) \ + ./files/99_snort_bypass_migration \ + $(1)/etc/uci-defaults/ $(INSTALL_DIR) $(1)/usr/lib/snort $(CP) \ diff --git a/packages/snort3/files/99_snort_bypass_migration b/packages/snort3/files/99_snort_bypass_migration new file mode 100644 index 000000000..87abd671a --- /dev/null +++ b/packages/snort3/files/99_snort_bypass_migration @@ -0,0 +1,50 @@ +#!/bin/sh +# Migrate legacy bypass_src_*/bypass_dst_* keys to unified bypass_v4/bypass_v6 + +# Check if migration has already been done +if uci -q get snort.nfq.bypass_v4 > /dev/null 2>&1 || uci -q get snort.nfq.bypass_v6 > /dev/null 2>&1; then + exit 0 +fi + +# Migrate IPv4 bypasses +# First, add all src bypasses +grep "^[[:space:]]*list bypass_src_v4" /etc/config/snort 2>/dev/null | sed "s/^[[:space:]]*list bypass_src_v4[[:space:]]*'//" | sed "s/'$//" | while IFS= read -r value; do + [ -z "$value" ] || uci add_list snort.nfq.bypass_v4="$value" +done + +# Second, add dst bypasses only if the IP doesn't already exist +grep "^[[:space:]]*list bypass_dst_v4" /etc/config/snort 2>/dev/null | sed "s/^[[:space:]]*list bypass_dst_v4[[:space:]]*'//" | sed "s/'$//" | while IFS= read -r value; do + [ -z "$value" ] && continue + ip=$(echo "$value" | cut -d, -f1) + # Check if this IP already exists in bypass_v4 + if ! uci changes snort | grep "snort.nfq.bypass_v4" | sed "s/.*+='\([^,]*\).*/\1/" | grep -Fx "$ip" > /dev/null 2>&1; then + uci add_list snort.nfq.bypass_v4="$value" + fi +done + +# Migrate IPv6 bypasses +# First, add all src bypasses +grep "^[[:space:]]*list bypass_src_v6" /etc/config/snort 2>/dev/null | sed "s/^[[:space:]]*list bypass_src_v6[[:space:]]*'//" | sed "s/'$//" | while IFS= read -r value; do + [ -z "$value" ] || uci add_list snort.nfq.bypass_v6="$value" +done + +# Second, add dst bypasses only if the IP doesn't already exist +grep "^[[:space:]]*list bypass_dst_v6" /etc/config/snort 2>/dev/null | sed "s/^[[:space:]]*list bypass_dst_v6[[:space:]]*'//" | sed "s/'$//" | while IFS= read -r value; do + [ -z "$value" ] && continue + ip=$(echo "$value" | cut -d, -f1) + # Check if this IP already exists in bypass_v6 + if ! uci changes snort | grep "snort.nfq.bypass_v6" | sed "s/.*+='\([^,]*\).*/\1/" | grep -Fx "$ip" > /dev/null 2>&1; then + uci add_list snort.nfq.bypass_v6="$value" + fi +done + +# Clean up old keys +uci -q delete snort.nfq.bypass_src_v6 +uci -q delete snort.nfq.bypass_dst_v6 +uci -q delete snort.nfq.bypass_src_v4 +uci -q delete snort.nfq.bypass_dst_v4 + +# Save changes +uci commit snort + +exit 0 diff --git a/packages/snort3/files/nftables.uc b/packages/snort3/files/nftables.uc index 0fa826754..edc6adcb1 100644 --- a/packages/snort3/files/nftables.uc +++ b/packages/snort3/files/nftables.uc @@ -8,34 +8,24 @@ let chain_type = nfq.chain_type; -%} table inet snort { - set bypass_src_v4 { + set bypass_v4 { type ipv4_addr flags interval - include "/var/ns-snort/bypass_src_v4.conf" + include "/var/ns-snort/bypass_v4.conf" } - set bypass_dst_v4 { - type ipv4_addr - flags interval - include "/var/ns-snort/bypass_dst_v4.conf" - } - set bypass_src_v6 { - type ipv6_addr - flags interval - include "/var/ns-snort/bypass_src_v6.conf" - } - set bypass_dst_v6 { + set bypass_v6 { type ipv6_addr flags interval - include "/var/ns-snort/bypass_dst_v6.conf" + include "/var/ns-snort/bypass_v6.conf" } chain {{ chain_type }}_{{ snort.mode }} { type filter hook {{ chain_type }} priority {{ nfq.chain_priority }} policy accept - ip saddr @bypass_src_v4 counter accept - ip daddr @bypass_dst_v4 counter accept - ip6 saddr @bypass_src_v6 counter accept - ip6 daddr @bypass_dst_v6 counter accept + ip saddr @bypass_v4 counter accept + ip daddr @bypass_v4 counter accept + ip6 saddr @bypass_v6 counter accept + ip6 daddr @bypass_v6 counter accept {% if (nfq.include) { // We use the ucode include here, so that the included file is also // part of the template and can use values passed in from the config. diff --git a/packages/snort3/files/ns-bypass-config b/packages/snort3/files/ns-snort-bypass-config similarity index 89% rename from packages/snort3/files/ns-bypass-config rename to packages/snort3/files/ns-snort-bypass-config index c845da98a..c16e0ff1f 100644 --- a/packages/snort3/files/ns-bypass-config +++ b/packages/snort3/files/ns-snort-bypass-config @@ -9,10 +9,8 @@ from euci import EUci from jinja2 import Environment, BaseLoader CONFIGS = [ - 'bypass_src_v4', - 'bypass_dst_v4', - 'bypass_src_v6', - 'bypass_dst_v6' + 'bypass_v4', + 'bypass_v6' ] TEMPLATE = """ diff --git a/packages/snort3/files/snort.init b/packages/snort3/files/snort.init index 33d259bff..fada08c5a 100644 --- a/packages/snort3/files/snort.init +++ b/packages/snort3/files/snort.init @@ -72,7 +72,7 @@ start_service() { cdir="$(uci -q get snort.snort.config_dir)" mkdir -p "${cdir}/rules" find /etc/snort -type f ! -name snort.rules -exec cp '{}' "${cdir}" \; - /usr/bin/ns-bypass-config + /usr/libexec/ns-snort-bypass-config download_rules setup_tweaks fi