summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--data/templates/conntrackd/conntrackd.conf.j22
-rw-r--r--data/templates/high-availability/keepalived.conf.j231
-rw-r--r--data/templates/https/nginx.default.j24
-rw-r--r--data/templates/snmp/etc.snmpd.conf.j27
-rw-r--r--debian/control4
-rw-r--r--interface-definitions/high-availability.xml.in49
-rw-r--r--interface-definitions/interfaces_wireless.xml.in8
-rw-r--r--interface-definitions/service_conntrack-sync.xml.in6
-rw-r--r--interface-definitions/service_https.xml.in13
-rw-r--r--interface-definitions/service_snmp.xml.in1
-rw-r--r--op-mode-definitions/clear-dhcp-server-lease.xml.in20
-rw-r--r--op-mode-definitions/crypt.xml.in28
-rw-r--r--op-mode-definitions/dhcp.xml.in30
-rw-r--r--python/vyos/firewall.py4
-rw-r--r--python/vyos/kea.py24
-rw-r--r--python/vyos/remote.py16
-rw-r--r--python/vyos/tpm.py98
-rwxr-xr-xsmoketest/scripts/cli/test_firewall.py4
-rwxr-xr-xsmoketest/scripts/cli/test_high-availability_vrrp.py53
-rwxr-xr-xsmoketest/scripts/cli/test_service_snmp.py17
-rwxr-xr-xsrc/conf_mode/firewall.py9
-rwxr-xr-xsrc/conf_mode/high-availability.py37
-rwxr-xr-xsrc/conf_mode/nat.py18
-rwxr-xr-xsrc/helpers/vyos-config-encrypt.py276
-rwxr-xr-xsrc/init/vyos-router71
-rwxr-xr-xsrc/op_mode/clear_dhcp_lease.py88
-rwxr-xr-xsrc/op_mode/dhcp.py23
-rwxr-xr-xsrc/op_mode/image_installer.py79
-rwxr-xr-xsrc/op_mode/image_manager.py21
-rwxr-xr-xsrc/services/vyos-http-api-server3
30 files changed, 875 insertions, 169 deletions
diff --git a/data/templates/conntrackd/conntrackd.conf.j2 b/data/templates/conntrackd/conntrackd.conf.j2
index 8f56c8171..669b20877 100644
--- a/data/templates/conntrackd/conntrackd.conf.j2
+++ b/data/templates/conntrackd/conntrackd.conf.j2
@@ -76,7 +76,7 @@ General {
HashSize {{ hash_size }}
HashLimit {{ table_size | int *2 }}
LogFile off
- Syslog on
+ Syslog {{ 'off' if disable_syslog is vyos_defined else 'on' }}
LockFile /var/lock/conntrack.lock
UNIX {
Path /var/run/conntrackd.ctl
diff --git a/data/templates/high-availability/keepalived.conf.j2 b/data/templates/high-availability/keepalived.conf.j2
index f34ce64e2..240161748 100644
--- a/data/templates/high-availability/keepalived.conf.j2
+++ b/data/templates/high-availability/keepalived.conf.j2
@@ -33,6 +33,24 @@ global_defs {
notify_fifo_script /usr/libexec/vyos/system/keepalived-fifo.py
}
+{# Sync group has own health-check scripts T6020 #}
+{% if vrrp.sync_group is vyos_defined %}
+{% for name, sync_group_config in vrrp.sync_group.items() if sync_group_config.disable is not vyos_defined %}
+{% if sync_group_config.health_check is vyos_defined %}
+vrrp_script healthcheck_sg_{{ name }} {
+{% if sync_group_config.health_check.script is vyos_defined %}
+ script "{{ sync_group_config.health_check.script }}"
+{% elif sync_group_config.health_check.ping is vyos_defined %}
+ script "/usr/bin/ping -c1 {{ sync_group_config.health_check.ping }}"
+{% endif %}
+ interval {{ sync_group_config.health_check.interval }}
+ fall {{ sync_group_config.health_check.failure_count }}
+ rise 1
+}
+{% endif %}
+{% endfor %}
+{% endif %}
+
{% if vrrp.group is vyos_defined %}
{% for name, group_config in vrrp.group.items() if group_config.disable is not vyos_defined %}
{% if group_config.health_check is vyos_defined %}
@@ -132,7 +150,8 @@ vrrp_instance {{ name }} {
{% endfor %}
}
{% endif %}
-{% if group_config.health_check is vyos_defined %}
+{# Sync group member can't use own health check script #}
+{% if group_config.health_check is vyos_defined and group_config._is_sync_group_member is not vyos_defined %}
track_script {
healthcheck_{{ name }}
}
@@ -152,16 +171,12 @@ vrrp_sync_group {{ name }} {
{% endif %}
}
-{# Health-check scripts should be in section sync-group if member is part of the sync-group T4081 #}
-{% if vrrp.group is vyos_defined %}
-{% for name, group_config in vrrp.group.items() if group_config.disable is not vyos_defined %}
-{% if group_config.health_check.script is vyos_defined and name in sync_group_config.member %}
+{% if sync_group_config.health_check is vyos_defined %}
track_script {
- healthcheck_{{ name }}
+ healthcheck_sg_{{ name }}
}
-{% endif %}
-{% endfor %}
{% endif %}
+
{% if conntrack_sync_group is vyos_defined(name) %}
{% set vyos_helper = "/usr/libexec/vyos/vyos-vrrp-conntracksync.sh" %}
notify_master "{{ vyos_helper }} master {{ name }}"
diff --git a/data/templates/https/nginx.default.j2 b/data/templates/https/nginx.default.j2
index 5d17df001..4619361e5 100644
--- a/data/templates/https/nginx.default.j2
+++ b/data/templates/https/nginx.default.j2
@@ -21,6 +21,10 @@ server {
server_name {{ hostname }};
root /srv/localui;
+{% if request_body_size_limit is vyos_defined %}
+ client_max_body_size {{ request_body_size_limit }}M;
+{% endif %}
+
# SSL configuration
{% if certificates.cert_path is vyos_defined and certificates.key_path is vyos_defined %}
ssl_certificate {{ certificates.cert_path }};
diff --git a/data/templates/snmp/etc.snmpd.conf.j2 b/data/templates/snmp/etc.snmpd.conf.j2
index b1ceb0451..9d91192fc 100644
--- a/data/templates/snmp/etc.snmpd.conf.j2
+++ b/data/templates/snmp/etc.snmpd.conf.j2
@@ -141,8 +141,13 @@ trap2sink {{ trap }}:{{ trap_config.port }} {{ trap_config.community }}
# views
{% for view, view_config in v3.view.items() %}
{% if view_config.oid is vyos_defined %}
-{% for oid in view_config.oid %}
+{% for oid, oid_config in view_config.oid.items() %}
view {{ view }} included .{{ oid }}
+{% if oid_config.exclude is vyos_defined %}
+{% for excluded in oid_config.exclude %}
+view {{ view }} excluded .{{ excluded }}
+{% endfor %}
+{% endif %}
{% endfor %}
{% endif %}
{% endfor %}
diff --git a/debian/control b/debian/control
index 726a083f2..dddc4e14c 100644
--- a/debian/control
+++ b/debian/control
@@ -304,6 +304,10 @@ Depends:
# For "run monitor bandwidth"
bmon,
# End Operational mode
+## TPM tools
+ cryptsetup,
+ tpm2-tools,
+## End TPM tools
## Optional utilities
easy-rsa,
tcptraceroute,
diff --git a/interface-definitions/high-availability.xml.in b/interface-definitions/high-availability.xml.in
index aef57f8ae..558404882 100644
--- a/interface-definitions/high-availability.xml.in
+++ b/interface-definitions/high-availability.xml.in
@@ -345,6 +345,55 @@
</completionHelp>
</properties>
</leafNode>
+ <node name="health-check">
+ <properties>
+ <help>Health check</help>
+ </properties>
+ <children>
+ <leafNode name="failure-count">
+ <properties>
+ <help>Health check failure count required for transition to fault</help>
+ <constraint>
+ <validator name="numeric" argument="--positive" />
+ </constraint>
+ </properties>
+ <defaultValue>3</defaultValue>
+ </leafNode>
+ <leafNode name="interval">
+ <properties>
+ <help>Health check execution interval in seconds</help>
+ <constraint>
+ <validator name="numeric" argument="--positive"/>
+ </constraint>
+ </properties>
+ <defaultValue>60</defaultValue>
+ </leafNode>
+ <leafNode name="ping">
+ <properties>
+ <help>ICMP ping health check</help>
+ <valueHelp>
+ <format>ipv4</format>
+ <description>IPv4 ping target address</description>
+ </valueHelp>
+ <valueHelp>
+ <format>ipv6</format>
+ <description>IPv6 ping target address</description>
+ </valueHelp>
+ <constraint>
+ <validator name="ip-address"/>
+ </constraint>
+ </properties>
+ </leafNode>
+ <leafNode name="script">
+ <properties>
+ <help>Health check script file</help>
+ <constraint>
+ <validator name="script"/>
+ </constraint>
+ </properties>
+ </leafNode>
+ </children>
+ </node>
#include <include/vrrp-transition-script.xml.i>
</children>
</tagNode>
diff --git a/interface-definitions/interfaces_wireless.xml.in b/interface-definitions/interfaces_wireless.xml.in
index b5da0a556..458f7ebb3 100644
--- a/interface-definitions/interfaces_wireless.xml.in
+++ b/interface-definitions/interfaces_wireless.xml.in
@@ -455,14 +455,18 @@
<properties>
<help>Indicate country in which device is operating</help>
<completionHelp>
- <list>us eu jp de uk cn es fr ru</list>
+ <list>00 ad ae af ai al am an ar as at au aw az ba bb bd be bf bg bh bl bm bn bo br bs bt by bz ca cf ch ci cl cn co cr cu cx cy cz de dk dm do dz ec ee eg es et fi fm fr gb gd ge gf gh gl gp gr gt gu gy hk hn hr ht hu id ie il in ir is it jm jo jp ke kh kn kp kr kw ky kz lb lc li lk ls lt lu lv ma mc md me mf mh mk mn mo mp mq mr mt mu mv mw mx my ng ni nl no np nz om pa pe pf pg ph pk pl pm pr pt pw py qa re ro rs ru rw sa se sg si sk sn sr sv sy tc td tg th tn tr tt tw tz ua ug us uy uz vc ve vi vn vu wf ws ye yt za zw</list>
</completionHelp>
<valueHelp>
+ <format>00</format>
+ <description>World regulatory domain</description>
+ </valueHelp>
+ <valueHelp>
<format>txt</format>
<description>ISO/IEC 3166-1 Country Code</description>
</valueHelp>
<constraint>
- <regex>[a-z][a-z]</regex>
+ <regex>(00|ad|ae|af|ai|al|am|an|ar|as|at|au|aw|az|ba|bb|bd|be|bf|bg|bh|bl|bm|bn|bo|br|bs|bt|by|bz|ca|cf|ch|ci|cl|cn|co|cr|cu|cx|cy|cz|de|dk|dm|do|dz|ec|ee|eg|es|et|fi|fm|fr|gb|gd|ge|gf|gh|gl|gp|gr|gt|gu|gy|hk|hn|hr|ht|hu|id|ie|il|in|ir|is|it|jm|jo|jp|ke|kh|kn|kp|kr|kw|ky|kz|lb|lc|li|lk|ls|lt|lu|lv|ma|mc|md|me|mf|mh|mk|mn|mo|mp|mq|mr|mt|mu|mv|mw|mx|my|ng|ni|nl|no|np|nz|om|pa|pe|pf|pg|ph|pk|pl|pm|pr|pt|pw|py|qa|re|ro|rs|ru|rw|sa|se|sg|si|sk|sn|sr|sv|sy|tc|td|tg|th|tn|tr|tt|tw|tz|ua|ug|us|uy|uz|vc|ve|vi|vn|vu|wf|ws|ye|yt|za|zw)</regex>
</constraint>
<constraintErrorMessage>Invalid ISO/IEC 3166-1 Country Code</constraintErrorMessage>
</properties>
diff --git a/interface-definitions/service_conntrack-sync.xml.in b/interface-definitions/service_conntrack-sync.xml.in
index 46dc8adc0..397864867 100644
--- a/interface-definitions/service_conntrack-sync.xml.in
+++ b/interface-definitions/service_conntrack-sync.xml.in
@@ -52,6 +52,12 @@
<valueless/>
</properties>
</leafNode>
+ <leafNode name="disable-syslog">
+ <properties>
+ <help>Disable connection logging via Syslog</help>
+ <valueless/>
+ </properties>
+ </leafNode>
<leafNode name="event-listen-queue-size">
<properties>
<help>Queue size for local conntrack events</help>
diff --git a/interface-definitions/service_https.xml.in b/interface-definitions/service_https.xml.in
index b60c7ff2e..afe430c0c 100644
--- a/interface-definitions/service_https.xml.in
+++ b/interface-definitions/service_https.xml.in
@@ -138,6 +138,19 @@
<leafNode name='port'>
<defaultValue>443</defaultValue>
</leafNode>
+ <leafNode name="request-body-size-limit">
+ <properties>
+ <help>Maximum request body size in megabytes</help>
+ <valueHelp>
+ <format>u32:1-256</format>
+ <description>Request body size in megabytes</description>
+ </valueHelp>
+ <constraint>
+ <validator name="numeric" argument="--range 1-256"/>
+ </constraint>
+ </properties>
+ <defaultValue>1</defaultValue>
+ </leafNode>
<node name="certificates">
<properties>
<help>TLS certificates</help>
diff --git a/interface-definitions/service_snmp.xml.in b/interface-definitions/service_snmp.xml.in
index e16e9daa1..f23151ef9 100644
--- a/interface-definitions/service_snmp.xml.in
+++ b/interface-definitions/service_snmp.xml.in
@@ -543,6 +543,7 @@
<leafNode name="exclude">
<properties>
<help>Exclude is an optional argument</help>
+ <multi/>
</properties>
</leafNode>
<leafNode name="mask">
diff --git a/op-mode-definitions/clear-dhcp-server-lease.xml.in b/op-mode-definitions/clear-dhcp-server-lease.xml.in
deleted file mode 100644
index aef0eb22a..000000000
--- a/op-mode-definitions/clear-dhcp-server-lease.xml.in
+++ /dev/null
@@ -1,20 +0,0 @@
-<?xml version="1.0"?>
-<interfaceDefinition>
- <node name="clear">
- <children>
- <node name="dhcp-server">
- <properties>
- <help>Clear DHCP server lease</help>
- </properties>
- <children>
- <tagNode name="lease">
- <properties>
- <help>DHCP server lease</help>
- </properties>
- <command>sudo ${vyos_op_scripts_dir}/clear_dhcp_lease.py --ip $4</command>
- </tagNode>
- </children>
- </node>
- </children>
- </node>
-</interfaceDefinition>
diff --git a/op-mode-definitions/crypt.xml.in b/op-mode-definitions/crypt.xml.in
new file mode 100644
index 000000000..105592a1a
--- /dev/null
+++ b/op-mode-definitions/crypt.xml.in
@@ -0,0 +1,28 @@
+<?xml version="1.0"?>
+<interfaceDefinition>
+ <node name="encryption">
+ <properties>
+ <help>Manage config encryption</help>
+ </properties>
+ <children>
+ <node name="disable">
+ <properties>
+ <help>Disable config encryption using TPM or recovery key</help>
+ </properties>
+ <command>sudo ${vyos_libexec_dir}/vyos-config-encrypt.py --disable</command>
+ </node>
+ <node name="enable">
+ <properties>
+ <help>Enable config encryption using TPM</help>
+ </properties>
+ <command>sudo ${vyos_libexec_dir}/vyos-config-encrypt.py --enable</command>
+ </node>
+ <node name="load">
+ <properties>
+ <help>Load encrypted config volume using TPM or recovery key</help>
+ </properties>
+ <command>sudo ${vyos_libexec_dir}/vyos-config-encrypt.py --load</command>
+ </node>
+ </children>
+ </node>
+</interfaceDefinition>
diff --git a/op-mode-definitions/dhcp.xml.in b/op-mode-definitions/dhcp.xml.in
index 0db7471e5..3c42c8e8f 100644
--- a/op-mode-definitions/dhcp.xml.in
+++ b/op-mode-definitions/dhcp.xml.in
@@ -1,5 +1,35 @@
<?xml version="1.0" encoding="UTF-8"?>
<interfaceDefinition>
+ <node name="clear">
+ <children>
+ <node name="dhcp-server">
+ <properties>
+ <help>Clear DHCP server lease</help>
+ </properties>
+ <children>
+ <tagNode name="lease">
+ <properties>
+ <help>DHCP server lease</help>
+ </properties>
+ <command>${vyos_op_scripts_dir}/dhcp.py clear_dhcp_server_lease --family inet --address $4</command>
+ </tagNode>
+ </children>
+ </node>
+ <node name="dhcpv6-server">
+ <properties>
+ <help>Clear DHCPv6 server lease</help>
+ </properties>
+ <children>
+ <tagNode name="lease">
+ <properties>
+ <help>DHCPv6 server lease</help>
+ </properties>
+ <command>${vyos_op_scripts_dir}/dhcp.py clear_dhcp_server_lease --family inet6 --address $4</command>
+ </tagNode>
+ </children>
+ </node>
+ </children>
+ </node>
<node name="show">
<children>
<node name="dhcp">
diff --git a/python/vyos/firewall.py b/python/vyos/firewall.py
index 49e095946..e70b4f0d9 100644
--- a/python/vyos/firewall.py
+++ b/python/vyos/firewall.py
@@ -136,10 +136,10 @@ def parse_rule(rule_conf, hook, fw_name, rule_id, ip_name):
if 'connection_status' in rule_conf and rule_conf['connection_status']:
status = rule_conf['connection_status']
if status['nat'] == 'destination':
- nat_status = '{dnat}'
+ nat_status = 'dnat'
output.append(f'ct status {nat_status}')
if status['nat'] == 'source':
- nat_status = '{snat}'
+ nat_status = 'snat'
output.append(f'ct status {nat_status}')
if 'protocol' in rule_conf and rule_conf['protocol'] != 'all':
diff --git a/python/vyos/kea.py b/python/vyos/kea.py
index 2328d0b00..89ae7ca81 100644
--- a/python/vyos/kea.py
+++ b/python/vyos/kea.py
@@ -56,6 +56,8 @@ kea6_options = {
'captive_portal': 'v6-captive-portal'
}
+kea_ctrl_socket = '/run/kea/dhcp{inet}-ctrl-socket'
+
def kea_parse_options(config):
options = []
@@ -294,7 +296,9 @@ def kea6_parse_subnet(subnet, config):
return out
-def _ctrl_socket_command(path, command, args=None):
+def _ctrl_socket_command(inet, command, args=None):
+ path = kea_ctrl_socket.format(inet=inet)
+
if not os.path.exists(path):
return None
@@ -319,19 +323,25 @@ def _ctrl_socket_command(path, command, args=None):
return json.loads(result.decode('utf-8'))
def kea_get_leases(inet):
- ctrl_socket = f'/run/kea/dhcp{inet}-ctrl-socket'
-
- leases = _ctrl_socket_command(ctrl_socket, f'lease{inet}-get-all')
+ leases = _ctrl_socket_command(inet, f'lease{inet}-get-all')
if not leases or 'result' not in leases or leases['result'] != 0:
return []
return leases['arguments']['leases']
-def kea_get_active_config(inet):
- ctrl_socket = f'/run/kea/dhcp{inet}-ctrl-socket'
+def kea_delete_lease(inet, ip_address):
+ args = {'ip-address': ip_address}
- config = _ctrl_socket_command(ctrl_socket, 'config-get')
+ result = _ctrl_socket_command(inet, f'lease{inet}-del', args)
+
+ if result and 'result' in result:
+ return result['result'] == 0
+
+ return False
+
+def kea_get_active_config(inet):
+ config = _ctrl_socket_command(inet, 'config-get')
if not config or 'result' not in config or config['result'] != 0:
return None
diff --git a/python/vyos/remote.py b/python/vyos/remote.py
index 129e65772..d87fd24f6 100644
--- a/python/vyos/remote.py
+++ b/python/vyos/remote.py
@@ -43,6 +43,7 @@ from vyos.utils.io import print_error
from vyos.utils.misc import begin
from vyos.utils.process import cmd, rc_cmd
from vyos.version import get_version
+from vyos.base import Warning
CHUNK_SIZE = 8192
@@ -54,13 +55,16 @@ class InteractivePolicy(MissingHostKeyPolicy):
def missing_host_key(self, client, hostname, key):
print_error(f"Host '{hostname}' not found in known hosts.")
print_error('Fingerprint: ' + key.get_fingerprint().hex())
- if sys.stdin.isatty() and ask_yes_no('Do you wish to continue?'):
- if client._host_keys_filename\
- and ask_yes_no('Do you wish to permanently add this host/key pair to known hosts?'):
- client._host_keys.add(hostname, key.get_name(), key)
- client.save_host_keys(client._host_keys_filename)
- else:
+ if not sys.stdin.isatty():
+ return
+ if not ask_yes_no('Do you wish to continue?'):
raise SSHException(f"Cannot connect to unknown host '{hostname}'.")
+ if client._host_keys_filename is None:
+ Warning('no \'known_hosts\' file; create to store keys permanently')
+ return
+ if ask_yes_no('Do you wish to permanently add this host/key pair to known_hosts file?'):
+ client._host_keys.add(hostname, key.get_name(), key)
+ client.save_host_keys(client._host_keys_filename)
class SourceAdapter(HTTPAdapter):
"""
diff --git a/python/vyos/tpm.py b/python/vyos/tpm.py
new file mode 100644
index 000000000..f120e10c4
--- /dev/null
+++ b/python/vyos/tpm.py
@@ -0,0 +1,98 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2024 VyOS maintainers and contributors
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License version 2 or later as
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+import os
+import tempfile
+
+from vyos.util import rc_cmd
+
+default_pcrs = ['0','2','4','7']
+tpm_handle = 0x81000000
+
+def init_tpm(clear=False):
+ """
+ Initialize TPM
+ """
+ code, output = rc_cmd('tpm2_startup' + (' -c' if clear else ''))
+ if code != 0:
+ raise Exception('init_tpm: Failed to initialize TPM')
+
+def clear_tpm_key():
+ """
+ Clear existing key on TPM
+ """
+ code, output = rc_cmd(f'tpm2_evictcontrol -C o -c {tpm_handle}')
+ if code != 0:
+ raise Exception('clear_tpm_key: Failed to clear TPM key')
+
+def read_tpm_key(index=0, pcrs=default_pcrs):
+ """
+ Read existing key on TPM
+ """
+ with tempfile.TemporaryDirectory() as tpm_dir:
+ pcr_str = ",".join(pcrs)
+
+ tpm_key_file = os.path.join(tpm_dir, 'tpm_key.key')
+ code, output = rc_cmd(f'tpm2_unseal -c {tpm_handle + index} -p pcr:sha256:{pcr_str} -o {tpm_key_file}')
+ if code != 0:
+ raise Exception('read_tpm_key: Failed to read key from TPM')
+
+ with open(tpm_key_file, 'rb') as f:
+ tpm_key = f.read()
+
+ return tpm_key
+
+def write_tpm_key(key, index=0, pcrs=default_pcrs):
+ """
+ Saves key to TPM
+ """
+ with tempfile.TemporaryDirectory() as tpm_dir:
+ pcr_str = ",".join(pcrs)
+
+ policy_file = os.path.join(tpm_dir, 'policy.digest')
+ code, output = rc_cmd(f'tpm2_createpolicy --policy-pcr -l sha256:{pcr_str} -L {policy_file}')
+ if code != 0:
+ raise Exception('write_tpm_key: Failed to create policy digest')
+
+ primary_context_file = os.path.join(tpm_dir, 'primary.ctx')
+ code, output = rc_cmd(f'tpm2_createprimary -C e -g sha256 -G rsa -c {primary_context_file}')
+ if code != 0:
+ raise Exception('write_tpm_key: Failed to create primary key')
+
+ key_file = os.path.join(tpm_dir, 'crypt.key')
+ with open(key_file, 'wb') as f:
+ f.write(key)
+
+ public_obj = os.path.join(tpm_dir, 'obj.pub')
+ private_obj = os.path.join(tpm_dir, 'obj.key')
+ code, output = rc_cmd(
+ f'tpm2_create -g sha256 \
+ -u {public_obj} -r {private_obj} \
+ -C {primary_context_file} -L {policy_file} -i {key_file}')
+
+ if code != 0:
+ raise Exception('write_tpm_key: Failed to create object')
+
+ load_context_file = os.path.join(tpm_dir, 'load.ctx')
+ code, output = rc_cmd(f'tpm2_load -C {primary_context_file} -u {public_obj} -r {private_obj} -c {load_context_file}')
+
+ if code != 0:
+ raise Exception('write_tpm_key: Failed to load object')
+
+ code, output = rc_cmd(f'tpm2_evictcontrol -c {load_context_file} -C o {tpm_handle + index}')
+
+ if code != 0:
+ raise Exception('write_tpm_key: Failed to write object to TPM')
diff --git a/smoketest/scripts/cli/test_firewall.py b/smoketest/scripts/cli/test_firewall.py
index be5960bbd..9e8473fa4 100755
--- a/smoketest/scripts/cli/test_firewall.py
+++ b/smoketest/scripts/cli/test_firewall.py
@@ -629,8 +629,8 @@ class TestFirewall(VyOSUnitTestSHIM.TestCase):
nftables_search = [
['ct state { established, related }', 'accept'],
['ct state invalid', 'reject'],
- ['ct state new', 'ct status == dnat', 'accept'],
- ['ct state { established, new }', 'ct status == snat', 'accept'],
+ ['ct state new', 'ct status dnat', 'accept'],
+ ['ct state { established, new }', 'ct status snat', 'accept'],
['ct state related', 'ct helper { "ftp", "pptp" }', 'accept'],
['drop', f'comment "{name} default-action drop"'],
['jump VYOS_STATE_POLICY'],
diff --git a/smoketest/scripts/cli/test_high-availability_vrrp.py b/smoketest/scripts/cli/test_high-availability_vrrp.py
index 1bb35e422..9ba06aef6 100755
--- a/smoketest/scripts/cli/test_high-availability_vrrp.py
+++ b/smoketest/scripts/cli/test_high-availability_vrrp.py
@@ -264,5 +264,58 @@ class TestVRRP(VyOSUnitTestSHIM.TestCase):
self.assertIn(f' {peer_address_1}', config)
self.assertIn(f' {peer_address_2}', config)
+ def test_check_health_script(self):
+ sync_group = 'VyOS'
+
+ for group in groups:
+ vlan_id = group.lstrip('VLAN')
+ vip = f'100.64.{vlan_id}.1/24'
+ group_base = base_path + ['vrrp', 'group', group]
+
+ self.cli_set(['interfaces', 'ethernet', vrrp_interface, 'vif', vlan_id, 'address', inc_ip(vip, 1) + '/' + vip.split('/')[-1]])
+
+ self.cli_set(group_base + ['interface', f'{vrrp_interface}.{vlan_id}'])
+ self.cli_set(group_base + ['address', vip])
+ self.cli_set(group_base + ['vrid', vlan_id])
+
+ self.cli_set(group_base + ['health-check', 'ping', '127.0.0.1'])
+
+ # commit changes
+ self.cli_commit()
+
+ for group in groups:
+ config = getConfig(f'vrrp_instance {group}')
+ self.assertIn(f'track_script', config)
+
+ self.cli_set(base_path + ['vrrp', 'sync-group', sync_group, 'member', groups[0]])
+
+ with self.assertRaises(ConfigSessionError):
+ self.cli_commit()
+
+ self.cli_delete(base_path + ['vrrp', 'group', groups[0], 'health-check'])
+ self.cli_commit()
+
+ for group in groups[1:]:
+ config = getConfig(f'vrrp_instance {group}')
+ self.assertIn(f'track_script', config)
+
+ config = getConfig(f'vrrp_instance {groups[0]}')
+ self.assertNotIn(f'track_script', config)
+
+ config = getConfig(f'vrrp_sync_group {sync_group}')
+ self.assertNotIn(f'track_script', config)
+
+ self.cli_set(base_path + ['vrrp', 'sync-group', sync_group, 'health-check', 'ping', '127.0.0.1'])
+
+ # commit changes
+ self.cli_commit()
+
+ config = getConfig(f'vrrp_instance {groups[0]}')
+ self.assertNotIn(f'track_script', config)
+
+ config = getConfig(f'vrrp_sync_group {sync_group}')
+ self.assertIn(f'track_script', config)
+
+
if __name__ == '__main__':
unittest.main(verbosity=2)
diff --git a/smoketest/scripts/cli/test_service_snmp.py b/smoketest/scripts/cli/test_service_snmp.py
index 52a72ec4f..b3daa90d0 100755
--- a/smoketest/scripts/cli/test_service_snmp.py
+++ b/smoketest/scripts/cli/test_service_snmp.py
@@ -229,5 +229,22 @@ class TestSNMPService(VyOSUnitTestSHIM.TestCase):
tmp = call(f'snmpwalk -v 3 -u {snmpv3_user} -a MD5 -A {snmpv3_auth_pw} -x DES -X {snmpv3_priv_pw} -l authPriv 127.0.0.1', stdout=DEVNULL)
self.assertEqual(tmp, 0)
+ def test_snmpv3_view_exclude(self):
+ snmpv3_view_oid_exclude = ['1.3.6.1.2.1.4.21', '1.3.6.1.2.1.4.24']
+
+ self.cli_set(base_path + ['v3', 'group', snmpv3_group, 'view', snmpv3_view])
+ self.cli_set(base_path + ['v3', 'view', snmpv3_view, 'oid', snmpv3_view_oid])
+
+ for excluded in snmpv3_view_oid_exclude:
+ self.cli_set(base_path + ['v3', 'view', snmpv3_view, 'oid', snmpv3_view_oid, 'exclude', excluded])
+
+ self.cli_commit()
+
+ tmp = read_file(SNMPD_CONF)
+ # views
+ self.assertIn(f'view {snmpv3_view} included .{snmpv3_view_oid}', tmp)
+ for excluded in snmpv3_view_oid_exclude:
+ self.assertIn(f'view {snmpv3_view} excluded .{excluded}', tmp)
+
if __name__ == '__main__':
unittest.main(verbosity=2)
diff --git a/src/conf_mode/firewall.py b/src/conf_mode/firewall.py
index acb7dfa41..3c27655b0 100755
--- a/src/conf_mode/firewall.py
+++ b/src/conf_mode/firewall.py
@@ -282,6 +282,15 @@ def verify_rule(firewall, rule_conf, ipv6):
if direction in rule_conf:
if 'name' in rule_conf[direction] and 'group' in rule_conf[direction]:
raise ConfigError(f'Cannot specify both interface group and interface name for {direction}')
+ if 'group' in rule_conf[direction]:
+ group_name = rule_conf[direction]['group']
+ if group_name[0] == '!':
+ group_name = group_name[1:]
+ group_obj = dict_search_args(firewall, 'group', 'interface_group', group_name)
+ if group_obj is None:
+ raise ConfigError(f'Invalid interface group "{group_name}" on firewall rule')
+ if not group_obj:
+ Warning(f'interface-group "{group_name}" has no members!')
def verify_nested_group(group_name, group, groups, seen):
if 'include' not in group:
diff --git a/src/conf_mode/high-availability.py b/src/conf_mode/high-availability.py
index 59d49ea67..c726db8b2 100755
--- a/src/conf_mode/high-availability.py
+++ b/src/conf_mode/high-availability.py
@@ -86,16 +86,7 @@ def verify(ha):
raise ConfigError(f'Authentication requires both type and passwortd to be set in VRRP group "{group}"')
if 'health_check' in group_config:
- health_check_types = ["script", "ping"]
- from vyos.utils.dict import check_mutually_exclusive_options
- try:
- check_mutually_exclusive_options(group_config["health_check"], health_check_types, required=True)
- except ValueError:
- Warning(f'Health check configuration for VRRP group "{group}" will remain unused ' \
- f'until it has one of the following options: {health_check_types}')
- # XXX: health check has default options so we need to remove it
- # to avoid generating useless config statements in keepalived.conf
- del group_config["health_check"]
+ _validate_health_check(group, group_config)
# Keepalived doesn't allow mixing IPv4 and IPv6 in one group, so we mirror that restriction
# We also need to make sure VRID is not used twice on the same interface with the
@@ -146,11 +137,22 @@ def verify(ha):
# Check sync groups
if 'vrrp' in ha and 'sync_group' in ha['vrrp']:
for sync_group, sync_config in ha['vrrp']['sync_group'].items():
+ if 'health_check' in sync_config:
+ _validate_health_check(sync_group, sync_config)
+
if 'member' in sync_config:
for member in sync_config['member']:
if member not in ha['vrrp']['group']:
raise ConfigError(f'VRRP sync-group "{sync_group}" refers to VRRP group "{member}", '\
'but it does not exist!')
+ else:
+ ha['vrrp']['group'][member]['_is_sync_group_member'] = True
+ if ha['vrrp']['group'][member].get('health_check') is not None:
+ raise ConfigError(
+ f'Health check configuration for VRRP group "{member}" will remain unused '
+ f'while it has member of sync group "{sync_group}" '
+ f'Only sync group health check will be used'
+ )
# Virtual-server
if 'virtual_server' in ha:
@@ -172,6 +174,21 @@ def verify(ha):
raise ConfigError(f'Port is required but not set for virtual-server "{vs}" real-server "{rs}"')
+def _validate_health_check(group, group_config):
+ health_check_types = ["script", "ping"]
+ from vyos.utils.dict import check_mutually_exclusive_options
+ try:
+ check_mutually_exclusive_options(group_config["health_check"],
+ health_check_types, required=True)
+ except ValueError:
+ Warning(
+ f'Health check configuration for VRRP group "{group}" will remain unused ' \
+ f'until it has one of the following options: {health_check_types}')
+ # XXX: health check has default options so we need to remove it
+ # to avoid generating useless config statements in keepalived.conf
+ del group_config["health_check"]
+
+
def generate(ha):
if not ha or 'disable' in ha:
if os.path.isfile(systemd_override):
diff --git a/src/conf_mode/nat.py b/src/conf_mode/nat.py
index 26822b755..b3f38c04a 100755
--- a/src/conf_mode/nat.py
+++ b/src/conf_mode/nat.py
@@ -153,6 +153,15 @@ def verify(nat):
elif 'name' in config['outbound_interface']:
if config['outbound_interface']['name'] not in 'any' and config['outbound_interface']['name'] not in interfaces():
Warning(f'NAT interface "{config["outbound_interface"]["name"]}" for source NAT rule "{rule}" does not exist!')
+ else:
+ group_name = config['outbound_interface']['group']
+ if group_name[0] == '!':
+ group_name = group_name[1:]
+ group_obj = dict_search_args(nat['firewall_group'], 'interface_group', group_name)
+ if group_obj is None:
+ raise ConfigError(f'Invalid interface group "{group_name}" on source nat rule')
+ if not group_obj:
+ Warning(f'interface-group "{group_name}" has no members!')
if not dict_search('translation.address', config) and not dict_search('translation.port', config):
if 'exclude' not in config and 'backend' not in config['load_balance']:
@@ -177,6 +186,15 @@ def verify(nat):
elif 'name' in config['inbound_interface']:
if config['inbound_interface']['name'] not in 'any' and config['inbound_interface']['name'] not in interfaces():
Warning(f'NAT interface "{config["inbound_interface"]["name"]}" for destination NAT rule "{rule}" does not exist!')
+ else:
+ group_name = config['inbound_interface']['group']
+ if group_name[0] == '!':
+ group_name = group_name[1:]
+ group_obj = dict_search_args(nat['firewall_group'], 'interface_group', group_name)
+ if group_obj is None:
+ raise ConfigError(f'Invalid interface group "{group_name}" on destination nat rule')
+ if not group_obj:
+ Warning(f'interface-group "{group_name}" has no members!')
if not dict_search('translation.address', config) and not dict_search('translation.port', config) and 'redirect' not in config['translation']:
if 'exclude' not in config and 'backend' not in config['load_balance']:
diff --git a/src/helpers/vyos-config-encrypt.py b/src/helpers/vyos-config-encrypt.py
new file mode 100755
index 000000000..8f7359767
--- /dev/null
+++ b/src/helpers/vyos-config-encrypt.py
@@ -0,0 +1,276 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2024 VyOS maintainers and contributors
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License version 2 or later as
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+import os
+import re
+import shutil
+import sys
+
+from argparse import ArgumentParser
+from cryptography.fernet import Fernet
+from tempfile import NamedTemporaryFile
+from tempfile import TemporaryDirectory
+
+from vyos.tpm import clear_tpm_key
+from vyos.tpm import init_tpm
+from vyos.tpm import read_tpm_key
+from vyos.tpm import write_tpm_key
+from vyos.util import ask_input
+from vyos.util import ask_yes_no
+from vyos.util import cmd
+
+persistpath_cmd = '/opt/vyatta/sbin/vyos-persistpath'
+mount_paths = ['/config', '/opt/vyatta/etc/config']
+dm_device = '/dev/mapper/vyos_config'
+
+def is_opened():
+ return os.path.exists(dm_device)
+
+def get_current_image():
+ with open('/proc/cmdline', 'r') as f:
+ args = f.read().split(" ")
+ for arg in args:
+ if 'vyos-union' in arg:
+ k, v = arg.split("=")
+ path_split = v.split("/")
+ return path_split[-1]
+ return None
+
+def load_config(key):
+ if not key:
+ return
+
+ persist_path = cmd(persistpath_cmd).strip()
+ image_name = get_current_image()
+ image_path = os.path.join(persist_path, 'luks', image_name)
+
+ if not os.path.exists(image_path):
+ raise Exception("Encrypted config volume doesn't exist")
+
+ if is_opened():
+ print('Encrypted config volume is already mounted')
+ return
+
+ with NamedTemporaryFile(dir='/dev/shm', delete=False) as f:
+ f.write(key)
+ key_file = f.name
+
+ cmd(f'cryptsetup -q open {image_path} vyos_config --key-file={key_file}')
+
+ for path in mount_paths:
+ cmd(f'mount /dev/mapper/vyos_config {path}')
+ cmd(f'chgrp -R vyattacfg {path}')
+
+ os.unlink(key_file)
+
+ return True
+
+def encrypt_config(key, recovery_key):
+ if is_opened():
+ raise Exception('An encrypted config volume is already mapped')
+
+ # Clear and write key to TPM
+ try:
+ clear_tpm_key()
+ except:
+ pass
+ write_tpm_key(key)
+
+ persist_path = cmd(persistpath_cmd).strip()
+ size = ask_input('Enter size of encrypted config partition (MB): ', numeric_only=True, default=512)
+
+ luks_folder = os.path.join(persist_path, 'luks')
+
+ if not os.path.isdir(luks_folder):
+ os.mkdir(luks_folder)
+
+ image_name = get_current_image()
+ image_path = os.path.join(luks_folder, image_name)
+
+ # Create file for encrypted config
+ cmd(f'fallocate -l {size}M {image_path}')
+
+ # Write TPM key for slot #1
+ with NamedTemporaryFile(dir='/dev/shm', delete=False) as f:
+ f.write(key)
+ key_file = f.name
+
+ # Format and add main key to volume
+ cmd(f'cryptsetup -q luksFormat {image_path} {key_file}')
+
+ if recovery_key:
+ # Write recovery key for slot 2
+ with NamedTemporaryFile(dir='/dev/shm', delete=False) as f:
+ f.write(recovery_key)
+ recovery_key_file = f.name
+
+ cmd(f'cryptsetup -q luksAddKey {image_path} {recovery_key_file} --key-file={key_file}')
+
+ # Open encrypted volume and format with ext4
+ cmd(f'cryptsetup -q open {image_path} vyos_config --key-file={key_file}')
+ cmd('mkfs.ext4 /dev/mapper/vyos_config')
+
+ with TemporaryDirectory() as d:
+ cmd(f'mount /dev/mapper/vyos_config {d}')
+
+ # Move /config to encrypted volume
+ shutil.copytree('/config', d, copy_function=shutil.move, dirs_exist_ok=True)
+
+ cmd(f'umount {d}')
+
+ os.unlink(key_file)
+
+ if recovery_key:
+ os.unlink(recovery_key_file)
+
+ for path in mount_paths:
+ cmd(f'mount /dev/mapper/vyos_config {path}')
+ cmd(f'chgrp vyattacfg {path}')
+
+ return True
+
+def decrypt_config(key):
+ if not key:
+ return
+
+ persist_path = cmd(persistpath_cmd).strip()
+ image_name = get_current_image()
+ image_path = os.path.join(persist_path, 'luks', image_name)
+
+ if not os.path.exists(image_path):
+ raise Exception("Encrypted config volume doesn't exist")
+
+ key_file = None
+
+ if not is_opened():
+ with NamedTemporaryFile(dir='/dev/shm', delete=False) as f:
+ f.write(key)
+ key_file = f.name
+
+ cmd(f'cryptsetup -q open {image_path} vyos_config --key-file={key_file}')
+
+ # unmount encrypted volume mount points
+ for path in mount_paths:
+ if os.path.ismount(path):
+ cmd(f'umount {path}')
+
+ # If /config is populated, move to /config.old
+ if len(os.listdir('/config')) > 0:
+ print('Moving existing /config folder to /config.old')
+ shutil.move('/config', '/config.old')
+
+ # Temporarily mount encrypted volume and migrate files to /config on rootfs
+ with TemporaryDirectory() as d:
+ cmd(f'mount /dev/mapper/vyos_config {d}')
+
+ # Move encrypted volume to /config
+ shutil.copytree(d, '/config', copy_function=shutil.move, dirs_exist_ok=True)
+ cmd(f'chgrp -R vyattacfg /config')
+
+ cmd(f'umount {d}')
+
+ # Close encrypted volume
+ cmd('cryptsetup -q close vyos_config')
+
+ # Remove encrypted volume image file and key
+ if key_file:
+ os.unlink(key_file)
+ os.unlink(image_path)
+
+ try:
+ clear_tpm_key()
+ except:
+ pass
+
+ return True
+
+if __name__ == '__main__':
+ if len(sys.argv) < 2:
+ print("Must specify action.")
+ sys.exit(1)
+
+ parser = ArgumentParser(description='Config encryption')
+ parser.add_argument('--disable', help='Disable encryption', action="store_true")
+ parser.add_argument('--enable', help='Enable encryption', action="store_true")
+ parser.add_argument('--load', help='Load encrypted config volume', action="store_true")
+ args = parser.parse_args()
+
+ tpm_exists = os.path.exists('/sys/class/tpm/tpm0')
+
+ key = None
+ recovery_key = None
+ need_recovery = False
+
+ question_key_str = 'recovery key' if tpm_exists else 'key'
+
+ if tpm_exists:
+ if args.enable:
+ key = Fernet.generate_key()
+ elif args.disable or args.load:
+ try:
+ key = read_tpm_key()
+ need_recovery = False
+ except:
+ print('Failed to read key from TPM, recovery key required')
+ need_recovery = True
+ else:
+ need_recovery = True
+
+ if args.enable and not tpm_exists:
+ print('WARNING: VyOS will boot into a default config when encrypted without a TPM')
+ print('You will need to manually login with default credentials and use "encryption load"')
+ print('to mount the encrypted volume and use "load /config/config.boot"')
+
+ if not ask_yes_no('Are you sure you want to proceed?'):
+ sys.exit(0)
+
+ if need_recovery or (args.enable and not ask_yes_no(f'Automatically generate a {question_key_str}?', default=True)):
+ while True:
+ recovery_key = ask_input(f'Enter {question_key_str}:', default=None).encode()
+
+ if len(recovery_key) >= 32:
+ break
+
+ print('Invalid key - must be at least 32 characters, try again.')
+ else:
+ recovery_key = Fernet.generate_key()
+
+ try:
+ if args.disable:
+ decrypt_config(key or recovery_key)
+
+ print('Encrypted config volume has been disabled')
+ print('Contents have been migrated to /config on rootfs')
+ elif args.load:
+ load_config(key or recovery_key)
+
+ print('Encrypted config volume has been mounted')
+ print('Use "load /config/config.boot" to load configuration')
+ elif args.enable and tpm_exists:
+ encrypt_config(key, recovery_key)
+
+ print('Encrypted config volume has been enabled with TPM')
+ print('Backup the recovery key in a safe place!')
+ print('Recovery key: ' + recovery_key.decode())
+ elif args.enable:
+ encrypt_config(recovery_key)
+
+ print('Encrypted config volume has been enabled without TPM')
+ print('Backup the key in a safe place!')
+ print('Key: ' + recovery_key.decode())
+ except Exception as e:
+ word = 'decrypt' if args.disable or args.load else 'encrypt'
+ print(f'Failed to {word} config: {e}')
diff --git a/src/init/vyos-router b/src/init/vyos-router
index eac3e7e47..adf892371 100755
--- a/src/init/vyos-router
+++ b/src/init/vyos-router
@@ -64,6 +64,69 @@ disabled () {
grep -q -w no-vyos-$1 /proc/cmdline
}
+# Load encrypted config volume
+mount_encrypted_config() {
+ persist_path=$(/opt/vyatta/sbin/vyos-persistpath)
+ if [ $? == 0 ]; then
+ if [ -e $persist_path/boot ]; then
+ image_name=$(cat /proc/cmdline | sed -e s+^.*vyos-union=/boot/++ | sed -e 's/ .*$//')
+
+ if [ -z "$image_name" ]; then
+ return
+ fi
+
+ if [ ! -f $persist_path/luks/$image_name ]; then
+ return
+ fi
+
+ vyos_tpm_key=$(python3 -c 'from vyos.tpm import read_tpm_key; print(read_tpm_key().decode())' 2>/dev/null)
+
+ if [ $? -ne 0 ]; then
+ echo "ERROR: Failed to fetch encryption key from TPM. Encrypted config volume has not been mounted"
+ echo "Use 'encryption load' to load volume with recovery key"
+ echo "or 'encryption disable' to decrypt volume with recovery key"
+ return
+ fi
+
+ echo $vyos_tpm_key | tr -d '\r\n' | cryptsetup open $persist_path/luks/$image_name vyos_config --key-file=-
+
+ if [ $? -ne 0 ]; then
+ echo "ERROR: Failed to decrypt config volume. Encrypted config volume has not been mounted"
+ echo "Use 'encryption load' to load volume with recovery key"
+ echo "or 'encryption disable' to decrypt volume with recovery key"
+ return
+ fi
+
+ mount /dev/mapper/vyos_config /config
+ mount /dev/mapper/vyos_config $vyatta_sysconfdir/config
+
+ echo "Mounted encrypted config volume"
+ fi
+ fi
+}
+
+unmount_encrypted_config() {
+ persist_path=$(/opt/vyatta/sbin/vyos-persistpath)
+ if [ $? == 0 ]; then
+ if [ -e $persist_path/boot ]; then
+ image_name=$(cat /proc/cmdline | sed -e s+^.*vyos-union=/boot/++ | sed -e 's/ .*$//')
+
+ if [ -z "$image_name" ]; then
+ return
+ fi
+
+ if [ ! -f $persist_path/luks/$image_name ]; then
+ return
+ fi
+
+ umount /config
+ umount $vyatta_sysconfdir/config
+
+ cryptsetup close vyos_config
+ fi
+ fi
+}
+
# if necessary, provide initial config
init_bootfile () {
if [ ! -r $BOOTFILE ] ; then
@@ -218,8 +281,8 @@ cleanup_post_commit_hooks () {
# note that this approach only supports hooks that are "configured",
# i.e., it does not support hooks that need to always be present.
cpostdir=$(cli-shell-api getPostCommitHookDir)
- # exclude commits hooks from vyatta-cfg
- excluded="10vyatta-log-commit.pl 99vyos-user-postcommit-hooks"
+ # exclude commit hooks that need to always be present
+ excluded="00vyos-sync 10vyatta-log-commit.pl 99vyos-user-postcommit-hooks"
if [ -d "$cpostdir" ]; then
for f in $cpostdir/*; do
if [[ ! $excluded =~ $(basename $f) ]]; then
@@ -402,6 +465,8 @@ start ()
&& chgrp ${GROUP} ${vyatta_configdir}
log_action_end_msg $?
+ mount_encrypted_config
+
# T5239: early read of system hostname as this value is read-only once during
# FRR initialisation
tmp=$(${vyos_libexec_dir}/read-saved-value.py --path "system host-name")
@@ -470,6 +535,8 @@ stop()
log_action_end_msg $?
systemctl stop frr.service
+
+ unmount_encrypted_config
}
case "$action" in
diff --git a/src/op_mode/clear_dhcp_lease.py b/src/op_mode/clear_dhcp_lease.py
deleted file mode 100755
index 7d4b47104..000000000
--- a/src/op_mode/clear_dhcp_lease.py
+++ /dev/null
@@ -1,88 +0,0 @@
-#!/usr/bin/env python3
-#
-# Copyright 2023 VyOS maintainers and contributors <maintainers@vyos.io>
-#
-# This library is free software; you can redistribute it and/or
-# modify it under the terms of the GNU Lesser General Public
-# License as published by the Free Software Foundation; either
-# version 2.1 of the License, or (at your option) any later version.
-#
-# This library is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
-# Lesser General Public License for more details.
-#
-# You should have received a copy of the GNU Lesser General Public
-# License along with this library. If not, see <http://www.gnu.org/licenses/>.
-
-import argparse
-import re
-
-from vyos.configquery import ConfigTreeQuery
-from vyos.kea import kea_parse_leases
-from vyos.utils.io import ask_yes_no
-from vyos.utils.process import call
-from vyos.utils.commit import commit_in_progress
-
-# TODO: Update to use Kea control socket command "lease4-del"
-
-config = ConfigTreeQuery()
-base = ['service', 'dhcp-server']
-lease_file = '/config/dhcp/dhcp4-leases.csv'
-
-
-def del_lease_ip(address):
- """
- Read lease_file and write data to this file
- without specific section "lease ip"
- Delete section "lease x.x.x.x { x;x;x; }"
- """
- with open(lease_file, encoding='utf-8') as f:
- data = f.read().rstrip()
- pattern = rf"^{address},[^\n]+\n"
- # Delete lease for ip block
- data = re.sub(pattern, '', data)
-
- # Write new data to original lease_file
- with open(lease_file, 'w', encoding='utf-8') as f:
- f.write(data)
-
-def is_ip_in_leases(address):
- """
- Return True if address found in the lease file
- """
- leases = kea_parse_leases(lease_file)
- for lease in leases:
- if address == lease['address']:
- return True
- print(f'Address "{address}" not found in "{lease_file}"')
- return False
-
-if not config.exists(base):
- print('DHCP-server not configured!')
- exit(0)
-
-if config.exists(base + ['failover']):
- print('Lease cannot be reset in failover mode!')
- exit(0)
-
-
-if __name__ == '__main__':
- parser = argparse.ArgumentParser()
- parser.add_argument('--ip', help='IPv4 address', action='store', required=True)
-
- args = parser.parse_args()
- address = args.ip
-
- if not is_ip_in_leases(address):
- exit(1)
-
- if commit_in_progress():
- print('Cannot clear DHCP lease while a commit is in progress')
- exit(1)
-
- if not ask_yes_no(f'This will restart DHCP server.\nContinue?'):
- exit(1)
- else:
- del_lease_ip(address)
- call('systemctl restart kea-dhcp4-server.service')
diff --git a/src/op_mode/dhcp.py b/src/op_mode/dhcp.py
index 1d9ad0e76..d27e1baf7 100755
--- a/src/op_mode/dhcp.py
+++ b/src/op_mode/dhcp.py
@@ -31,6 +31,7 @@ from vyos.configquery import ConfigTreeQuery
from vyos.kea import kea_get_active_config
from vyos.kea import kea_get_leases
from vyos.kea import kea_get_pool_from_subnet_id
+from vyos.kea import kea_delete_lease
from vyos.utils.process import is_systemd_service_running
time_string = "%a %b %d %H:%M:%S %Z %Y"
@@ -360,6 +361,28 @@ def show_server_static_mappings(raw: bool, family: ArgFamily, pool: typing.Optio
else:
return _get_formatted_server_static_mappings(static_mappings, family=family)
+def _lease_valid(inet, address):
+ leases = kea_get_leases(inet)
+ for lease in leases:
+ if address == lease['ip-address']:
+ return True
+ return False
+
+@_verify
+def clear_dhcp_server_lease(family: ArgFamily, address: str):
+ v = 'v6' if family == 'inet6' else ''
+ inet = '6' if family == 'inet6' else '4'
+
+ if not _lease_valid(inet, address):
+ print(f'Lease not found on DHCP{v} server')
+ return None
+
+ if not kea_delete_lease(inet, address):
+ print(f'Failed to clear lease for "{address}"')
+ return None
+
+ print(f'Lease "{address}" has been cleared')
+
def _get_raw_client_leases(family='inet', interface=None):
from time import mktime
from datetime import datetime
diff --git a/src/op_mode/image_installer.py b/src/op_mode/image_installer.py
index d677c2cf8..85ebd19ba 100755
--- a/src/op_mode/image_installer.py
+++ b/src/op_mode/image_installer.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright 2023 VyOS maintainers and contributors <maintainers@vyos.io>
+# Copyright 2023-2024 VyOS maintainers and contributors <maintainers@vyos.io>
#
# This file is part of VyOS.
#
@@ -65,6 +65,8 @@ MSG_INPUT_ROOT_SIZE_SET: str = 'Please specify the size (in GB) of the root part
MSG_INPUT_CONSOLE_TYPE: str = 'What console should be used by default? (K: KVM, S: Serial, U: USB-Serial)?'
MSG_INPUT_COPY_DATA: str = 'Would you like to copy data to the new image?'
MSG_INPUT_CHOOSE_COPY_DATA: str = 'From which image would you like to save config information?'
+MSG_INPUT_COPY_ENC_DATA: str = 'Would you like to copy the encrypted config to the new image?'
+MSG_INPUT_CHOOSE_COPY_ENC_DATA: str = 'From which image would you like to copy the encrypted config?'
MSG_WARN_ISO_SIGN_INVALID: str = 'Signature is not valid. Do you want to continue with installation?'
MSG_WARN_ISO_SIGN_UNAVAL: str = 'Signature is not available. Do you want to continue with installation?'
MSG_WARN_ROOT_SIZE_TOOBIG: str = 'The size is too big. Try again.'
@@ -212,14 +214,17 @@ def search_previous_installation(disks: list[str]) -> None:
disks (list[str]): a list of available disks
"""
mnt_config = '/mnt/config'
+ mnt_encrypted_config = '/mnt/encrypted_config'
mnt_ssh = '/mnt/ssh'
mnt_tmp = '/mnt/tmp'
rmtree(Path(mnt_config), ignore_errors=True)
rmtree(Path(mnt_ssh), ignore_errors=True)
Path(mnt_tmp).mkdir(exist_ok=True)
+ Path(mnt_encrypted_config).unlink(missing_ok=True)
print('Searching for data from previous installations')
image_data = []
+ encrypted_configs = []
for disk_name in disks:
for partition in disk.partition_list(disk_name):
if disk.partition_mount(partition, mnt_tmp):
@@ -227,32 +232,61 @@ def search_previous_installation(disks: list[str]) -> None:
for path in Path(mnt_tmp + '/boot').iterdir():
if path.joinpath('rw/config/.vyatta_config').exists():
image_data.append((path.name, partition))
+ if Path(mnt_tmp + '/luks').exists():
+ for path in Path(mnt_tmp + '/luks').iterdir():
+ encrypted_configs.append((path.name, partition))
disk.partition_umount(partition)
- if len(image_data) == 1:
- image_name, image_drive = image_data[0]
- print('Found data from previous installation:')
- print(f'\t{image_name} on {image_drive}')
- if not ask_yes_no(MSG_INPUT_COPY_DATA, default=True):
- return
-
- elif len(image_data) > 1:
- print('Found data from previous installations')
- if not ask_yes_no(MSG_INPUT_COPY_DATA, default=True):
- return
-
- image_name, image_drive = select_entry(image_data,
- 'Available versions:',
- MSG_INPUT_CHOOSE_COPY_DATA,
- search_format_selection)
+ image_name = None
+ image_drive = None
+ encrypted = False
+
+ if len(image_data) > 0:
+ if len(image_data) == 1:
+ print('Found data from previous installation:')
+ print(f'\t{" on ".join(image_data[0])}')
+ if ask_yes_no(MSG_INPUT_COPY_DATA, default=True):
+ image_name, image_drive = image_data[0]
+
+ elif len(image_data) > 1:
+ print('Found data from previous installations')
+ if ask_yes_no(MSG_INPUT_COPY_DATA, default=True):
+ image_name, image_drive = select_entry(image_data,
+ 'Available versions:',
+ MSG_INPUT_CHOOSE_COPY_DATA,
+ search_format_selection)
+ elif len(encrypted_configs) > 0:
+ if len(encrypted_configs) == 1:
+ print('Found encrypted config from previous installation:')
+ print(f'\t{" on ".join(encrypted_configs[0])}')
+ if ask_yes_no(MSG_INPUT_COPY_ENC_DATA, default=True):
+ image_name, image_drive = encrypted_configs[0]
+ encrypted = True
+
+ elif len(encrypted_configs) > 1:
+ print('Found encrypted configs from previous installations')
+ if ask_yes_no(MSG_INPUT_COPY_ENC_DATA, default=True):
+ image_name, image_drive = select_entry(encrypted_configs,
+ 'Available versions:',
+ MSG_INPUT_CHOOSE_COPY_ENC_DATA,
+ search_format_selection)
+ encrypted = True
+
else:
print('No previous installation found')
return
+ if not image_name:
+ return
+
disk.partition_mount(image_drive, mnt_tmp)
- copytree(f'{mnt_tmp}/boot/{image_name}/rw/config', mnt_config)
+ if not encrypted:
+ copytree(f'{mnt_tmp}/boot/{image_name}/rw/config', mnt_config)
+ else:
+ copy(f'{mnt_tmp}/luks/{image_name}', mnt_encrypted_config)
+
Path(mnt_ssh).mkdir()
host_keys: list[str] = glob(f'{mnt_tmp}/boot/{image_name}/rw/etc/ssh/ssh_host*')
for host_key in host_keys:
@@ -279,6 +313,12 @@ def copy_previous_installation_data(target_dir: str) -> None:
dirs_exist_ok=True)
+def copy_previous_encrypted_config(target_dir: str, image_name: str) -> None:
+ if Path('/mnt/encrypted_config').exists():
+ Path(target_dir).mkdir(exist_ok=True)
+ copy('/mnt/encrypted_config', Path(target_dir).joinpath(image_name))
+
+
def ask_single_disk(disks_available: dict[str, int]) -> str:
"""Ask user to select a disk for installation
@@ -712,6 +752,9 @@ def install_image() -> None:
# owner restored on copy of config data by chmod_2775, above
copy_previous_installation_data(f'{DIR_DST_ROOT}/boot/{image_name}/rw')
+ # copy saved encrypted config volume
+ copy_previous_encrypted_config(f'{DIR_DST_ROOT}/luks', image_name)
+
if is_raid_install(install_target):
write_dir: str = f'{DIR_DST_ROOT}/boot/{image_name}/rw'
raid.update_default(write_dir)
diff --git a/src/op_mode/image_manager.py b/src/op_mode/image_manager.py
index e64a85b95..1510a667c 100755
--- a/src/op_mode/image_manager.py
+++ b/src/op_mode/image_manager.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright 2023 VyOS maintainers and contributors <maintainers@vyos.io>
+# Copyright 2023-2024 VyOS maintainers and contributors <maintainers@vyos.io>
#
# This file is part of VyOS.
#
@@ -95,6 +95,15 @@ def delete_image(image_name: Optional[str] = None,
except Exception as err:
exit(f'Unable to remove the image "{image_name}": {err}')
+ # remove LUKS volume if it exists
+ luks_path: Path = Path(f'{persistence_storage}/luks/{image_name}')
+ if luks_path.is_file():
+ try:
+ luks_path.unlink()
+ print(f'The encrypted config for "{image_name}" was successfully deleted')
+ except Exception as err:
+ exit(f'Unable to remove the encrypted config for "{image_name}": {err}')
+
@compat.grub_cfg_update
def set_image(image_name: Optional[str] = None,
@@ -174,6 +183,16 @@ def rename_image(name_old: str, name_new: str) -> None:
except Exception as err:
exit(f'Unable to rename image "{name_old}" to "{name_new}": {err}')
+ # rename LUKS volume if it exists
+ old_luks_path: Path = Path(f'{persistence_storage}/luks/{name_old}')
+ if old_luks_path.is_file():
+ try:
+ new_luks_path: Path = Path(f'{persistence_storage}/luks/{name_new}')
+ old_luks_path.rename(new_luks_path)
+ print(f'The encrypted config for "{name_old}" was successfully renamed to "{name_new}"')
+ except Exception as err:
+ exit(f'Unable to rename the encrypted config for "{name_old}" to "{name_new}": {err}')
+
def list_images() -> None:
"""Print list of available images for CLI hints"""
diff --git a/src/services/vyos-http-api-server b/src/services/vyos-http-api-server
index 40d442e30..a7b14a1a3 100755
--- a/src/services/vyos-http-api-server
+++ b/src/services/vyos-http-api-server
@@ -459,7 +459,6 @@ def _configure_op(data: Union[ConfigureModel, ConfigureListModel,
request: Request, background_tasks: BackgroundTasks):
session = app.state.vyos_session
env = session.get_session_env()
- config = Config(session_env=env)
endpoint = request.url.path
@@ -474,6 +473,8 @@ def _configure_op(data: Union[ConfigureModel, ConfigureListModel,
# so the lock is really global
lock.acquire()
+ config = Config(session_env=env)
+
status = 200
msg = None
error_msg = None