diff --git a/README.md b/README.md index 5e68569f..da543497 100644 --- a/README.md +++ b/README.md @@ -37,8 +37,8 @@ Let's assume that the nethsecurity-controller instance is named `nethsecurity-co Launch `configure-module`, by setting the following parameters: - `host`: a fully qualified domain name for the controller - `lets_encrypt`: enable or disable Let's Encrypt certificate -- `ovpn_network`: OpenVPN network -- `ovpn_netmask`: OpenVPN netmask +- `ovpn_network`: OpenVPN network, it must be the first address of the network +- `ovpn_netmask`: OpenVPN netmask, from `255.255.240.0` (/20) to `255.255.255.0` (/24) - `ovpn_cn`: OpenVPN Certificate CN - `api_user`: controller admin user - `api_password`: controller admin password, change it after first login @@ -49,7 +49,7 @@ Launch `configure-module`, by setting the following parameters: Example: - api-cli run module/nethsecurity-controller1/configure-module --data '{"host": "mycontroller.nethsecurity.org", "lets_encrypt": false, "ovpn_network": "172.19.64.0", "ovpn_netmask": "255.255.255.0", "ovpn_cn": "nethsec", "api_user": "admin", "api_password": "password", "loki_retention": 180, "prometheus_retention": 15, "maxmind_license": "xxx"}' + api-cli run module/nethsecurity-controller1/configure-module --data '{"host": "mycontroller.nethsecurity.org", "lets_encrypt": false, "ovpn_network": "172.19.64.0", "ovpn_netmask": "255.255.240.0", "ovpn_cn": "nethsec", "api_user": "admin", "api_password": "password", "loki_retention": 180, "prometheus_retention": 15, "maxmind_license": "xxx"}' The above command will: - start and configure the nethsecurity-controller instance @@ -65,6 +65,38 @@ The above command will: Once the controller is configured, you access the controller URL, eg. `mycontroller.nethsecurity.org`, and manage NethSecurity units. +New controllers get a random `/20` VPN network, controllers installed before got a `/24`. +VPN network and netmask can't be changed after the first configuration. + +### Change the VPN network + +This is not supported by `configure-module`, follow these steps only if you really need it. +The steps below widen the netmask keeping the same network address, e.g. from `172.19.64.0/24` to `172.19.64.0/20`: +units keep their VPN IP and need no change. The network address must be aligned to the new netmask +(`172.19.64.0` is valid for a `/20`, `172.19.65.0` is not). + +Moving to a different network address changes the controller VPN IP: units must be removed and added again. + +1. Stop the controller and read the tun name and the node ID: + + runagent -m nethsecurity-controller1 systemctl --user stop controller.service + runagent -m nethsecurity-controller1 grep OVPN_TUN network.env + runagent -m nethsecurity-controller1 printenv NODE_ID + +2. Remove the tun, replace `tunnsc1` and `1` with the values from step 1: + + api-cli run node/1/remove-tun --data '{"tun": "tunnsc1"}' + +3. Set the new netmask inside the saved configuration: + + runagent -m nethsecurity-controller1 python3 -c 'import json; c = json.load(open("config.json")); c["ovpn_netmask"] = "255.255.240.0"; json.dump(c, open("config.json", "w"))' + +4. Run `configure-module` with the current configuration, it creates the tun and the firewall rules again and starts the controller: + + api-cli run module/nethsecurity-controller1/get-configuration | jq -c 'del(.api_password)' | api-cli run module/nethsecurity-controller1/configure-module --data - + + Units reconnect on their own. + ## Module overview The module is composed by the following systemd units: diff --git a/controller/README.md b/controller/README.md index f5daed5c..989e3bc0 100644 --- a/controller/README.md +++ b/controller/README.md @@ -221,6 +221,7 @@ The following environment variables can be used to configure the containers: - `OVPN_TUN`: OpenVPN tun device name, default is `tunsec` - `OVPN_TUN_MTU`: OpenVPN tun device MTU, default is `1500` - `OVPN_MSSFIX`: OpenVPN mssfix value, default is `1450` +- `OVPN_MAX_CLIENTS`: OpenVPN max connected clients, default is `1024` - `UI_PORT`: UI listening port, default is `3000` - `UI_BIND_IP`: UI binding IP, default is `0.0.0.0` - `API_PORT`: API server listening port, default is `5000` diff --git a/controller/api/utils/utils_test.go b/controller/api/utils/utils_test.go index 0a856709..355cdb04 100644 --- a/controller/api/utils/utils_test.go +++ b/controller/api/utils/utils_test.go @@ -183,6 +183,8 @@ func TestListIPsEdgeCases(t *testing.T) { {"32", "192.168.1.1", "255.255.255.255", 1}, {"31", "192.168.1.0", "255.255.255.254", 0}, {"30", "192.168.1.0", "255.255.255.252", 2}, + {"24", "172.20.16.0", "255.255.255.0", 254}, + {"20", "172.20.16.0", "255.255.240.0", 4094}, } for _, tc := range testCases { diff --git a/controller/vpn/entrypoint.sh b/controller/vpn/entrypoint.sh index 1c6682d5..51012fb7 100755 --- a/controller/vpn/entrypoint.sh +++ b/controller/vpn/entrypoint.sh @@ -9,6 +9,7 @@ ovpn_port=${OVPN_UDP_PORT:-1194} tun=${OVPN_TUN:-tunsec} tun_mtu=${OVPN_TUN_MTU:-1500} mssfix=${OVPN_MSSFIX:-1450} +max_clients=${OVPN_MAX_CLIENTS:-1024} if [ ! -f /etc/openvpn/pki/ca.crt ]; then cd /etc/openvpn @@ -50,6 +51,8 @@ client-config-dir /etc/openvpn/ccd ifconfig-pool-persist host-to-net.pool 0 port $ovpn_port +max-clients $max_clients +explicit-exit-notify 1 script-security 3 float multihome diff --git a/imageroot/actions/configure-module/20configure b/imageroot/actions/configure-module/20configure index b1e3c1ed..392ace2c 100755 --- a/imageroot/actions/configure-module/20configure +++ b/imageroot/actions/configure-module/20configure @@ -10,18 +10,42 @@ import sys import agent import os import uuid +import ipaddress request = json.load(sys.stdin) (start,end) = os.environ["TCP_PORTS_RANGE"].split('-') ports = [*range(int(start), int(end)+1)] +def validation_failed(field, value, error): + agent.set_status('validation-failed') + json.dump([{'field': field, 'parameter': field, 'value': value, 'error': error}], fp=sys.stdout) + sys.exit(2) + try: with open('config.json', 'r') as tmp: config = json.load(tmp) except: config = request +ovpn_network = request['ovpn_network'] +ovpn_netmask = request['ovpn_netmask'] + +# The tun address is set only once by add_tun, so the network can't change after the first configuration +if config['ovpn_network'] != ovpn_network: + validation_failed('network', ovpn_network, 'network_change_not_supported') +if config['ovpn_netmask'] != ovpn_netmask: + validation_failed('netmask', ovpn_netmask, 'network_change_not_supported') + +try: + vpn_network = ipaddress.IPv4Network(f'{ovpn_network}/{ovpn_netmask}') +except ipaddress.NetmaskValueError: + validation_failed('netmask', ovpn_netmask, 'invalid_netmask') +except ValueError: + validation_failed('network', ovpn_network, 'invalid_network') +if not 20 <= vpn_network.prefixlen <= 24: + validation_failed('netmask', ovpn_netmask, 'netmask_out_of_range') + for path in ['loki_path', 'prometheus_path', 'webssh_path']: if not config.get(path): config[path] = f'/{uuid.uuid4()}' @@ -120,6 +144,8 @@ with open('config.env', 'w') as env: env.write(f'ADMIN_PASSWORD={request.get("api_password", config["api_password"])}\n') env.write(f'OVPN_NETWORK={request["ovpn_network"]}\n') env.write(f'OVPN_NETMASK={request["ovpn_netmask"]}\n') + # network, broadcast and server addresses excluded + env.write(f'OVPN_MAX_CLIENTS={vpn_network.num_addresses - 3}\n') env.write(f'OVPN_CN={request["ovpn_cn"]}\n') env.write(f'FQDN={request["host"]}\n') env.write(f'ISSUER_2FA={request["host"]}\n') @@ -183,8 +209,8 @@ agent.write_envfile('network.env', network) # Setup firewall tun = network.get('OVPN_TUN') -bits = sum(bin(int(x)).count('1') for x in request["ovpn_netmask"].split('.')) -cidr = f'{request["ovpn_network"]}/{bits}' +bits = vpn_network.prefixlen +cidr = vpn_network.with_prefixlen # Allow access only on Promtail and API ports, reject everything else rules = [ f'rule family=ipv4 priority=-100 source address={server_address} destination address={cidr} accept', diff --git a/imageroot/actions/configure-module/70platform_info b/imageroot/actions/configure-module/70platform_info index a493dbd1..8beb5e64 100755 --- a/imageroot/actions/configure-module/70platform_info +++ b/imageroot/actions/configure-module/70platform_info @@ -8,13 +8,13 @@ import json import agent import os +import ipaddress vpn_port = os.environ['TCP_PORTS_RANGE'].split('-')[0] controller_version = os.environ.get('IMAGE_URL', '').split(':')[-1] config = agent.read_envfile('config.env') -bits = sum(bin(int(x)).count('1') for x in config.get('OVPN_NETWORK', '').split('.')) -vpn_network = f"{config.get('OVPN_NETWORK', '')}/{bits}" +vpn_network = ipaddress.IPv4Network(f"{config['OVPN_NETWORK']}/{config['OVPN_NETMASK']}").with_prefixlen metrics_retension_days = config.get('RETENTION_DAYS', '30') loki = agent.read_envfile('loki.env') diff --git a/imageroot/actions/configure-module/validate-input.json b/imageroot/actions/configure-module/validate-input.json index 0584b48c..ce886bbd 100644 --- a/imageroot/actions/configure-module/validate-input.json +++ b/imageroot/actions/configure-module/validate-input.json @@ -8,8 +8,8 @@ "api_user": "admin", "api_password": "admin", "host": "controller.nethserver.org", - "ovpn_network": "127.2.10.0", - "ovpn_netmask": "255.255.0.0", + "ovpn_network": "172.20.16.0", + "ovpn_netmask": "255.255.240.0", "ovpn_cn": "nethsec", "loki_retention": 180, "prometheus_retention": 15, @@ -47,12 +47,12 @@ "ovpn_network": { "type": "string", "format": "ipv4", - "description": "VPN client network, like '127.2.1.0.0'" + "description": "VPN client network, like '172.20.16.0'. It can't change after the first configuration" }, "ovpn_netmask": { "type": "string", "format": "ipv4", - "description": "VPN client netmask, like '255.255.0.0'" + "description": "VPN client netmask, from '255.255.240.0' (/20) to '255.255.255.0' (/24). It can't change after the first configuration" }, "ovpn_cn": { "type": "string", diff --git a/imageroot/actions/get-configuration/20read b/imageroot/actions/get-configuration/20read index f05b949e..1798d8e8 100755 --- a/imageroot/actions/get-configuration/20read +++ b/imageroot/actions/get-configuration/20read @@ -26,11 +26,11 @@ if os.path.isfile('config.json'): config["lets_encrypt"] = agent.get_route(os.environ['MODULE_ID']).get('lets_encrypt', False) else: # Prepare random values for first-configuration - # Pick a newtork inside 172.16.0.0/12 (range 172.16.0.0-172.31.255.255) - foctet=random.randrange(16,31) - soctet=random.randrange(1,254) + # Pick a /20 network inside 172.16.0.0/12 (range 172.16.0.0-172.31.255.255) + foctet=random.randrange(16,32) + soctet=16*random.randrange(0,16) - config = {'host': '', 'lets_encrypt': False, 'ovpn_network': f'172.{foctet}.{soctet}.0', 'ovpn_netmask': '255.255.255.0', 'ovpn_cn': 'nethsec', 'api_user': 'admin', 'api_password': '', 'loki_retention': 180, 'prometheus_retention': 15, 'vpn_port': '', 'allowed_ips': [], 'tun_mtu': 1500, 'mssfix': 1450} + config = {'host': '', 'lets_encrypt': False, 'ovpn_network': f'172.{foctet}.{soctet}.0', 'ovpn_netmask': '255.255.240.0', 'ovpn_cn': 'nethsec', 'api_user': 'admin', 'api_password': '', 'loki_retention': 180, 'prometheus_retention': 15, 'vpn_port': '', 'allowed_ips': [], 'tun_mtu': 1500, 'mssfix': 1450} characters = list(string.ascii_letters + string.digits + "!@#%^+_") random.shuffle(characters) diff --git a/imageroot/actions/get-configuration/validate-output.json b/imageroot/actions/get-configuration/validate-output.json index c6724e27..751b051d 100644 --- a/imageroot/actions/get-configuration/validate-output.json +++ b/imageroot/actions/get-configuration/validate-output.json @@ -9,8 +9,8 @@ "api_password": "admin", "host": "controller.nethserver.org", "lets_encrypt": true, - "ovpn_network": "127.2.10.0", - "ovpn_netmask": "255.255.0.0", + "ovpn_network": "172.20.16.0", + "ovpn_netmask": "255.255.240.0", "ovpn_cn": "nethsec", "loki_retention": 180, "maxmind_license": "1234567890", diff --git a/imageroot/update-module.d/40firewall b/imageroot/update-module.d/40firewall index 6e07582a..bfea44a1 100755 --- a/imageroot/update-module.d/40firewall +++ b/imageroot/update-module.d/40firewall @@ -11,6 +11,7 @@ import json import sys import agent import os +import ipaddress (start,end) = os.environ["TCP_PORTS_RANGE"].split('-') ports = [*range(int(start), int(end)+1)] @@ -26,8 +27,7 @@ server_address = config["ovpn_network"].removesuffix('.0') + '.1' network = agent.read_envfile('network.env') tun = network.get('OVPN_TUN') -bits = sum(bin(int(x)).count('1') for x in config["ovpn_netmask"].split('.')) -cidr = f'{config["ovpn_network"]}/{bits}' +cidr = ipaddress.IPv4Network(f'{config["ovpn_network"]}/{config["ovpn_netmask"]}').with_prefixlen rules = [ f'rule family=ipv4 priority=-100 source address={server_address} destination address={cidr} accept', f'rule family=ipv4 priority=-99 source address={cidr} destination address={server_address} port port={ports[4]} protocol=tcp accept', diff --git a/tests/nextsec-controller.robot b/tests/nextsec-controller.robot index 4168330a..cc5452e8 100644 --- a/tests/nextsec-controller.robot +++ b/tests/nextsec-controller.robot @@ -16,7 +16,7 @@ Check if nethsecurity-controller is installed correctly Set Global Variable ${module_id} ${output.module_id} Check if nethsecurity-controller can be configured - ${out} ${err} ${rc} = Execute Command api-cli run module/${module_id}/configure-module --data '{"host": "controller.dom.test", "lets_encrypt": false, "api_user": "admin", "api_password": "Nethesis,1234", "ovpn_network": "172.19.64.0", "ovpn_netmask": "255.255.255.0", "ovpn_cn": "nethsec", "loki_retention": 180, "prometheus_retention": 15}' + ${out} ${err} ${rc} = Execute Command api-cli run module/${module_id}/configure-module --data '{"host": "controller.dom.test", "lets_encrypt": false, "api_user": "admin", "api_password": "Nethesis,1234", "ovpn_network": "172.19.64.0", "ovpn_netmask": "255.255.240.0", "ovpn_cn": "nethsec", "loki_retention": 180, "prometheus_retention": 15}' ... return_rc=True return_stdout=True return_stderr=True Should Be Equal As Integers ${rc} 0 diff --git a/ui/public/i18n/en/translation.json b/ui/public/i18n/en/translation.json index 78b574eb..c4a4c70a 100644 --- a/ui/public/i18n/en/translation.json +++ b/ui/public/i18n/en/translation.json @@ -51,7 +51,11 @@ "cn_tooltip": "The field will be read-only after the first configuration", "network_tooltip": "Make sure this network does not overlap with the units' networks. The field will be read-only after the first configuration", "netmask_tooltip": "Do not change this value unless you know what you are doing. The field will be read-only after the first configuration", - "netmask_helper": "The netmask determines the size of the VPN network. Use only class C networks", + "netmask_helper": "The netmask determines the size of the VPN network, from 255.255.240.0 (/20) to 255.255.255.0 (/24)", + "invalid_network": "Invalid network, it must be the first address of the network, like 172.20.16.0 for a /20", + "invalid_netmask": "Invalid netmask", + "netmask_out_of_range": "Netmask must be between 255.255.240.0 (/20) and 255.255.255.0 (/24)", + "network_change_not_supported": "VPN network and netmask can't be changed after the first configuration", "password_placeholder": "Password can be modified from the controller webapp", "password_information_title": "Default password", "password_information_description": "The default administrator password is displayed only once, please store it in a safe place", @@ -105,8 +109,8 @@ "prometheus_rention_min": "Metrics retention must be greater than 1 day", "loki_retention_max": "Logs retention must be less than 365 days", "loki_retention_min": "Logs retention must be greater than 1 day", - "invalid_network": "Invalid network, it must be a class C network like 192.168.200.0", - "invalid_netmask": "Invalid netmask, it must be a valid netmask like 255.255.255.0", + "invalid_network": "Invalid network, it must end with .0, like 172.20.16.0", + "invalid_netmask": "Invalid netmask, it must be between 255.255.240.0 (/20) and 255.255.255.0 (/24)", "invalid_allowed_ips": "Invalid allowed IPs, each entry must be a and IP or CIDR network.", "tun_mtu_min": "TUN MTU must be at least 576", "mssfix_min": "MSS Fix must be at least 0" diff --git a/ui/src/views/Settings.vue b/ui/src/views/Settings.vue index 077c3805..a997e6d1 100644 --- a/ui/src/views/Settings.vue +++ b/ui/src/views/Settings.vue @@ -635,9 +635,7 @@ export default { } // validate netmask - const netmask_re = new RegExp( - /^(255|254|252|248|240|224|192|128|0)\.(255|254|252|248|240|224|192|128|0)\.(255|254|252|248|240|224|192|128|0)\.(0|128|192|224|240|248|252|254|255)$/ - ); + const netmask_re = new RegExp(/^255\.255\.(240|248|252|254|255)\.0$/); if (!netmask_re.test(this.netmask)) { this.error.netmask = this.$t("error.invalid_netmask"); this.focusElement("netmask");