From d63f624aaaafa6ef3a8ecf9818a6fe8681db4508 Mon Sep 17 00:00:00 2001 From: Bailetti Tommaso Date: Wed, 2 Oct 2024 18:10:21 +0200 Subject: [PATCH] ns-api(threathshield): added additional options to api (#793) Signed-off-by: Tommaso Bailetti Signed-off-by: Andrea Leardini Co-authored-by: Andrea Leardini --- packages/ns-api/README.md | 84 ++++++++- packages/ns-api/files/ns.threatshield | 169 +++++++++++++++++- packages/ns-threat_shield/Makefile | 2 + .../ns-threat_shield/files/banip-defaults | 22 +++ 4 files changed, 271 insertions(+), 6 deletions(-) create mode 100644 packages/ns-threat_shield/files/banip-defaults diff --git a/packages/ns-api/README.md b/packages/ns-api/README.md index 2b6581843..9fe94f936 100644 --- a/packages/ns-api/README.md +++ b/packages/ns-api/README.md @@ -5584,8 +5584,23 @@ Response example: ### edit-settings Configure banip settings: -``` -api-cli ns.threatshield edit-settings --data '{"enabled": true}' + +- `enabled`: disable or enable banip (true or false). +- `ban_logprerouting`: Log suspicious packets in the prerouting chain (true or false). +- `ban_loginput`: Log suspicious packets in the WAN-input chain (true or false). +- `ban_logforwardwan`: Log suspicious packets in the WAN-forward chain (true or false). +- `ban_logforwardlan`: Log suspicious packets in the LAN-forward chain (true or false). +- `ban_loglimit`: Enable or disable scanning of logfiles (true or false). +- `ban_logcount`: Specify how many times an IP must appear in the log to be considered suspicious (integer). +- `ban_logterm`: List of regex entries for logfile parsing (list of strings). +- `ban_icmplimit`: Enable or disable icmp DoS detection (true or false). +- `ban_synlimit`: Enable or disable syn DoS detection (true or false). +- `ban_udplimit`: Enable or disable udp DoS detection (true or false). +- `ban_nftexpiry`: Set the ban expiry, format is `1d` for 1 day, `2h` for 2 hours, `1m` for 1 minute. (string) + + +```bash +api-cli ns.threatshield edit-settings --data '{"enabled": true, "ban_logprerouting": true, "ban_loginput": true, "ban_logforwardwan": true, "ban_logforwardlan": true, "ban_loglimit": false, "ban_logcount": 5, "ban_logterm": ["regex1", "regex2"], "ban_icmplimit": true, "ban_synlimit": true, "ban_udplimit": true, "ban_nftexpiry": "1d"}' ``` Response example: @@ -5670,6 +5685,71 @@ Response example: It can raise the following validation errors: - `address_not_found` if the address is not inside the allow list +### list-blocked + +List blocked addresses from the local blocklist: +``` +api-cli ns.threatshield list-blocked +``` + +Response example: +```json +{ + "data": [ + { + "address": "10.10.0.221/24", + "description": "WAN" + }, + { + "address": "52:54:00:6A:50:BF", + "description": "my MAC address" + } + ] +} +``` + +### add-blocked + +Add an address to the block list: +``` +api-cli ns.threatshield add-blocked --data '{"address": "1.2.3.4", "description": "my block1"}' +``` + +The `address` field can be an IPv4/IPv6, a CIDR, a MAC or host name + +Response example: +```json +{"message": "success"} +``` + +It can raise the following validation errors: +- `address_already_present` if the address is already inside the block list + +### edit-blocked + +Change the description of an address already inside the block list: +``` +api-cli ns.threatshield edit-blocked --data '{"address": "1.2.3.4", "description": "my new desc"}' +``` + +It can raise the following validation errors: +- `address_not_found` if the address is not inside the allow list + +### delete-blocked + +Delete an address from the block list: +``` +api-cli ns.threatshield delete-blocked --data '{"address": "1.2.3.4"}' +``` + +Response example: +```json +{"message": "success"} +``` + +It can raise the following validation errors: +- `address_not_found` if the address is not inside the block list + ### dns-list-blocklist List current dns blocklist: diff --git a/packages/ns-api/files/ns.threatshield b/packages/ns-api/files/ns.threatshield index b8c60800e..93b9bd4c3 100644 --- a/packages/ns-api/files/ns.threatshield +++ b/packages/ns-api/files/ns.threatshield @@ -62,6 +62,11 @@ def list_feeds(): with open('/etc/banip/banip.feeds') as f: return json.loads(f.read()) + +def get_block_list(): + return get_allow_list('/etc/banip/banip.blocklist') + + def get_allow_list(file='/etc/banip/banip.allowlist'): ret = [] try: @@ -88,6 +93,11 @@ def write_allow_list(allow_list, file='/etc/banip/banip.allowlist'): f.write('\n') subprocess.run(["/etc/init.d/banip", "reload"], capture_output=True) + +def write_block_list(block_list): + write_allow_list(block_list, '/etc/banip/banip.blocklist') + + def list_dns_feeds(): # Decompress and read the JSON file /etc/adblock/combined.sources.gz sources = '/etc/adblock/combined.sources.gz' @@ -137,7 +147,22 @@ def list_blocklist(e_uci): return { "data": ret } def list_settings(e_uci): - return { 'data': {'enabled': e_uci.get('banip', 'global', 'ban_enabled') == '1' } } + return { + 'data': { + 'enabled': e_uci.get('banip', 'global', 'ban_enabled') == '1', + 'ban_logprerouting': e_uci.get('banip', 'global', 'ban_logprerouting', default=False) == '1', + 'ban_loginput': e_uci.get('banip', 'global', 'ban_loginput', default=False) == '1', + 'ban_logforwardwan': e_uci.get('banip', 'global', 'ban_logforwardwan', default=False) == '1', + 'ban_logforwardlan': e_uci.get('banip', 'global', 'ban_logforwardlan', default=False) == '1', + 'ban_loglimit': True if int(e_uci.get('banip', 'global', 'ban_loglimit', default=100)) > 0 else False, + 'ban_logcount': e_uci.get('banip', 'global', 'ban_logcount', default=1), + 'ban_logterm': e_uci.get('banip', 'global', 'ban_logterm', list=True, default=[]), + 'ban_icmplimit': True if int(e_uci.get('banip', 'global', 'ban_icmplimit', default=10)) > 0 else False, + 'ban_synlimit': True if int(e_uci.get('banip', 'global', 'ban_synlimit', default=10)) > 0 else False, + 'ban_udplimit': True if int(e_uci.get('banip', 'global', 'ban_udplimit', default=100)) > 0 else False, + 'ban_nftexpiry': e_uci.get('banip', 'global', 'ban_nftexpiry', default='30m') + } + } def edit_blocklist(e_uci, payload): feeds = list_feeds() @@ -160,21 +185,93 @@ def set_default(e_uci, option, value): e_uci.set('banip', 'global', option, value) def edit_settings(e_uci, payload): + if 'enabled' not in payload: + raise ValidationError('enabled', 'required') + if not isinstance(payload['enabled'], bool): + raise ValidationError('enabled', 'invalid', payload['enabled']) + if payload['enabled']: + # validate all other payload options + if 'ban_logprerouting' not in payload: + raise ValidationError('ban_logprerouting', 'required') + if not isinstance(payload['ban_logprerouting'], bool): + raise ValidationError('ban_logprerouting', 'invalid', payload['ban_logprerouting']) + if 'ban_loginput' not in payload: + raise ValidationError('ban_loginput', 'required') + if not isinstance(payload['ban_loginput'], bool): + raise ValidationError('ban_loginput', 'invalid', payload['ban_loginput']) + if 'ban_logforwardwan' not in payload: + raise ValidationError('ban_logforwardwan', 'required') + if not isinstance(payload['ban_logforwardwan'], bool): + raise ValidationError('ban_logforwardwan', 'invalid', payload['ban_logforwardwan']) + if 'ban_logforwardlan' not in payload: + raise ValidationError('ban_logforwardlan', 'required') + if not isinstance(payload['ban_logforwardlan'], bool): + raise ValidationError('ban_logforwardlan', 'invalid', payload['ban_logforwardlan']) + if 'ban_icmplimit' not in payload: + raise ValidationError('ban_icmplimit', 'required') + if not isinstance(payload['ban_icmplimit'], bool): + raise ValidationError('ban_icmplimit', 'invalid', payload['ban_icmplimit']) + if 'ban_synlimit' not in payload: + raise ValidationError('ban_synlimit', 'required') + if not isinstance(payload['ban_synlimit'], bool): + raise ValidationError('ban_synlimit', 'invalid', payload['ban_synlimit']) + if 'ban_udplimit' not in payload: + raise ValidationError('ban_udplimit', 'required') + if not isinstance(payload['ban_udplimit'], bool): + raise ValidationError('ban_udplimit', 'invalid', payload['ban_udplimit']) + if 'ban_loglimit' not in payload: + raise ValidationError('ban_loglimit', 'required') + if not isinstance(payload['ban_loglimit'], bool): + raise ValidationError('ban_loglimit', 'invalid', payload['ban_loglimit']) + + if payload['ban_loglimit']: + if 'ban_logcount' not in payload: + raise ValidationError('ban_logcount', 'required') + if not isinstance(payload['ban_logcount'], int): + raise ValidationError('ban_logcount', 'invalid', payload['ban_logcount']) + if 'ban_logterm' not in payload: + raise ValidationError('ban_logterm', 'required') + if not isinstance(payload['ban_logterm'], list): + raise ValidationError('ban_logterm', 'invalid', payload['ban_logterm']) + if 'ban_nftexpiry' not in payload: + raise ValidationError('ban_nftexpiry', 'required') + if not isinstance(payload['ban_nftexpiry'], str): + raise ValidationError('ban_nftexpiry', 'invalid', payload['ban_nftexpiry']) + + # Validation completed, set the values e_uci.set('banip', 'global', 'ban_enabled', '1') set_default(e_uci, 'ban_fetchcmd', 'curl') set_default(e_uci, 'ban_protov4', '1') set_default(e_uci, 'ban_protov6', '1') - set_default(e_uci, 'ban_nftexpiry', '30m') - set_default(e_uci, 'ban_logcount', '3') + + e_uci.set('banip', 'global', 'ban_logprerouting', payload['ban_logprerouting']) + e_uci.set('banip', 'global', 'ban_loginput', payload['ban_loginput']) + e_uci.set('banip', 'global', 'ban_logforwardwan', payload['ban_logforwardwan']) + e_uci.set('banip', 'global', 'ban_logforwardlan', payload['ban_logforwardlan']) + e_uci.set('banip', 'global', 'ban_loglimit', 100 if payload['ban_loglimit'] else 0) + + e_uci.set('banip', 'global', 'ban_icmplimit', 10 if payload['ban_icmplimit'] else 0) + e_uci.set('banip', 'global', 'ban_synlimit', 10 if payload['ban_synlimit'] else 0) + e_uci.set('banip', 'global', 'ban_udplimit', 100 if payload['ban_udplimit'] else 0) + + if payload['ban_loglimit']: + e_uci.set('banip', 'global', 'ban_logcount', payload['ban_logcount']) + e_uci.set('banip', 'global', 'ban_logterm', payload['ban_logterm']) + e_uci.set('banip', 'global', 'ban_nftexpiry', payload['ban_nftexpiry']) else: e_uci.set('banip', 'global', 'ban_enabled', '0') + e_uci.save('banip') return {'message': 'success'} def list_allowed(): return { "data": get_allow_list() } + +def list_blocked(): + return {"data": get_block_list()} + def add_allowed(payload): cur = get_allow_list() # extract address from cur list @@ -184,6 +281,16 @@ def add_allowed(payload): write_allow_list(cur) return {'message': 'success'} + +def add_blocked(payload): + cur = get_block_list() + if payload['address'] in [x['address'] for x in cur]: + raise ValidationError('address', 'address_already_present', payload['address']) + cur.append({"address": payload['address'], "description": payload['description']}) + write_block_list(cur) + return {'message': 'success'} + + def delete_allowed(payload): cur = get_allow_list() if payload['address'] not in [x['address'] for x in cur]: @@ -196,6 +303,20 @@ def delete_allowed(payload): write_allow_list(cur) return {'message': 'success'} + +def delete_blocked(payload): + cur = get_block_list() + if payload['address'] not in [x['address'] for x in cur]: + raise ValidationError('address', 'address_not_found', payload['address']) + # remove address from cur list + for i in range(len(cur)): + if cur[i]['address'] == payload['address']: + del cur[i] + break + write_block_list(cur) + return {'message': 'success'} + + def edit_allowed(payload): cur = get_allow_list() if payload['address'] not in [x['address'] for x in cur]: @@ -207,6 +328,19 @@ def edit_allowed(payload): write_allow_list(cur) return {'message': 'success'} + +def edit_blocked(payload): + cur = get_block_list() + if payload['address'] not in [x['address'] for x in cur]: + raise ValidationError('address', 'address_not_found', payload['address']) + for i in range(len(cur)): + if cur[i]['address'] == payload['address']: + cur[i]['description'] = payload['description'] + break + write_block_list(cur) + return {'message': 'success'} + + def dns_list_blocklist(e_uci): ret = [] feeds = list_dns_feeds() @@ -345,11 +479,27 @@ if cmd == 'list': 'list-blocklist': {}, 'edit-blocklist': { "blocklist": "blocklist_name", "enabled": True }, 'list-settings': {}, - 'edit-settings': { 'enabled': True }, + 'edit-settings': { + 'enabled': True, + 'ban_logprerouting': True, + 'ban_loginput': True, + 'ban_logforwardwan': True, + 'ban_logforwardlan': True, + 'ban_loglimit': True, + 'ban_logcount': 3, + 'ban_logterm': ['string'], + 'ban_icmplimit': True, + 'ban_synlimit': True, + 'ban_udplimit': True + }, 'list-allowed': {}, 'add-allowed': { 'address': '1.2.3.4', 'description': 'optional' }, 'edit-allowed': { 'address': '1.2.3.4', 'description': 'optional' }, 'delete-allowed': { 'address': '1.2.3.4' }, + 'list-blocked': {}, + 'add-blocked': {'address': '1.2.3.4', 'description': 'optional'}, + 'edit-blocked': {'address': '1.2.3.4', 'description': 'optional'}, + 'delete-blocked': {'address': '1.2.3.4'}, 'dns-list-blocklist': {}, 'dns-edit-blocklist': { "blocklist": "blocklist_name", "enabled": True }, 'dns-list-settings': {}, @@ -387,6 +537,17 @@ elif cmd == 'call': elif action == 'delete-allowed': payload = json.loads(sys.stdin.read()) ret = delete_allowed(payload) + elif action == 'list-blocked': + ret = list_blocked() + elif action == 'add-blocked': + payload = json.loads(sys.stdin.read()) + ret = add_blocked(payload) + elif action == 'edit-blocked': + payload = json.loads(sys.stdin.read()) + ret = edit_blocked(payload) + elif action == 'delete-blocked': + payload = json.loads(sys.stdin.read()) + ret = delete_blocked(payload) if action == 'dns-list-blocklist': ret = dns_list_blocklist(e_uci) diff --git a/packages/ns-threat_shield/Makefile b/packages/ns-threat_shield/Makefile index 78d3519dc..90bfb8f72 100644 --- a/packages/ns-threat_shield/Makefile +++ b/packages/ns-threat_shield/Makefile @@ -55,6 +55,8 @@ define Package/ns-threat_shield/install $(INSTALL_DATA) ./files/banip.nethesis.feeds $(1)/etc/banip $(INSTALL_BIN) ./files/adjust-banip.py $(1)/usr/libexec/ns-api/post-commit/ $(INSTALL_BIN) ./files/configure-banip-wans.py $(1)/usr/libexec/ns-api/pre-commit/ + $(INSTALL_DIR) $(1)/etc/uci-defaults + $(INSTALL_BIN) ./files/banip-defaults $(1)/etc/uci-defaults/99-nethsec-banip gzip -9n $(1)/usr/share/threat_shield/nethesis-dns.sources gzip -9n $(1)/usr/share/threat_shield/community-dns.sources endef diff --git a/packages/ns-threat_shield/files/banip-defaults b/packages/ns-threat_shield/files/banip-defaults new file mode 100644 index 000000000..46d8f08a4 --- /dev/null +++ b/packages/ns-threat_shield/files/banip-defaults @@ -0,0 +1,22 @@ +[ "$(uci -q get banip.global.ban_logforwardwan)" != "" ] && exit 0 + +uci -q batch << EOI +set banip.global.ban_logforwardwan="1" +set banip.global.ban_logforwardlan="1" +set banip.global.ban_logprerouting="0" +set banip.global.ban_loginput="0" + +set banip.global.ban_loglimit="100" +set banip.global.ban_logcount="3" +set banip.global.ban_nftexpiry="30m" + +delete banip.global.ban_logterm +add_list banip.global.ban_logterm="Exit before auth from" +add_list banip.global.ban_logterm="authentication failed for user" + +set banip.global.ban_icmplimit="10" +set banip.global.ban_synlimit="10" +set banip.global.ban_udplimit="100" + +commit banip +EOI