Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ns-api: add nathelpers api #731

Merged
merged 3 commits into from
Sep 6, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions docs/design/nat_helpers.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ parent: Design

# NAT helpers

NAT helpers management is implemented by `ns.nathelpers` API of `ns-api` package. The rest of this page provides some low-level details regarding NAT helpers.

The image contains already all commonly used NAT helpers,
but helpers are not loaded by default on a new installation.

Expand Down
69 changes: 69 additions & 0 deletions packages/ns-api/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6953,3 +6953,72 @@ Response example:
}
}
```

## ns.nathelpers

List and manage NAT helpers.

### list-nat-helpers

List all NAT helpers and their configuration:
```bash
api-cli ns.nathelpers list-nat-helpers
```

Response example:
```json
{
"values": [
{
"enabled": false,
"loaded": false,
"name": "nf_conntrack_amanda",
"params": {
"master_timeout": "300",
"ts_algo": "kmp"
}
},
{
"enabled": false,
"loaded": false,
"name": "nf_conntrack_broadcast",
"params": {}
},
{
"enabled": false,
"loaded": false,
"name": "nf_nat_sip",
"params": {}
}
]
}
```

The `enabled` attribute tells if the user has activated the NAT helper; the `loaded` attribute tells if the module of the NAT helper is currently loaded in the kernel.

Every NAT helper has its own set of parameters; this API returns either the configured value for each parameter (if the helper is enabled) or the default value.

### edit-nat-helper

Enable or disable a NAT helper and set its parameters.
```bash
api-cli ns.nathelpers edit-nat-helper --data '{"name": "nf_conntrack_h323", "enabled": true, "params": {"callforward_filter": "N", "default_rrq_ttl": "600", "gkrouted_only": "1"}}'
```

Response example:
```json
{"reboot_needed": false}
```

Required parameters:
- `name`: name of the NAT helper
- `enabled`: `true` to activate the NAT helper, `false` to disable it

It may raise the following validation errors:
- `nat_helper_not_found`: if a NAT helper named `name` does not exist

The output attribute `reboot_needed` tells if a reboot of the unit is required to apply the changes to the NAT helper. A reboot is needed when:
- changing the parameters of a NAT helper already loaded in the kernel
- disabling a NAT helper

If `enabled` is `false`, all parameter changes are ignored and not applied.
4 changes: 4 additions & 0 deletions packages/ns-api/files/load-kernel-modules
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,10 @@ exit_code=0
# Load all module
grep -v '^#' /etc/modules.d/ns-nathelpers | while IFS= read -r line ; do
module=$(echo "$line" | awk '{print $1}')
if lsmod | grep -q "$module" ; then
# skipping already loaded module
continue
fi
modprobe $module
for param in $(echo $line | awk '{for(i=2;i<=NF;++i)print $i}'); do
# Set parameter using /sys since modprobe doesn't support parameters
Expand Down
270 changes: 270 additions & 0 deletions packages/ns-api/files/ns.nathelpers
Original file line number Diff line number Diff line change
@@ -0,0 +1,270 @@
#!/usr/bin/python3

#
# Copyright (C) 2024 Nethesis S.r.l.
# SPDX-License-Identifier: GPL-2.0-only
#

import json
import sys

from nethsec import utils
import subprocess

DEFAULT_PARAMS = {
'nf_conntrack_ftp': {
'loose': 'N',
'ports': '21'
},
'nf_nat_ftp': {},
'nf_nat_amanda': {},
'nf_nat_tftp': {},
'nf_conntrack_irc': {
'dcc_timeout': '300',
'max_dcc_channels': '8',
'ports': '6667'
},
'nf_nat_sip': {},
'nf_nat_snmp_basic': {},
'nf_conntrack_h323': {
'callforward_filter': 'Y',
'default_rrq_ttl': '300',
'gkrouted_only': '1'
},
'nf_nat_irc': {},
'nf_conntrack_pptp': {},
'nf_conntrack_broadcast': {},
'nf_conntrack_amanda': {
'master_timeout': '300',
'ts_algo': 'kmp'
},
'nf_nat_h323': {},
'nf_conntrack_tftp': {
'ports': '69'
},
'nf_conntrack_sip': {
'ports': '5060',
'sip_direct_media': '1',
'sip_direct_signalling': '1',
'sip_external_media': '1',
'sip_timeout': '3600'
},
'nf_nat_pptp': {},
'nf_conntrack_snmp': {
'timeout': '30'
}
}


def get_nat_helper_names():
nat_helpers = []
proc = subprocess.run("/bin/opkg files kmod-nf-nathelper | grep -e '\.ko$' | cut -d'/' -f 5 | cut -d'.' -f1", shell=True, check=True,
capture_output=True, text=True)
nat_helpers = proc.stdout.splitlines()

nat_helpers_extra = []
proc = subprocess.run("/bin/opkg files kmod-nf-nathelper-extra | grep -e '\.ko$' | cut -d'/' -f 5 | cut -d'.' -f1", shell=True, check=True,
capture_output=True, text=True)
nat_helpers_extra = proc.stdout.splitlines()
return nat_helpers + nat_helpers_extra


def get_loaded_nat_helper_names(all_nat_helper_names):
loaded_nat_helpers = []
proc = subprocess.run("grep nf_ /proc/modules", shell=True,
check=True, capture_output=True, text=True)
for line in proc.stdout.splitlines():
line_tokens = line.strip().split()

if len(line_tokens) == 0:
continue

nat_helper_name = line_tokens[0]

if len(line_tokens) > 0 and nat_helper_name in all_nat_helper_names:
loaded_nat_helpers.append(nat_helper_name)

return loaded_nat_helpers


def add_nat_helper_to_config_file(nat_helper_name, params):
with open("/etc/modules.d/ns-nathelpers", "a") as f:
f.write(f"{nat_helper_name}")

for param_name, param_value in params.items():
f.write(f" {param_name}={param_value}")

f.write("\n")


def enable_nat_helper(nat_helper_name, params):
add_nat_helper_to_config_file(nat_helper_name, params)
subprocess.run(["/usr/sbin/load-kernel-modules"], check=True)
subprocess.run(["/sbin/service", "firewall", "restart"], check=True)


def delete_nat_helper_from_config_file(nat_helper_name):
try:
with open("/etc/modules.d/ns-nathelpers", "r") as f:
lines = f.readlines()
except FileNotFoundError:
lines = []

with open("/etc/modules.d/ns-nathelpers", "w") as f:
for line in lines:
line_tokens = line.strip().split()

if len(line_tokens) == 0:
continue

if line_tokens[0] != nat_helper_name:
f.write(line)


def get_enabled_nat_helpers():
enabled_nat_helpers = {}

try:
with open("/etc/modules.d/ns-nathelpers", "r") as f:
for line in f:
line_tokens = line.strip().split()

if len(line_tokens) == 0:
continue

nat_helper_name = line_tokens[0]
nat_helper = {'name': nat_helper_name,
'enabled': True, 'params': {}}

if len(line_tokens) > 1:
# read params
params = {}

for i in range(1, len(line_tokens)):
param_tokens = line_tokens[i].split("=")
params[param_tokens[0]] = param_tokens[1]

nat_helper['params'] = params

enabled_nat_helpers[nat_helper_name] = nat_helper

return enabled_nat_helpers
except FileNotFoundError:
# no nat helpers enabled
return []


def list_nat_helpers():
# get names of all nat helpers
all_nat_helper_names = get_nat_helper_names()

# get names of nat helpers loaded in the kernel
loaded_nat_helper_names = get_loaded_nat_helper_names(all_nat_helper_names)

# get data of nat helpers enabled in the configuration
enabled_nat_helpers = get_enabled_nat_helpers()

# build list of nat helpers
nat_helpers = []

for nat_helper_name in all_nat_helper_names:
is_enabled = nat_helper_name in enabled_nat_helpers
is_loaded = nat_helper_name in loaded_nat_helper_names
params = {}

if is_enabled:
# merge default params with configured parameters
params = DEFAULT_PARAMS.get(
nat_helper_name, {}) | enabled_nat_helpers[nat_helper_name]['params']
else:
params = DEFAULT_PARAMS.get(nat_helper_name, {})

nat_helper = {
'name': nat_helper_name, 'enabled': is_enabled, 'loaded': is_loaded, 'params': params}
nat_helpers.append(nat_helper)

# sort nat helpers by name
nat_helpers.sort(key=lambda x: x['name'])
return nat_helpers


def edit_nat_helper(nat_helper_name, enabled, params):
if not nat_helper_name:
raise utils.ValidationError('name', 'required')

if enabled is None:
raise utils.ValidationError('enabled', 'required')

all_nat_helper_names = get_nat_helper_names()

if nat_helper_name not in all_nat_helper_names:
raise utils.ValidationError('name', 'nat_helper_not_found')

reboot_needed = False

if enabled:
# check if nat helper is already enabled
enabled_nat_helpers = get_enabled_nat_helpers()
nat_helper_currently_enabled = enabled_nat_helpers.get(
nat_helper_name, None)

if nat_helper_currently_enabled:
# check if params have changed
params_changed = False

for param_name, new_param_value in params.items():
default_param_value = DEFAULT_PARAMS.get(
nat_helper_name, {}).get(param_name, None)
current_param_value = nat_helper_currently_enabled['params'].get(
param_name, default_param_value)
if new_param_value != current_param_value:
params_changed = True
break

if params_changed:
# editing params of a nat helper already enabled
delete_nat_helper_from_config_file(nat_helper_name)
add_nat_helper_to_config_file(nat_helper_name, params)
reboot_needed = True
else:
# nat helper is already enabled with the same params, nothing to do
pass
else:
# enable nat helper

# merge default params with the given params
new_params = DEFAULT_PARAMS.get(nat_helper_name, {}) | params
enable_nat_helper(nat_helper_name, new_params)
reboot_needed = False
else:
# disabling nat helper
delete_nat_helper_from_config_file(nat_helper_name)
reboot_needed = True

return reboot_needed


cmd = sys.argv[1]

if cmd == 'list':
print(json.dumps({
'list-nat-helpers': {},
'edit-nat-helper': {
'name': 'str',
'enabled': False,
'params': {}
}
}))
elif cmd == 'call':
action = sys.argv[2]
try:
if action == 'list-nat-helpers':
print(json.dumps({'values': list_nat_helpers()}))
elif action == 'edit-nat-helper':
data = json.JSONDecoder().decode(sys.stdin.read())
print(json.dumps({'reboot_needed': edit_nat_helper(
data.get('name'), data.get('enabled'), data.get('params', {}))}))
except json.JSONDecodeError:
print(json.dumps(utils.generic_error("json given is invalid")))
except utils.ValidationError as e:
print(json.dumps(utils.validation_error(e.parameter, e.message, e.value)))
13 changes: 13 additions & 0 deletions packages/ns-api/files/ns.nathelpers.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"nathelpers-manager": {
"description": "NAT helpers manager",
"write": {},
"read": {
"ubus": {
"ns.nathelpers": [
"*"
]
}
}
}
}
Loading