summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.github/PULL_REQUEST_TEMPLATE.md2
-rw-r--r--Makefile7
-rw-r--r--data/configd-include.json3
-rw-r--r--data/templates/accel-ppp/sstp.config.tmpl6
-rw-r--r--data/templates/ethernet/wpa_supplicant.conf.tmpl8
-rw-r--r--data/templates/firewall/nftables-vrf-zones.tmpl17
-rw-r--r--data/templates/frr/bgpd.frr.tmpl3
-rw-r--r--data/templates/frr/isisd.frr.tmpl (renamed from data/templates/frr/isis.frr.tmpl)33
-rw-r--r--data/templates/https/nginx.default.tmpl7
-rw-r--r--data/templates/ipsec/charon/dhcp.conf.tmpl11
-rw-r--r--data/templates/ipsec/charon/eap-radius.conf.tmpl115
-rw-r--r--data/templates/ipsec/interfaces_use.conf.tmpl5
-rw-r--r--data/templates/ipsec/ios_profile.tmpl104
-rw-r--r--data/templates/ipsec/ipsec.conf.tmpl6
-rw-r--r--data/templates/ipsec/ipsec.secrets.tmpl10
-rw-r--r--data/templates/ipsec/remote-access.tmpl28
-rw-r--r--data/templates/ipsec/swanctl.conf.tmpl46
-rw-r--r--data/templates/ipsec/swanctl/l2tp.tmpl30
-rw-r--r--data/templates/ipsec/swanctl/peer.tmpl4
-rw-r--r--data/templates/ipsec/swanctl/remote_access.tmpl9
-rw-r--r--data/templates/ipsec/windows_profile.tmpl4
-rw-r--r--data/templates/ocserv/ocserv_config.tmpl14
-rw-r--r--data/templates/openvpn/server.conf.tmpl83
-rw-r--r--data/templates/router-advert/radvd.conf.tmpl80
-rw-r--r--data/templates/snmp/override.conf.tmpl3
-rw-r--r--debian/control2
-rw-r--r--debian/vyos-1x.install1
-rw-r--r--debian/vyos-1x.postinst7
-rw-r--r--interface-definitions/containers.xml.in1
-rw-r--r--interface-definitions/https.xml.in19
-rw-r--r--interface-definitions/include/accel-ppp/radius-additions.xml.i6
-rw-r--r--interface-definitions/include/auth-local-users.xml.i22
-rw-r--r--interface-definitions/include/bgp/afi-l2vpn-common.xml.i13
-rw-r--r--interface-definitions/include/bgp/protocol-common-config.xml.i32
-rw-r--r--interface-definitions/include/bgp/route-distinguisher.xml.i14
-rw-r--r--interface-definitions/include/dhcp-interface.xml.i15
-rw-r--r--interface-definitions/include/interface/interface-eapol.xml.i5
-rw-r--r--interface-definitions/include/ipsec/authentication-pre-shared-secret.xml.i11
-rw-r--r--interface-definitions/include/isis/protocol-common-config.xml.i57
-rw-r--r--interface-definitions/include/isis/redistribute-ipv6.xml.i42
-rw-r--r--interface-definitions/include/radius-nas-identifier.xml.i7
-rw-r--r--interface-definitions/include/static/static-route.xml.i16
-rw-r--r--interface-definitions/interfaces-openvpn.xml.in73
-rw-r--r--interface-definitions/interfaces-tunnel.xml.in25
-rw-r--r--interface-definitions/interfaces-vxlan.xml.in1
-rw-r--r--interface-definitions/interfaces-wireguard.xml.in12
-rw-r--r--interface-definitions/snmp.xml.in20
-rw-r--r--interface-definitions/vpn_ipsec.xml.in303
-rw-r--r--interface-definitions/vpn_l2tp.xml.in63
-rw-r--r--interface-definitions/vpn_openconnect.xml.in30
-rw-r--r--interface-definitions/vpn_sstp.xml.in7
-rw-r--r--interface-definitions/vrf.xml.in8
-rw-r--r--op-mode-definitions/generate-ipsec-profile.xml.in145
-rw-r--r--op-mode-definitions/generate-wireguard.xml.in72
-rw-r--r--op-mode-definitions/include/bgp/afi-common.xml.i19
-rw-r--r--op-mode-definitions/include/vtysh-generic-wide.xml.i8
-rw-r--r--op-mode-definitions/monitor-bridge.xml.in33
-rw-r--r--op-mode-definitions/openvpn.xml.in44
-rw-r--r--op-mode-definitions/pki.xml.in116
-rw-r--r--op-mode-definitions/show-bgp.xml.in75
-rw-r--r--op-mode-definitions/show-bridge.xml.in31
-rw-r--r--op-mode-definitions/show-interfaces-bridge.xml.in6
-rw-r--r--op-mode-definitions/show-interfaces-wireguard.xml.in66
-rw-r--r--op-mode-definitions/wireguard.xml.in189
-rw-r--r--python/vyos/airbag.py24
-rw-r--r--python/vyos/configquery.py44
-rw-r--r--python/vyos/configsession.py7
-rw-r--r--python/vyos/configverify.py39
-rw-r--r--python/vyos/defaults.py5
-rwxr-xr-x[-rw-r--r--]python/vyos/ifconfig/interface.py23
-rw-r--r--python/vyos/ifconfig/l2tpv3.py24
-rw-r--r--python/vyos/ifconfig/vrrp.py9
-rw-r--r--python/vyos/ifconfig/vti.py7
-rw-r--r--python/vyos/ifconfig/wireguard.py12
-rw-r--r--python/vyos/pki.py5
-rw-r--r--python/vyos/template.py50
-rw-r--r--python/vyos/util.py10
-rwxr-xr-xscripts/override-default38
-rw-r--r--smoketest/configs/bgp-azure-ipsec-gateway17
-rw-r--r--smoketest/configs/pki-ipsec27
-rw-r--r--smoketest/configs/pki-misc98
-rwxr-xr-xsmoketest/scripts/cli/test_interfaces_ethernet.py41
-rwxr-xr-xsmoketest/scripts/cli/test_interfaces_openvpn.py151
-rwxr-xr-xsmoketest/scripts/cli/test_interfaces_wireguard.py16
-rwxr-xr-xsmoketest/scripts/cli/test_protocols_bgp.py16
-rwxr-xr-xsmoketest/scripts/cli/test_protocols_nhrp.py2
-rwxr-xr-xsmoketest/scripts/cli/test_protocols_ospf.py15
-rwxr-xr-xsmoketest/scripts/cli/test_protocols_rpki.py1
-rwxr-xr-xsmoketest/scripts/cli/test_service_https.py17
-rwxr-xr-xsmoketest/scripts/cli/test_service_router-advert.py26
-rwxr-xr-xsmoketest/scripts/cli/test_system_login.py11
-rwxr-xr-xsmoketest/scripts/cli/test_vpn_ipsec.py2
-rwxr-xr-xsmoketest/scripts/cli/test_vpn_openconnect.py18
-rwxr-xr-xsmoketest/scripts/cli/test_vpn_sstp.py33
-rwxr-xr-xsrc/conf_mode/containers.py4
-rwxr-xr-xsrc/conf_mode/https.py123
-rwxr-xr-xsrc/conf_mode/interfaces-ethernet.py31
-rwxr-xr-xsrc/conf_mode/interfaces-loopback.py4
-rwxr-xr-xsrc/conf_mode/interfaces-openvpn.py326
-rwxr-xr-xsrc/conf_mode/interfaces-vti.py10
-rwxr-xr-xsrc/conf_mode/interfaces-vxlan.py21
-rwxr-xr-xsrc/conf_mode/interfaces-wireguard.py16
-rwxr-xr-xsrc/conf_mode/ipsec-settings.py223
-rwxr-xr-xsrc/conf_mode/le_cert.py4
-rwxr-xr-xsrc/conf_mode/protocols_bfd.py6
-rwxr-xr-xsrc/conf_mode/protocols_bgp.py19
-rwxr-xr-xsrc/conf_mode/protocols_isis.py23
-rwxr-xr-xsrc/conf_mode/protocols_ospf.py9
-rwxr-xr-xsrc/conf_mode/protocols_ospfv3.py6
-rwxr-xr-xsrc/conf_mode/protocols_rip.py6
-rwxr-xr-xsrc/conf_mode/protocols_ripng.py6
-rwxr-xr-xsrc/conf_mode/protocols_rpki.py6
-rwxr-xr-xsrc/conf_mode/protocols_static.py6
-rwxr-xr-xsrc/conf_mode/snmp.py6
-rwxr-xr-xsrc/conf_mode/system-login.py16
-rwxr-xr-xsrc/conf_mode/system-option.py3
-rwxr-xr-xsrc/conf_mode/vpn_ipsec.py239
-rwxr-xr-xsrc/conf_mode/vpn_l2tp.py1
-rwxr-xr-xsrc/conf_mode/vpn_openconnect.py65
-rwxr-xr-xsrc/conf_mode/vpn_sstp.py72
-rwxr-xr-xsrc/conf_mode/vrf.py58
-rwxr-xr-xsrc/conf_mode/vyos_cert.py147
-rwxr-xr-xsrc/etc/ipsec.d/vti-up-down26
-rw-r--r--src/etc/sysctl.d/30-vyos-router.conf7
-rwxr-xr-xsrc/etc/update-motd.d/99-reboot7
-rwxr-xr-xsrc/etc/vmware-tools/scripts/resume-vm-default.d/ether-resume.py11
-rwxr-xr-xsrc/helpers/strip-private.py17
-rwxr-xr-xsrc/helpers/vyos-bridge-sync.py51
-rwxr-xr-xsrc/migration-scripts/https/2-to-386
-rwxr-xr-xsrc/migration-scripts/interfaces/19-to-20130
-rwxr-xr-xsrc/migration-scripts/interfaces/20-to-21130
-rwxr-xr-xsrc/migration-scripts/interfaces/22-to-23369
-rwxr-xr-xsrc/migration-scripts/ipsec/5-to-611
-rwxr-xr-xsrc/migration-scripts/l2tp/3-to-4169
-rwxr-xr-xsrc/migration-scripts/openconnect/0-to-1136
-rwxr-xr-xsrc/migration-scripts/quagga/7-to-899
-rwxr-xr-xsrc/migration-scripts/quagga/8-to-9114
-rwxr-xr-xsrc/migration-scripts/sstp/3-to-4136
-rwxr-xr-xsrc/migration-scripts/vrf/0-to-110
-rwxr-xr-xsrc/migration-scripts/vrf/1-to-21
-rwxr-xr-xsrc/migration-scripts/vrf/2-to-3144
-rwxr-xr-xsrc/op_mode/dns_forwarding_statistics.py2
-rwxr-xr-xsrc/op_mode/ikev2_profile_generator.py230
-rwxr-xr-xsrc/op_mode/ping.py4
-rwxr-xr-xsrc/op_mode/pki.py189
-rwxr-xr-xsrc/op_mode/show_dhcp.py7
-rwxr-xr-xsrc/op_mode/show_dhcpv6.py6
-rwxr-xr-xsrc/op_mode/show_vrf.py7
-rwxr-xr-xsrc/op_mode/wireguard.py159
-rwxr-xr-xsrc/op_mode/wireguard_client.py2
-rw-r--r--src/services/api/graphql/README.graphql116
-rw-r--r--src/services/api/graphql/graphql/__init__.py0
-rw-r--r--src/services/api/graphql/graphql/directives.py17
-rw-r--r--src/services/api/graphql/graphql/mutations.py60
-rw-r--r--src/services/api/graphql/graphql/schema/dhcp_server.graphql35
-rw-r--r--src/services/api/graphql/graphql/schema/interface_ethernet.graphql18
-rw-r--r--src/services/api/graphql/graphql/schema/schema.graphql15
-rw-r--r--src/services/api/graphql/recipes/__init__.py0
-rw-r--r--src/services/api/graphql/recipes/dhcp_server.py13
-rw-r--r--src/services/api/graphql/recipes/interface_ethernet.py13
-rw-r--r--src/services/api/graphql/recipes/recipe.py49
-rw-r--r--src/services/api/graphql/recipes/templates/dhcp_server.tmpl9
-rw-r--r--src/services/api/graphql/recipes/templates/interface_ethernet.tmpl5
-rw-r--r--src/services/api/graphql/state.py4
-rwxr-xr-xsrc/services/vyos-configd7
-rwxr-xr-xsrc/services/vyos-http-api-server27
-rw-r--r--src/systemd/isc-dhcp-server.service6
-rwxr-xr-xsrc/validators/vrf-name4
168 files changed, 4806 insertions, 2350 deletions
diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md
index 04ca4070d..61ee1d9ff 100644
--- a/.github/PULL_REQUEST_TEMPLATE.md
+++ b/.github/PULL_REQUEST_TEMPLATE.md
@@ -1,5 +1,5 @@
<!-- All PR should follow this template to allow a clean and transparent review -->
-<!-- Text placed between these delimiters is considered a commend and is not rendered -->
+<!-- Text placed between these delimiters is considered a comment and is not rendered -->
## Change Summary
<!--- Provide a general summary of your changes in the Title above -->
diff --git a/Makefile b/Makefile
index 84dbab401..c8f6ac7c9 100644
--- a/Makefile
+++ b/Makefile
@@ -27,7 +27,10 @@ interface_definitions: $(config_xml_obj)
find $(BUILD_DIR)/interface-definitions -type f -name "*.xml" | xargs -I {} $(CURDIR)/scripts/build-command-templates {} $(CURDIR)/schema/interface_definition.rng $(TMPL_DIR) || exit 1
# XXX: delete top level node.def's that now live in other packages
- rm -f $(TMPL_DIR)/firewall/node.def
+ # IPSec VPN EAP-RADIUS does not support source-address
+ rm -rf $(TMPL_DIR)/vpn/ipsec/remote-access/radius/source-address
+ # T3568: firewall is yet not migrated to XML and Python - this is only a dummy
+ rm -rf $(TMPL_DIR)/firewall/node.def
rm -rf $(TMPL_DIR)/nfirewall
# XXX: test if there are empty node.def files - this is not allowed as these
# could mask help strings or mandatory priority statements
@@ -86,7 +89,7 @@ clean:
.PHONY: test
test:
- set -e; python3 -m compileall -q .
+ set -e; python3 -m compileall -q -x '/vmware-tools/scripts/' .
PYTHONPATH=python/ python3 -m "nose" --with-xunit src --with-coverage --cover-erase --cover-xml --cover-package src/conf_mode,src/op_mode,src/completion,src/helpers,src/validators,src/tests --verbose
.PHONY: sonar
diff --git a/data/configd-include.json b/data/configd-include.json
index a03360bdb..3b4e2925b 100644
--- a/data/configd-include.json
+++ b/data/configd-include.json
@@ -69,6 +69,5 @@
"vpn_pptp.py",
"vpn_sstp.py",
"vrf.py",
-"vrrp.py",
-"vyos_cert.py"
+"vrrp.py"
]
diff --git a/data/templates/accel-ppp/sstp.config.tmpl b/data/templates/accel-ppp/sstp.config.tmpl
index 7ca7b1c1e..fad91d118 100644
--- a/data/templates/accel-ppp/sstp.config.tmpl
+++ b/data/templates/accel-ppp/sstp.config.tmpl
@@ -29,9 +29,9 @@ disable
verbose=1
ifname=sstp%d
accept=ssl
-ssl-ca-file={{ ssl.ca_cert_file }}
-ssl-pemfile={{ ssl.cert_file }}
-ssl-keyfile={{ ssl.key_file }}
+ssl-ca-file=/run/accel-pppd/sstp-ca.pem
+ssl-pemfile=/run/accel-pppd/sstp-cert.pem
+ssl-keyfile=/run/accel-pppd/sstp-cert.key
{# Common IP pool definitions #}
{% include 'accel-ppp/config_ip_pool.j2' %}
diff --git a/data/templates/ethernet/wpa_supplicant.conf.tmpl b/data/templates/ethernet/wpa_supplicant.conf.tmpl
index fe518ad45..308d777f1 100644
--- a/data/templates/ethernet/wpa_supplicant.conf.tmpl
+++ b/data/templates/ethernet/wpa_supplicant.conf.tmpl
@@ -32,11 +32,11 @@ fast_reauth=1
network={
{% if eapol is defined and eapol is not none %}
-{% if eapol.ca_cert_file is defined and eapol.ca_cert_file is not none %}
- ca_cert="{{ eapol.ca_cert_file }}"
+{% if eapol.ca_certificate is defined and eapol.ca_certificate is not none %}
+ ca_cert="/run/wpa_supplicant/{{ ifname }}_ca.pem"
{% endif %}
- client_cert="{{ eapol.cert_file }}"
- private_key="{{ eapol.key_file }}"
+ client_cert="/run/wpa_supplicant/{{ ifname }}_cert.pem"
+ private_key="/run/wpa_supplicant/{{ ifname }}_cert.key"
{% endif %}
# list of accepted authenticated key management protocols
diff --git a/data/templates/firewall/nftables-vrf-zones.tmpl b/data/templates/firewall/nftables-vrf-zones.tmpl
new file mode 100644
index 000000000..eecf47b78
--- /dev/null
+++ b/data/templates/firewall/nftables-vrf-zones.tmpl
@@ -0,0 +1,17 @@
+table inet vrf_zones {
+ # Map of interfaces and connections tracking zones
+ map ct_iface_map {
+ typeof iifname : ct zone
+ }
+ # Assign unique zones for each VRF
+ # Chain for inbound traffic
+ chain vrf_zones_ct_in {
+ type filter hook prerouting priority raw; policy accept;
+ counter ct zone set iifname map @ct_iface_map
+ }
+ # Chain for locally-generated traffic
+ chain vrf_zones_ct_out {
+ type filter hook output priority raw; policy accept;
+ counter ct zone set oifname map @ct_iface_map
+ }
+}
diff --git a/data/templates/frr/bgpd.frr.tmpl b/data/templates/frr/bgpd.frr.tmpl
index c21e7f234..aa297876b 100644
--- a/data/templates/frr/bgpd.frr.tmpl
+++ b/data/templates/frr/bgpd.frr.tmpl
@@ -65,6 +65,9 @@
{% if config.shutdown is defined %}
neighbor {{ neighbor }} shutdown
{% endif %}
+{% if config.solo is defined %}
+ neighbor {{ neighbor }} solo
+{% endif %}
{% if config.strict_capability_match is defined %}
neighbor {{ neighbor }} strict-capability-match
{% endif %}
diff --git a/data/templates/frr/isis.frr.tmpl b/data/templates/frr/isisd.frr.tmpl
index 1e651898b..6cfa076d0 100644
--- a/data/templates/frr/isis.frr.tmpl
+++ b/data/templates/frr/isisd.frr.tmpl
@@ -116,18 +116,33 @@ router isis VyOS {{ 'vrf ' + vrf if vrf is defined and vrf is not none }}
{% endfor %}
{% endfor %}
{% endif %}
-{% if redistribute is defined and redistribute.ipv4 is defined and redistribute.ipv4 is not none %}
-{% for protocol in redistribute.ipv4 %}
-{% for level, level_config in redistribute.ipv4[protocol].items() %}
-{% if level_config.metric is defined and level_config.metric is not none %}
+{% if redistribute is defined %}
+{% if redistribute.ipv4 is defined and redistribute.ipv4 is not none %}
+{% for protocol, protocol_options in redistribute.ipv4.items() %}
+{% for level, level_config in protocol_options.items() %}
+{% if level_config.metric is defined and level_config.metric is not none %}
redistribute ipv4 {{ protocol }} {{ level | replace('_', '-') }} metric {{ level_config.metric }}
-{% elif level_config.route_map is defined and level_config.route_map is not none %}
+{% elif level_config.route_map is defined and level_config.route_map is not none %}
redistribute ipv4 {{ protocol }} {{ level | replace('_', '-') }} route-map {{ level_config.route_map }}
-{% else %}
+{% else %}
redistribute ipv4 {{ protocol }} {{ level | replace('_', '-') }}
-{% endif %}
+{% endif %}
+{% endfor %}
{% endfor %}
-{% endfor %}
+{% endif %}
+{% if redistribute.ipv6 is defined and redistribute.ipv6 is not none %}
+{% for protocol, protocol_options in redistribute.ipv6.items() %}
+{% for level, level_config in protocol_options.items() %}
+{% if level_config.metric is defined and level_config.metric is not none %}
+ redistribute ipv6 {{ protocol }} {{ level | replace('_', '-') }} metric {{ level_config.metric }}
+{% elif level_config.route_map is defined and level_config.route_map is not none %}
+ redistribute ipv6 {{ protocol }} {{ level | replace('_', '-') }} route-map {{ level_config.route_map }}
+{% else %}
+ redistribute ipv6 {{ protocol }} {{ level | replace('_', '-') }}
+{% endif %}
+{% endfor %}
+{% endfor %}
+{% endif %}
{% endif %}
{% if level is defined and level is not none %}
{% if level == 'level-2' %}
@@ -180,4 +195,4 @@ interface {{ iface }} {{ 'vrf ' + vrf if vrf is defined and vrf is not none }}
{% endif %}
{% endfor %}
{% endif %}
-!
+! \ No newline at end of file
diff --git a/data/templates/https/nginx.default.tmpl b/data/templates/https/nginx.default.tmpl
index 916764410..2f8aa06c2 100644
--- a/data/templates/https/nginx.default.tmpl
+++ b/data/templates/https/nginx.default.tmpl
@@ -17,7 +17,7 @@ server {
listen {{ server.port }} ssl;
listen [::]:{{ server.port }} ssl;
{% else %}
- listen {{ server.address }}:{{ server.port }} ssl;
+ listen {{ server.address | bracketize_ipv6 }}:{{ server.port }} ssl;
{% endif %}
{% for name in server.name %}
@@ -30,7 +30,8 @@ server {
include {{ server.certbot_dir }}/options-ssl-nginx.conf;
ssl_dhparam {{ server.certbot_dir }}/ssl-dhparams.pem;
{% elif server.vyos_cert %}
- include {{ server.vyos_cert.conf }};
+ ssl_certificate {{ server.vyos_cert.crt }};
+ ssl_certificate_key {{ server.vyos_cert.key }};
{% else %}
#
# Self signed certs generated by the ssl-cert package
@@ -40,7 +41,7 @@ server {
{% endif %}
# proxy settings for HTTP API, if enabled; 503, if not
- location ~ /(retrieve|configure|config-file|image|generate|show|docs|openapi.json|redoc) {
+ location ~ /(retrieve|configure|config-file|image|generate|show|docs|openapi.json|redoc|graphql) {
{% if server.api %}
proxy_pass http://localhost:{{ server.api.port }};
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
diff --git a/data/templates/ipsec/charon/dhcp.conf.tmpl b/data/templates/ipsec/charon/dhcp.conf.tmpl
index 2879550a8..92774b275 100644
--- a/data/templates/ipsec/charon/dhcp.conf.tmpl
+++ b/data/templates/ipsec/charon/dhcp.conf.tmpl
@@ -1,12 +1,11 @@
dhcp {
load = yes
-
-{% if options is defined and options.remote_access is defined and options.remote_access.dhcp_pool is defined %}
-{% if options.remote_access.dhcp_pool.interface is defined %}
- interface = {{ options.remote_access.dhcp_pool.interface }}
+{% if remote_access is defined and remote_access.dhcp is defined %}
+{% if remote_access.dhcp.interface is defined %}
+ interface = {{ remote_access.dhcp.interface }}
{% endif %}
-{% if options.remote_access.dhcp_pool.server is defined %}
- server = {{ options.remote_access.dhcp_pool.server }}
+{% if remote_access.dhcp.server is defined %}
+ server = {{ remote_access.dhcp.server }}
{% endif %}
{% endif %}
diff --git a/data/templates/ipsec/charon/eap-radius.conf.tmpl b/data/templates/ipsec/charon/eap-radius.conf.tmpl
new file mode 100644
index 000000000..5ec35c988
--- /dev/null
+++ b/data/templates/ipsec/charon/eap-radius.conf.tmpl
@@ -0,0 +1,115 @@
+eap-radius {
+ # Send RADIUS accounting information to RADIUS servers.
+ # accounting = no
+
+ # Close the IKE_SA if there is a timeout during interim RADIUS accounting
+ # updates.
+ # accounting_close_on_timeout = yes
+
+ # Interval in seconds for interim RADIUS accounting updates, if not
+ # specified by the RADIUS server in the Access-Accept message.
+ # accounting_interval = 0
+
+ # If enabled, accounting is disabled unless an IKE_SA has at least one
+ # virtual IP. Only for IKEv2, for IKEv1 a virtual IP is strictly necessary.
+ # accounting_requires_vip = no
+
+ # If enabled, adds the Class attributes received in Access-Accept message to
+ # the RADIUS accounting messages.
+ # accounting_send_class = no
+
+ # Use class attributes in Access-Accept messages as group membership
+ # information.
+ # class_group = no
+
+ # Closes all IKE_SAs if communication with the RADIUS server times out. If
+ # it is not set only the current IKE_SA is closed.
+ # close_all_on_timeout = no
+
+ # Send EAP-Start instead of EAP-Identity to start RADIUS conversation.
+ # eap_start = no
+
+ # Use filter_id attribute as group membership information.
+ # filter_id = no
+
+ # Prefix to EAP-Identity, some AAA servers use a IMSI prefix to select the
+ # EAP method.
+ # id_prefix =
+
+ # Whether to load the plugin. Can also be an integer to increase the
+ # priority of this plugin.
+ load = yes
+
+ # NAS-Identifier to include in RADIUS messages.
+ nas_identifier = {{ remote_access.radius.nas_identifier if remote_access is defined and remote_access.radius is defined and remote_access.radius.nas_identifier is defined else 'strongSwan' }}
+
+ # Port of RADIUS server (authentication).
+ # port = 1812
+
+ # Base to use for calculating exponential back off.
+ # retransmit_base = 1.4
+
+ # Timeout in seconds before sending first retransmit.
+ # retransmit_timeout = 2.0
+
+ # Number of times to retransmit a packet before giving up.
+ # retransmit_tries = 4
+
+ # Shared secret between RADIUS and NAS. If set, make sure to adjust the
+ # permissions of the config file accordingly.
+ # secret =
+
+ # IP/Hostname of RADIUS server.
+ # server =
+
+ # Number of sockets (ports) to use, increase for high load.
+ # sockets = 1
+
+ # Whether to include the UDP port in the Called- and Calling-Station-Id
+ # RADIUS attributes.
+ # station_id_with_port = yes
+
+ dae {
+ # Enables support for the Dynamic Authorization Extension (RFC 5176).
+ # enable = no
+
+ # Address to listen for DAE messages from the RADIUS server.
+ # listen = 0.0.0.0
+
+ # Port to listen for DAE requests.
+ # port = 3799
+
+ # Shared secret used to verify/sign DAE messages. If set, make sure to
+ # adjust the permissions of the config file accordingly.
+ # secret =
+ }
+
+ forward {
+ # RADIUS attributes to be forwarded from IKEv2 to RADIUS.
+ # ike_to_radius =
+
+ # Same as ike_to_radius but from RADIUS to IKEv2.
+ # radius_to_ike =
+ }
+
+ # Section to specify multiple RADIUS servers.
+ servers {
+{% if remote_access is defined and remote_access.radius is defined and remote_access.radius.server is defined %}
+{% for server, server_options in remote_access.radius.server.items() if server_options.disable is not defined %}
+ {{ server | replace('.', '-') }} {
+ address = {{ server }}
+ secret = {{ server_options.key }}
+ auth_port = {{ server_options.port }}
+{% if server_options.disable_accounting is not defined %}
+ acct_port = {{ server_options.port | int +1 }}
+{% endif %}
+ sockets = 20
+ }
+{% endfor %}
+{% endif %}
+ }
+
+ # Section to configure multiple XAuth authentication rounds via RADIUS.
+ xauth {
+ }
+}
diff --git a/data/templates/ipsec/interfaces_use.conf.tmpl b/data/templates/ipsec/interfaces_use.conf.tmpl
index 3d285b9be..a77102396 100644
--- a/data/templates/ipsec/interfaces_use.conf.tmpl
+++ b/data/templates/ipsec/interfaces_use.conf.tmpl
@@ -1,6 +1,5 @@
-{% if ipsec_interfaces is defined and 'interface' in ipsec_interfaces %}
-{% set interfaces = ipsec_interfaces['interface'] %}
+{% if interface is defined %}
charon {
- interfaces_use = {{ ', '.join(interfaces) if interfaces is not string else interfaces }}
+ interfaces_use = {{ ', '.join(interface) }}
}
{% endif %} \ No newline at end of file
diff --git a/data/templates/ipsec/ios_profile.tmpl b/data/templates/ipsec/ios_profile.tmpl
new file mode 100644
index 000000000..af6c79d6e
--- /dev/null
+++ b/data/templates/ipsec/ios_profile.tmpl
@@ -0,0 +1,104 @@
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+ <!-- Set the name to whatever you like, it is used in the profile list on the device -->
+ <key>PayloadDisplayName</key>
+ <string>{{ profile_name }}</string>
+ <!-- This is a reverse-DNS style unique identifier used to detect duplicate profiles -->
+ <key>PayloadIdentifier</key>
+ <string>{{ rfqdn }}</string>
+ <!-- A globally unique identifier, use uuidgen on Linux/Mac OS X to generate it -->
+ <key>PayloadUUID</key>
+ <string>{{ '' | get_uuid }}</string>
+ <key>PayloadType</key>
+ <string>Configuration</string>
+ <key>PayloadVersion</key>
+ <integer>1</integer>
+ <key>PayloadContent</key>
+ <array>
+ <!-- It is possible to add multiple VPN payloads with different identifiers/UUIDs and names -->
+ <dict>
+ <!-- This is an extension of the identifier given above -->
+ <key>PayloadIdentifier</key>
+ <string>{{ rfqdn }}.conf1</string>
+ <!-- A globally unique identifier for this payload -->
+ <key>PayloadUUID</key>
+ <string>{{ '' | get_uuid }}</string>
+ <key>PayloadType</key>
+ <string>com.apple.vpn.managed</string>
+ <key>PayloadVersion</key>
+ <integer>1</integer>
+ <!-- This is the name of the VPN connection as seen in the VPN application later -->
+ <key>UserDefinedName</key>
+ <string>{{ vpn_name }}</string>
+ <key>VPNType</key>
+ <string>IKEv2</string>
+ <key>IKEv2</key>
+ <dict>
+ <!-- Hostname or IP address of the VPN server -->
+ <key>RemoteAddress</key>
+ <string>{{ remote }}</string>
+ <!-- Remote identity, can be a FQDN, a userFQDN, an IP or (theoretically) a certificate's subject DN. Can't be empty.
+ IMPORTANT: DNs are currently not handled correctly, they are always sent as identities of type FQDN -->
+ <key>RemoteIdentifier</key>
+ <string>{{ authentication.id if authentication.id is defined else 'fooo' }}</string>
+ <!-- Local IKE identity, same restrictions as above. If it is empty the client's IP address will be used -->
+ <key>LocalIdentifier</key>
+ <string></string>
+ <!-- Optional, if it matches the CN of the root CA certificate (not the full subject DN) a certificate request will be sent
+ NOTE: If this is not configured make sure to configure leftsendcert=always on the server, otherwise it won't send its certificate -->
+ <key>ServerCertificateIssuerCommonName</key>
+ <string>{{ ca_cn }}</string>
+ <!-- Optional, the CN or one of the subjectAltNames of the server certificate to verify it, if not set RemoteIdentifier will be used -->
+ <key>ServerCertificateCommonName</key>
+ <string>{{ cert_cn }}</string>
+ <!-- The server is authenticated using a certificate -->
+ <key>AuthenticationMethod</key>
+ <string>Certificate</string>
+ <!-- The client uses EAP to authenticate -->
+ <key>ExtendedAuthEnabled</key>
+ <integer>1</integer>
+ <!-- The next two dictionaries are optional (as are the keys in them), but it is recommended to specify them as the default is to use 3DES.
+ IMPORTANT: Because only one proposal is sent (even if nothing is configured here) it must match the server configuration -->
+ <key>IKESecurityAssociationParameters</key>
+ <dict>
+ <!-- @see https://developer.apple.com/documentation/networkextension/nevpnikev2encryptionalgorithm -->
+ <key>EncryptionAlgorithm</key>
+ <string>{{ ike_encryption.encryption }}</string>
+ <!-- @see https://developer.apple.com/documentation/networkextension/nevpnikev2integrityalgorithm -->
+ <key>IntegrityAlgorithm</key>
+ <string>{{ ike_encryption.hash }}</string>
+ <!-- @see https://developer.apple.com/documentation/networkextension/nevpnikev2diffiehellmangroup -->
+ <key>DiffieHellmanGroup</key>
+ <integer>{{ ike_encryption.dh_group }}</integer>
+ </dict>
+ <key>ChildSecurityAssociationParameters</key>
+ <dict>
+ <key>EncryptionAlgorithm</key>
+ <string>{{ esp_encryption.encryption }}</string>
+ <key>IntegrityAlgorithm</key>
+ <string>{{ esp_encryption.hash }}</string>
+ <key>DiffieHellmanGroup</key>
+ <integer>{{ ike_encryption.dh_group }}</integer>
+ </dict>
+ </dict>
+ </dict>
+ <!-- This payload is optional but it provides an easy way to install the CA certificate together with the configuration -->
+ <dict>
+ <key>PayloadIdentifier</key>
+ <string>org.example.ca</string>
+ <key>PayloadUUID</key>
+ <string>{{ '' | get_uuid }}</string>
+ <key>PayloadType</key>
+ <string>com.apple.security.root</string>
+ <key>PayloadVersion</key>
+ <integer>1</integer>
+ <!-- This is the Base64 (PEM) encoded CA certificate -->
+ <key>PayloadContent</key>
+ <data>
+ {{ ca_cert }}
+ </data>
+ </dict>
+ </array>
+</dict>
+</plist>
diff --git a/data/templates/ipsec/ipsec.conf.tmpl b/data/templates/ipsec/ipsec.conf.tmpl
index a9ea1aac7..1cb531e76 100644
--- a/data/templates/ipsec/ipsec.conf.tmpl
+++ b/data/templates/ipsec/ipsec.conf.tmpl
@@ -16,9 +16,3 @@ config setup
{% if include_ipsec_conf is defined %}
include {{ include_ipsec_conf }}
{% endif %}
-
-{% if delim_ipsec_l2tp_begin is defined %}
-{{delim_ipsec_l2tp_begin}}
-include {{ipsec_ra_conn_file}}
-{{delim_ipsec_l2tp_end}}
-{% endif %}
diff --git a/data/templates/ipsec/ipsec.secrets.tmpl b/data/templates/ipsec/ipsec.secrets.tmpl
index 43b5fe0d2..057e291ed 100644
--- a/data/templates/ipsec/ipsec.secrets.tmpl
+++ b/data/templates/ipsec/ipsec.secrets.tmpl
@@ -3,13 +3,3 @@
{% if include_ipsec_secrets is defined %}
include {{ include_ipsec_secrets }}
{% endif %}
-
-{% if delim_ipsec_l2tp_begin is defined %}
-{{delim_ipsec_l2tp_begin}}
-{% if ipsec_l2tp_auth_mode == 'pre-shared-secret' %}
-{{outside_addr}} %any : PSK "{{ipsec_l2tp_secret}}"
-{% elif ipsec_l2tp_auth_mode == 'x509' %}
-: RSA {{server_key_file_copied}}
-{% endif %}
-{{delim_ipsec_l2tp_end}}
-{% endif %}
diff --git a/data/templates/ipsec/remote-access.tmpl b/data/templates/ipsec/remote-access.tmpl
deleted file mode 100644
index fae48232f..000000000
--- a/data/templates/ipsec/remote-access.tmpl
+++ /dev/null
@@ -1,28 +0,0 @@
-{{delim_ipsec_l2tp_begin}}
-conn {{ra_conn_name}}
- type=transport
- left={{outside_addr}}
- leftsubnet=%dynamic[/1701]
- rightsubnet=%dynamic
- mark_in=%unique
- auto=add
- ike=aes256-sha1-modp1024,3des-sha1-modp1024,3des-sha1-modp1024!
- dpddelay=15
- dpdtimeout=45
- dpdaction=clear
- esp=aes256-sha1,3des-sha1!
- rekey=no
-{% if ipsec_l2tp_auth_mode == 'pre-shared-secret' %}
- authby=secret
- leftauth=psk
- rightauth=psk
-{% elif ipsec_l2tp_auth_mode == 'x509' %}
- authby=rsasig
- leftrsasigkey=%cert
- rightrsasigkey=%cert
- rightca=%same
- leftcert={{server_cert_file_copied}}
-{% endif %}
- ikelifetime={{ipsec_l2tp_ike_lifetime}}
- keylife={{ipsec_l2tp_lifetime}}
-{{delim_ipsec_l2tp_end}}
diff --git a/data/templates/ipsec/swanctl.conf.tmpl b/data/templates/ipsec/swanctl.conf.tmpl
index a6ab73cc2..161f19f95 100644
--- a/data/templates/ipsec/swanctl.conf.tmpl
+++ b/data/templates/ipsec/swanctl.conf.tmpl
@@ -1,4 +1,5 @@
### Autogenerated by vpn_ipsec.py ###
+{% import 'ipsec/swanctl/l2tp.tmpl' as l2tp_tmpl %}
{% import 'ipsec/swanctl/profile.tmpl' as profile_tmpl %}
{% import 'ipsec/swanctl/peer.tmpl' as peer_tmpl %}
{% import 'ipsec/swanctl/remote_access.tmpl' as remote_access_tmpl %}
@@ -14,21 +15,28 @@ connections {
{{ peer_tmpl.conn(peer, peer_conf, ike_group, esp_group) }}
{% endfor %}
{% endif %}
-{% if remote_access is defined and remote_access is not none %}
-{% for rw, rw_conf in remote_access.items() if rw_conf.disable is not defined %}
+{% if remote_access is defined and remote_access.connection is defined and remote_access.connection is not none %}
+{% for rw, rw_conf in remote_access.connection.items() if rw_conf.disable is not defined %}
{{ remote_access_tmpl.conn(rw, rw_conf, ike_group, esp_group) }}
{% endfor %}
{% endif %}
+{% if l2tp %}
+{{ l2tp_tmpl.conn(l2tp, l2tp_outside_address, l2tp_ike_default, l2tp_esp_default, ike_group, esp_group) }}
+{% endif %}
}
pools {
-{% if remote_access is defined %}
-{% for ra, ra_conf in remote_access.items() if ra_conf.pool.dhcp_enable is not defined %}
- ra-{{ ra }} {
- addrs = {{ ra_conf.pool.prefix }}
- dns = {{ ra_conf.pool.name_server | join(",") }}
-{% if ra_conf.pool.exclude is defined %}
- split_exclude = {{ ra_conf.pool.exclude | join(",") }}
+{% if remote_access is defined and remote_access.pool is defined and remote_access.pool is not none %}
+{% for pool, pool_config in remote_access.pool.items() %}
+ {{ pool }} {
+{% if pool_config.prefix is defined and pool_config.prefix is not none %}
+ addrs = {{ pool_config.prefix }}
+{% endif %}
+{% if pool_config.name_server is defined and pool_config.name_server is not none %}
+ dns = {{ pool_config.name_server | join(',') }}
+{% endif %}
+{% if pool_config.exclude is defined and pool_config.exclude is not none %}
+ split_exclude = {{ pool_config.exclude | join(',') }}
{% endif %}
}
{% endfor %}
@@ -81,8 +89,8 @@ secrets {
{% endif %}
{% endfor %}
{% endif %}
-{% if remote_access is defined %}
-{% for ra, ra_conf in remote_access.items() if remote_access is defined %}
+{% if remote_access is defined and remote_access.connection is defined and remote_access.connection is not none %}
+{% for ra, ra_conf in remote_access.connection.items() if ra_conf.disable is not defined %}
{% if ra_conf.authentication.server_mode == 'pre-shared-secret' %}
ike_{{ ra }} {
{% if ra_conf.authentication.id is defined %}
@@ -103,5 +111,21 @@ secrets {
{% endif %}
{% endfor %}
{% endif %}
+{% if l2tp %}
+{% if l2tp.authentication.mode == 'pre-shared-secret' %}
+ ike_l2tp_remote_access {
+ id = "{{ l2tp_outside_address }}"
+ secret = "{{ l2tp.authentication.pre_shared_secret }}"
+ }
+{% elif l2tp.authentication.mode == 'x509' %}
+ private_l2tp_remote_access {
+ id = "{{ l2tp_outside_address }}"
+ file = {{ l2tp.authentication.x509.certificate }}.pem
+{% if l2tp.authentication.x509.passphrase is defined %}
+ secret = "{{ l2tp.authentication.x509.passphrase }}"
+{% endif %}
+ }
+{% endif %}
+{% endif %}
}
diff --git a/data/templates/ipsec/swanctl/l2tp.tmpl b/data/templates/ipsec/swanctl/l2tp.tmpl
new file mode 100644
index 000000000..2df5c2a4d
--- /dev/null
+++ b/data/templates/ipsec/swanctl/l2tp.tmpl
@@ -0,0 +1,30 @@
+{% macro conn(l2tp, l2tp_outside_address, l2tp_ike_default, l2tp_esp_default, ike_group, esp_group) %}
+{% set l2tp_ike = ike_group[l2tp.ike_group] if l2tp.ike_group is defined else None %}
+{% set l2tp_esp = esp_group[l2tp.esp_group] if l2tp.esp_group is defined else None %}
+ l2tp_remote_access {
+ proposals = {{ l2tp_ike | get_esp_ike_cipher | join(',') if l2tp_ike else l2tp_ike_default }}
+ local_addrs = {{ l2tp_outside_address }}
+ dpd_delay = 15s
+ dpd_timeout = 45s
+ rekey_time = {{ l2tp_ike.lifetime if l2tp_ike else l2tp.ike_lifetime }}s
+ reauth_time = 0
+ local {
+ auth = {{ 'psk' if l2tp.authentication.mode == 'pre-shared-secret' else 'pubkey' }}
+{% if l2tp.authentication.mode == 'x509' %}
+ certs = {{ l2tp.authentication.x509.certificate }}.pem
+{% endif %}
+ }
+ remote {
+ auth = {{ 'psk' if l2tp.authentication.mode == 'pre-shared-secret' else 'pubkey' }}
+ }
+ children {
+ l2tp_remote_access_esp {
+ mode = transport
+ esp_proposals = {{ l2tp_esp | get_esp_ike_cipher | join(',') if l2tp_esp else l2tp_esp_default }}
+ life_time = {{ l2tp_esp.lifetime if l2tp_esp else l2tp.lifetime }}s
+ local_ts = dynamic[/1701]
+ remote_ts = dynamic
+ }
+ }
+ }
+{% endmacro %}
diff --git a/data/templates/ipsec/swanctl/peer.tmpl b/data/templates/ipsec/swanctl/peer.tmpl
index 8e46e8892..dd29ea7d4 100644
--- a/data/templates/ipsec/swanctl/peer.tmpl
+++ b/data/templates/ipsec/swanctl/peer.tmpl
@@ -54,7 +54,7 @@
}
children {
{% if peer_conf.vti is defined and peer_conf.vti.bind is defined and peer_conf.tunnel is not defined %}
-{% set vti_esp = esp_group[peer_conf.vti.esp_group] if peer_conf.vti.esp_group is defined else None %}
+{% set vti_esp = esp_group[ peer_conf.vti.esp_group ] if peer_conf.vti.esp_group is defined else esp_group[ peer_conf.default_esp_group ] %}
peer_{{ name }}_vti {
esp_proposals = {{ vti_esp | get_esp_ike_cipher | join(',') }}
local_ts = 0.0.0.0/0,::/0
@@ -86,7 +86,7 @@
{% set remote_port = tunnel_conf.remote.port if tunnel_conf.remote is defined and tunnel_conf.remote.port is defined else '' %}
{% set remote_suffix = '[{0}/{1}]'.format(proto, remote_port) if proto or remote_port else '' %}
peer_{{ name }}_tunnel_{{ tunnel_id }} {
- esp_proposals = {{ esp_group[peer_conf.default_esp_group] | get_esp_ike_cipher | join(',') }}
+ esp_proposals = {{ tunnel_esp | get_esp_ike_cipher | join(',') }}
{% if tunnel_esp.mode is not defined or tunnel_esp.mode == 'tunnel' %}
{% if tunnel_conf.local is defined and tunnel_conf.local.prefix is defined %}
{% set local_prefix = tunnel_conf.local.prefix if 'any' not in tunnel_conf.local.prefix else ['0.0.0.0/0', '::/0'] %}
diff --git a/data/templates/ipsec/swanctl/remote_access.tmpl b/data/templates/ipsec/swanctl/remote_access.tmpl
index 95f2108fb..456842488 100644
--- a/data/templates/ipsec/swanctl/remote_access.tmpl
+++ b/data/templates/ipsec/swanctl/remote_access.tmpl
@@ -10,10 +10,9 @@
send_certreq = no
rekey_time = {{ ike.lifetime }}s
keyingtries = 0
-{% if rw_conf.pool.dhcp_enable is defined %}
- pools = dhcp
-{% else %}
- pools = ra-{{ name }}
+ unique = {{ rw_conf.unique }}
+{% if rw_conf.pool is defined and rw_conf.pool is not none %}
+ pools = {{ rw_conf.pool | join(',') }}
{% endif %}
local {
{% if rw_conf.authentication.id is defined and rw_conf.authentication.use_x509_id is not defined %}
@@ -34,7 +33,7 @@
}
children {
ikev2-vpn {
- esp_proposals = {{ esp | get_esp_ike_cipher | join(',') }}
+ esp_proposals = {{ esp | get_esp_ike_cipher | join(',') }}
rekey_time = {{ esp.lifetime }}s
rand_time = 540s
dpd_action = clear
diff --git a/data/templates/ipsec/windows_profile.tmpl b/data/templates/ipsec/windows_profile.tmpl
new file mode 100644
index 000000000..8c26944be
--- /dev/null
+++ b/data/templates/ipsec/windows_profile.tmpl
@@ -0,0 +1,4 @@
+Remove-VpnConnection -Name "{{ vpn_name }}" -Force -PassThru
+
+Add-VpnConnection -Name "{{ vpn_name }}" -ServerAddress "{{ remote }}" -TunnelType "Ikev2"
+Set-VpnConnectionIPsecConfiguration -ConnectionName "{{ vpn_name }}" -AuthenticationTransformConstants {{ ike_encryption.encryption }} -CipherTransformConstants {{ ike_encryption.encryption }} -EncryptionMethod {{ esp_encryption.encryption }} -IntegrityCheckMethod {{ esp_encryption.hash }} -PfsGroup None -DHGroup "Group{{ ike_encryption.dh_group }}" -PassThru -Force
diff --git a/data/templates/ocserv/ocserv_config.tmpl b/data/templates/ocserv/ocserv_config.tmpl
index 328af0c0d..0be805235 100644
--- a/data/templates/ocserv/ocserv_config.tmpl
+++ b/data/templates/ocserv/ocserv_config.tmpl
@@ -12,16 +12,16 @@ auth = "radius [config=/run/ocserv/radiusclient.conf]"
auth = "plain[/run/ocserv/ocpasswd]"
{% endif %}
-{% if ssl.cert_file %}
-server-cert = {{ ssl.cert_file }}
+{% if ssl.certificate is defined %}
+server-cert = /run/ocserv/cert.pem
+server-key = /run/ocserv/cert.key
+{% if ssl.passphrase is defined %}
+key-pin = {{ ssl.passphrase }}
{% endif %}
-
-{% if ssl.key_file %}
-server-key = {{ ssl.key_file }}
{% endif %}
-{% if ssl.ca_cert_file %}
-ca-cert = {{ ssl.ca_cert_file }}
+{% if ssl.ca_certificate is defined %}
+ca-cert = /run/ocserv/ca.pem
{% endif %}
socket-file = /run/ocserv/ocserv.socket
diff --git a/data/templates/openvpn/server.conf.tmpl b/data/templates/openvpn/server.conf.tmpl
index c5d665c0b..d9f01310e 100644
--- a/data/templates/openvpn/server.conf.tmpl
+++ b/data/templates/openvpn/server.conf.tmpl
@@ -36,8 +36,8 @@ rport {{ remote_port }}
remote {{ remote }}
{% endfor %}
{% endif %}
-{% if shared_secret_key_file is defined and shared_secret_key_file is not none %}
-secret {{ shared_secret_key_file }}
+{% if shared_secret_key is defined and shared_secret_key is not none %}
+secret /run/openvpn/{{ ifname }}_shared.key
{% endif %}
{% if persistent_tunnel is defined %}
persist-tun
@@ -157,32 +157,32 @@ ifconfig-ipv6 {{ laddr }} {{ raddr }}
{% if tls is defined and tls is not none %}
# TLS options
-{% if tls.ca_cert_file is defined and tls.ca_cert_file is not none %}
-ca {{ tls.ca_cert_file }}
+{% if tls.ca_certificate is defined and tls.ca_certificate is not none %}
+ca /run/openvpn/{{ ifname }}_ca.pem
{% endif %}
-{% if tls.cert_file is defined and tls.cert_file is not none %}
-cert {{ tls.cert_file }}
+{% if tls.certificate is defined and tls.certificate is not none %}
+cert /run/openvpn/{{ ifname }}_cert.pem
{% endif %}
-{% if tls.key_file is defined and tls.key_file is not none %}
-key {{ tls.key_file }}
+{% if tls.private_key is defined %}
+key /run/openvpn/{{ ifname }}_cert.key
{% endif %}
-{% if tls.crypt_file is defined and tls.crypt_file is not none %}
-tls-crypt {{ tls.crypt_file }}
+{% if tls.crypt_key is defined and tls.crypt_key is not none %}
+tls-crypt /run/openvpn/{{ ifname }}_crypt.key
{% endif %}
-{% if tls.crl_file is defined and tls.crl_file is not none %}
-crl-verify {{ tls.crl_file }}
+{% if tls.crl is defined %}
+crl-verify /run/openvpn/{{ ifname }}_crl.pem
{% endif %}
{% if tls.tls_version_min is defined and tls.tls_version_min is not none %}
tls-version-min {{ tls.tls_version_min }}
{% endif %}
-{% if tls.dh_file is defined and tls.dh_file is not none %}
-dh {{ tls.dh_file }}
+{% if tls.dh_params is defined and tls.dh_params is not none %}
+dh /run/openvpn/{{ ifname }}_dh.pem
{% endif %}
-{% if tls.auth_file is defined and tls.auth_file is not none %}
+{% if tls.auth_key is defined and tls.auth_key is not none %}
{% if mode == 'client' %}
-tls-auth {{ tls.auth_file }} 1
+tls-auth /run/openvpn/{{ ifname }}_auth.key 1
{% elif mode == 'server' %}
-tls-auth {{ tls.auth_file }} 0
+tls-auth /run/openvpn/{{ ifname }}_auth.key 0
{% endif %}
{% endif %}
{% if tls.role is defined and tls.role is not none %}
@@ -197,56 +197,15 @@ tls-server
# Encryption options
{% if encryption is defined and encryption is not none %}
{% if encryption.cipher is defined and encryption.cipher is not none %}
-{% if encryption.cipher == 'none' %}
-cipher none
-{% elif encryption.cipher == 'des' %}
-cipher des-cbc
-{% elif encryption.cipher == '3des' %}
-cipher des-ede3-cbc
-{% elif encryption.cipher == 'bf128' %}
-cipher bf-cbc
+cipher {{ encryption.cipher | openvpn_cipher }}
+{% if encryption.cipher == 'bf128' %}
keysize 128
{% elif encryption.cipher == 'bf256' %}
-cipher bf-cbc
-keysize 25
-{% elif encryption.cipher == 'aes128gcm' %}
-cipher aes-128-gcm
-{% elif encryption.cipher == 'aes128' %}
-cipher aes-128-cbc
-{% elif encryption.cipher == 'aes192gcm' %}
-cipher aes-192-gcm
-{% elif encryption.cipher == 'aes192' %}
-cipher aes-192-cbc
-{% elif encryption.cipher == 'aes256gcm' %}
-cipher aes-256-gcm
-{% elif encryption.cipher == 'aes256' %}
-cipher aes-256-cbc
+keysize 256
{% endif %}
{% endif %}
{% if encryption.ncp_ciphers is defined and encryption.ncp_ciphers is not none %}
-{% set cipher_list = [] %}
-{% for cipher in encryption.ncp_ciphers %}
-{% if cipher == 'none' %}
-{% set cipher_list = cipher_list.append('none') %}
-{% elif cipher == 'des' %}
-{% set cipher_list = cipher_list.append('des-cbc') %}
-{% elif cipher == '3des' %}
-{% set cipher_list = cipher_list.append('des-ede3-cbc') %}
-{% elif cipher == 'aes128' %}
-{% set cipher_list = cipher_list.append('aes-128-cbc') %}
-{% elif cipher == 'aes128gcm' %}
-{% set cipher_list = cipher_list.append('aes-128-gcm') %}
-{% elif cipher == 'aes192' %}
-{% set cipher_list = cipher_list.append('aes-192-cbc') %}
-{% elif cipher == 'aes192gcm' %}
-{% set cipher_list = cipher_list.append('aes-192-gcm') %}
-{% elif cipher == 'aes256' %}
-{% set cipher_list = cipher_list.append('aes-256-cbc') %}
-{% elif cipher == 'aes256gcm' %}
-{% set cipher_list = cipher_list.append('aes-256-gcm') %}
-{% endif %}
-{% endfor %}
-ncp-ciphers {{ cipher_list | join(':') }}:{{ cipher_list | join(':') | upper }}
+data-ciphers {{ encryption.ncp_ciphers | openvpn_ncp_ciphers }}
{% endif %}
{% endif %}
diff --git a/data/templates/router-advert/radvd.conf.tmpl b/data/templates/router-advert/radvd.conf.tmpl
index 9cc237512..88d066491 100644
--- a/data/templates/router-advert/radvd.conf.tmpl
+++ b/data/templates/router-advert/radvd.conf.tmpl
@@ -1,58 +1,64 @@
### Autogenerated by service_router-advert.py ###
{% if interface is defined and interface is not none %}
-{% for iface in interface %}
+{% for iface, iface_config in interface.items() %}
interface {{ iface }} {
IgnoreIfMissing on;
-{% if interface[iface].default_preference is defined and interface[iface].default_preference is not none %}
- AdvDefaultPreference {{ interface[iface].default_preference }};
+{% if iface_config.default_preference is defined and iface_config.default_preference is not none %}
+ AdvDefaultPreference {{ iface_config.default_preference }};
{% endif %}
-{% if interface[iface].managed_flag is defined and interface[iface].managed_flag is not none %}
- AdvManagedFlag {{ 'on' if interface[iface].managed_flag is defined else 'off' }};
+{% if iface_config.managed_flag is defined and iface_config.managed_flag is not none %}
+ AdvManagedFlag {{ 'on' if iface_config.managed_flag is defined else 'off' }};
{% endif %}
-{% if interface[iface].interval.max is defined and interface[iface].interval.max is not none %}
- MaxRtrAdvInterval {{ interface[iface].interval.max }};
+{% if iface_config.interval.max is defined and iface_config.interval.max is not none %}
+ MaxRtrAdvInterval {{ iface_config.interval.max }};
{% endif %}
-{% if interface[iface].interval.min is defined and interface[iface].interval.min is not none %}
- MinRtrAdvInterval {{ interface[iface].interval.min }};
+{% if iface_config.interval.min is defined and iface_config.interval.min is not none %}
+ MinRtrAdvInterval {{ iface_config.interval.min }};
{% endif %}
-{% if interface[iface].reachable_time is defined and interface[iface].reachable_time is not none %}
- AdvReachableTime {{ interface[iface].reachable_time }};
+{% if iface_config.reachable_time is defined and iface_config.reachable_time is not none %}
+ AdvReachableTime {{ iface_config.reachable_time }};
{% endif %}
- AdvIntervalOpt {{ 'off' if interface[iface].no_send_advert is defined else 'on' }};
- AdvSendAdvert {{ 'off' if interface[iface].no_send_advert is defined else 'on' }};
-{% if interface[iface].default_lifetime is defined %}
- AdvDefaultLifetime {{ interface[iface].default_lifetime }};
-{% endif %}
-{% if interface[iface].link_mtu is defined %}
- AdvLinkMTU {{ interface[iface].link_mtu }};
-{% endif %}
- AdvOtherConfigFlag {{ 'on' if interface[iface].other_config_flag is defined else 'off' }};
- AdvRetransTimer {{ interface[iface].retrans_timer }};
- AdvCurHopLimit {{ interface[iface].hop_limit }};
-{% if interface[iface].route is defined %}
-{% for route in interface[iface].route %}
+ AdvIntervalOpt {{ 'off' if iface_config.no_send_advert is defined else 'on' }};
+ AdvSendAdvert {{ 'off' if iface_config.no_send_advert is defined else 'on' }};
+{% if iface_config.default_lifetime is defined %}
+ AdvDefaultLifetime {{ iface_config.default_lifetime }};
+{% endif %}
+{% if iface_config.link_mtu is defined %}
+ AdvLinkMTU {{ iface_config.link_mtu }};
+{% endif %}
+ AdvOtherConfigFlag {{ 'on' if iface_config.other_config_flag is defined else 'off' }};
+ AdvRetransTimer {{ iface_config.retrans_timer }};
+ AdvCurHopLimit {{ iface_config.hop_limit }};
+{% if iface_config.route is defined %}
+{% for route, route_options in iface_config.route.items() %}
route {{ route }} {
-{% if interface[iface].route[route].valid_lifetime is defined %}
- AdvRouteLifetime {{ interface[iface].route[route].valid_lifetime }};
+{% if route_options.valid_lifetime is defined %}
+ AdvRouteLifetime {{ route_options.valid_lifetime }};
{% endif %}
-{% if interface[iface].route[route].route_preference is defined %}
- AdvRoutePreference {{ interface[iface].route[route].route_preference }};
+{% if route_options.route_preference is defined %}
+ AdvRoutePreference {{ route_options.route_preference }};
{% endif %}
- RemoveRoute {{ 'off' if interface[iface].route[route].no_remove_route is defined else 'on' }};
+ RemoveRoute {{ 'off' if route_options.no_remove_route is defined else 'on' }};
};
{% endfor %}
{% endif %}
-{% for prefix in interface[iface].prefix %}
+{% if iface_config.prefix is defined and iface_config.prefix is not none %}
+{% for prefix, prefix_options in iface_config.prefix.items() %}
prefix {{ prefix }} {
- AdvAutonomous {{ 'off' if interface[iface].prefix[prefix].no_autonomous_flag is defined else 'on' }};
- AdvValidLifetime {{ interface[iface].prefix[prefix].valid_lifetime }};
- AdvOnLink {{ 'off' if interface[iface].prefix[prefix].no_on_link_flag is defined else 'on' }};
- AdvPreferredLifetime {{ interface[iface].prefix[prefix].preferred_lifetime }};
+ AdvAutonomous {{ 'off' if prefix_options.no_autonomous_flag is defined else 'on' }};
+ AdvValidLifetime {{ prefix_options.valid_lifetime }};
+ AdvOnLink {{ 'off' if prefix_options.no_on_link_flag is defined else 'on' }};
+ AdvPreferredLifetime {{ prefix_options.preferred_lifetime }};
+ };
+{% endfor %}
+{% endif %}
+{% if iface_config.name_server is defined %}
+ RDNSS {{ iface_config.name_server | join(" ") }} {
};
-{% endfor %}
-{% if interface[iface].name_server is defined %}
- RDNSS {{ interface[iface].name_server | join(" ") }} {
+{% endif %}
+{% if iface_config.dnssl is defined %}
+ DNSSL {{ iface_config.dnssl | join(" ") }} {
};
{% endif %}
};
diff --git a/data/templates/snmp/override.conf.tmpl b/data/templates/snmp/override.conf.tmpl
index 90c294ed0..2ac45a89f 100644
--- a/data/templates/snmp/override.conf.tmpl
+++ b/data/templates/snmp/override.conf.tmpl
@@ -1,4 +1,5 @@
{% set vrf_command = 'ip vrf exec ' + vrf + ' ' if vrf is defined else '' %}
+{% set oid_route_table = ' ' if route_table is sameas true else '-I -ipCidrRouteTable,inetCidrRouteTable' %}
[Unit]
StartLimitIntervalSec=0
After=vyos-router.service
@@ -7,7 +8,7 @@ After=vyos-router.service
Environment=
Environment="MIBDIRS=/usr/share/snmp/mibs:/usr/share/snmp/mibs/iana:/usr/share/snmp/mibs/ietf:/usr/share/vyos/mibs"
ExecStart=
-ExecStart={{vrf_command}}/usr/sbin/snmpd -LS0-5d -Lf /dev/null -u Debian-snmp -g Debian-snmp -I -ipCidrRouteTable,inetCidrRouteTable -f -p /run/snmpd.pid
+ExecStart={{vrf_command}}/usr/sbin/snmpd -LS0-5d -Lf /dev/null -u Debian-snmp -g Debian-snmp {{oid_route_table}} -f -p /run/snmpd.pid
Restart=always
RestartSec=10
diff --git a/debian/control b/debian/control
index 9ac82cf03..c5cbeb7d4 100644
--- a/debian/control
+++ b/debian/control
@@ -51,6 +51,7 @@ Depends:
etherwake,
ethtool,
fdisk,
+ fastnetmon [amd64],
file,
frr (>= 7.5),
frr-pythontools,
@@ -89,6 +90,7 @@ Depends:
minisign,
modemmanager,
mtr-tiny,
+ ndisc6,
ndppd,
netplug,
nftables (>= 0.9.3),
diff --git a/debian/vyos-1x.install b/debian/vyos-1x.install
index 2ed25755f..7ca568eff 100644
--- a/debian/vyos-1x.install
+++ b/debian/vyos-1x.install
@@ -10,6 +10,7 @@ etc/sudoers.d
etc/systemd
etc/sysctl.d
etc/udev
+etc/update-motd.d
etc/vyos
lib/
opt/
diff --git a/debian/vyos-1x.postinst b/debian/vyos-1x.postinst
index a17fe351c..4b4c4c13e 100644
--- a/debian/vyos-1x.postinst
+++ b/debian/vyos-1x.postinst
@@ -76,3 +76,10 @@ if [ ! -x $POSTCONFIG_SCRIPT ]; then
EOF
fi
+# symlink destination is deleted during ISO assembly - this generates some noise
+# when the system boots: systemd-sysv-generator[1881]: stat() failed on
+# /etc/init.d/README, ignoring: No such file or directory. Thus we simply drop
+# the file.
+if [ -L /etc/init.d/README ]; then
+ rm -f /etc/init.d/README
+fi
diff --git a/interface-definitions/containers.xml.in b/interface-definitions/containers.xml.in
index 6fc53c105..124b1f65e 100644
--- a/interface-definitions/containers.xml.in
+++ b/interface-definitions/containers.xml.in
@@ -3,6 +3,7 @@
<node name="container" owner="${vyos_conf_scripts_dir}/containers.py">
<properties>
<help>Container applications</help>
+ <priority>1280</priority>
</properties>
<children>
<tagNode name="name">
diff --git a/interface-definitions/https.xml.in b/interface-definitions/https.xml.in
index b613e30c1..b65a89b56 100644
--- a/interface-definitions/https.xml.in
+++ b/interface-definitions/https.xml.in
@@ -1,7 +1,7 @@
<?xml version="1.0"?>
<!-- HTTPS configuration -->
<interfaceDefinition>
- <syntaxVersion component='https' version='2'></syntaxVersion>
+ <syntaxVersion component='https' version='3'></syntaxVersion>
<node name="service">
<children>
<node name="https" owner="${vyos_conf_scripts_dir}/https.py">
@@ -123,22 +123,7 @@
<help>TLS certificates</help>
</properties>
<children>
- <node name="system-generated-certificate" owner="${vyos_conf_scripts_dir}/vyos_cert.py">
- <properties>
- <help>Use an automatically generated self-signed certificate</help>
- </properties>
- <children>
- <leafNode name="lifetime">
- <properties>
- <help>Lifetime in days; default is 365</help>
- <valueHelp>
- <format>1-65535</format>
- <description>Number of days</description>
- </valueHelp>
- </properties>
- </leafNode>
- </children>
- </node>
+ #include <include/pki/certificate.xml.i>
<node name="certbot" owner="${vyos_conf_scripts_dir}/le_cert.py">
<properties>
<help>Request or apply a letsencrypt certificate for domain-name</help>
diff --git a/interface-definitions/include/accel-ppp/radius-additions.xml.i b/interface-definitions/include/accel-ppp/radius-additions.xml.i
index e65088c43..fdcff36bf 100644
--- a/interface-definitions/include/accel-ppp/radius-additions.xml.i
+++ b/interface-definitions/include/accel-ppp/radius-additions.xml.i
@@ -88,11 +88,7 @@
</properties>
<defaultValue>3</defaultValue>
</leafNode>
- <leafNode name="nas-identifier">
- <properties>
- <help>NAS-Identifier attribute sent to RADIUS</help>
- </properties>
- </leafNode>
+ #include <include/radius-nas-identifier.xml.i>
<leafNode name="nas-ip-address">
<properties>
<help>NAS-IP-Address attribute sent to RADIUS</help>
diff --git a/interface-definitions/include/auth-local-users.xml.i b/interface-definitions/include/auth-local-users.xml.i
new file mode 100644
index 000000000..8ef09554e
--- /dev/null
+++ b/interface-definitions/include/auth-local-users.xml.i
@@ -0,0 +1,22 @@
+<!-- include start from auth-local-users.xml.i -->
+<node name="local-users">
+ <properties>
+ <help>Local user authentication</help>
+ </properties>
+ <children>
+ <tagNode name="username">
+ <properties>
+ <help>Username used for authentication</help>
+ </properties>
+ <children>
+ #include <include/generic-disable-node.xml.i>
+ <leafNode name="password">
+ <properties>
+ <help>Password used for authentication</help>
+ </properties>
+ </leafNode>
+ </children>
+ </tagNode>
+ </children>
+</node>
+<!-- include end -->
diff --git a/interface-definitions/include/bgp/afi-l2vpn-common.xml.i b/interface-definitions/include/bgp/afi-l2vpn-common.xml.i
index 1673f25a5..aaa69e6c8 100644
--- a/interface-definitions/include/bgp/afi-l2vpn-common.xml.i
+++ b/interface-definitions/include/bgp/afi-l2vpn-common.xml.i
@@ -11,17 +11,6 @@
<valueless/>
</properties>
</leafNode>
-<leafNode name="rd">
- <properties>
- <help>Route Distinguisher</help>
- <valueHelp>
- <format>txt</format>
- <description>Route Distinguisher, (x.x.x.x:yyy|xxxx:yyyy)</description>
- </valueHelp>
- <constraint>
- <regex>^((25[0-5]|2[0-4][0-9]|[1][0-9][0-9]|[1-9][0-9]|[0-9]?)(\.(25[0-5]|2[0-4][0-9]|[1][0-9][0-9]|[1-9][0-9]|[0-9]?)){3}|[0-9]{1,10}):[0-9]{1,5}$</regex>
- </constraint>
- </properties>
-</leafNode>
+#include <include/bgp/route-distinguisher.xml.i>
#include <include/bgp/route-target.xml.i>
<!-- include end -->
diff --git a/interface-definitions/include/bgp/protocol-common-config.xml.i b/interface-definitions/include/bgp/protocol-common-config.xml.i
index 37fc7259f..552e85aa4 100644
--- a/interface-definitions/include/bgp/protocol-common-config.xml.i
+++ b/interface-definitions/include/bgp/protocol-common-config.xml.i
@@ -372,18 +372,7 @@
</constraint>
</properties>
<children>
- <leafNode name="rd">
- <properties>
- <help>Route Distinguisher</help>
- <valueHelp>
- <format>txt</format>
- <description>Route Distinguisher, asn:xxx</description>
- </valueHelp>
- <constraint>
- <regex>^[0-9]{1,10}:[0-9]{1,5}$</regex>
- </constraint>
- </properties>
- </leafNode>
+ #include <include/bgp/route-distinguisher.xml.i>
<leafNode name="label">
<properties>
<help>MPLS label value assigned to route</help>
@@ -772,18 +761,7 @@
</constraint>
</properties>
<children>
- <leafNode name="rd">
- <properties>
- <help>Route Distinguisher</help>
- <valueHelp>
- <format>txt</format>
- <description>Route Distinguisher, asn:xxx</description>
- </valueHelp>
- <constraint>
- <regex>^[0-9]{1,10}:[0-9]{1,5}$</regex>
- </constraint>
- </properties>
- </leafNode>
+ #include <include/bgp/route-distinguisher.xml.i>
<leafNode name="label">
<properties>
<help>MPLS label value assigned to route</help>
@@ -1038,6 +1016,12 @@
</leafNode>
#include <include/bgp/remote-as.xml.i>
#include <include/bgp/neighbor-shutdown.xml.i>
+ <leafNode name="solo">
+ <properties>
+ <help>Do not send back prefixes learned from the neighbor</help>
+ <valueless/>
+ </properties>
+ </leafNode>
<leafNode name="strict-capability-match">
<properties>
<help>Enable strict capability negotiation</help>
diff --git a/interface-definitions/include/bgp/route-distinguisher.xml.i b/interface-definitions/include/bgp/route-distinguisher.xml.i
new file mode 100644
index 000000000..fdfbe7076
--- /dev/null
+++ b/interface-definitions/include/bgp/route-distinguisher.xml.i
@@ -0,0 +1,14 @@
+<!-- include start from bgp/route-distinguisher.xml.i -->
+<leafNode name="rd">
+ <properties>
+ <help>Route Distinguisher</help>
+ <valueHelp>
+ <format>txt</format>
+ <description>Route Distinguisher, (x.x.x.x:yyy|xxxx:yyyy)</description>
+ </valueHelp>
+ <constraint>
+ <regex>^((25[0-5]|2[0-4][0-9]|[1][0-9][0-9]|[1-9][0-9]|[0-9]?)(\.(25[0-5]|2[0-4][0-9]|[1][0-9][0-9]|[1-9][0-9]|[0-9]?)){3}|[0-9]{1,10}):[0-9]{1,5}$</regex>
+ </constraint>
+ </properties>
+</leafNode>
+<!-- include end -->
diff --git a/interface-definitions/include/dhcp-interface.xml.i b/interface-definitions/include/dhcp-interface.xml.i
new file mode 100644
index 000000000..939b45f15
--- /dev/null
+++ b/interface-definitions/include/dhcp-interface.xml.i
@@ -0,0 +1,15 @@
+ <leafNode name="dhcp-interface">
+ <properties>
+ <help>DHCP interface supplying next-hop IP address</help>
+ <completionHelp>
+ <script>${vyos_completion_dir}/list_interfaces.py</script>
+ </completionHelp>
+ <valueHelp>
+ <format>txt</format>
+ <description>DHCP interface name</description>
+ </valueHelp>
+ <constraint>
+ <validator name="interface-name"/>
+ </constraint>
+ </properties>
+ </leafNode>
diff --git a/interface-definitions/include/interface/interface-eapol.xml.i b/interface-definitions/include/interface/interface-eapol.xml.i
index 92b7a3f35..270ec5b13 100644
--- a/interface-definitions/include/interface/interface-eapol.xml.i
+++ b/interface-definitions/include/interface/interface-eapol.xml.i
@@ -4,9 +4,8 @@
<help>Extensible Authentication Protocol over Local Area Network</help>
</properties>
<children>
- #include <include/certificate.xml.i>
- #include <include/certificate-ca.xml.i>
- #include <include/certificate-key.xml.i>
+ #include <include/pki/ca-certificate.xml.i>
+ #include <include/pki/certificate-key.xml.i>
</children>
</node>
<!-- include end -->
diff --git a/interface-definitions/include/ipsec/authentication-pre-shared-secret.xml.i b/interface-definitions/include/ipsec/authentication-pre-shared-secret.xml.i
new file mode 100644
index 000000000..af2669335
--- /dev/null
+++ b/interface-definitions/include/ipsec/authentication-pre-shared-secret.xml.i
@@ -0,0 +1,11 @@
+<!-- include start from ipsec/authentication-pre-shared-secret.xml.i -->
+<leafNode name="pre-shared-secret">
+ <properties>
+ <help>Pre-shared secret key</help>
+ <valueHelp>
+ <format>txt</format>
+ <description>Pre-shared secret key</description>
+ </valueHelp>
+ </properties>
+</leafNode>
+<!-- include end -->
diff --git a/interface-definitions/include/isis/protocol-common-config.xml.i b/interface-definitions/include/isis/protocol-common-config.xml.i
index 831d12694..af5a21f49 100644
--- a/interface-definitions/include/isis/protocol-common-config.xml.i
+++ b/interface-definitions/include/isis/protocol-common-config.xml.i
@@ -492,6 +492,61 @@
</node>
</children>
</node>
+ <node name="ipv6">
+ <properties>
+ <help>Redistribute IPv6 routes</help>
+ </properties>
+ <children>
+ <node name="bgp">
+ <properties>
+ <help>Redistribute BGP routes into IS-IS</help>
+ </properties>
+ <children>
+ #include <include/isis/redistribute-ipv6.xml.i>
+ </children>
+ </node>
+ <node name="connected">
+ <properties>
+ <help>Redistribute connected routes into IS-IS</help>
+ </properties>
+ <children>
+ #include <include/isis/redistribute-ipv6.xml.i>
+ </children>
+ </node>
+ <node name="kernel">
+ <properties>
+ <help>Redistribute kernel routes into IS-IS</help>
+ </properties>
+ <children>
+ #include <include/isis/redistribute-ipv6.xml.i>
+ </children>
+ </node>
+ <node name="ospf6">
+ <properties>
+ <help>Redistribute OSPFv3 routes into IS-IS</help>
+ </properties>
+ <children>
+ #include <include/isis/redistribute-ipv6.xml.i>
+ </children>
+ </node>
+ <node name="ripng">
+ <properties>
+ <help>Redistribute RIPng routes into IS-IS</help>
+ </properties>
+ <children>
+ #include <include/isis/redistribute-ipv6.xml.i>
+ </children>
+ </node>
+ <node name="static">
+ <properties>
+ <help>Redistribute static routes into IS-IS</help>
+ </properties>
+ <children>
+ #include <include/isis/redistribute-ipv6.xml.i>
+ </children>
+ </node>
+ </children>
+ </node>
</children>
</node>
<leafNode name="set-attached-bit">
@@ -711,4 +766,4 @@
</children>
</tagNode>
#include <include/route-map.xml.i>
-<!-- include end -->
+<!-- include end --> \ No newline at end of file
diff --git a/interface-definitions/include/isis/redistribute-ipv6.xml.i b/interface-definitions/include/isis/redistribute-ipv6.xml.i
new file mode 100644
index 000000000..7e679e38a
--- /dev/null
+++ b/interface-definitions/include/isis/redistribute-ipv6.xml.i
@@ -0,0 +1,42 @@
+<!-- include start from isis/redistribute-ipv6.xml.i -->
+<node name="level-1">
+ <properties>
+ <help>Redistribute into level-1</help>
+ </properties>
+ <children>
+ <leafNode name="metric">
+ <properties>
+ <help>Metric for redistributed routes</help>
+ <valueHelp>
+ <format>u32:0-16777215</format>
+ <description>ISIS default metric</description>
+ </valueHelp>
+ <constraint>
+ <validator name="numeric" argument="--range 0-16777215"/>
+ </constraint>
+ </properties>
+ </leafNode>
+ #include <include/route-map.xml.i>
+ </children>
+</node>
+<node name="level-2">
+ <properties>
+ <help>Redistribute into level-2</help>
+ </properties>
+ <children>
+ <leafNode name="metric">
+ <properties>
+ <help>Metric for redistributed routes</help>
+ <valueHelp>
+ <format>u32:0-16777215</format>
+ <description>ISIS default metric</description>
+ </valueHelp>
+ <constraint>
+ <validator name="numeric" argument="--range 0-16777215"/>
+ </constraint>
+ </properties>
+ </leafNode>
+ #include <include/route-map.xml.i>
+ </children>
+</node>
+<!-- include end --> \ No newline at end of file
diff --git a/interface-definitions/include/radius-nas-identifier.xml.i b/interface-definitions/include/radius-nas-identifier.xml.i
new file mode 100644
index 000000000..8e6933cc0
--- /dev/null
+++ b/interface-definitions/include/radius-nas-identifier.xml.i
@@ -0,0 +1,7 @@
+<!-- include start from radius-nas-identifier.xml.i -->
+<leafNode name="nas-identifier">
+ <properties>
+ <help>NAS-Identifier attribute sent to RADIUS</help>
+ </properties>
+</leafNode>
+<!-- include end -->
diff --git a/interface-definitions/include/static/static-route.xml.i b/interface-definitions/include/static/static-route.xml.i
index 254ea3163..21babc015 100644
--- a/interface-definitions/include/static/static-route.xml.i
+++ b/interface-definitions/include/static/static-route.xml.i
@@ -31,21 +31,7 @@
</leafNode>
</children>
</node>
- <leafNode name="dhcp-interface">
- <properties>
- <help>DHCP interface supplying next-hop IP address</help>
- <completionHelp>
- <script>${vyos_completion_dir}/list_interfaces.py</script>
- </completionHelp>
- <valueHelp>
- <format>txt</format>
- <description>DHCP interface name</description>
- </valueHelp>
- <constraint>
- <validator name="interface-name"/>
- </constraint>
- </properties>
- </leafNode>
+ #include <include/dhcp-interface.xml.i>
<tagNode name="interface">
<properties>
<help>Next-hop IPv4 router interface</help>
diff --git a/interface-definitions/interfaces-openvpn.xml.in b/interface-definitions/interfaces-openvpn.xml.in
index 681290570..7ff08ac86 100644
--- a/interface-definitions/interfaces-openvpn.xml.in
+++ b/interface-definitions/interfaces-openvpn.xml.in
@@ -637,16 +637,12 @@
</leafNode>
</children>
</node>
- <leafNode name="shared-secret-key-file">
+ <leafNode name="shared-secret-key">
<properties>
- <help>File containing the secret key shared with remote end of tunnel</help>
- <valueHelp>
- <format>filename</format>
- <description>File in /config/auth directory</description>
- </valueHelp>
- <constraint>
- <validator name="file-exists" argument="--directory /config/auth"/>
- </constraint>
+ <help>Secret key shared with remote end of tunnel</help>
+ <completionHelp>
+ <path>pki openvpn shared-secret</path>
+ </completionHelp>
</properties>
</leafNode>
<node name="tls">
@@ -654,55 +650,30 @@
<help>Transport Layer Security (TLS) options</help>
</properties>
<children>
- <leafNode name="auth-file">
- <properties>
- <help>File containing tls static key for tls-auth</help>
- <valueHelp>
- <format>filename</format>
- <description>File in /config/auth directory</description>
- </valueHelp>
- <constraint>
- <validator name="file-exists" argument="--directory /config/auth"/>
- </constraint>
- </properties>
- </leafNode>
- #include <include/certificate.xml.i>
- #include <include/certificate-ca.xml.i>
- <leafNode name="crl-file">
+ <leafNode name="auth-key">
<properties>
- <help>File containing certificate revocation list (CRL) for this host</help>
- <valueHelp>
- <format>filename</format>
- <description>File in /config/auth directory</description>
- </valueHelp>
- <constraint>
- <validator name="file-exists" argument="--directory /config/auth"/>
- </constraint>
+ <help>TLS shared secret key for tls-auth</help>
+ <completionHelp>
+ <path>pki openvpn shared-secret</path>
+ </completionHelp>
</properties>
</leafNode>
- <leafNode name="dh-file">
+ #include <include/pki/certificate.xml.i>
+ #include <include/pki/ca-certificate.xml.i>
+ <leafNode name="dh-params">
<properties>
- <help>File containing Diffie Hellman parameters (server only)</help>
- <valueHelp>
- <format>filename</format>
- <description>File in /config/auth directory</description>
- </valueHelp>
- <constraint>
- <validator name="file-exists" argument="--directory /config/auth"/>
- </constraint>
+ <help>Diffie Hellman parameters (server only)</help>
+ <completionHelp>
+ <path>pki dh</path>
+ </completionHelp>
</properties>
</leafNode>
- #include <include/certificate-key.xml.i>
- <leafNode name="crypt-file">
+ <leafNode name="crypt-key">
<properties>
- <help>File containing encryption key to authenticate control channel</help>
- <valueHelp>
- <format>filename</format>
- <description>File in /config/auth directory</description>
- </valueHelp>
- <constraint>
- <validator name="file-exists" argument="--directory /config/auth"/>
- </constraint>
+ <help>Static key to use to authenticate control channel</help>
+ <completionHelp>
+ <path>pki openvpn shared-secret</path>
+ </completionHelp>
</properties>
</leafNode>
<leafNode name="tls-version-min">
diff --git a/interface-definitions/interfaces-tunnel.xml.in b/interface-definitions/interfaces-tunnel.xml.in
index 56f8ea79c..b994bdafc 100644
--- a/interface-definitions/interfaces-tunnel.xml.in
+++ b/interface-definitions/interfaces-tunnel.xml.in
@@ -29,14 +29,7 @@
#include <include/interface/interface-ipv6-options.xml.i>
#include <include/source-address-ipv4-ipv6.xml.i>
#include <include/interface/tunnel-remote.xml.i>
- <leafNode name="source-interface">
- <properties>
- <help>Physical Interface used for underlaying traffic</help>
- <completionHelp>
- <script>${vyos_completion_dir}/list_interfaces.py</script>
- </completionHelp>
- </properties>
- </leafNode>
+ #include <include/source-interface.xml.i>
<leafNode name="6rd-prefix">
<properties>
<help>6rd network prefix</help>
@@ -61,21 +54,7 @@
</constraint>
</properties>
</leafNode>
- <leafNode name="dhcp-interface">
- <properties>
- <help>dhcp interface</help>
- <valueHelp>
- <format>interface</format>
- <description>DHCP interface that supplies the local IP address for this tunnel</description>
- </valueHelp>
- <completionHelp>
- <script>${vyos_completion_dir}/list_interfaces.py</script>
- </completionHelp>
- <constraint>
- <regex>^(en|eth|br|bond|gnv|vxlan|wg|tun)[0-9]+$</regex>
- </constraint>
- </properties>
- </leafNode>
+ #include <include/dhcp-interface.xml.i>
<leafNode name="encapsulation">
<properties>
<help>Encapsulation of this tunnel interface</help>
diff --git a/interface-definitions/interfaces-vxlan.xml.in b/interface-definitions/interfaces-vxlan.xml.in
index 7a286eaf2..56d01dfb6 100644
--- a/interface-definitions/interfaces-vxlan.xml.in
+++ b/interface-definitions/interfaces-vxlan.xml.in
@@ -92,6 +92,7 @@
#include <include/source-address-ipv4-ipv6.xml.i>
#include <include/source-interface.xml.i>
#include <include/interface/tunnel-remote.xml.i>
+ #include <include/interface/interface-vrf.xml.i>
#include <include/vni.xml.i>
</children>
</tagNode>
diff --git a/interface-definitions/interfaces-wireguard.xml.in b/interface-definitions/interfaces-wireguard.xml.in
index 378251fed..773bde09c 100644
--- a/interface-definitions/interfaces-wireguard.xml.in
+++ b/interface-definitions/interfaces-wireguard.xml.in
@@ -42,12 +42,12 @@
</leafNode>
<leafNode name="private-key">
<properties>
- <help>Private key to use on that interface</help>
- <completionHelp>
- <script>${vyos_op_scripts_dir}/wireguard.py --listkdir</script>
- </completionHelp>
+ <help>Base64 encoded private key</help>
+ <constraint>
+ <regex>[0-9a-zA-Z\+/]{43}=$</regex>
+ </constraint>
+ <constraintErrorMessage>Key is not valid 44-character (32-bytes) base64</constraintErrorMessage>
</properties>
- <defaultValue>default</defaultValue>
</leafNode>
<tagNode name="peer">
<properties>
@@ -59,7 +59,7 @@
</properties>
<children>
#include <include/generic-disable-node.xml.i>
- <leafNode name="pubkey">
+ <leafNode name="public-key">
<properties>
<help>base64 encoded public key</help>
<constraint>
diff --git a/interface-definitions/snmp.xml.in b/interface-definitions/snmp.xml.in
index f57103eac..2654449a1 100644
--- a/interface-definitions/snmp.xml.in
+++ b/interface-definitions/snmp.xml.in
@@ -129,6 +129,26 @@
<constraintErrorMessage>Location is limited to 255 characters or less</constraintErrorMessage>
</properties>
</leafNode>
+ <leafNode name="oid-enable">
+ <properties>
+ <help>Enable specific oids</help>
+ <valueHelp>
+ <format>txt</format>
+ <description>Enable specific oids</description>
+ </valueHelp>
+ <valueHelp>
+ <format>route-table</format>
+ <description>Enable route table oids (ipCidrRouteTable inetCidrRouteTable)</description>
+ </valueHelp>
+ <completionHelp>
+ <list>route-table</list>
+ </completionHelp>
+ <constraint>
+ <regex>^(route-table)$</regex>
+ </constraint>
+ <constraintErrorMessage>Oid must be 'route-table'</constraintErrorMessage>
+ </properties>
+ </leafNode>
<leafNode name="smux-peer">
<properties>
<help>Register a subtree for SMUX-based processing</help>
diff --git a/interface-definitions/vpn_ipsec.xml.in b/interface-definitions/vpn_ipsec.xml.in
index 147f351f2..b28c86ae6 100644
--- a/interface-definitions/vpn_ipsec.xml.in
+++ b/interface-definitions/vpn_ipsec.xml.in
@@ -11,18 +11,6 @@
<priority>901</priority>
</properties>
<children>
- <leafNode name="auto-update">
- <properties>
- <help>Set auto-update interval for IPsec daemon</help>
- <valueHelp>
- <format>u32:30-65535</format>
- <description>Auto-update interval (s)</description>
- </valueHelp>
- <constraint>
- <validator name="numeric" argument="--range 30-65535"/>
- </constraint>
- </properties>
- </leafNode>
<leafNode name="disable-uniqreqids">
<properties>
<help>Option to disable requirement for unique IDs in the Security Database</help>
@@ -52,6 +40,7 @@
<regex>^(disable|enable)$</regex>
</constraint>
</properties>
+ <defaultValue>disable</defaultValue>
</leafNode>
<leafNode name="lifetime">
<properties>
@@ -394,7 +383,6 @@
</properties>
<children>
<leafNode name="dh-group">
- <defaultValue>2</defaultValue>
<properties>
<help>dh-grouphelp</help>
<completionHelp>
@@ -492,6 +480,7 @@
<regex>^(1|2|5|14|15|16|17|18|19|20|21|22|23|24|25|26|27|28|29|30|31|32)$</regex>
</constraint>
</properties>
+ <defaultValue>2</defaultValue>
</leafNode>
#include <include/vpn-ipsec-encryption.xml.i>
#include <include/vpn-ipsec-hash.xml.i>
@@ -509,22 +498,15 @@
<help>Sets to include an additional secrets file for strongSwan. Use an absolute path to specify the included file.</help>
</properties>
</leafNode>
- <node name="ipsec-interfaces">
+ <leafNode name="interface">
<properties>
- <help>Interface to use for VPN [REQUIRED]</help>
+ <help>Onterface used for IPsec communication</help>
+ <completionHelp>
+ <script>${vyos_completion_dir}/list_interfaces.py</script>
+ </completionHelp>
+ <multi/>
</properties>
- <children>
- <leafNode name="interface">
- <properties>
- <help>IPsec interface [REQUIRED]</help>
- <completionHelp>
- <script>${vyos_completion_dir}/list_interfaces.py</script>
- </completionHelp>
- <multi/>
- </properties>
- </leafNode>
- </children>
- </node>
+ </leafNode>
<node name="log">
<properties>
<help>IPsec logging</help>
@@ -648,37 +630,6 @@
<valueless/>
</properties>
</leafNode>
- <node name="remote-access">
- <properties>
- <help>remote-access global options</help>
- </properties>
- <children>
- <node name="dhcp-pool">
- <properties>
- <help>DHCP pool options for remote-access</help>
- </properties>
- <children>
- <leafNode name="interface">
- <properties>
- <help>Interface with DHCP server to use</help>
- <completionHelp>
- <script>${vyos_completion_dir}/list_interfaces.py</script>
- </completionHelp>
- </properties>
- </leafNode>
- <leafNode name="server">
- <properties>
- <help>DHCP server address</help>
- <valueHelp>
- <format>ipv4</format>
- <description>IPv4 address of the DHCP server</description>
- </valueHelp>
- </properties>
- </leafNode>
- </children>
- </node>
- </children>
- </node>
</children>
</node>
<tagNode name="profile">
@@ -704,15 +655,7 @@
</valueHelp>
</properties>
</leafNode>
- <leafNode name="pre-shared-secret">
- <properties>
- <help>Pre-shared secret key</help>
- <valueHelp>
- <format>txt</format>
- <description>Pre-shared secret key</description>
- </valueHelp>
- </properties>
- </leafNode>
+ #include <include/ipsec/authentication-pre-shared-secret.xml.i>
</children>
</node>
<node name="bind">
@@ -739,102 +682,161 @@
#include <include/ipsec/ike-group.xml.i>
</children>
</tagNode>
- <tagNode name="remote-access">
+ <node name="remote-access">
<properties>
- <help>Remote access IKEv2 VPN </help>
+ <help>IKEv2 remote access VPN</help>
</properties>
<children>
- <node name="authentication">
+ <tagNode name="connection">
<properties>
- <help>Authentication for remote access</help>
+ <help>IKEv2 VPN connection name</help>
</properties>
<children>
- #include <include/ipsec/authentication-id.xml.i>
- #include <include/ipsec/authentication-x509.xml.i>
- <leafNode name="client-mode">
+ <node name="authentication">
<properties>
- <help>Client authentication mode</help>
- <completionHelp>
- <list>eap-tls eap-mschapv2</list>
- </completionHelp>
- <valueHelp>
- <format>eap-tls</format>
- <description>EAP-TLS</description>
- </valueHelp>
+ <help>Authentication for remote access</help>
+ </properties>
+ <children>
+ #include <include/ipsec/authentication-id.xml.i>
+ #include <include/ipsec/authentication-x509.xml.i>
+ <leafNode name="client-mode">
+ <properties>
+ <help>Client authentication mode</help>
+ <completionHelp>
+ <list>eap-tls eap-mschapv2 eap-radius</list>
+ </completionHelp>
+ <valueHelp>
+ <format>eap-tls</format>
+ <description>Client uses EAP-TLS authentication</description>
+ </valueHelp>
+ <valueHelp>
+ <format>eap-mschapv2</format>
+ <description>Client uses EAP-MSCHAPv2 authentication</description>
+ </valueHelp>
+ <valueHelp>
+ <format>eap-radius</format>
+ <description>Client uses EAP-RADIUS authentication</description>
+ </valueHelp>
+ <constraint>
+ <regex>^(eap-tls|eap-mschapv2|eap-radius)$</regex>
+ </constraint>
+ </properties>
+ <defaultValue>eap-mschapv2</defaultValue>
+ </leafNode>
+ #include <include/auth-local-users.xml.i>
+ <leafNode name="server-mode">
+ <properties>
+ <help>Server authentication mode</help>
+ <completionHelp>
+ <list>pre-shared-secret x509</list>
+ </completionHelp>
+ <valueHelp>
+ <format>pre-shared-secret</format>
+ <description>pre-shared-secret_description</description>
+ </valueHelp>
+ <valueHelp>
+ <format>x509</format>
+ <description>x509_description</description>
+ </valueHelp>
+ <constraint>
+ <regex>^(pre-shared-secret|x509)$</regex>
+ </constraint>
+ </properties>
+ <defaultValue>x509</defaultValue>
+ </leafNode>
+ #include <include/ipsec/authentication-pre-shared-secret.xml.i>
+ </children>
+ </node>
+ #include <include/generic-description.xml.i>
+ #include <include/generic-disable-node.xml.i>
+ #include <include/ipsec/esp-group.xml.i>
+ #include <include/ipsec/ike-group.xml.i>
+ #include <include/ipsec/local-address.xml.i>
+ #include <include/ipsec/local-traffic-selector.xml.i>
+ <leafNode name="timeout">
+ <properties>
+ <help>Timeout to close connection if no data is transmitted</help>
<valueHelp>
- <format>eap-mschapv2</format>
- <description>EAP-MSCHAPv2</description>
+ <format>u32:10-86400</format>
+ <description>Timeout in seconds (default 28800)</description>
</valueHelp>
<constraint>
- <regex>^(eap-tls|eap-mschapv2)$</regex>
+ <validator name="numeric" argument="--range 10-86400"/>
</constraint>
</properties>
- <defaultValue>eap-mschapv2</defaultValue>
+ <defaultValue>28800</defaultValue>
</leafNode>
- <node name="local-users">
+ <leafNode name="pool">
<properties>
- <help>Local user authentication for PPPoE server</help>
+ <help>Pool name used for IP address assignments</help>
+ <completionHelp>
+ <path>vpn ipsec remote-access pool</path>
+ <list>dhcp</list>
+ </completionHelp>
+ <valueHelp>
+ <format>txt</format>
+ <description>Pool name</description>
+ </valueHelp>
+ <multi/>
</properties>
- <children>
- <tagNode name="username">
- <properties>
- <help>User name for authentication</help>
- </properties>
- <children>
- #include <include/generic-disable-node.xml.i>
- <leafNode name="password">
- <properties>
- <help>Password for authentication</help>
- </properties>
- </leafNode>
- </children>
- </tagNode>
- </children>
- </node>
- <leafNode name="server-mode">
+ </leafNode>
+ <leafNode name="unique">
<properties>
- <help>Server authentication mode</help>
+ <help>Connection uniqueness policy to enforce</help>
<completionHelp>
- <list>pre-shared-secret x509</list>
+ <list>never keep replace</list>
</completionHelp>
<valueHelp>
- <format>pre-shared-secret</format>
- <description>pre-shared-secret_description</description>
+ <format>never</format>
+ <description>Never enforce connection uniqueness policy</description>
</valueHelp>
<valueHelp>
- <format>x509</format>
- <description>x509_description</description>
+ <format>keep</format>
+ <description>Rejects new connection attempts if the same user already has an active connection</description>
+ </valueHelp>
+ <valueHelp>
+ <format>replace</format>
+ <description>Delete any existing connection if a new one for the same user gets established</description>
</valueHelp>
<constraint>
- <regex>^(pre-shared-secret|x509)$</regex>
+ <regex>^(never|keep|replace)$</regex>
</constraint>
</properties>
- <defaultValue>x509</defaultValue>
</leafNode>
- <leafNode name="pre-shared-secret">
+ </children>
+ </tagNode>
+ <node name="dhcp">
+ <properties>
+ <help>DHCP pool options for remote-access</help>
+ </properties>
+ <children>
+ <leafNode name="interface">
+ <properties>
+ <help>Interface with DHCP server to use</help>
+ <completionHelp>
+ <script>${vyos_completion_dir}/list_interfaces.py</script>
+ </completionHelp>
+ </properties>
+ </leafNode>
+ <leafNode name="server">
<properties>
- <help>Pre-shared-secret used for server authentication</help>
+ <help>DHCP server address</help>
+ <valueHelp>
+ <format>ipv4</format>
+ <description>DHCP server IPv4 address</description>
+ </valueHelp>
+ <constraint>
+ <validator name="ipv4-address"/>
+ </constraint>
</properties>
</leafNode>
</children>
</node>
- #include <include/generic-description.xml.i>
- #include <include/generic-disable-node.xml.i>
- #include <include/ipsec/esp-group.xml.i>
- #include <include/ipsec/ike-group.xml.i>
- #include <include/ipsec/local-address.xml.i>
- #include <include/ipsec/local-traffic-selector.xml.i>
- <node name="pool">
+ <tagNode name="pool">
<properties>
<help>IP address pool for remote-access users</help>
</properties>
<children>
- <leafNode name="dhcp-enable">
- <properties>
- <help>Enable DHCP pool for clients on this connection</help>
- <valueless/>
- </properties>
- </leafNode>
<leafNode name="exclude">
<properties>
<help>Local IPv4 or IPv6 pool prefix exclusions</help>
@@ -873,22 +875,20 @@
<!-- Include Accel-PPP definition here, maybe time for a rename? -->
#include <include/accel-ppp/name-server.xml.i>
</children>
+ </tagNode>
+ #include <include/radius-server-ipv4.xml.i>
+ <node name="radius">
+ <children>
+ #include <include/radius-nas-identifier.xml.i>
+ <tagNode name="server">
+ <children>
+ #include <include/accel-ppp/radius-additions-disable-accounting.xml.i>
+ </children>
+ </tagNode>
+ </children>
</node>
- <leafNode name="timeout">
- <properties>
- <help>Timeout to close connection if no data is transmitted</help>
- <valueHelp>
- <format>u32:10-86400</format>
- <description>Timeout in seconds (default 28800)</description>
- </valueHelp>
- <constraint>
- <validator name="numeric" argument="--range 10-86400"/>
- </constraint>
- </properties>
- <defaultValue>28800</defaultValue>
- </leafNode>
</children>
- </tagNode>
+ </node>
<node name="site-to-site">
<properties>
<help>Site-to-site VPN</help>
@@ -947,15 +947,7 @@
</constraint>
</properties>
</leafNode>
- <leafNode name="pre-shared-secret">
- <properties>
- <help>Pre-shared secret key</help>
- <valueHelp>
- <format>txt</format>
- <description>Pre-shared secret key</description>
- </valueHelp>
- </properties>
- </leafNode>
+ #include <include/ipsec/authentication-pre-shared-secret.xml.i>
<leafNode name="remote-id">
<properties>
<help>ID for remote authentication</help>
@@ -1001,14 +993,7 @@
</properties>
</leafNode>
#include <include/generic-description.xml.i>
- <leafNode name="dhcp-interface">
- <properties>
- <help>DHCP interface to listen on</help>
- <completionHelp>
- <script>${vyos_completion_dir}/list_interfaces.py</script>
- </completionHelp>
- </properties>
- </leafNode>
+ #include <include/dhcp-interface.xml.i>
<leafNode name="force-encapsulation">
<properties>
<help>Force UDP Encapsulation for ESP Payloads</help>
diff --git a/interface-definitions/vpn_l2tp.xml.in b/interface-definitions/vpn_l2tp.xml.in
index 4fbf3fa44..cf31af70f 100644
--- a/interface-definitions/vpn_l2tp.xml.in
+++ b/interface-definitions/vpn_l2tp.xml.in
@@ -70,51 +70,8 @@
</completionHelp>
</properties>
</leafNode>
- <leafNode name="pre-shared-secret">
- <properties>
- <help>Pre-shared secret for IPsec</help>
- </properties>
- </leafNode>
- <node name="x509">
- <properties>
- <help>X.509 certificate</help>
- </properties>
- <children>
- #include <include/certificate-ca.xml.i>
- <leafNode name="crl-file">
- <properties>
- <help>File containing the X.509 Certificate Revocation List (CRL)</help>
- <valueHelp>
- <format>txt</format>
- <description>File in /config/auth</description>
- </valueHelp>
- </properties>
- </leafNode>
- <leafNode name="server-cert-file">
- <properties>
- <help>File containing the X.509 certificate for the remote access VPN server (this host)</help>
- <valueHelp>
- <format>txt</format>
- <description>File in /config/auth</description>
- </valueHelp>
- </properties>
- </leafNode>
- <leafNode name="server-key-file">
- <properties>
- <help>File containing the private key for the X.509 certificate for the remote access VPN server (this host)</help>
- <valueHelp>
- <format>txt</format>
- <description>File in /config/auth</description>
- </valueHelp>
- </properties>
- </leafNode>
- <leafNode name="server-key-password">
- <properties>
- <help>Password that protects the private key</help>
- </properties>
- </leafNode>
- </children>
- </node>
+ #include <include/ipsec/authentication-pre-shared-secret.xml.i>
+ #include <include/ipsec/authentication-x509.xml.i>
</children>
</node>
<leafNode name="ike-lifetime">
@@ -128,6 +85,7 @@
<validator name="numeric" argument="--range 30-86400"/>
</constraint>
</properties>
+ <defaultValue>3600</defaultValue>
</leafNode>
<leafNode name="lifetime">
<properties>
@@ -140,7 +98,10 @@
<validator name="numeric" argument="--range 30-86400"/>
</constraint>
</properties>
+ <defaultValue>3600</defaultValue>
</leafNode>
+ #include <include/ipsec/esp-group.xml.i>
+ #include <include/ipsec/ike-group.xml.i>
</children>
</node>
#include <include/accel-ppp/wins-server.xml.i>
@@ -159,11 +120,7 @@
<help>Description for L2TP remote-access settings</help>
</properties>
</leafNode>
- <leafNode name="dhcp-interface">
- <properties>
- <help>DHCP interface to listen on</help>
- </properties>
- </leafNode>
+ #include <include/dhcp-interface.xml.i>
<leafNode name="idle">
<properties>
<help>PPP idle timeout</help>
@@ -248,11 +205,7 @@
<help>Maximum number of tries to send Access-Request/Accounting-Request queries</help>
</properties>
</leafNode>
- <leafNode name="nas-identifier">
- <properties>
- <help>Value to send to RADIUS server in NAS-Identifier attribute and to be matched in DM/CoA requests.</help>
- </properties>
- </leafNode>
+ #include <include/radius-nas-identifier.xml.i>
<node name="dae-server">
<properties>
<help>IPv4 address and port to bind Dynamic Authorization Extension server (DM/CoA)</help>
diff --git a/interface-definitions/vpn_openconnect.xml.in b/interface-definitions/vpn_openconnect.xml.in
index 1a9d39a12..a33ff67ea 100644
--- a/interface-definitions/vpn_openconnect.xml.in
+++ b/interface-definitions/vpn_openconnect.xml.in
@@ -32,26 +32,7 @@
</completionHelp>
</properties>
</leafNode>
- <node name="local-users">
- <properties>
- <help>Local user authentication for SSL VPN server</help>
- </properties>
- <children>
- <tagNode name="username">
- <properties>
- <help>User name for authentication</help>
- </properties>
- <children>
- #include <include/generic-disable-node.xml.i>
- <leafNode name="password">
- <properties>
- <help>Password for authentication</help>
- </properties>
- </leafNode>
- </children>
- </tagNode>
- </children>
- </node>
+ #include <include/auth-local-users.xml.i>
#include <include/radius-server-ipv4.xml.i>
<node name="radius">
<children>
@@ -75,7 +56,7 @@
</node>
<node name="listen-ports">
<properties>
- <help>SSL Certificate, SSL Key and CA (/config/auth)</help>
+ <help>Specify custom ports to use for client connections</help>
</properties>
<children>
<leafNode name="tcp">
@@ -108,12 +89,11 @@
</node>
<node name="ssl">
<properties>
- <help>SSL Certificate, SSL Key and CA (/config/auth)</help>
+ <help>SSL Certificate, SSL Key and CA</help>
</properties>
<children>
- #include <include/certificate.xml.i>
- #include <include/certificate-ca.xml.i>
- #include <include/certificate-key.xml.i>
+ #include <include/pki/ca-certificate.xml.i>
+ #include <include/pki/certificate-key.xml.i>
</children>
</node>
<node name="network-settings">
diff --git a/interface-definitions/vpn_sstp.xml.in b/interface-definitions/vpn_sstp.xml.in
index c09603028..3576bac90 100644
--- a/interface-definitions/vpn_sstp.xml.in
+++ b/interface-definitions/vpn_sstp.xml.in
@@ -50,12 +50,11 @@
</node>
<node name="ssl">
<properties>
- <help>SSL Certificate, SSL Key and CA (/config/user-data/sstp)</help>
+ <help>SSL Certificate, SSL Key and CA</help>
</properties>
<children>
- #include <include/certificate.xml.i>
- #include <include/certificate-ca.xml.i>
- #include <include/certificate-key.xml.i>
+ #include <include/pki/ca-certificate.xml.i>
+ #include <include/pki/certificate.xml.i>
</children>
</node>
</children>
diff --git a/interface-definitions/vrf.xml.in b/interface-definitions/vrf.xml.in
index 426884a11..9d513945c 100644
--- a/interface-definitions/vrf.xml.in
+++ b/interface-definitions/vrf.xml.in
@@ -19,7 +19,7 @@
<constraint>
<validator name="vrf-name"/>
</constraint>
- <constraintErrorMessage>VRF instance name must be 15 characters or less and can not\nbe named as regular network interfaces.\n</constraintErrorMessage>
+ <constraintErrorMessage>VRF instance name must be 15 characters or less and can not\nbe named as regular network interfaces.\nA name must starts from a letter.\n</constraintErrorMessage>
<valueHelp>
<format>txt</format>
<description>VRF instance name</description>
@@ -76,13 +76,13 @@
<properties>
<help>Routing table associated with this instance</help>
<valueHelp>
- <format>100-2147483647</format>
+ <format>100-65535</format>
<description>Routing table ID</description>
</valueHelp>
<constraint>
- <validator name="numeric" argument="--range 100-2147483647"/>
+ <validator name="numeric" argument="--range 100-65535"/>
</constraint>
- <constraintErrorMessage>VRF routing table must be in range from 100 to 2147483647</constraintErrorMessage>
+ <constraintErrorMessage>VRF routing table must be in range from 100 to 65535</constraintErrorMessage>
</properties>
</leafNode>
#include <include/vni.xml.i>
diff --git a/op-mode-definitions/generate-ipsec-profile.xml.in b/op-mode-definitions/generate-ipsec-profile.xml.in
new file mode 100644
index 000000000..be9227971
--- /dev/null
+++ b/op-mode-definitions/generate-ipsec-profile.xml.in
@@ -0,0 +1,145 @@
+<?xml version="1.0"?>
+<interfaceDefinition>
+ <node name="generate">
+ <children>
+ <node name="ipsec">
+ <properties>
+ <help>Generate IPsec related configurations</help>
+ </properties>
+ <children>
+ <node name="profile">
+ <properties>
+ <help>Generate IKEv2 IPSec remote-access VPN profiles</help>
+ </properties>
+ <children>
+ <tagNode name="ios-remote-access">
+ <properties>
+ <help>Generate iOS profile for specified remote-access connection name</help>
+ <completionHelp>
+ <path>vpn ipsec remote-access connection</path>
+ </completionHelp>
+ </properties>
+ <children>
+ <tagNode name="remote">
+ <properties>
+ <help>Remote address where the client will connect to</help>
+ <completionHelp>
+ <list>&lt;fqdn&gt;</list>
+ <script>${vyos_completion_dir}/list_local_ips.sh --both</script>
+ </completionHelp>
+ </properties>
+ <command>${vyos_op_scripts_dir}/ikev2_profile_generator.py --os ios --connection "$5" --remote "$7"</command>
+ <children>
+ <tagNode name="name">
+ <properties>
+ <help>Connection name as seen in the VPN application</help>
+ <completionHelp>
+ <list>&lt;name&gt;</list>
+ </completionHelp>
+ </properties>
+ <command>${vyos_op_scripts_dir}/ikev2_profile_generator.py --os ios --connection "$5" --remote "$7" --name "$9"</command>
+ <children>
+ <tagNode name="profile">
+ <properties>
+ <help>Profile name as seen under system profiles</help>
+ <completionHelp>
+ <list>&lt;name&gt;</list>
+ </completionHelp>
+ </properties>
+ <command>${vyos_op_scripts_dir}/ikev2_profile_generator.py --os ios --connection "$5" --remote "$7" --name "$9" --profile "${11}"</command>
+ </tagNode>
+ </children>
+ </tagNode>
+ <tagNode name="profile">
+ <properties>
+ <help>Profile name as seen under system profiles</help>
+ <completionHelp>
+ <list>&lt;name&gt;</list>
+ </completionHelp>
+ </properties>
+ <command>${vyos_op_scripts_dir}/ikev2_profile_generator.py --os ios --connection "$5" --remote "$7" --profile "$9"</command>
+ <children>
+ <tagNode name="name">
+ <properties>
+ <help>Connection name as seen in the VPN application</help>
+ <completionHelp>
+ <list>&lt;name&gt;</list>
+ </completionHelp>
+ </properties>
+ <command>${vyos_op_scripts_dir}/ikev2_profile_generator.py --os ios --connection "$5" --remote "$7" --profile "$9" --name "${11}"</command>
+ </tagNode>
+ </children>
+ </tagNode>
+ </children>
+ </tagNode>
+ </children>
+ </tagNode>
+ <tagNode name="windows-remote-access">
+ <properties>
+ <help>Generate iOS profile for specified remote-access connection name</help>
+ <completionHelp>
+ <path>vpn ipsec remote-access connection</path>
+ </completionHelp>
+ </properties>
+ <children>
+ <tagNode name="remote">
+ <properties>
+ <help>Remote address where the client will connect to</help>
+ <completionHelp>
+ <list>&lt;fqdn&gt;</list>
+ <script>${vyos_completion_dir}/list_local_ips.sh --both</script>
+ </completionHelp>
+ </properties>
+ <command>${vyos_op_scripts_dir}/ikev2_profile_generator.py --os windows --connection "$5" --remote "$7"</command>
+ <children>
+ <tagNode name="name">
+ <properties>
+ <help>Connection name as seen in the VPN application</help>
+ <completionHelp>
+ <list>&lt;name&gt;</list>
+ </completionHelp>
+ </properties>
+ <command>${vyos_op_scripts_dir}/ikev2_profile_generator.py --os windows --connection "$5" --remote "$7" --name "$9"</command>
+ <children>
+ <tagNode name="profile">
+ <properties>
+ <help>Profile name as seen under system profiles</help>
+ <completionHelp>
+ <list>&lt;name&gt;</list>
+ </completionHelp>
+ </properties>
+ <command>${vyos_op_scripts_dir}/ikev2_profile_generator.py --os windows --connection "$5" --remote "$7" --name "$9" --profile "${11}"</command>
+ </tagNode>
+ </children>
+ </tagNode>
+ <tagNode name="profile">
+ <properties>
+ <help>Profile name as seen under system profiles</help>
+ <completionHelp>
+ <list>&lt;name&gt;</list>
+ </completionHelp>
+ </properties>
+ <command>${vyos_op_scripts_dir}/ikev2_profile_generator.py --os windows --connection "$5" --remote "$7" --profile "$9"</command>
+ <children>
+ <tagNode name="name">
+ <properties>
+ <help>Connection name as seen in the VPN application</help>
+ <completionHelp>
+ <list>&lt;name&gt;</list>
+ </completionHelp>
+ </properties>
+ <command>${vyos_op_scripts_dir}/ikev2_profile_generator.py --os windows --connection "$5" --remote "$7" --profile "$9" --name "${11}"</command>
+ </tagNode>
+ </children>
+ </tagNode>
+ </children>
+ </tagNode>
+ </children>
+ </tagNode>
+ </children>
+ </node>
+ </children>
+ </node>
+ </children>
+ </node>
+</interfaceDefinition>
diff --git a/op-mode-definitions/generate-wireguard.xml.in b/op-mode-definitions/generate-wireguard.xml.in
new file mode 100644
index 000000000..6557b463b
--- /dev/null
+++ b/op-mode-definitions/generate-wireguard.xml.in
@@ -0,0 +1,72 @@
+<?xml version="1.0"?>
+<interfaceDefinition>
+ <node name="generate">
+ <children>
+ <node name="wireguard">
+ <properties>
+ <help>Generate Wireguard keys</help>
+ </properties>
+ <children>
+ <tagNode name="client-config">
+ <properties>
+ <help>Generate Client config QR code</help>
+ <completionHelp>
+ <list>&lt;client-name&gt;</list>
+ </completionHelp>
+ </properties>
+ <children>
+ <tagNode name="interface">
+ <properties>
+ <help>Local interface used for connection</help>
+ <completionHelp>
+ <script>${vyos_completion_dir}/list_interfaces.py --type wireguard</script>
+ </completionHelp>
+ </properties>
+ <children>
+ <tagNode name="server">
+ <properties>
+ <help>IP address/FQDN used for client connection</help>
+ <completionHelp>
+ <script>${vyos_completion_dir}/list_local_ips.sh --both</script>
+ <list>&lt;hostname&gt;</list>
+ </completionHelp>
+ </properties>
+ <command>sudo ${vyos_op_scripts_dir}/wireguard_client.py --name "$4" --interface "$6" --server "$8"</command>
+ <children>
+ <tagNode name="address">
+ <properties>
+ <help>IPv4/IPv6 address used by client</help>
+ <completionHelp>
+ <list>&lt;x.x.x.x&gt; &lt;h:h:h:h:h:h:h:h&gt;</list>
+ </completionHelp>
+ </properties>
+ <command>sudo ${vyos_op_scripts_dir}/wireguard_client.py --name "$4" --interface "$6" --server "$8" --address "${10}"</command>
+ <children>
+ <tagNode name="address">
+ <properties>
+ <help>IPv4/IPv6 address used by client</help>
+ <completionHelp>
+ <list>&lt;x.x.x.x&gt; &lt;h:h:h:h:h:h:h:h&gt;</list>
+ </completionHelp>
+ </properties>
+ <command>sudo ${vyos_op_scripts_dir}/wireguard_client.py --name "$4" --interface "$6" --server "$8" --address "${10}" --address "${12}"</command>
+ </tagNode>
+ </children>
+ </tagNode>
+ </children>
+ </tagNode>
+ </children>
+ </tagNode>
+ </children>
+ </tagNode>
+ <leafNode name="key-pair">
+ <properties>
+ <help>Generate Wireguard key pair for use with server or peer</help>
+ </properties>
+ <command>sudo ${vyos_op_scripts_dir}/pki.py --action generate --wireguard --key "noname"</command>
+ </leafNode>
+ </children>
+ </node>
+ </children>
+ </node>
+</interfaceDefinition>
diff --git a/op-mode-definitions/include/bgp/afi-common.xml.i b/op-mode-definitions/include/bgp/afi-common.xml.i
index e48482282..7fc59f3b0 100644
--- a/op-mode-definitions/include/bgp/afi-common.xml.i
+++ b/op-mode-definitions/include/bgp/afi-common.xml.i
@@ -31,10 +31,25 @@
</properties>
<command>${vyos_op_scripts_dir}/vtysh_wrapper.sh $@</command>
</leafNode>
-<leafNode name="summary">
+<node name="summary">
<properties>
<help>Summary of BGP neighbor status</help>
</properties>
<command>${vyos_op_scripts_dir}/vtysh_wrapper.sh $@</command>
-</leafNode>
+ <children>
+ <leafNode name="established">
+ <properties>
+ <help>Show only sessions in Established state</help>
+ </properties>
+ <command>${vyos_op_scripts_dir}/vtysh_wrapper.sh $@</command>
+ </leafNode>
+ <leafNode name="failed">
+ <properties>
+ <help>Show only sessions not in Established state</help>
+ </properties>
+ <command>${vyos_op_scripts_dir}/vtysh_wrapper.sh $@</command>
+ </leafNode>
+ </children>
+</node>
+#include <include/vtysh-generic-wide.xml.i>
<!-- included end -->
diff --git a/op-mode-definitions/include/vtysh-generic-wide.xml.i b/op-mode-definitions/include/vtysh-generic-wide.xml.i
new file mode 100644
index 000000000..acc68b4c0
--- /dev/null
+++ b/op-mode-definitions/include/vtysh-generic-wide.xml.i
@@ -0,0 +1,8 @@
+<!-- included start from vtysh-generic-wide.xml.i -->
+<leafNode name="wide">
+ <properties>
+ <help>Increase table width for longer prefixes</help>
+ </properties>
+ <command>${vyos_op_scripts_dir}/vtysh_wrapper.sh $@</command>
+</leafNode>
+<!-- included end -->
diff --git a/op-mode-definitions/monitor-bridge.xml.in b/op-mode-definitions/monitor-bridge.xml.in
new file mode 100644
index 000000000..712a924f1
--- /dev/null
+++ b/op-mode-definitions/monitor-bridge.xml.in
@@ -0,0 +1,33 @@
+<?xml version="1.0"?>
+<interfaceDefinition>
+ <node name="monitor">
+ <children>
+ <node name="bridge">
+ <properties>
+ <help>Monitoring bridge database generated objects and address changes</help>
+ </properties>
+ <command>sudo bridge monitor all</command>
+ <children>
+ <node name="link">
+ <command>sudo bridge monitor link</command>
+ <properties>
+ <help>Monitoring bridge database generated connection interface changes</help>
+ </properties>
+ </node>
+ <node name="fdb">
+ <command>sudo bridge monitor fdb</command>
+ <properties>
+ <help>Monitor the forwarding database changes generated by the bridge database</help>
+ </properties>
+ </node>
+ <node name="mdb">
+ <command>sudo bridge monitor mdb</command>
+ <properties>
+ <help>Monitor the multicast database changes generated by the bridge database</help>
+ </properties>
+ </node>
+ </children>
+ </node>
+ </children>
+ </node>
+</interfaceDefinition>
diff --git a/op-mode-definitions/openvpn.xml.in b/op-mode-definitions/openvpn.xml.in
index f8dc0cff0..781fbdc9d 100644
--- a/op-mode-definitions/openvpn.xml.in
+++ b/op-mode-definitions/openvpn.xml.in
@@ -1,49 +1,5 @@
<?xml version="1.0"?>
<interfaceDefinition>
- <node name="generate">
- <children>
- <node name="openvpn">
- <properties>
- <help>OpenVPN key generation tool</help>
- </properties>
- <children>
- <tagNode name="key">
- <properties>
- <help>Generate shared-secret key with specified file name</help>
- <completionHelp>
- <list>&lt;filename&gt;</list>
- </completionHelp>
- </properties>
- <command>
- result=1;
- key_path=$4
- full_path=
-
- if echo $key_path | egrep -ve '^/.*' &gt; /dev/null; then
- full_path=/config/auth/$key_path
- else
- full_path=$key_path
- fi
-
- key_dir=`dirname $full_path`
- if [ ! -d $key_dir ]; then
- echo "Directory $key_dir does not exist!"
- exit 1
- fi
-
- echo "Generating OpenVPN key to $full_path"
- sudo /usr/sbin/openvpn --genkey secret "$full_path"
- result=$?
- if [ $result = 0 ]; then
- echo "Your new local OpenVPN key has been generated"
- fi
- /usr/libexec/vyos/validators/file-exists --directory /config/auth "$full_path"
- </command>
- </tagNode>
- </children>
- </node>
- </children>
- </node>
<node name="reset">
<properties>
<help>Reset a service</help>
diff --git a/op-mode-definitions/pki.xml.in b/op-mode-definitions/pki.xml.in
index 9c6b56a68..a11814c8a 100644
--- a/op-mode-definitions/pki.xml.in
+++ b/op-mode-definitions/pki.xml.in
@@ -20,9 +20,18 @@
</completionHelp>
</properties>
<children>
+ <tagNode name="file">
+ <properties>
+ <help>Write generated CA certificate into the specified filename</help>
+ <completionHelp>
+ <list>&lt;filename&gt;</list>
+ </completionHelp>
+ </properties>
+ <command>sudo ${vyos_op_scripts_dir}/pki.py --action generate --ca "$7" --sign "$5" --file</command>
+ </tagNode>
<tagNode name="install">
<properties>
- <help>Commands for installing generated certificate into running configuration</help>
+ <help>Commands for installing generated CA certificate into running configuration</help>
<completionHelp>
<list>&lt;certificate name&gt;</list>
</completionHelp>
@@ -32,9 +41,18 @@
</children>
<command>sudo ${vyos_op_scripts_dir}/pki.py --action generate --ca "noname" --sign "$5"</command>
</tagNode>
+ <tagNode name="file">
+ <properties>
+ <help>Write generated CA certificate into the specified filename</help>
+ <completionHelp>
+ <list>&lt;filename&gt;</list>
+ </completionHelp>
+ </properties>
+ <command>sudo ${vyos_op_scripts_dir}/pki.py --action generate --ca "$5" --file</command>
+ </tagNode>
<tagNode name="install">
<properties>
- <help>Commands for installing generated certificate into running configuration</help>
+ <help>Commands for installing generated CA certificate into running configuration</help>
<completionHelp>
<list>&lt;CA name&gt;</list>
</completionHelp>
@@ -54,6 +72,15 @@
<help>Generate self-signed certificate</help>
</properties>
<children>
+ <tagNode name="file">
+ <properties>
+ <help>Write generated self-signed certificate into the specified filename</help>
+ <completionHelp>
+ <list>&lt;filename&gt;</list>
+ </completionHelp>
+ </properties>
+ <command>sudo ${vyos_op_scripts_dir}/pki.py --action generate --certificate "$6" --self-sign --file</command>
+ </tagNode>
<tagNode name="install">
<properties>
<help>Commands for installing generated self-signed certificate into running configuration</help>
@@ -74,9 +101,18 @@
</completionHelp>
</properties>
<children>
+ <tagNode name="file">
+ <properties>
+ <help>Write generated signed certificate into the specified filename</help>
+ <completionHelp>
+ <list>&lt;filename&gt;</list>
+ </completionHelp>
+ </properties>
+ <command>sudo ${vyos_op_scripts_dir}/pki.py --action generate --certificate "$7" --sign "$5" --file</command>
+ </tagNode>
<tagNode name="install">
<properties>
- <help>Commands for installing generated certificate into running configuration</help>
+ <help>Commands for installing generated signed certificate into running configuration</help>
<completionHelp>
<list>&lt;certificate name&gt;</list>
</completionHelp>
@@ -86,6 +122,15 @@
</children>
<command>sudo ${vyos_op_scripts_dir}/pki.py --action generate --certificate "noname" --sign "$5"</command>
</tagNode>
+ <tagNode name="file">
+ <properties>
+ <help>Write generated certificate request and key into the specified filename</help>
+ <completionHelp>
+ <list>&lt;filename&gt;</list>
+ </completionHelp>
+ </properties>
+ <command>sudo ${vyos_op_scripts_dir}/pki.py --action generate --certificate "$5" --file</command>
+ </tagNode>
<tagNode name="install">
<properties>
<help>Commands for installing generated certificate private key into running configuration</help>
@@ -106,6 +151,15 @@
</completionHelp>
</properties>
<children>
+ <tagNode name="file">
+ <properties>
+ <help>Write generated CRL into the specified filename</help>
+ <completionHelp>
+ <list>&lt;filename&gt;</list>
+ </completionHelp>
+ </properties>
+ <command>sudo ${vyos_op_scripts_dir}/pki.py --action generate --crl "$4" --file</command>
+ </tagNode>
<leafNode name="install">
<properties>
<help>Commands for installing generated CRL into running configuration</help>
@@ -120,6 +174,15 @@
<help>Generate DH parameters</help>
</properties>
<children>
+ <tagNode name="file">
+ <properties>
+ <help>Write generated DH parameters into the specified filename</help>
+ <completionHelp>
+ <list>&lt;filename&gt;</list>
+ </completionHelp>
+ </properties>
+ <command>sudo ${vyos_op_scripts_dir}/pki.py --action generate --dh "$5" --file</command>
+ </tagNode>
<tagNode name="install">
<properties>
<help>Commands for installing generated DH parameters into running configuration</help>
@@ -137,6 +200,15 @@
<help>Generate a key pair</help>
</properties>
<children>
+ <tagNode name="file">
+ <properties>
+ <help>Write generated key pair into the specified filename</help>
+ <completionHelp>
+ <list>&lt;filename&gt;</list>
+ </completionHelp>
+ </properties>
+ <command>sudo ${vyos_op_scripts_dir}/pki.py --action generate --keypair "$5" --file</command>
+ </tagNode>
<tagNode name="install">
<properties>
<help>Commands for installing generated key pair into running configuration</help>
@@ -159,6 +231,15 @@
<help>Generate OpenVPN shared secret key</help>
</properties>
<children>
+ <tagNode name="file">
+ <properties>
+ <help>Write generated OpenVPN shared secret key into the specified filename</help>
+ <completionHelp>
+ <list>&lt;filename&gt;</list>
+ </completionHelp>
+ </properties>
+ <command>sudo ${vyos_op_scripts_dir}/pki.py --action generate --openvpn "$6" --file</command>
+ </tagNode>
<tagNode name="install">
<properties>
<help>Commands for installing generated OpenVPN shared secret key into running configuration</help>
@@ -178,6 +259,15 @@
<help>Generate SSH key</help>
</properties>
<children>
+ <tagNode name="file">
+ <properties>
+ <help>Write generated SSH keys into the specified filename</help>
+ <completionHelp>
+ <list>&lt;filename&gt;</list>
+ </completionHelp>
+ </properties>
+ <command>sudo ${vyos_op_scripts_dir}/pki.py --action generate --ssh "$5" --file</command>
+ </tagNode>
<tagNode name="install">
<properties>
<help>Commands for installing generated SSH key into running configuration</help>
@@ -200,6 +290,15 @@
<help>Generate Wireguard key pair for use with server or peer</help>
</properties>
<children>
+ <tagNode name="file">
+ <properties>
+ <help>Write generated Wireguard keys into the specified filename</help>
+ <completionHelp>
+ <list>&lt;filename&gt;</list>
+ </completionHelp>
+ </properties>
+ <command>sudo ${vyos_op_scripts_dir}/pki.py --action generate --wireguard --key "$6" --file</command>
+ </tagNode>
<tagNode name="install">
<properties>
<help>Commands for installing generated Wireguard key into running configuration</help>
@@ -217,9 +316,18 @@
<help>Generate pre-shared key for use with a Wireguard peer</help>
</properties>
<children>
+ <tagNode name="file">
+ <properties>
+ <help>Write generated Wireguard PSK into the specified filename</help>
+ <completionHelp>
+ <list>&lt;filename&gt;</list>
+ </completionHelp>
+ </properties>
+ <command>sudo ${vyos_op_scripts_dir}/pki.py --action generate --wireguard --psk "$6" --file</command>
+ </tagNode>
<tagNode name="install">
<properties>
- <help>Commands for installing generated Wireguard psk on specified peer into running configuration</help>
+ <help>Commands for installing generated Wireguard PSK on specified peer into running configuration</help>
<completionHelp>
<list>&lt;peer&gt;</list>
</completionHelp>
diff --git a/op-mode-definitions/show-bgp.xml.in b/op-mode-definitions/show-bgp.xml.in
index 36e7062df..c33a9dacf 100644
--- a/op-mode-definitions/show-bgp.xml.in
+++ b/op-mode-definitions/show-bgp.xml.in
@@ -8,7 +8,81 @@
</properties>
<command>${vyos_op_scripts_dir}/vtysh_wrapper.sh $@</command>
<children>
+ <node name="cidr-only">
+ <properties>
+ <help>Display only routes with non-natural netmasks</help>
+ </properties>
+ <command>${vyos_op_scripts_dir}/vtysh_wrapper.sh $@</command>
+ <children>
+ #include <include/vtysh-generic-wide.xml.i>
+ </children>
+ </node>
#include <include/bgp/show-bgp-common.xml.i>
+ <node name="mac">
+ <properties>
+ <help>MAC address</help>
+ </properties>
+ <children>
+ <leafNode name="hash">
+ <properties>
+ <help>MAC address database</help>
+ </properties>
+ <command>${vyos_op_scripts_dir}/vtysh_wrapper.sh $@</command>
+ </leafNode>
+ </children>
+ </node>
+ <node name="martian">
+ <properties>
+ <help>martian next-hops</help>
+ </properties>
+ <children>
+ <leafNode name="next-hop">
+ <properties>
+ <help>martian next-hop database</help>
+ </properties>
+ <command>${vyos_op_scripts_dir}/vtysh_wrapper.sh $@</command>
+ </leafNode>
+ </children>
+ </node>
+ <leafNode name="memory">
+ <properties>
+ <help>Global BGP memory statistics</help>
+ </properties>
+ <command>${vyos_op_scripts_dir}/vtysh_wrapper.sh $@</command>
+ </leafNode>
+ <node name="nexthop">
+ <properties>
+ <help>Show BGP nexthop table</help>
+ </properties>
+ <command>${vyos_op_scripts_dir}/vtysh_wrapper.sh $@</command>
+ <children>
+ #include <include/vtysh-generic-detail.xml.i>
+ </children>
+ </node>
+ <tagNode name="nexthop">
+ <properties>
+ <help>IPv4/IPv6 nexthop address</help>
+ <completionHelp>
+ <list>&lt;x.x.x.x&gt; &lt;h:h:h:h:h:h:h:h&gt;</list>
+ </completionHelp>
+ </properties>
+ <command>${vyos_op_scripts_dir}/vtysh_wrapper.sh $@</command>
+ <children>
+ #include <include/vtysh-generic-detail.xml.i>
+ </children>
+ </tagNode>
+ <leafNode name="statistics">
+ <properties>
+ <help>BGP RIB advertisement statistics</help>
+ </properties>
+ <command>${vyos_op_scripts_dir}/vtysh_wrapper.sh $@</command>
+ </leafNode>
+ <leafNode name="statistics-all">
+ <properties>
+ <help>Display number of prefixes for all afi/safi</help>
+ </properties>
+ <command>${vyos_op_scripts_dir}/vtysh_wrapper.sh $@</command>
+ </leafNode>
<leafNode name="vrf">
<properties>
<help>Show BGP VRF information</help>
@@ -28,6 +102,7 @@
#include <include/bgp/show-bgp-common.xml.i>
</children>
</tagNode>
+ #include <include/vtysh-generic-wide.xml.i>
</children>
</node>
</children>
diff --git a/op-mode-definitions/show-bridge.xml.in b/op-mode-definitions/show-bridge.xml.in
index 78c350e44..0f8d3064d 100644
--- a/op-mode-definitions/show-bridge.xml.in
+++ b/op-mode-definitions/show-bridge.xml.in
@@ -2,11 +2,24 @@
<interfaceDefinition>
<node name="show">
<children>
+ <node name="bridge">
+ <properties>
+ <help>Show bridging information</help>
+ </properties>
+ <children>
+ <leafNode name="vlan">
+ <properties>
+ <help>View the VLAN filter settings of the bridge</help>
+ </properties>
+ <command>bridge -c vlan show</command>
+ </leafNode>
+ </children>
+ </node>
<leafNode name="bridge">
<properties>
<help>Show bridging information</help>
</properties>
- <command>/sbin/brctl show</command>
+ <command>bridge -c link show</command>
</leafNode>
<tagNode name="bridge">
<properties>
@@ -15,25 +28,19 @@
<script>${vyos_completion_dir}/list_interfaces.py --type bridge</script>
</completionHelp>
</properties>
- <command>/sbin/brctl show $3</command>
+ <command>bridge -c link show | grep "master $3"</command>
<children>
- <leafNode name="macs">
- <properties>
- <help>Show bridge Media Access Control (MAC) address table</help>
- </properties>
- <command>/sbin/brctl showmacs $3</command>
- </leafNode>
- <leafNode name="spanning-tree">
+ <leafNode name="mdb">
<properties>
- <help>Show bridge spanning tree information</help>
+ <help>Displays the multicast group database for the bridge</help>
</properties>
- <command>/sbin/brctl showstp $3</command>
+ <command>bridge -c mdb show dev $3</command>
</leafNode>
<leafNode name="fdb">
<properties>
<help>Show the forwarding database of the bridge</help>
</properties>
- <command>/usr/sbin/bridge -c fdb show br $3</command>
+ <command>bridge -c fdb show br $3</command>
</leafNode>
</children>
</tagNode>
diff --git a/op-mode-definitions/show-interfaces-bridge.xml.in b/op-mode-definitions/show-interfaces-bridge.xml.in
index cc4b248b6..85fde95b5 100644
--- a/op-mode-definitions/show-interfaces-bridge.xml.in
+++ b/op-mode-definitions/show-interfaces-bridge.xml.in
@@ -33,12 +33,6 @@
</properties>
<command>${vyos_op_scripts_dir}/show_interfaces.py --intf-type=bridge --action=show</command>
</leafNode>
- <leafNode name="vlan">
- <properties>
- <help>View the VLAN filter settings of the bridge</help>
- </properties>
- <command>/usr/sbin/bridge -c vlan show</command>
- </leafNode>
</children>
</node>
</children>
diff --git a/op-mode-definitions/show-interfaces-wireguard.xml.in b/op-mode-definitions/show-interfaces-wireguard.xml.in
new file mode 100644
index 000000000..863357ef7
--- /dev/null
+++ b/op-mode-definitions/show-interfaces-wireguard.xml.in
@@ -0,0 +1,66 @@
+<?xml version="1.0"?>
+<interfaceDefinition>
+ <node name="show">
+ <children>
+ <node name="interfaces">
+ <children>
+ <tagNode name="wireguard">
+ <properties>
+ <help>Show Wireguard interface information</help>
+ <completionHelp>
+ <script>${vyos_completion_dir}/list_interfaces.py --type wireguard</script>
+ </completionHelp>
+ </properties>
+ <command>${vyos_op_scripts_dir}/show_interfaces.py --intf="$4"</command>
+ <children>
+ <leafNode name="allowed-ips">
+ <properties>
+ <help>Show all IP addresses allowed for the specified interface</help>
+ </properties>
+ <command>sudo wg show "$4" allowed-ips</command>
+ </leafNode>
+ <leafNode name="endpoints">
+ <properties>
+ <help>Show all endpoints for the specified interface</help>
+ </properties>
+ <command>sudo wg show "$4" endpoints</command>
+ </leafNode>
+ <leafNode name="peers">
+ <properties>
+ <help>Show all peer IDs for the specified interface</help>
+ </properties>
+ <command>sudo wg show "$4" peers</command>
+ </leafNode>
+ <leafNode name="public-key">
+ <properties>
+ <help>Show interface public-key</help>
+ </properties>
+ <command>sudo wg show "$4" public-key</command>
+ </leafNode>
+ <leafNode name="summary">
+ <properties>
+ <help>Shows current configuration and device information</help>
+ </properties>
+ <command>sudo wg show "$4"</command>
+ </leafNode>
+ </children>
+ </tagNode>
+ <node name="wireguard">
+ <properties>
+ <help>Show Wireguard interface information</help>
+ </properties>
+ <command>${vyos_op_scripts_dir}/show_interfaces.py --intf-type=wireguard --action=show-brief</command>
+ <children>
+ <leafNode name="detail">
+ <properties>
+ <help>Show detailed Wireguard interface information</help>
+ </properties>
+ <command>${vyos_op_scripts_dir}/show_interfaces.py --intf-type=wireguard --action=show</command>
+ </leafNode>
+ </children>
+ </node>
+ </children>
+ </node>
+ </children>
+ </node>
+</interfaceDefinition>
diff --git a/op-mode-definitions/wireguard.xml.in b/op-mode-definitions/wireguard.xml.in
deleted file mode 100644
index 0df838b50..000000000
--- a/op-mode-definitions/wireguard.xml.in
+++ /dev/null
@@ -1,189 +0,0 @@
-<?xml version="1.0"?>
-<!-- Wireguard key management -->
-<interfaceDefinition>
- <node name="generate">
- <children>
- <node name="wireguard">
- <properties>
- <help>Generate Wireguard keys</help>
- </properties>
- <children>
- <leafNode name="default-keypair">
- <properties>
- <help>Generate the default Wireguard keypair</help>
- </properties>
- <command>sudo ${vyos_op_scripts_dir}/wireguard.py --genkey</command>
- </leafNode>
- <leafNode name="preshared-key">
- <properties>
- <help>Generate a Wireguard preshared key</help>
- </properties>
- <command>${vyos_op_scripts_dir}/wireguard.py --genpsk</command>
- </leafNode>
- <tagNode name="named-keypairs">
- <properties>
- <help>Generate specified Wireguard keypairs</help>
- </properties>
- <command>sudo ${vyos_op_scripts_dir}/wireguard.py --genkey --location "$4"</command>
- </tagNode>
- <tagNode name="client-config">
- <properties>
- <help>Generate Client config QR code</help>
- <completionHelp>
- <list>&lt;client-name&gt;</list>
- </completionHelp>
- </properties>
- <children>
- <tagNode name="interface">
- <properties>
- <help>Local interface used for connection</help>
- <completionHelp>
- <script>${vyos_completion_dir}/list_interfaces.py --type wireguard</script>
- </completionHelp>
- </properties>
- <children>
- <tagNode name="server">
- <properties>
- <help>IP address/FQDN used for client connection</help>
- <completionHelp>
- <script>${vyos_completion_dir}/list_local_ips.sh --both</script>
- <list>&lt;hostname&gt;</list>
- </completionHelp>
- </properties>
- <command>sudo ${vyos_op_scripts_dir}/wireguard_client.py --name "$4" --interface "$6" --server "$8"</command>
- <children>
- <tagNode name="address">
- <properties>
- <help>IPv4/IPv6 address used by client</help>
- <completionHelp>
- <list>&lt;x.x.x.x&gt; &lt;h:h:h:h:h:h:h:h&gt;</list>
- </completionHelp>
- </properties>
- <command>sudo ${vyos_op_scripts_dir}/wireguard_client.py --name "$4" --interface "$6" --server "$8" --address "${10}"</command>
- <children>
- <tagNode name="address">
- <properties>
- <help>IPv4/IPv6 address used by client</help>
- <completionHelp>
- <list>&lt;x.x.x.x&gt; &lt;h:h:h:h:h:h:h:h&gt;</list>
- </completionHelp>
- </properties>
- <command>sudo ${vyos_op_scripts_dir}/wireguard_client.py --name "$4" --interface "$6" --server "$8" --address "${10}" --address "${12}"</command>
- </tagNode>
- </children>
- </tagNode>
- </children>
- </tagNode>
- </children>
- </tagNode>
- </children>
- </tagNode>
- </children>
- </node>
- </children>
- </node>
- <node name="show">
- <children>
- <node name="wireguard">
- <properties>
- <help>Show Wireguard properties</help>
- </properties>
- <children>
- <node name="keypairs">
- <properties>
- <help>Show Wireguard keys</help>
- </properties>
- <children>
- <tagNode name="pubkey">
- <properties>
- <help>Show specified Wireguard public key</help>
- <completionHelp>
- <script>${vyos_op_scripts_dir}/wireguard.py --listkdir</script>
- </completionHelp>
- </properties>
- <command>${vyos_op_scripts_dir}/wireguard.py --showpub --location "$5"</command>
- </tagNode>
- <tagNode name="privkey">
- <properties>
- <help>Show specified Wireguard private key</help>
- <completionHelp>
- <script>${vyos_op_scripts_dir}/wireguard.py --listkdir</script>
- </completionHelp>
- </properties>
- <command>${vyos_op_scripts_dir}/wireguard.py --showpriv --location "$5"</command>
- </tagNode>
- </children>
- </node>
- </children>
- </node>
- <node name="interfaces">
- <children>
- <tagNode name="wireguard">
- <properties>
- <help>Show Wireguard interface information</help>
- <completionHelp>
- <script>${vyos_completion_dir}/list_interfaces.py --type wireguard</script>
- </completionHelp>
- </properties>
- <command>sudo ${vyos_op_scripts_dir}/wireguard.py --showinterface "$4"</command>
- <children>
- <leafNode name="allowed-ips">
- <properties>
- <help>Show all IP addresses allowed for the specified interface</help>
- </properties>
- <command>sudo wg show "$4" allowed-ips</command>
- </leafNode>
- <leafNode name="endpoints">
- <properties>
- <help>Show all endpoints for the specified interface</help>
- </properties>
- <command>sudo wg show "$4" endpoints</command>
- </leafNode>
- <leafNode name="peers">
- <properties>
- <help>Show all peer IDs for the specified interface</help>
- </properties>
- <command>sudo wg show "$4" peers</command>
- </leafNode>
- <!-- more commands upon request -->
- </children>
- </tagNode>
- <node name="wireguard">
- <properties>
- <help>Show Wireguard interface information</help>
- </properties>
- <command>${vyos_op_scripts_dir}/show_interfaces.py --intf-type=wireguard --action=show-brief</command>
- <children>
- <leafNode name="detail">
- <properties>
- <help>Show detailed Wireguard interface information</help>
- </properties>
- <command>${vyos_op_scripts_dir}/show_interfaces.py --intf-type=wireguard --action=show</command>
- </leafNode>
- </children>
- </node>
- </children>
- </node>
- </children>
- </node>
- <node name="delete">
- <children>
- <node name="wireguard">
- <properties>
- <help>Delete Wireguard properties</help>
- </properties>
- <children>
- <tagNode name="keypair">
- <properties>
- <help>Delete a Wireguard keypair</help>
- <completionHelp>
- <script>${vyos_op_scripts_dir}/wireguard.py --listkdir</script>
- </completionHelp>
- </properties>
- <command>sudo ${vyos_op_scripts_dir}/wireguard.py --delkdir --location "$4"</command>
- </tagNode>
- </children>
- </node>
- </children>
- </node>
-</interfaceDefinition>
diff --git a/python/vyos/airbag.py b/python/vyos/airbag.py
index 510ab7f46..a20f44207 100644
--- a/python/vyos/airbag.py
+++ b/python/vyos/airbag.py
@@ -18,7 +18,6 @@ from datetime import datetime
from vyos import debug
from vyos.logger import syslog
-from vyos.version import get_version
from vyos.version import get_full_version_data
@@ -78,7 +77,7 @@ def bug_report(dtype, value, trace):
information.update({
'date': datetime.now().strftime('%Y-%m-%d %H:%M:%S'),
'trace': trace,
- 'instructions': COMMUNITY if 'rolling' in get_version() else SUPPORTED,
+ 'instructions': INSTRUCTIONS,
'note': note,
})
@@ -162,20 +161,13 @@ When reporting problems, please include as much information as possible:
"""
-COMMUNITY = """\
-- Make sure you are running the latest version of the code available at
- https://downloads.vyos.io/rolling/current/amd64/vyos-rolling-latest.iso
-- Consult the forum to see how to handle this issue
- https://forum.vyos.io
-- Join our community on slack where our users exchange help and advice
- https://vyos.slack.com
-""".strip()
-
-SUPPORTED = """\
-- Make sure you are running the latest stable version of VyOS
- the code is available at https://downloads.vyos.io/?dir=release/current
-- Contact us using the online help desk
+INSTRUCTIONS = """\
+- Contact us using the online help desk if you have a subscription:
https://support.vyos.io/
-- Join our community on slack where our users exchange help and advice
+- Make sure you are running the latest version of VyOS available at:
+ https://vyos.net/get/
+- Consult the community forum to see how to handle this issue:
+ https://forum.vyos.io
+- Join us on Slack where our users exchange help and advice:
https://vyos.slack.com
""".strip()
diff --git a/python/vyos/configquery.py b/python/vyos/configquery.py
index ed7346f1f..1cdcbcf39 100644
--- a/python/vyos/configquery.py
+++ b/python/vyos/configquery.py
@@ -18,9 +18,16 @@ A small library that allows querying existence or value(s) of config
settings from op mode, and execution of arbitrary op mode commands.
'''
+import re
+import json
+from copy import deepcopy
from subprocess import STDOUT
-from vyos.util import popen
+import vyos.util
+import vyos.xml
+from vyos.config import Config
+from vyos.configtree import ConfigTree
+from vyos.configsource import ConfigSourceSession
class ConfigQueryError(Exception):
pass
@@ -51,32 +58,59 @@ class CliShellApiConfigQuery(GenericConfigQuery):
def exists(self, path: list):
cmd = ' '.join(path)
- (_, err) = popen(f'cli-shell-api existsActive {cmd}')
+ (_, err) = vyos.util.popen(f'cli-shell-api existsActive {cmd}')
if err:
return False
return True
def value(self, path: list):
cmd = ' '.join(path)
- (out, err) = popen(f'cli-shell-api returnActiveValue {cmd}')
+ (out, err) = vyos.util.popen(f'cli-shell-api returnActiveValue {cmd}')
if err:
raise ConfigQueryError('No value for given path')
return out
def values(self, path: list):
cmd = ' '.join(path)
- (out, err) = popen(f'cli-shell-api returnActiveValues {cmd}')
+ (out, err) = vyos.util.popen(f'cli-shell-api returnActiveValues {cmd}')
if err:
raise ConfigQueryError('No values for given path')
return out
+class ConfigTreeQuery(GenericConfigQuery):
+ def __init__(self):
+ super().__init__()
+
+ config_source = ConfigSourceSession()
+ self.configtree = Config(config_source=config_source)
+
+ def exists(self, path: list):
+ return self.configtree.exists(path)
+
+ def value(self, path: list):
+ return self.configtree.return_value(path)
+
+ def values(self, path: list):
+ return self.configtree.return_values(path)
+
+ def list_nodes(self, path: list):
+ return self.configtree.list_nodes(path)
+
+ def get_config_dict(self, path=[], effective=False, key_mangling=None,
+ get_first_key=False, no_multi_convert=False,
+ no_tag_node_value_mangle=False):
+ return self.configtree.get_config_dict(path, effective=effective,
+ key_mangling=key_mangling, get_first_key=get_first_key,
+ no_multi_convert=no_multi_convert,
+ no_tag_node_value_mangle=no_tag_node_value_mangle)
+
class VbashOpRun(GenericOpRun):
def __init__(self):
super().__init__()
def run(self, path: list, **kwargs):
cmd = ' '.join(path)
- (out, err) = popen(f'. /opt/vyatta/share/vyatta-op/functions/interpreter/vyatta-op-run; _vyatta_op_run {cmd}', stderr=STDOUT, **kwargs)
+ (out, err) = vyos.util.popen(f'. /opt/vyatta/share/vyatta-op/functions/interpreter/vyatta-op-run; _vyatta_op_run {cmd}', stderr=STDOUT, **kwargs)
if err:
raise ConfigQueryError(out)
return out
diff --git a/python/vyos/configsession.py b/python/vyos/configsession.py
index 670e6c7fc..f28ad09c5 100644
--- a/python/vyos/configsession.py
+++ b/python/vyos/configsession.py
@@ -10,14 +10,14 @@
# 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, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+# if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
import os
import re
import sys
import subprocess
-from vyos.util import call
+from vyos.util import is_systemd_service_running
CLI_SHELL_API = '/bin/cli-shell-api'
SET = '/opt/vyatta/sbin/my_set'
@@ -73,8 +73,7 @@ def inject_vyos_env(env):
env['vyos_validators_dir'] = '/usr/libexec/vyos/validators'
# if running the vyos-configd daemon, inject the vyshim env var
- ret = call('systemctl is-active --quiet vyos-configd.service')
- if not ret:
+ if is_systemd_service_running('vyos-configd.service'):
env['vyshim'] = '/usr/sbin/vyshim'
return env
diff --git a/python/vyos/configverify.py b/python/vyos/configverify.py
index 979e28b11..4279e6982 100644
--- a/python/vyos/configverify.py
+++ b/python/vyos/configverify.py
@@ -149,9 +149,38 @@ def verify_eapol(config):
recurring validation of EAPoL configuration.
"""
if 'eapol' in config:
- if not {'cert_file', 'key_file'} <= set(config['eapol']):
- raise ConfigError('Both cert and key-file must be specified '\
- 'when using EAPoL!')
+ if 'certificate' not in config['eapol']:
+ raise ConfigError('Certificate must be specified when using EAPoL!')
+
+ if 'certificate' not in config['pki']:
+ raise ConfigError('Invalid certificate specified for EAPoL')
+
+ cert_name = config['eapol']['certificate']
+
+ if cert_name not in config['pki']['certificate']:
+ raise ConfigError('Invalid certificate specified for EAPoL')
+
+ cert = config['pki']['certificate'][cert_name]
+
+ if 'certificate' not in cert or 'private' not in cert or 'key' not in cert['private']:
+ raise ConfigError('Invalid certificate/private key specified for EAPoL')
+
+ if 'password_protected' in cert['private']:
+ raise ConfigError('Encrypted private key cannot be used for EAPoL')
+
+ if 'ca_certificate' in config['eapol']:
+ if 'ca' not in config['pki']:
+ raise ConfigError('Invalid CA certificate specified for EAPoL')
+
+ ca_cert_name = config['eapol']['ca_certificate']
+
+ if ca_cert_name not in config['pki']['ca']:
+ raise ConfigError('Invalid CA certificate specified for EAPoL')
+
+ ca_cert = config['pki']['ca'][cert_name]
+
+ if 'certificate' not in ca_cert:
+ raise ConfigError('Invalid CA certificate specified for EAPoL')
def verify_mirror(config):
"""
@@ -315,7 +344,7 @@ def verify_accel_ppp_base_service(config):
# vertify auth settings
if dict_search('authentication.mode', config) == 'local':
if not dict_search('authentication.local_users', config):
- raise ConfigError('PPPoE local auth mode requires local users to be configured!')
+ raise ConfigError('Authentication mode local requires local users to be configured!')
for user in dict_search('authentication.local_users.username', config):
user_config = config['authentication']['local_users']['username'][user]
@@ -339,7 +368,7 @@ def verify_accel_ppp_base_service(config):
raise ConfigError(f'Missing RADIUS secret key for server "{server}"')
if 'gateway_address' not in config:
- raise ConfigError('PPPoE server requires gateway-address to be configured!')
+ raise ConfigError('Server requires gateway-address to be configured!')
if 'name_server_ipv4' in config:
if len(config['name_server_ipv4']) > 2:
diff --git a/python/vyos/defaults.py b/python/vyos/defaults.py
index 9921e3b5f..03006c383 100644
--- a/python/vyos/defaults.py
+++ b/python/vyos/defaults.py
@@ -22,7 +22,10 @@ directories = {
"migrate": "/opt/vyatta/etc/config-migrate/migrate",
"log": "/var/log/vyatta",
"templates": "/usr/share/vyos/templates/",
- "certbot": "/config/auth/letsencrypt"
+ "certbot": "/config/auth/letsencrypt",
+ "api_schema": "/usr/libexec/vyos/services/api/graphql/graphql/schema/",
+ "api_templates": "/usr/libexec/vyos/services/api/graphql/recipes/templates/"
+
}
cfg_group = 'vyattacfg'
diff --git a/python/vyos/ifconfig/interface.py b/python/vyos/ifconfig/interface.py
index 08b7af90b..a1928ba51 100644..100755
--- a/python/vyos/ifconfig/interface.py
+++ b/python/vyos/ifconfig/interface.py
@@ -311,6 +311,28 @@ class Interface(Control):
cmd = 'ip link del dev {ifname}'.format(**self.config)
return self._cmd(cmd)
+ def _set_vrf_ct_zone(self, vrf):
+ """
+ Add/Remove rules in nftables to associate traffic in VRF to an
+ individual conntack zone
+ """
+ if vrf:
+ # Get routing table ID for VRF
+ vrf_table_id = get_interface_config(vrf).get('linkinfo', {}).get(
+ 'info_data', {}).get('table')
+ # Add map element with interface and zone ID
+ if vrf_table_id:
+ self._cmd(
+ f'nft add element inet vrf_zones ct_iface_map {{ "{self.ifname}" : {vrf_table_id} }}'
+ )
+ else:
+ nft_del_element = f'delete element inet vrf_zones ct_iface_map {{ "{self.ifname}" }}'
+ # Check if deleting is possible first to avoid raising errors
+ _, err = self._popen(f'nft -c {nft_del_element}')
+ if not err:
+ # Remove map element
+ self._cmd(f'nft {nft_del_element}')
+
def get_min_mtu(self):
"""
Get hardware minimum supported MTU
@@ -401,6 +423,7 @@ class Interface(Control):
>>> Interface('eth0').set_vrf()
"""
self.set_interface('vrf', vrf)
+ self._set_vrf_ct_zone(vrf)
def set_arp_cache_tmo(self, tmo):
"""
diff --git a/python/vyos/ifconfig/l2tpv3.py b/python/vyos/ifconfig/l2tpv3.py
index 7ff0fdd0e..fcd1fbf81 100644
--- a/python/vyos/ifconfig/l2tpv3.py
+++ b/python/vyos/ifconfig/l2tpv3.py
@@ -13,8 +13,28 @@
# 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/>.
+from time import sleep
+from time import time
+from vyos.util import run
from vyos.ifconfig.interface import Interface
+def wait_for_add_l2tpv3(timeout=10, sleep_interval=1, cmd=None):
+ '''
+ In some cases, we need to wait until local address is assigned.
+ And only then can the l2tpv3 tunnel be configured.
+ For example when ipv6 address in tentative state
+ or we wait for some routing daemon for remote address.
+ '''
+ start_time = time()
+ test_command = cmd
+ while True:
+ if (start_time + timeout) < time():
+ return None
+ result = run(test_command)
+ if result == 0:
+ return True
+ sleep(sleep_interval)
+
@Interface.register
class L2TPv3If(Interface):
"""
@@ -43,7 +63,9 @@ class L2TPv3If(Interface):
cmd += ' encap {encapsulation}'
cmd += ' local {source_address}'
cmd += ' remote {remote}'
- self._cmd(cmd.format(**self.config))
+ c = cmd.format(**self.config)
+ # wait until the local/remote address is available, but no more 10 sec.
+ wait_for_add_l2tpv3(cmd=c)
# setup session
cmd = 'ip l2tp add session name {ifname}'
diff --git a/python/vyos/ifconfig/vrrp.py b/python/vyos/ifconfig/vrrp.py
index d3e9d5df2..b522cc1ab 100644
--- a/python/vyos/ifconfig/vrrp.py
+++ b/python/vyos/ifconfig/vrrp.py
@@ -92,11 +92,14 @@ class VRRP(object):
try:
# send signal to generate the configuration file
pid = util.read_file(cls.location['pid'])
- os.kill(int(pid), cls._signal[what])
+ util.wait_for_file_write_complete(fname,
+ pre_hook=(lambda: os.kill(int(pid), cls._signal[what])),
+ timeout=30)
- # should look for file size change?
- sleep(0.2)
return util.read_file(fname)
+ except OSError:
+ # raised by vyos.util.read_file
+ raise VRRPNoData("VRRP data is not available (wait time exceeded)")
except FileNotFoundError:
raise VRRPNoData("VRRP data is not available (process not running or no active groups)")
except Exception:
diff --git a/python/vyos/ifconfig/vti.py b/python/vyos/ifconfig/vti.py
index a217d28ea..470ebbff3 100644
--- a/python/vyos/ifconfig/vti.py
+++ b/python/vyos/ifconfig/vti.py
@@ -33,7 +33,7 @@ class VTIIf(Interface):
# - https://man7.org/linux/man-pages/man8/ip-link.8.html
# - https://man7.org/linux/man-pages/man8/ip-tunnel.8.html
mapping = {
- 'source_interface' : 'dev',
+ 'source_interface' : 'dev',
}
if_id = self.ifname.lstrip('vti')
@@ -50,8 +50,3 @@ class VTIIf(Interface):
self._cmd(cmd.format(**self.config))
self.set_interface('admin_state', 'down')
-
- def set_admin_state(self, state):
- # function is not implemented for VTI interfaces as this is entirely
- # handled by the ipsec up/down scripts
- pass
diff --git a/python/vyos/ifconfig/wireguard.py b/python/vyos/ifconfig/wireguard.py
index e5b9c4408..c4cf2fbbf 100644
--- a/python/vyos/ifconfig/wireguard.py
+++ b/python/vyos/ifconfig/wireguard.py
@@ -95,7 +95,7 @@ class WireGuardOperational(Operational):
for peer in c.list_effective_nodes(["peer"]):
if wgdump['peers']:
- pubkey = c.return_effective_value(["peer", peer, "pubkey"])
+ pubkey = c.return_effective_value(["peer", peer, "public_key"])
if pubkey in wgdump['peers']:
wgpeer = wgdump['peers'][pubkey]
@@ -194,11 +194,15 @@ class WireGuardIf(Interface):
peer = config['peer_remove'][tmp]
peer['ifname'] = config['ifname']
- cmd = 'wg set {ifname} peer {pubkey} remove'
+ cmd = 'wg set {ifname} peer {public_key} remove'
self._cmd(cmd.format(**peer))
+ config['private_key_file'] = '/tmp/tmp.wireguard.key'
+ with open(config['private_key_file'], 'w') as f:
+ f.write(config['private_key'])
+
# Wireguard base command is identical for every peer
- base_cmd = 'wg set {ifname} private-key {private_key}'
+ base_cmd = 'wg set {ifname} private-key {private_key_file}'
if 'port' in config:
base_cmd += ' listen-port {port}'
if 'fwmark' in config:
@@ -210,7 +214,7 @@ class WireGuardIf(Interface):
peer = config['peer'][tmp]
# start of with a fresh 'wg' command
- cmd = base_cmd + ' peer {pubkey}'
+ cmd = base_cmd + ' peer {public_key}'
# If no PSK is given remove it by using /dev/null - passing keys via
# the shell (usually bash) is considered insecure, thus we use a file
diff --git a/python/vyos/pki.py b/python/vyos/pki.py
index 1c6282d84..68ad73bf2 100644
--- a/python/vyos/pki.py
+++ b/python/vyos/pki.py
@@ -43,6 +43,8 @@ CSR_BEGIN='-----BEGIN CERTIFICATE REQUEST-----\n'
CSR_END='\n-----END CERTIFICATE REQUEST-----'
DH_BEGIN='-----BEGIN DH PARAMETERS-----\n'
DH_END='\n-----END DH PARAMETERS-----'
+OVPN_BEGIN = '-----BEGIN OpenVPN Static key V{0}-----\n'
+OVPN_END = '\n-----END OpenVPN Static key V{0}-----'
# Print functions
@@ -227,6 +229,9 @@ def wrap_crl(raw_data):
def wrap_dh_parameters(raw_data):
return DH_BEGIN + raw_data + DH_END
+def wrap_openvpn_key(raw_data, version='1'):
+ return OVPN_BEGIN.format(version) + raw_data + OVPN_END.format(version)
+
# Load functions
def load_public_key(raw_data, wrap_tags=True):
diff --git a/python/vyos/template.py b/python/vyos/template.py
index f03fd7ee7..08a5712af 100644
--- a/python/vyos/template.py
+++ b/python/vyos/template.py
@@ -29,13 +29,17 @@ _FILTERS = {}
# reuse Environments with identical settings to improve performance
@functools.lru_cache(maxsize=2)
-def _get_environment():
+def _get_environment(location=None):
+ if location is None:
+ loc_loader=FileSystemLoader(directories["templates"])
+ else:
+ loc_loader=FileSystemLoader(location)
env = Environment(
# Don't check if template files were modified upon re-rendering
auto_reload=False,
# Cache up to this number of templates for quick re-rendering
cache_size=100,
- loader=FileSystemLoader(directories["templates"]),
+ loader=loc_loader,
trim_blocks=True,
)
env.filters.update(_FILTERS)
@@ -63,7 +67,7 @@ def register_filter(name, func=None):
return func
-def render_to_string(template, content, formater=None):
+def render_to_string(template, content, formater=None, location=None):
"""Render a template from the template directory, raise on any errors.
:param template: the path to the template relative to the template folder
@@ -78,7 +82,7 @@ def render_to_string(template, content, formater=None):
package is build (recovering the load time and overhead caused by having the
file out of the code).
"""
- template = _get_environment().get_template(template)
+ template = _get_environment(location).get_template(template)
rendered = template.render(content)
if formater is not None:
rendered = formater(rendered)
@@ -93,6 +97,7 @@ def render(
permission=None,
user=None,
group=None,
+ location=None,
):
"""Render a template from the template directory to a file, raise on any errors.
@@ -109,7 +114,7 @@ def render(
# As we are opening the file with 'w', we are performing the rendering before
# calling open() to not accidentally erase the file if rendering fails
- rendered = render_to_string(template, content, formater)
+ rendered = render_to_string(template, content, formater, location)
# Write to file
with open(destination, "w") as file:
@@ -433,3 +438,38 @@ def get_esp_ike_cipher(group_config):
ciphers.append(tmp)
return ciphers
+
+@register_filter('get_uuid')
+def get_uuid(interface):
+ """ Get interface IP addresses"""
+ from uuid import uuid1
+ return uuid1()
+
+openvpn_translate = {
+ 'des': 'des-cbc',
+ '3des': 'des-ede3-cbc',
+ 'bf128': 'bf-cbc',
+ 'bf256': 'bf-cbc',
+ 'aes128gcm': 'aes-128-gcm',
+ 'aes128': 'aes-128-cbc',
+ 'aes192gcm': 'aes-192-gcm',
+ 'aes192': 'aes-192-cbc',
+ 'aes256gcm': 'aes-256-gcm',
+ 'aes256': 'aes-256-cbc'
+}
+
+@register_filter('openvpn_cipher')
+def get_openvpn_cipher(cipher):
+ if cipher in openvpn_translate:
+ return openvpn_translate[cipher].upper()
+ return cipher.upper()
+
+@register_filter('openvpn_ncp_ciphers')
+def get_openvpn_ncp_ciphers(ciphers):
+ out = []
+ for cipher in ciphers:
+ if cipher in openvpn_translate:
+ out.append(openvpn_translate[cipher])
+ else:
+ out.append(cipher)
+ return ':'.join(out).upper()
diff --git a/python/vyos/util.py b/python/vyos/util.py
index 5a96013ea..59f9f1c44 100644
--- a/python/vyos/util.py
+++ b/python/vyos/util.py
@@ -515,6 +515,7 @@ def wait_for_inotify(file_path, pre_hook=None, event_type=None, timeout=None, sl
from inotify.adapters import Inotify
from time import time
+ from time import sleep
time_start = time()
@@ -530,11 +531,14 @@ def wait_for_inotify(file_path, pre_hook=None, event_type=None, timeout=None, sl
# the file failed to have been written to and closed within the timeout
raise OSError("Waiting for file {} to be written has failed".format(file_path))
+ # Most such events don't take much time, so it's better to check right away
+ # and sleep later.
if event is not None:
(_, type_names, path, filename) = event
if filename == os.path.basename(file_path):
if event_type in type_names:
return
+ sleep(sleep_interval)
def wait_for_file_write_complete(file_path, pre_hook=None, timeout=None, sleep_interval=0.1):
""" Waits for a process to close a file after opening it in write mode. """
@@ -800,3 +804,9 @@ def make_incremental_progressbar(increment: float):
# Ignore further calls.
while True:
yield
+
+def is_systemd_service_running(service):
+ """ Test is a specified systemd service is actually running.
+ Returns True if service is running, false otherwise. """
+ tmp = run(f'systemctl is-active --quiet {service}')
+ return bool((tmp == 0))
diff --git a/scripts/override-default b/scripts/override-default
index c8a0ff1da..0c49087c8 100755
--- a/scripts/override-default
+++ b/scripts/override-default
@@ -27,6 +27,7 @@
import sys
import glob
import logging
+from copy import deepcopy
from lxml import etree
debug = False
@@ -60,30 +61,55 @@ def override_element(l: list):
for el in parents:
el.getparent().remove(el)
+def merge_remaining(l: list, elementtree):
+ """
+ Merge (now) single leaf node containing 'defaultValue' with leaf nodes
+ of same path and no 'defaultValue'.
+ """
+ for p in l:
+ p = p.split()
+ path_str = f'/interfaceDefinition/*'
+ path_list = []
+ for i in range(len(p)):
+ path_list.append(f'[@name="{p[i]}"]')
+ path_str += '/children/*'.join(path_list)
+ rp = elementtree.xpath(path_str)
+ if len(rp) > 1:
+ for el in rp[1:]:
+ # in practice there will only be one child of the path,
+ # either defaultValue or Properties, since
+ # override_element() has already run
+ for child in el:
+ rp[0].append(deepcopy(child))
+ el.getparent().remove(el)
+
def collect_and_override(dir_name):
"""
- Collect elements with defaultValue tag into dictionary indexed by tuple
- of (name: str, ancestor path: str).
+ Collect elements with defaultValue tag into dictionary indexed by name
+ attributes of ancestor path.
"""
for fname in glob.glob(f'{dir_name}/*.xml'):
tree = etree.parse(fname)
root = tree.getroot()
defv = {}
- xpath_str = f'//defaultValue'
+ xpath_str = '//defaultValue'
xp = tree.xpath(xpath_str)
for element in xp:
ap = element.xpath('ancestor::*[@name]')
ap_name = [el.get("name") for el in ap]
- ap_path_str = ' '.join(ap_name[:-1])
- defv.setdefault((ap_name[-1], ap_path_str), []).append(element)
+ ap_path_str = ' '.join(ap_name)
+ defv.setdefault(ap_path_str, []).append(element)
for k, v in defv.items():
if len(v) > 1:
- logger.info(f"overridding default in {k[0]}, path '{k[1]}'")
+ logger.info(f"overridding default in path '{k}'")
override_element(v)
+ to_merge = list(defv)
+ merge_remaining(to_merge, tree)
+
revised_str = etree.tostring(root, encoding='unicode', pretty_print=True)
with open(f'{fname}', 'w') as f:
diff --git a/smoketest/configs/bgp-azure-ipsec-gateway b/smoketest/configs/bgp-azure-ipsec-gateway
index 0862531fd..ddcd459ae 100644
--- a/smoketest/configs/bgp-azure-ipsec-gateway
+++ b/smoketest/configs/bgp-azure-ipsec-gateway
@@ -307,6 +307,7 @@ system {
}
vpn {
ipsec {
+ auto-update 120
esp-group ESP-AZURE {
compression disable
lifetime 27000
@@ -341,35 +342,35 @@ vpn {
log-modes ike
}
site-to-site {
- peer 51.105.0.2 {
+ peer 51.105.0.1 {
authentication {
mode pre-shared-secret
pre-shared-secret averysecretpsktowardsazure
}
connection-type respond
+ default-esp-group ESP-AZURE
ike-group IKE-AZURE
ikev2-reauth inherit
local-address 192.0.2.189
vti {
bind vti51
- esp-group ESP-AZURE
}
}
- peer 51.105.0.3 {
+ peer 51.105.0.2 {
authentication {
mode pre-shared-secret
pre-shared-secret averysecretpsktowardsazure
}
connection-type respond
+ default-esp-group ESP-AZURE
ike-group IKE-AZURE
ikev2-reauth inherit
local-address 192.0.2.189
vti {
bind vti52
- esp-group ESP-AZURE
}
}
- peer 51.105.0.246 {
+ peer 51.105.0.3 {
authentication {
mode pre-shared-secret
pre-shared-secret averysecretpsktowardsazure
@@ -383,7 +384,7 @@ vpn {
esp-group ESP-AZURE
}
}
- peer 51.105.0.247 {
+ peer 51.105.0.4 {
authentication {
mode pre-shared-secret
pre-shared-secret averysecretpsktowardsazure
@@ -397,7 +398,7 @@ vpn {
esp-group ESP-AZURE
}
}
- peer 51.105.0.18 {
+ peer 51.105.0.5 {
authentication {
mode pre-shared-secret
pre-shared-secret averysecretpsktowardsazure
@@ -411,7 +412,7 @@ vpn {
esp-group ESP-AZURE
}
}
- peer 51.105.0.19 {
+ peer 51.105.0.6 {
authentication {
mode pre-shared-secret
pre-shared-secret averysecretpsktowardsazure
diff --git a/smoketest/configs/pki-ipsec b/smoketest/configs/pki-ipsec
index 5025117f7..6fc239d27 100644
--- a/smoketest/configs/pki-ipsec
+++ b/smoketest/configs/pki-ipsec
@@ -105,6 +105,33 @@ vpn {
}
}
}
+ l2tp {
+ remote-access {
+ authentication {
+ local-users {
+ username alice {
+ password notsecure
+ }
+ }
+ mode local
+ }
+ client-ip-pool {
+ start 192.168.255.2
+ stop 192.168.255.254
+ }
+ ipsec-settings {
+ authentication {
+ mode x509
+ x509 {
+ ca-cert-file /config/auth/ovpn_test_ca.pem
+ server-cert-file /config/auth/ovpn_test_server.pem
+ server-key-file /config/auth/ovpn_test_server.key
+ }
+ }
+ }
+ outside-address 192.168.150.1
+ }
+ }
rsa-keys {
local-key {
file /config/auth/ovpn_test_server.key
diff --git a/smoketest/configs/pki-misc b/smoketest/configs/pki-misc
new file mode 100644
index 000000000..c90226a2a
--- /dev/null
+++ b/smoketest/configs/pki-misc
@@ -0,0 +1,98 @@
+interfaces {
+ ethernet eth0 {
+ address 192.168.150.1/24
+ }
+}
+service {
+ https {
+ certificates {
+ system-generated-certificate {
+ lifetime 365
+ }
+ }
+ }
+}
+system {
+ config-management {
+ commit-revisions 100
+ }
+ console {
+ device ttyS0 {
+ speed 115200
+ }
+ }
+ host-name vyos
+ login {
+ user vyos {
+ authentication {
+ encrypted-password $6$2Ta6TWHd/U$NmrX0x9kexCimeOcYK1MfhMpITF9ELxHcaBU/znBq.X2ukQOj61fVI2UYP/xBzP4QtiTcdkgs7WOQMHWsRymO/
+ plaintext-password ""
+ }
+ }
+ }
+ ntp {
+ server time1.vyos.net {
+ }
+ server time2.vyos.net {
+ }
+ server time3.vyos.net {
+ }
+ }
+ syslog {
+ global {
+ facility all {
+ level info
+ }
+ facility protocols {
+ level debug
+ }
+ }
+ }
+}
+vpn {
+ openconnect {
+ authentication {
+ local-users {
+ username test {
+ password test
+ }
+ }
+ mode local
+ }
+ network-settings {
+ client-ip-settings {
+ subnet 192.168.160.0/24
+ }
+ }
+ ssl {
+ ca-cert-file /config/auth/ovpn_test_ca.pem
+ cert-file /config/auth/ovpn_test_server.pem
+ key-file /config/auth/ovpn_test_server.key
+ }
+ }
+ sstp {
+ authentication {
+ local-users {
+ username test {
+ password test
+ }
+ }
+ mode local
+ protocols mschap-v2
+ }
+ client-ip-pool {
+ subnet 192.168.170.0/24
+ }
+ gateway-address 192.168.150.1
+ ssl {
+ ca-cert-file /config/auth/ovpn_test_ca.pem
+ cert-file /config/auth/ovpn_test_server.pem
+ key-file /config/auth/ovpn_test_server.key
+ }
+ }
+}
+
+
+// Warning: Do not remove the following line.
+// vyos-config-version: "bgp@1:broadcast-relay@1:cluster@1:config-management@1:conntrack@2:conntrack-sync@2:dhcp-relay@2:dhcp-server@5:dhcpv6-server@1:dns-forwarding@3:firewall@5:https@2:interfaces@22:ipoe-server@1:ipsec@6:isis@1:l2tp@3:lldp@1:mdns@1:nat@5:nat66@1:ntp@1:policy@1:pppoe-server@5:pptp@2:qos@1:quagga@9:rpki@1:salt@1:snmp@2:ssh@2:sstp@3:system@21:vrf@2:vrrp@2:vyos-accel-ppp@2:wanloadbalance@3:webproxy@2:zone-policy@1"
+// Release version: 1.4-rolling-202106290839
diff --git a/smoketest/scripts/cli/test_interfaces_ethernet.py b/smoketest/scripts/cli/test_interfaces_ethernet.py
index a31d75423..a9cdab16a 100755
--- a/smoketest/scripts/cli/test_interfaces_ethernet.py
+++ b/smoketest/scripts/cli/test_interfaces_ethernet.py
@@ -25,9 +25,9 @@ from vyos.util import cmd
from vyos.util import process_named_running
from vyos.util import read_file
-ca_cert = '/config/auth/eapol_test_ca.pem'
-ssl_cert = '/config/auth/eapol_test_server.pem'
-ssl_key = '/config/auth/eapol_test_server.key'
+pki_path = ['pki']
+cert_data = 'MIICFDCCAbugAwIBAgIUfMbIsB/ozMXijYgUYG80T1ry+mcwCgYIKoZIzj0EAwIwWTELMAkGA1UEBhMCR0IxEzARBgNVBAgMClNvbWUtU3RhdGUxEjAQBgNVBAcMCVNvbWUtQ2l0eTENMAsGA1UECgwEVnlPUzESMBAGA1UEAwwJVnlPUyBUZXN0MB4XDTIxMDcyMDEyNDUxMloXDTI2MDcxOTEyNDUxMlowWTELMAkGA1UEBhMCR0IxEzARBgNVBAgMClNvbWUtU3RhdGUxEjAQBgNVBAcMCVNvbWUtQ2l0eTENMAsGA1UECgwEVnlPUzESMBAGA1UEAwwJVnlPUyBUZXN0MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE01HrLcNttqq4/PtoMua8rMWEkOdBu7vP94xzDO7A8C92ls1v86eePy4QllKCzIw3QxBIoCuH2peGRfWgPRdFsKNhMF8wDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAYYwHQYDVR0lBBYwFAYIKwYBBQUHAwIGCCsGAQUFBwMBMB0GA1UdDgQWBBSu+JnU5ZC4mkuEpqg2+Mk4K79oeDAKBggqhkjOPQQDAgNHADBEAiBEFdzQ/Bc3LftzngrY605UhA6UprHhAogKgROv7iR4QgIgEFUxTtW3xXJcnUPWhhUFhyZoqfn8dE93+dm/LDnp7C0='
+key_data = 'MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgPLpD0Ohhoq0g4nhx2KMIuze7ucKUt/lBEB2wc03IxXyhRANCAATTUestw222qrj8+2gy5rysxYSQ50G7u8/3jHMM7sDwL3aWzW/zp54/LhCWUoLMjDdDEEigK4fal4ZF9aA9F0Ww'
def get_wpa_supplicant_value(interface, key):
tmp = read_file(f'/run/wpa_supplicant/{interface}.conf')
@@ -66,6 +66,8 @@ class EthernetInterfaceTest(BasicInterfaceTest.TestCase):
def tearDown(self):
+ self.cli_delete(pki_path)
+
for interface in self._interfaces:
# when using a dedicated interface to test via TEST_ETH environment
# variable only this one will be cleared in the end - usable to test
@@ -149,11 +151,14 @@ class EthernetInterfaceTest(BasicInterfaceTest.TestCase):
self.cli_commit()
def test_eapol_support(self):
+ self.cli_set(pki_path + ['ca', 'eapol', 'certificate', cert_data])
+ self.cli_set(pki_path + ['certificate', 'eapol', 'certificate', cert_data])
+ self.cli_set(pki_path + ['certificate', 'eapol', 'private', 'key', key_data])
+
for interface in self._interfaces:
# Enable EAPoL
- self.cli_set(self._base_path + [interface, 'eapol', 'ca-cert-file', ca_cert])
- self.cli_set(self._base_path + [interface, 'eapol', 'cert-file', ssl_cert])
- self.cli_set(self._base_path + [interface, 'eapol', 'key-file', ssl_key])
+ self.cli_set(self._base_path + [interface, 'eapol', 'ca-certificate', 'eapol'])
+ self.cli_set(self._base_path + [interface, 'eapol', 'certificate', 'eapol'])
self.cli_commit()
@@ -172,35 +177,17 @@ class EthernetInterfaceTest(BasicInterfaceTest.TestCase):
self.assertEqual('0', tmp)
tmp = get_wpa_supplicant_value(interface, 'ca_cert')
- self.assertEqual(f'"{ca_cert}"', tmp)
+ self.assertEqual(f'"/run/wpa_supplicant/{interface}_ca.pem"', tmp)
tmp = get_wpa_supplicant_value(interface, 'client_cert')
- self.assertEqual(f'"{ssl_cert}"', tmp)
+ self.assertEqual(f'"/run/wpa_supplicant/{interface}_cert.pem"', tmp)
tmp = get_wpa_supplicant_value(interface, 'private_key')
- self.assertEqual(f'"{ssl_key}"', tmp)
+ self.assertEqual(f'"/run/wpa_supplicant/{interface}_cert.key"', tmp)
mac = read_file(f'/sys/class/net/{interface}/address')
tmp = get_wpa_supplicant_value(interface, 'identity')
self.assertEqual(f'"{mac}"', tmp)
if __name__ == '__main__':
- # Our SSL certificates need a subject ...
- subject = '/C=DE/ST=BY/O=VyOS/localityName=Cloud/commonName=vyos/' \
- 'organizationalUnitName=VyOS/emailAddress=maintainers@vyos.io/'
-
- if not (os.path.isfile(ssl_key) and os.path.isfile(ssl_cert)):
- # Generate mandatory SSL certificate
- tmp = f'openssl req -newkey rsa:4096 -new -nodes -x509 -days 3650 '\
- f'-keyout {ssl_key} -out {ssl_cert} -subj {subject}'
- cmd(tmp)
-
- if not os.path.isfile(ca_cert):
- # Generate "CA"
- tmp = f'openssl req -new -x509 -key {ssl_key} -out {ca_cert} -subj {subject}'
- cmd(tmp)
-
- for file in [ca_cert, ssl_cert, ssl_key]:
- cmd(f'sudo chown radius_priv_user:vyattacfg {file}')
-
unittest.main(verbosity=2)
diff --git a/smoketest/scripts/cli/test_interfaces_openvpn.py b/smoketest/scripts/cli/test_interfaces_openvpn.py
index 68c61b98c..7ce1b9872 100755
--- a/smoketest/scripts/cli/test_interfaces_openvpn.py
+++ b/smoketest/scripts/cli/test_interfaces_openvpn.py
@@ -37,12 +37,11 @@ from vyos.template import netmask_from_cidr
PROCESS_NAME = 'openvpn'
base_path = ['interfaces', 'openvpn']
-ca_cert = '/config/auth/ovpn_test_ca.pem'
-ssl_cert = '/config/auth/ovpn_test_server.pem'
-ssl_key = '/config/auth/ovpn_test_server.key'
-dh_pem = '/config/auth/ovpn_test_dh.pem'
-s2s_key = '/config/auth/ovpn_test_site2site.key'
-auth_key = '/config/auth/ovpn_test_tls_auth.key'
+
+cert_data = 'MIICFDCCAbugAwIBAgIUfMbIsB/ozMXijYgUYG80T1ry+mcwCgYIKoZIzj0EAwIwWTELMAkGA1UEBhMCR0IxEzARBgNVBAgMClNvbWUtU3RhdGUxEjAQBgNVBAcMCVNvbWUtQ2l0eTENMAsGA1UECgwEVnlPUzESMBAGA1UEAwwJVnlPUyBUZXN0MB4XDTIxMDcyMDEyNDUxMloXDTI2MDcxOTEyNDUxMlowWTELMAkGA1UEBhMCR0IxEzARBgNVBAgMClNvbWUtU3RhdGUxEjAQBgNVBAcMCVNvbWUtQ2l0eTENMAsGA1UECgwEVnlPUzESMBAGA1UEAwwJVnlPUyBUZXN0MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE01HrLcNttqq4/PtoMua8rMWEkOdBu7vP94xzDO7A8C92ls1v86eePy4QllKCzIw3QxBIoCuH2peGRfWgPRdFsKNhMF8wDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAYYwHQYDVR0lBBYwFAYIKwYBBQUHAwIGCCsGAQUFBwMBMB0GA1UdDgQWBBSu+JnU5ZC4mkuEpqg2+Mk4K79oeDAKBggqhkjOPQQDAgNHADBEAiBEFdzQ/Bc3LftzngrY605UhA6UprHhAogKgROv7iR4QgIgEFUxTtW3xXJcnUPWhhUFhyZoqfn8dE93+dm/LDnp7C0='
+key_data = 'MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgPLpD0Ohhoq0g4nhx2KMIuze7ucKUt/lBEB2wc03IxXyhRANCAATTUestw222qrj8+2gy5rysxYSQ50G7u8/3jHMM7sDwL3aWzW/zp54/LhCWUoLMjDdDEEigK4fal4ZF9aA9F0Ww'
+dh_data = 'MIIBCAKCAQEApzGAPcQlLJiOyfGZgl1qxNgufXkdpjG7lMaOrO4TGr1giFe3jIFOFxJNC/G9Dn+KSukaWssVVR+Jwr/JesZFPawihS03wC7cZsccykNRIjiteqJDwYJZUHieOxyCuCeY4pqOUCl1uswRGjLvIFtwynpnXKKuz2YtjNifma90PEgv/vVWKix+Q0TAbdbzJzO5xp8UVn9DuYfSr10k3LbDqDM7w5ezHZxFk24S5pN/yoOpdbxB8TS67q3IYXxR3F+RseKu4J3AvkxXSP1j7COXddPpLnvbJT/SW8NrjuC/n0eKGvmeyqNv108Y89jnT79MxMMRQk66iwlsd1m4pa/OYwIBAg=='
+ovpn_key_data = '443f2a710ac411c36894b2531e62c4550b079b8f3f08997f4be57c64abfdaaa431d2396b01ecec3a2c0618959e8186d99f489742d25673ffb3268841ebb2e7042a2daabe584e79d51d2b1d7409bf8840f7e42efa3e660a521719b04ee88b9043e6315ae12da7c9abd55f67eeed71a9ee8c6e163b5d2661fc332cf90cb45658b4adf892f79537d37d3a3d90da283ce885adf325ffd2b5be92067cdf0345c7712c9d36b642c170351b6d9ce9f6230c7a2617b0c181121bce7d5373404fb68e65210b36e6d40ef2769cf8990503859f6f2db3c85ba74420430a6250d6a74ca51ece4b85124bfdfec0c8a530cefa7350378d81a4539f74bed832a902ae4798142e4a'
remote_port = '1194'
protocol = 'udp'
@@ -65,6 +64,12 @@ class TestInterfacesOpenVPN(VyOSUnitTestSHIM.TestCase):
self.cli_set(['interfaces', 'dummy', dummy_if, 'address', '192.0.2.1/32'])
self.cli_set(['vrf', 'name', vrf_name, 'table', '12345'])
+ self.cli_set(['pki', 'ca', 'ovpn_test', 'certificate', cert_data])
+ self.cli_set(['pki', 'certificate', 'ovpn_test', 'certificate', cert_data])
+ self.cli_set(['pki', 'certificate', 'ovpn_test', 'private', 'key', key_data])
+ self.cli_set(['pki', 'dh', 'ovpn_test', 'parameters', dh_data])
+ self.cli_set(['pki', 'openvpn', 'shared-secret', 'ovpn_test', 'key', ovpn_key_data])
+
def tearDown(self):
self.cli_delete(base_path)
self.cli_delete(['interfaces', 'dummy', dummy_if])
@@ -101,25 +106,24 @@ class TestInterfacesOpenVPN(VyOSUnitTestSHIM.TestCase):
self.cli_commit()
self.cli_set(path + ['remote-host', '192.0.9.9'])
- # check validate() - cannot specify "tls dh-file" in client mode
- self.cli_set(path + ['tls', 'dh-file', dh_pem])
+ # check validate() - cannot specify "tls dh-params" in client mode
+ self.cli_set(path + ['tls', 'dh-params', 'ovpn_test'])
with self.assertRaises(ConfigSessionError):
self.cli_commit()
self.cli_delete(path + ['tls'])
- # check validate() - must specify one of "shared-secret-key-file" and "tls"
+ # check validate() - must specify one of "shared-secret-key" and "tls"
with self.assertRaises(ConfigSessionError):
self.cli_commit()
- self.cli_set(path + ['shared-secret-key-file', s2s_key])
+ self.cli_set(path + ['shared-secret-key', 'ovpn_test'])
- # check validate() - must specify one of "shared-secret-key-file" and "tls"
+ # check validate() - must specify one of "shared-secret-key" and "tls"
with self.assertRaises(ConfigSessionError):
self.cli_commit()
- self.cli_delete(path + ['shared-secret-key-file', s2s_key])
+ self.cli_delete(path + ['shared-secret-key', 'ovpn_test'])
- self.cli_set(path + ['tls', 'ca-cert-file', ca_cert])
- self.cli_set(path + ['tls', 'cert-file', ssl_cert])
- self.cli_set(path + ['tls', 'key-file', ssl_key])
+ self.cli_set(path + ['tls', 'ca-certificate', 'ovpn_test'])
+ self.cli_set(path + ['tls', 'certificate', 'ovpn_test'])
# check validate() - can not have auth username without a password
self.cli_set(path + ['authentication', 'username', 'vyos'])
@@ -152,9 +156,8 @@ class TestInterfacesOpenVPN(VyOSUnitTestSHIM.TestCase):
self.cli_set(path + ['protocol', protocol])
self.cli_set(path + ['remote-host', remote_host])
self.cli_set(path + ['remote-port', remote_port])
- self.cli_set(path + ['tls', 'ca-cert-file', ca_cert])
- self.cli_set(path + ['tls', 'cert-file', ssl_cert])
- self.cli_set(path + ['tls', 'key-file', ssl_key])
+ self.cli_set(path + ['tls', 'ca-certificate', 'ovpn_test'])
+ self.cli_set(path + ['tls', 'certificate', 'ovpn_test'])
self.cli_set(path + ['vrf', vrf_name])
self.cli_set(path + ['authentication', 'username', interface+'user'])
self.cli_set(path + ['authentication', 'password', interface+'secretpw'])
@@ -176,12 +179,12 @@ class TestInterfacesOpenVPN(VyOSUnitTestSHIM.TestCase):
self.assertIn(f'remote {remote_host}', config)
self.assertIn(f'persist-tun', config)
self.assertIn(f'auth {auth_hash}', config)
- self.assertIn(f'cipher aes-256-cbc', config)
+ self.assertIn(f'cipher AES-256-CBC', config)
# TLS options
- self.assertIn(f'ca {ca_cert}', config)
- self.assertIn(f'cert {ssl_cert}', config)
- self.assertIn(f'key {ssl_key}', config)
+ self.assertIn(f'ca /run/openvpn/{interface}_ca.pem', config)
+ self.assertIn(f'cert /run/openvpn/{interface}_cert.pem', config)
+ self.assertIn(f'key /run/openvpn/{interface}_cert.key', config)
self.assertTrue(process_named_running(PROCESS_NAME))
self.assertEqual(get_vrf(interface), vrf_name)
@@ -228,11 +231,11 @@ class TestInterfacesOpenVPN(VyOSUnitTestSHIM.TestCase):
self.cli_commit()
self.cli_delete(path + ['remote-host'])
- # check validate() - must specify "tls dh-file" when not using EC keys
+ # check validate() - must specify "tls dh-params" when not using EC keys
# in server mode
with self.assertRaises(ConfigSessionError):
self.cli_commit()
- self.cli_set(path + ['tls', 'dh-file', dh_pem])
+ self.cli_set(path + ['tls', 'dh-params', 'ovpn_test'])
# check validate() - must specify "server subnet" or add interface to
# bridge in server mode
@@ -251,20 +254,15 @@ class TestInterfacesOpenVPN(VyOSUnitTestSHIM.TestCase):
self.cli_commit()
self.cli_delete(path + ['server', 'subnet', '100.64.0.0/10'])
- # check validate() - must specify "tls ca-cert-file"
- with self.assertRaises(ConfigSessionError):
- self.cli_commit()
- self.cli_set(path + ['tls', 'ca-cert-file', ca_cert])
-
- # check validate() - must specify "tls cert-file"
+ # check validate() - must specify "tls ca-certificate"
with self.assertRaises(ConfigSessionError):
self.cli_commit()
- self.cli_set(path + ['tls', 'cert-file', ssl_cert])
+ self.cli_set(path + ['tls', 'ca-certificate', 'ovpn_test'])
- # check validate() - must specify "tls key-file"
+ # check validate() - must specify "tls certificate"
with self.assertRaises(ConfigSessionError):
self.cli_commit()
- self.cli_set(path + ['tls', 'key-file', ssl_key])
+ self.cli_set(path + ['tls', 'certificate', 'ovpn_test'])
# check validate() - cannot specify "tls role" in client-server mode'
self.cli_set(path + ['tls', 'role', 'active'])
@@ -272,7 +270,7 @@ class TestInterfacesOpenVPN(VyOSUnitTestSHIM.TestCase):
self.cli_commit()
# check validate() - cannot specify "tls role" in client-server mode'
- self.cli_set(path + ['tls', 'auth-file', auth_key])
+ self.cli_set(path + ['tls', 'auth-key', 'ovpn_test'])
with self.assertRaises(ConfigSessionError):
self.cli_commit()
@@ -282,11 +280,11 @@ class TestInterfacesOpenVPN(VyOSUnitTestSHIM.TestCase):
self.cli_commit()
self.cli_delete(path + ['protocol'])
- # check validate() - cannot specify "tls dh-file" when "tls role" is "active"
- self.cli_set(path + ['tls', 'dh-file', dh_pem])
+ # check validate() - cannot specify "tls dh-params" when "tls role" is "active"
+ self.cli_set(path + ['tls', 'dh-params', 'ovpn_test'])
with self.assertRaises(ConfigSessionError):
self.cli_commit()
- self.cli_delete(path + ['tls', 'dh-file'])
+ self.cli_delete(path + ['tls', 'dh-params'])
# Now test the other path with tls role passive
self.cli_set(path + ['tls', 'role', 'passive'])
@@ -297,10 +295,10 @@ class TestInterfacesOpenVPN(VyOSUnitTestSHIM.TestCase):
self.cli_delete(path + ['protocol'])
- # check validate() - must specify "tls dh-file" when "tls role" is "passive"
+ # check validate() - must specify "tls dh-params" when "tls role" is "passive"
with self.assertRaises(ConfigSessionError):
self.cli_commit()
- self.cli_set(path + ['tls', 'dh-file', dh_pem])
+ self.cli_set(path + ['tls', 'dh-params', 'ovpn_test'])
self.cli_commit()
@@ -338,10 +336,9 @@ class TestInterfacesOpenVPN(VyOSUnitTestSHIM.TestCase):
self.cli_set(path + ['server', 'client', 'client1', 'subnet', route])
self.cli_set(path + ['replace-default-route'])
- self.cli_set(path + ['tls', 'ca-cert-file', ca_cert])
- self.cli_set(path + ['tls', 'cert-file', ssl_cert])
- self.cli_set(path + ['tls', 'key-file', ssl_key])
- self.cli_set(path + ['tls', 'dh-file', dh_pem])
+ self.cli_set(path + ['tls', 'ca-certificate', 'ovpn_test'])
+ self.cli_set(path + ['tls', 'certificate', 'ovpn_test'])
+ self.cli_set(path + ['tls', 'dh-params', 'ovpn_test'])
self.cli_set(path + ['vrf', vrf_name])
self.cli_commit()
@@ -367,17 +364,17 @@ class TestInterfacesOpenVPN(VyOSUnitTestSHIM.TestCase):
self.assertIn(f'persist-key', config)
self.assertIn(f'proto udp', config) # default protocol
self.assertIn(f'auth {auth_hash}', config)
- self.assertIn(f'cipher aes-192-cbc', config)
+ self.assertIn(f'cipher AES-192-CBC', config)
self.assertIn(f'topology subnet', config)
self.assertIn(f'lport {port}', config)
self.assertIn(f'push "redirect-gateway def1"', config)
self.assertIn(f'keepalive 5 25', config)
# TLS options
- self.assertIn(f'ca {ca_cert}', config)
- self.assertIn(f'cert {ssl_cert}', config)
- self.assertIn(f'key {ssl_key}', config)
- self.assertIn(f'dh {dh_pem}', config)
+ self.assertIn(f'ca /run/openvpn/{interface}_ca.pem', config)
+ self.assertIn(f'cert /run/openvpn/{interface}_cert.pem', config)
+ self.assertIn(f'key /run/openvpn/{interface}_cert.key', config)
+ self.assertIn(f'dh /run/openvpn/{interface}_dh.pem', config)
# IP pool configuration
netmask = IPv4Network(subnet).netmask
@@ -425,10 +422,9 @@ class TestInterfacesOpenVPN(VyOSUnitTestSHIM.TestCase):
self.cli_set(path + ['replace-default-route'])
self.cli_set(path + ['keep-alive', 'failure-count', '10'])
self.cli_set(path + ['keep-alive', 'interval', '5'])
- self.cli_set(path + ['tls', 'ca-cert-file', ca_cert])
- self.cli_set(path + ['tls', 'cert-file', ssl_cert])
- self.cli_set(path + ['tls', 'key-file', ssl_key])
- self.cli_set(path + ['tls', 'dh-file', dh_pem])
+ self.cli_set(path + ['tls', 'ca-certificate', 'ovpn_test'])
+ self.cli_set(path + ['tls', 'certificate', 'ovpn_test'])
+ self.cli_set(path + ['tls', 'dh-params', 'ovpn_test'])
self.cli_set(path + ['vrf', vrf_name])
self.cli_commit()
@@ -448,17 +444,17 @@ class TestInterfacesOpenVPN(VyOSUnitTestSHIM.TestCase):
self.assertIn(f'persist-key', config)
self.assertIn(f'proto udp', config) # default protocol
self.assertIn(f'auth {auth_hash}', config)
- self.assertIn(f'cipher aes-192-cbc', config)
+ self.assertIn(f'cipher AES-192-CBC', config)
self.assertIn(f'topology net30', config)
self.assertIn(f'lport {port}', config)
self.assertIn(f'push "redirect-gateway def1"', config)
self.assertIn(f'keepalive 5 50', config)
# TLS options
- self.assertIn(f'ca {ca_cert}', config)
- self.assertIn(f'cert {ssl_cert}', config)
- self.assertIn(f'key {ssl_key}', config)
- self.assertIn(f'dh {dh_pem}', config)
+ self.assertIn(f'ca /run/openvpn/{interface}_ca.pem', config)
+ self.assertIn(f'cert /run/openvpn/{interface}_cert.pem', config)
+ self.assertIn(f'key /run/openvpn/{interface}_cert.key', config)
+ self.assertIn(f'dh /run/openvpn/{interface}_dh.pem', config)
# IP pool configuration
netmask = IPv4Network(subnet).netmask
@@ -530,10 +526,10 @@ class TestInterfacesOpenVPN(VyOSUnitTestSHIM.TestCase):
self.cli_commit()
self.cli_delete(path + ['remote-address', '2001:db8:ffff::2'])
- # check validate() - Must specify one of "shared-secret-key-file" and "tls"
+ # check validate() - Must specify one of "shared-secret-key" and "tls"
with self.assertRaises(ConfigSessionError):
self.cli_commit()
- self.cli_set(path + ['shared-secret-key-file', s2s_key])
+ self.cli_set(path + ['shared-secret-key', 'ovpn_test'])
self.cli_commit()
@@ -565,7 +561,7 @@ class TestInterfacesOpenVPN(VyOSUnitTestSHIM.TestCase):
self.cli_set(path + ['mode', 'site-to-site'])
self.cli_set(path + ['local-port', port])
self.cli_set(path + ['remote-port', port])
- self.cli_set(path + ['shared-secret-key-file', s2s_key])
+ self.cli_set(path + ['shared-secret-key', 'ovpn_test'])
self.cli_set(path + ['remote-address', remote_address])
self.cli_set(path + ['vrf', vrf_name])
@@ -589,7 +585,7 @@ class TestInterfacesOpenVPN(VyOSUnitTestSHIM.TestCase):
self.assertIn(f'ifconfig {local_address} {local_address_subnet}', config)
self.assertIn(f'dev {interface}', config)
- self.assertIn(f'secret {s2s_key}', config)
+ self.assertIn(f'secret /run/openvpn/{interface}_shared.key', config)
self.assertIn(f'lport {port}', config)
self.assertIn(f'rport {port}', config)
@@ -609,37 +605,4 @@ class TestInterfacesOpenVPN(VyOSUnitTestSHIM.TestCase):
if __name__ == '__main__':
- # Our SSL certificates need a subject ...
- subject = '/C=DE/ST=BY/O=VyOS/localityName=Cloud/commonName=vyos/' \
- 'organizationalUnitName=VyOS/emailAddress=maintainers@vyos.io/'
-
- if not (os.path.isfile(ssl_key) and os.path.isfile(ssl_cert)):
- # Generate mandatory SSL certificate
- tmp = f'openssl req -newkey rsa:4096 -new -nodes -x509 -days 3650 '\
- f'-keyout {ssl_key} -out {ssl_cert} -subj {subject}'
- cmd(tmp)
-
- if not os.path.isfile(ca_cert):
- # Generate "CA"
- tmp = f'openssl req -new -x509 -key {ssl_key} -out {ca_cert} -subj {subject}'
- cmd(tmp)
-
- if not os.path.isfile(dh_pem):
- # Generate "DH" key
- tmp = f'openssl dhparam -out {dh_pem} 2048'
- cmd(tmp)
-
- if not os.path.isfile(s2s_key):
- # Generate site-2-site key
- tmp = f'openvpn --genkey --secret {s2s_key}'
- cmd(tmp)
-
- if not os.path.isfile(auth_key):
- # Generate TLS auth key
- tmp = f'openvpn --genkey --secret {auth_key}'
- cmd(tmp)
-
- for file in [ca_cert, ssl_cert, ssl_key, dh_pem, s2s_key, auth_key]:
- cmd(f'sudo chown openvpn:openvpn {file}')
-
unittest.main(verbosity=2)
diff --git a/smoketest/scripts/cli/test_interfaces_wireguard.py b/smoketest/scripts/cli/test_interfaces_wireguard.py
index d31ec0332..3707eaac3 100755
--- a/smoketest/scripts/cli/test_interfaces_wireguard.py
+++ b/smoketest/scripts/cli/test_interfaces_wireguard.py
@@ -21,11 +21,6 @@ from base_vyostest_shim import VyOSUnitTestSHIM
from vyos.configsession import ConfigSession
from vyos.configsession import ConfigSessionError
-
-# Generate WireGuard default keypair
-if not os.path.isdir('/config/auth/wireguard/default'):
- os.system('sudo /usr/libexec/vyos/op_mode/wireguard.py --genkey')
-
base_path = ['interfaces', 'wireguard']
class WireGuardInterfaceTest(VyOSUnitTestSHIM.TestCase):
@@ -42,12 +37,15 @@ class WireGuardInterfaceTest(VyOSUnitTestSHIM.TestCase):
# Create WireGuard interfaces with associated peers
for intf in self._interfaces:
peer = 'foo-' + intf
+ privkey = '6ISOkASm6VhHOOSz/5iIxw+Q9adq9zA17iMM4X40dlc='
psk = 'u2xdA70hkz0S1CG0dZlOh0aq2orwFXRIVrKo4DCvHgM='
pubkey = 'n6ZZL7ph/QJUJSUUTyu19c77my1dRCDHkMzFQUO9Z3A='
for addr in self._test_addr:
self.cli_set(base_path + [intf, 'address', addr])
+ self.cli_set(base_path + [intf, 'private-key', privkey])
+
self.cli_set(base_path + [intf, 'peer', peer, 'address', '127.0.0.1'])
self.cli_set(base_path + [intf, 'peer', peer, 'port', '1337'])
@@ -57,7 +55,7 @@ class WireGuardInterfaceTest(VyOSUnitTestSHIM.TestCase):
self.cli_set(base_path + [intf, 'peer', peer, 'allowed-ips', ip])
self.cli_set(base_path + [intf, 'peer', peer, 'preshared-key', psk])
- self.cli_set(base_path + [intf, 'peer', peer, 'pubkey', pubkey])
+ self.cli_set(base_path + [intf, 'peer', peer, 'public-key', pubkey])
self.cli_commit()
self.assertTrue(os.path.isdir(f'/sys/class/net/{intf}'))
@@ -68,17 +66,19 @@ class WireGuardInterfaceTest(VyOSUnitTestSHIM.TestCase):
# Remove one of the configured peers.
interface = 'wg0'
port = '12345'
+ privkey = '6ISOkASm6VhHOOSz/5iIxw+Q9adq9zA17iMM4X40dlc='
pubkey_1 = 'n1CUsmR0M2LUUsyicBd6blZICwUqqWWHbu4ifZ2/9gk='
pubkey_2 = 'ebFx/1G0ti8tvuZd94sEIosAZZIznX+dBAKG/8DFm0I='
self.cli_set(base_path + [interface, 'address', '172.16.0.1/24'])
+ self.cli_set(base_path + [interface, 'private-key', privkey])
- self.cli_set(base_path + [interface, 'peer', 'PEER01', 'pubkey', pubkey_1])
+ self.cli_set(base_path + [interface, 'peer', 'PEER01', 'public-key', pubkey_1])
self.cli_set(base_path + [interface, 'peer', 'PEER01', 'port', port])
self.cli_set(base_path + [interface, 'peer', 'PEER01', 'allowed-ips', '10.205.212.10/32'])
self.cli_set(base_path + [interface, 'peer', 'PEER01', 'address', '192.0.2.1'])
- self.cli_set(base_path + [interface, 'peer', 'PEER02', 'pubkey', pubkey_2])
+ self.cli_set(base_path + [interface, 'peer', 'PEER02', 'public-key', pubkey_2])
self.cli_set(base_path + [interface, 'peer', 'PEER02', 'port', port])
self.cli_set(base_path + [interface, 'peer', 'PEER02', 'allowed-ips', '10.205.212.11/32'])
self.cli_set(base_path + [interface, 'peer', 'PEER02', 'address', '192.0.2.2'])
diff --git a/smoketest/scripts/cli/test_protocols_bgp.py b/smoketest/scripts/cli/test_protocols_bgp.py
index a1b3356ce..c3a2ffbf9 100755
--- a/smoketest/scripts/cli/test_protocols_bgp.py
+++ b/smoketest/scripts/cli/test_protocols_bgp.py
@@ -694,5 +694,21 @@ class TestProtocolsBGP(VyOSUnitTestSHIM.TestCase):
self.assertIn(f' neighbor {interface} activate', frrconfig)
self.assertIn(f' exit-address-family', frrconfig)
+ def test_bgp_13_solo(self):
+ remote_asn = str(int(ASN) + 150)
+ neighbor = '192.0.2.55'
+
+ self.cli_set(base_path + ['local-as', ASN])
+ self.cli_set(base_path + ['neighbor', neighbor, 'remote-as', remote_asn])
+ self.cli_set(base_path + ['neighbor', neighbor, 'solo'])
+
+ # commit changes
+ self.cli_commit()
+
+ # Verify FRR bgpd configuration
+ frrconfig = self.getFRRconfig(f'router bgp {ASN}')
+ self.assertIn(f'router bgp {ASN}', frrconfig)
+ self.assertIn(f' neighbor {neighbor} solo', frrconfig)
+
if __name__ == '__main__':
unittest.main(verbosity=2) \ No newline at end of file
diff --git a/smoketest/scripts/cli/test_protocols_nhrp.py b/smoketest/scripts/cli/test_protocols_nhrp.py
index 8389e42e9..aa0ac268d 100755
--- a/smoketest/scripts/cli/test_protocols_nhrp.py
+++ b/smoketest/scripts/cli/test_protocols_nhrp.py
@@ -68,7 +68,7 @@ class TestProtocolsNHRP(VyOSUnitTestSHIM.TestCase):
self.cli_set(vpn_path + ["ike-group", "IKE-HUB", "proposal", "2", "hash", "sha1"])
# Profile - Not doing full DMVPN checks here, just want to verify the profile name in the output
- self.cli_set(vpn_path + ["ipsec-interfaces", "interface", "eth0"])
+ self.cli_set(vpn_path + ["interface", "eth0"])
self.cli_set(vpn_path + ["profile", "NHRPVPN", "authentication", "mode", "pre-shared-secret"])
self.cli_set(vpn_path + ["profile", "NHRPVPN", "authentication", "pre-shared-secret", "secret"])
self.cli_set(vpn_path + ["profile", "NHRPVPN", "bind", "tunnel", "tun100"])
diff --git a/smoketest/scripts/cli/test_protocols_ospf.py b/smoketest/scripts/cli/test_protocols_ospf.py
index 23e5f2678..59862ca3d 100755
--- a/smoketest/scripts/cli/test_protocols_ospf.py
+++ b/smoketest/scripts/cli/test_protocols_ospf.py
@@ -14,6 +14,8 @@
# 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 logging
+import sys
import unittest
from base_vyostest_shim import VyOSUnitTestSHIM
@@ -27,6 +29,8 @@ base_path = ['protocols', 'ospf']
route_map = 'foo-bar-baz10'
+log = logging.getLogger('TestProtocolsOSPF')
+
class TestProtocolsOSPF(VyOSUnitTestSHIM.TestCase):
def setUp(self):
self.cli_set(['policy', 'route-map', route_map, 'rule', '10', 'action', 'permit'])
@@ -202,11 +206,11 @@ class TestProtocolsOSPF(VyOSUnitTestSHIM.TestCase):
for interface in interfaces:
self.assertIn(f' no passive-interface {interface}', frrconfig) # default
except:
- tmp = cmd('tail -n 250 /var/log/messages')
- print(tmp)
- tmp = cmd('vtysh -c "show run"')
- print(tmp)
- self.fail('Now we can hopefully see why OSPF fails')
+ log.debug(frrconfig)
+ log.debug(cmd('sudo dmesg'))
+ log.debug(cmd('sudo cat /var/log/messages'))
+ log.debug(cmd('vtysh -c "show run"'))
+ self.fail('Now we can hopefully see why OSPF fails!')
def test_ospf_08_redistribute(self):
metric = '15'
@@ -346,4 +350,5 @@ class TestProtocolsOSPF(VyOSUnitTestSHIM.TestCase):
self.assertNotIn(zebra_route_map, frrconfig)
if __name__ == '__main__':
+ logging.basicConfig(stream=sys.stderr, level=logging.DEBUG)
unittest.main(verbosity=2)
diff --git a/smoketest/scripts/cli/test_protocols_rpki.py b/smoketest/scripts/cli/test_protocols_rpki.py
index 8212e9469..6d334a9f8 100755
--- a/smoketest/scripts/cli/test_protocols_rpki.py
+++ b/smoketest/scripts/cli/test_protocols_rpki.py
@@ -84,6 +84,7 @@ class TestProtocolsRPKI(VyOSUnitTestSHIM.TestCase):
self.assertIn(f'rpki cache {peer} {port} preference {preference}', frrconfig)
def test_rpki_ssh(self):
+ self.skipTest('Currently untested, see: https://github.com/FRRouting/frr/issues/7978')
polling = '7200'
cache = {
'192.0.2.3' : {
diff --git a/smoketest/scripts/cli/test_service_https.py b/smoketest/scripts/cli/test_service_https.py
index 3ed7655e9..3af63636a 100755
--- a/smoketest/scripts/cli/test_service_https.py
+++ b/smoketest/scripts/cli/test_service_https.py
@@ -22,14 +22,20 @@ from vyos.util import run
base_path = ['service', 'https']
+pki_base = ['pki']
+cert_data = 'MIICFDCCAbugAwIBAgIUfMbIsB/ozMXijYgUYG80T1ry+mcwCgYIKoZIzj0EAwIwWTELMAkGA1UEBhMCR0IxEzARBgNVBAgMClNvbWUtU3RhdGUxEjAQBgNVBAcMCVNvbWUtQ2l0eTENMAsGA1UECgwEVnlPUzESMBAGA1UEAwwJVnlPUyBUZXN0MB4XDTIxMDcyMDEyNDUxMloXDTI2MDcxOTEyNDUxMlowWTELMAkGA1UEBhMCR0IxEzARBgNVBAgMClNvbWUtU3RhdGUxEjAQBgNVBAcMCVNvbWUtQ2l0eTENMAsGA1UECgwEVnlPUzESMBAGA1UEAwwJVnlPUyBUZXN0MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE01HrLcNttqq4/PtoMua8rMWEkOdBu7vP94xzDO7A8C92ls1v86eePy4QllKCzIw3QxBIoCuH2peGRfWgPRdFsKNhMF8wDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAYYwHQYDVR0lBBYwFAYIKwYBBQUHAwIGCCsGAQUFBwMBMB0GA1UdDgQWBBSu+JnU5ZC4mkuEpqg2+Mk4K79oeDAKBggqhkjOPQQDAgNHADBEAiBEFdzQ/Bc3LftzngrY605UhA6UprHhAogKgROv7iR4QgIgEFUxTtW3xXJcnUPWhhUFhyZoqfn8dE93+dm/LDnp7C0='
+key_data = 'MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgPLpD0Ohhoq0g4nhx2KMIuze7ucKUt/lBEB2wc03IxXyhRANCAATTUestw222qrj8+2gy5rysxYSQ50G7u8/3jHMM7sDwL3aWzW/zp54/LhCWUoLMjDdDEEigK4fal4ZF9aA9F0Ww'
+
class TestHTTPSService(VyOSUnitTestSHIM.TestCase):
def setUp(self):
# ensure we can also run this test on a live system - so lets clean
# out the current configuration :)
self.cli_delete(base_path)
+ self.cli_delete(pki_base)
def tearDown(self):
self.cli_delete(base_path)
+ self.cli_delete(pki_base)
self.cli_commit()
def test_default(self):
@@ -56,5 +62,16 @@ class TestHTTPSService(VyOSUnitTestSHIM.TestCase):
ret = run('sudo /usr/sbin/nginx -t')
self.assertEqual(ret, 0)
+ def test_certificate(self):
+ self.cli_set(pki_base + ['certificate', 'test_https', 'certificate', cert_data])
+ self.cli_set(pki_base + ['certificate', 'test_https', 'private', 'key', key_data])
+
+ self.cli_set(base_path + ['certificates', 'certificate', 'test_https'])
+
+ self.cli_commit()
+
+ ret = run('sudo /usr/sbin/nginx -t')
+ self.assertEqual(ret, 0)
+
if __name__ == '__main__':
unittest.main(verbosity=2)
diff --git a/smoketest/scripts/cli/test_service_router-advert.py b/smoketest/scripts/cli/test_service_router-advert.py
index b19c49c6e..26b4626c2 100755
--- a/smoketest/scripts/cli/test_service_router-advert.py
+++ b/smoketest/scripts/cli/test_service_router-advert.py
@@ -43,11 +43,10 @@ class TestServiceRADVD(VyOSUnitTestSHIM.TestCase):
self.cli_delete(base_path)
self.cli_commit()
- def test_single(self):
+ def test_common(self):
self.cli_set(base_path + ['prefix', '::/64', 'no-on-link-flag'])
self.cli_set(base_path + ['prefix', '::/64', 'no-autonomous-flag'])
self.cli_set(base_path + ['prefix', '::/64', 'valid-lifetime', 'infinity'])
- self.cli_set(base_path + ['dnssl', '2001:db8::1234'])
self.cli_set(base_path + ['other-config-flag'])
# commit changes
@@ -92,5 +91,28 @@ class TestServiceRADVD(VyOSUnitTestSHIM.TestCase):
# Check for running process
self.assertTrue(process_named_running('radvd'))
+ def test_dns(self):
+ nameserver = ['2001:db8::1', '2001:db8::2']
+ dnssl = ['vyos.net', 'vyos.io']
+
+ self.cli_set(base_path + ['prefix', '::/64', 'valid-lifetime', 'infinity'])
+ self.cli_set(base_path + ['other-config-flag'])
+
+ for ns in nameserver:
+ self.cli_set(base_path + ['name-server', ns])
+ for sl in dnssl:
+ self.cli_set(base_path + ['dnssl', sl])
+
+ # commit changes
+ self.cli_commit()
+
+ config = read_file(RADVD_CONF)
+
+ tmp = 'RDNSS ' + ' '.join(nameserver) + ' {'
+ self.assertIn(tmp, config)
+
+ tmp = 'DNSSL ' + ' '.join(dnssl) + ' {'
+ self.assertIn(tmp, config)
+
if __name__ == '__main__':
unittest.main(verbosity=2)
diff --git a/smoketest/scripts/cli/test_system_login.py b/smoketest/scripts/cli/test_system_login.py
index aa97511e0..8327235fb 100755
--- a/smoketest/scripts/cli/test_system_login.py
+++ b/smoketest/scripts/cli/test_system_login.py
@@ -41,6 +41,17 @@ class TestSystemLogin(VyOSUnitTestSHIM.TestCase):
self.cli_commit()
+ def test_add_linux_system_user(self):
+ system_user = 'backup'
+ self.cli_set(base_path + ['user', system_user, 'authentication', 'plaintext-password', system_user])
+
+ # check validate() - can not add username which exists on the Debian
+ # base system (UID < 1000)
+ with self.assertRaises(ConfigSessionError):
+ self.cli_commit()
+
+ self.cli_delete(base_path + ['user', system_user])
+
def test_system_login_user(self):
# Check if user can be created and we can SSH to localhost
self.cli_set(['service', 'ssh', 'port', '22'])
diff --git a/smoketest/scripts/cli/test_vpn_ipsec.py b/smoketest/scripts/cli/test_vpn_ipsec.py
index fda8b74b1..a34387dc9 100755
--- a/smoketest/scripts/cli/test_vpn_ipsec.py
+++ b/smoketest/scripts/cli/test_vpn_ipsec.py
@@ -112,7 +112,7 @@ rgiyCHemtMepq57Pl1Nmj49eEA==
class TestVPNIPsec(VyOSUnitTestSHIM.TestCase):
def setUp(self):
- self.cli_set(base_path + ['ipsec-interfaces', 'interface', f'{interface}.{vif}'])
+ self.cli_set(base_path + ['interface', f'{interface}.{vif}'])
# Set IKE/ESP Groups
self.cli_set(base_path + ['esp-group', esp_group, 'proposal', '1', 'encryption', 'aes128'])
diff --git a/smoketest/scripts/cli/test_vpn_openconnect.py b/smoketest/scripts/cli/test_vpn_openconnect.py
index bf528c8b7..cad3b1182 100755
--- a/smoketest/scripts/cli/test_vpn_openconnect.py
+++ b/smoketest/scripts/cli/test_vpn_openconnect.py
@@ -23,25 +23,33 @@ from vyos.util import process_named_running
OCSERV_CONF = '/run/ocserv/ocserv.conf'
base_path = ['vpn', 'openconnect']
-cert = '/etc/ssl/certs/ssl-cert-snakeoil.pem'
-cert_key = '/etc/ssl/private/ssl-cert-snakeoil.key'
+
+pki_path = ['pki']
+cert_data = 'MIICFDCCAbugAwIBAgIUfMbIsB/ozMXijYgUYG80T1ry+mcwCgYIKoZIzj0EAwIwWTELMAkGA1UEBhMCR0IxEzARBgNVBAgMClNvbWUtU3RhdGUxEjAQBgNVBAcMCVNvbWUtQ2l0eTENMAsGA1UECgwEVnlPUzESMBAGA1UEAwwJVnlPUyBUZXN0MB4XDTIxMDcyMDEyNDUxMloXDTI2MDcxOTEyNDUxMlowWTELMAkGA1UEBhMCR0IxEzARBgNVBAgMClNvbWUtU3RhdGUxEjAQBgNVBAcMCVNvbWUtQ2l0eTENMAsGA1UECgwEVnlPUzESMBAGA1UEAwwJVnlPUyBUZXN0MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE01HrLcNttqq4/PtoMua8rMWEkOdBu7vP94xzDO7A8C92ls1v86eePy4QllKCzIw3QxBIoCuH2peGRfWgPRdFsKNhMF8wDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAYYwHQYDVR0lBBYwFAYIKwYBBQUHAwIGCCsGAQUFBwMBMB0GA1UdDgQWBBSu+JnU5ZC4mkuEpqg2+Mk4K79oeDAKBggqhkjOPQQDAgNHADBEAiBEFdzQ/Bc3LftzngrY605UhA6UprHhAogKgROv7iR4QgIgEFUxTtW3xXJcnUPWhhUFhyZoqfn8dE93+dm/LDnp7C0='
+key_data = 'MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgPLpD0Ohhoq0g4nhx2KMIuze7ucKUt/lBEB2wc03IxXyhRANCAATTUestw222qrj8+2gy5rysxYSQ50G7u8/3jHMM7sDwL3aWzW/zp54/LhCWUoLMjDdDEEigK4fal4ZF9aA9F0Ww'
class TestVpnOpenconnect(VyOSUnitTestSHIM.TestCase):
def tearDown(self):
# Delete vpn openconnect configuration
+ self.cli_delete(pki_path)
self.cli_delete(base_path)
self.cli_commit()
def test_vpn(self):
user = 'vyos_user'
password = 'vyos_pass'
+ self.cli_delete(pki_path)
self.cli_delete(base_path)
+
+ self.cli_set(pki_path + ['ca', 'openconnect', 'certificate', cert_data])
+ self.cli_set(pki_path + ['certificate', 'openconnect', 'certificate', cert_data])
+ self.cli_set(pki_path + ['certificate', 'openconnect', 'private', 'key', key_data])
+
self.cli_set(base_path + ["authentication", "local-users", "username", user, "password", password])
self.cli_set(base_path + ["authentication", "mode", "local"])
self.cli_set(base_path + ["network-settings", "client-ip-settings", "subnet", "192.0.2.0/24"])
- self.cli_set(base_path + ["ssl", "ca-cert-file", cert])
- self.cli_set(base_path + ["ssl", "cert-file", cert])
- self.cli_set(base_path + ["ssl", "key-file", cert_key])
+ self.cli_set(base_path + ["ssl", "ca-certificate", 'openconnect'])
+ self.cli_set(base_path + ["ssl", "certificate", 'openconnect'])
self.cli_commit()
diff --git a/smoketest/scripts/cli/test_vpn_sstp.py b/smoketest/scripts/cli/test_vpn_sstp.py
index 033338685..24673278b 100755
--- a/smoketest/scripts/cli/test_vpn_sstp.py
+++ b/smoketest/scripts/cli/test_vpn_sstp.py
@@ -19,9 +19,9 @@ import unittest
from base_accel_ppp_test import BasicAccelPPPTest
from vyos.util import cmd
-ca_cert = '/tmp/ca.crt'
-ssl_cert = '/tmp/server.crt'
-ssl_key = '/tmp/server.key'
+pki_path = ['pki']
+cert_data = 'MIICFDCCAbugAwIBAgIUfMbIsB/ozMXijYgUYG80T1ry+mcwCgYIKoZIzj0EAwIwWTELMAkGA1UEBhMCR0IxEzARBgNVBAgMClNvbWUtU3RhdGUxEjAQBgNVBAcMCVNvbWUtQ2l0eTENMAsGA1UECgwEVnlPUzESMBAGA1UEAwwJVnlPUyBUZXN0MB4XDTIxMDcyMDEyNDUxMloXDTI2MDcxOTEyNDUxMlowWTELMAkGA1UEBhMCR0IxEzARBgNVBAgMClNvbWUtU3RhdGUxEjAQBgNVBAcMCVNvbWUtQ2l0eTENMAsGA1UECgwEVnlPUzESMBAGA1UEAwwJVnlPUyBUZXN0MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE01HrLcNttqq4/PtoMua8rMWEkOdBu7vP94xzDO7A8C92ls1v86eePy4QllKCzIw3QxBIoCuH2peGRfWgPRdFsKNhMF8wDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAYYwHQYDVR0lBBYwFAYIKwYBBQUHAwIGCCsGAQUFBwMBMB0GA1UdDgQWBBSu+JnU5ZC4mkuEpqg2+Mk4K79oeDAKBggqhkjOPQQDAgNHADBEAiBEFdzQ/Bc3LftzngrY605UhA6UprHhAogKgROv7iR4QgIgEFUxTtW3xXJcnUPWhhUFhyZoqfn8dE93+dm/LDnp7C0='
+key_data = 'MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgPLpD0Ohhoq0g4nhx2KMIuze7ucKUt/lBEB2wc03IxXyhRANCAATTUestw222qrj8+2gy5rysxYSQ50G7u8/3jHMM7sDwL3aWzW/zp54/LhCWUoLMjDdDEEigK4fal4ZF9aA9F0Ww'
class TestVPNSSTPServer(BasicAccelPPPTest.TestCase):
def setUp(self):
@@ -31,28 +31,21 @@ class TestVPNSSTPServer(BasicAccelPPPTest.TestCase):
self._chap_secrets = '/run/accel-pppd/sstp.chap-secrets'
super().setUp()
+ def tearDown(self):
+ self.cli_delete(pki_path)
+ super().tearDown()
+
def basic_config(self):
+ self.cli_delete(pki_path)
+ self.cli_set(pki_path + ['ca', 'sstp', 'certificate', cert_data])
+ self.cli_set(pki_path + ['certificate', 'sstp', 'certificate', cert_data])
+ self.cli_set(pki_path + ['certificate', 'sstp', 'private', 'key', key_data])
# SSL is mandatory
- self.set(['ssl', 'ca-cert-file', ca_cert])
- self.set(['ssl', 'cert-file', ssl_cert])
- self.set(['ssl', 'key-file', ssl_key])
+ self.set(['ssl', 'ca-certificate', 'sstp'])
+ self.set(['ssl', 'certificate', 'sstp'])
self.set(['client-ip-pool', 'subnet', '192.0.2.0/24'])
super().basic_config()
if __name__ == '__main__':
- # Our SSL certificates need a subject ...
- subject = '/C=DE/ST=BY/O=VyOS/localityName=Cloud/commonName=vyos/' \
- 'organizationalUnitName=VyOS/emailAddress=maintainers@vyos.io/'
-
- # Generate mandatory SSL certificate
- tmp = f'openssl req -newkey rsa:4096 -new -nodes -x509 -days 3650 '\
- f'-keyout {ssl_key} -out {ssl_cert} -subj {subject}'
- cmd(tmp)
-
- # Generate "CA"
- tmp = f'openssl req -new -x509 -key {ssl_key} -out {ca_cert} '\
- f'-subj {subject}'
- cmd(tmp)
-
unittest.main(verbosity=2)
diff --git a/src/conf_mode/containers.py b/src/conf_mode/containers.py
index 21b47f42a..7544bd840 100755
--- a/src/conf_mode/containers.py
+++ b/src/conf_mode/containers.py
@@ -142,9 +142,9 @@ def verify(container):
# Add new network
if 'network' in container:
- v4_prefix = 0
- v6_prefix = 0
for network, network_config in container['network'].items():
+ v4_prefix = 0
+ v6_prefix = 0
# If ipv4-prefix not defined for user-defined network
if 'prefix' not in network_config:
raise ConfigError(f'prefix for network "{net}" must be defined!')
diff --git a/src/conf_mode/https.py b/src/conf_mode/https.py
index a6e2d9c8c..be4380462 100755
--- a/src/conf_mode/https.py
+++ b/src/conf_mode/https.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2019-2020 VyOS maintainers and contributors
+# Copyright (C) 2019-2021 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
@@ -14,6 +14,7 @@
# 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 sys
from copy import deepcopy
@@ -23,13 +24,17 @@ import vyos.certbot_util
from vyos.config import Config
from vyos import ConfigError
-from vyos.util import call
+from vyos.pki import wrap_certificate
+from vyos.pki import wrap_private_key
from vyos.template import render
+from vyos.util import call
from vyos import airbag
airbag.enable()
config_file = '/etc/nginx/sites-available/default'
+cert_dir = '/etc/ssl/certs'
+key_dir = '/etc/ssl/private'
certbot_dir = vyos.defaults.directories['certbot']
# https config needs to coordinate several subsystems: api, certbot,
@@ -56,12 +61,58 @@ def get_config(config=None):
if not conf.exists('service https'):
return None
+ https = conf.get_config_dict('service https', get_first_key=True)
+
+ if https:
+ https['pki'] = conf.get_config_dict(['pki'], key_mangling=('-', '_'),
+ get_first_key=True, no_tag_node_value_mangle=True)
+
+ return https
+
+def verify(https):
+ if https is None:
+ return None
+
+ if 'certificates' in https:
+ certificates = https['certificates']
+
+ if 'certificate' in certificates:
+ if not https['pki']:
+ raise ConfigError("PKI is not configured")
+
+ cert_name = certificates['certificate']
+
+ if cert_name not in https['pki']['certificate']:
+ raise ConfigError("Invalid certificate on https configuration")
+
+ pki_cert = https['pki']['certificate'][cert_name]
+
+ if 'certificate' not in pki_cert:
+ raise ConfigError("Missing certificate on https configuration")
+
+ if 'private' not in pki_cert or 'key' not in pki_cert['private']:
+ raise ConfigError("Missing certificate private key on https configuration")
+
+ if 'certbot' in https['certificates']:
+ vhost_names = []
+ for vh, vh_conf in https.get('virtual-host', {}).items():
+ vhost_names += vh_conf.get('server-name', [])
+ domains = https['certificates']['certbot'].get('domain-name', [])
+ domains_found = [domain for domain in domains if domain in vhost_names]
+ if not domains_found:
+ raise ConfigError("At least one 'virtual-host <id> server-name' "
+ "matching the 'certbot domain-name' is required.")
+ return None
+
+def generate(https):
+ if https is None:
+ return None
+
server_block_list = []
- https_dict = conf.get_config_dict('service https', get_first_key=True)
# organize by vhosts
- vhost_dict = https_dict.get('virtual-host', {})
+ vhost_dict = https.get('virtual-host', {})
if not vhost_dict:
# no specified virtual hosts (server blocks); use default
@@ -79,18 +130,30 @@ def get_config(config=None):
# get certificate data
- cert_dict = https_dict.get('certificates', {})
+ cert_dict = https.get('certificates', {})
+
+ if 'certificate' in cert_dict:
+ cert_name = cert_dict['certificate']
+ pki_cert = https['pki']['certificate'][cert_name]
+
+ cert_path = os.path.join(cert_dir, f'{cert_name}.pem')
+ key_path = os.path.join(key_dir, f'{cert_name}.pem')
+
+ with open(cert_path, 'w') as f:
+ f.write(wrap_certificate(pki_cert['certificate']))
+
+ with open(key_path, 'w') as f:
+ f.write(wrap_private_key(pki_cert['private']['key']))
- # self-signed certificate
+ vyos_cert_data = {
+ "crt": cert_path,
+ "key": key_path
+ }
- vyos_cert_data = {}
- if 'system-generated-certificate' in list(cert_dict):
- vyos_cert_data = vyos.defaults.vyos_cert_data
- if vyos_cert_data:
for block in server_block_list:
block['vyos_cert'] = vyos_cert_data
- # letsencrypt certificate using certbot
+ # letsencrypt certificate using certbot
certbot = False
cert_domains = cert_dict.get('certbot', {}).get('domain-name', [])
@@ -110,15 +173,15 @@ def get_config(config=None):
api_set = False
api_data = {}
- if 'api' in list(https_dict):
+ if 'api' in list(https):
api_set = True
api_data = vyos.defaults.api_data
- api_settings = https_dict.get('api', {})
+ api_settings = https.get('api', {})
if api_settings:
port = api_settings.get('port', '')
if port:
api_data['port'] = port
- vhosts = https_dict.get('api-restrict', {}).get('virtual-host', [])
+ vhosts = https.get('api-restrict', {}).get('virtual-host', [])
if vhosts:
api_data['vhost'] = vhosts[:]
@@ -132,34 +195,16 @@ def get_config(config=None):
if block['id'] in vhost_list:
block['api'] = api_data
- # return dict for use in template
-
- https = {'server_block_list' : server_block_list,
- 'api_set': api_set,
- 'certbot': certbot}
-
- return https
-
-def verify(https):
- if https is None:
- return None
-
- if https['certbot']:
- for sb in https['server_block_list']:
- if sb['certbot']:
- return None
- raise ConfigError("At least one 'virtual-host <id> server-name' "
- "matching the 'certbot domain-name' is required.")
- return None
-
-def generate(https):
- if https is None:
- return None
-
if 'server_block_list' not in https or not https['server_block_list']:
https['server_block_list'] = [default_server_block]
- render(config_file, 'https/nginx.default.tmpl', https)
+ data = {
+ 'server_block_list': server_block_list,
+ 'api_set': api_set,
+ 'certbot': certbot
+ }
+
+ render(config_file, 'https/nginx.default.tmpl', data)
return None
diff --git a/src/conf_mode/interfaces-ethernet.py b/src/conf_mode/interfaces-ethernet.py
index 378f400b8..78c24952b 100755
--- a/src/conf_mode/interfaces-ethernet.py
+++ b/src/conf_mode/interfaces-ethernet.py
@@ -32,6 +32,8 @@ from vyos.configverify import verify_vlan_config
from vyos.configverify import verify_vrf
from vyos.ethtool import Ethtool
from vyos.ifconfig import EthernetIf
+from vyos.pki import wrap_certificate
+from vyos.pki import wrap_private_key
from vyos.template import render
from vyos.util import call
from vyos.util import dict_search
@@ -40,6 +42,7 @@ from vyos import airbag
airbag.enable()
# XXX: wpa_supplicant works on the source interface
+cfg_dir = '/run/wpa_supplicant'
wpa_suppl_conf = '/run/wpa_supplicant/{ifname}.conf'
def get_config(config=None):
@@ -52,8 +55,15 @@ def get_config(config=None):
else:
conf = Config()
base = ['interfaces', 'ethernet']
+
+ tmp_pki = conf.get_config_dict(['pki'], key_mangling=('-', '_'),
+ get_first_key=True, no_tag_node_value_mangle=True)
+
ethernet = get_interface_dict(conf, base)
+ if 'deleted' not in ethernet:
+ ethernet['pki'] = tmp_pki
+
return ethernet
def verify(ethernet):
@@ -126,6 +136,27 @@ def generate(ethernet):
if 'eapol' in ethernet:
render(wpa_suppl_conf.format(**ethernet),
'ethernet/wpa_supplicant.conf.tmpl', ethernet)
+
+ ifname = ethernet['ifname']
+ cert_file_path = os.path.join(cfg_dir, f'{ifname}_cert.pem')
+ cert_key_path = os.path.join(cfg_dir, f'{ifname}_cert.key')
+
+ cert_name = ethernet['eapol']['certificate']
+ pki_cert = ethernet['pki']['certificate'][cert_name]
+
+ with open(cert_file_path, 'w') as f:
+ f.write(wrap_certificate(pki_cert['certificate']))
+
+ with open(cert_key_path, 'w') as f:
+ f.write(wrap_private_key(pki_cert['private']['key']))
+
+ if 'ca_certificate' in ethernet['eapol']:
+ ca_cert_file_path = os.path.join(cfg_dir, f'{ifname}_ca.pem')
+ ca_cert_name = ethernet['eapol']['ca_certificate']
+ pki_ca_cert = ethernet['pki']['ca'][cert_name]
+
+ with open(ca_cert_file_path, 'w') as f:
+ f.write(wrap_certificate(pki_ca_cert['certificate']))
else:
# delete configuration on interface removal
if os.path.isfile(wpa_suppl_conf.format(**ethernet)):
diff --git a/src/conf_mode/interfaces-loopback.py b/src/conf_mode/interfaces-loopback.py
index 30a27abb4..193334443 100755
--- a/src/conf_mode/interfaces-loopback.py
+++ b/src/conf_mode/interfaces-loopback.py
@@ -45,8 +45,8 @@ def generate(loopback):
return None
def apply(loopback):
- l = LoopbackIf(loopback['ifname'])
- if 'deleted' in loopback.keys():
+ l = LoopbackIf(**loopback)
+ if 'deleted' in loopback:
l.remove()
else:
l.update(loopback)
diff --git a/src/conf_mode/interfaces-openvpn.py b/src/conf_mode/interfaces-openvpn.py
index 0256ad62a..74e29ed82 100755
--- a/src/conf_mode/interfaces-openvpn.py
+++ b/src/conf_mode/interfaces-openvpn.py
@@ -17,6 +17,7 @@
import os
import re
+from cryptography.hazmat.primitives.asymmetric import ec
from glob import glob
from sys import exit
from ipaddress import IPv4Address
@@ -31,8 +32,14 @@ from vyos.config import Config
from vyos.configdict import get_interface_dict
from vyos.configverify import verify_vrf
from vyos.configverify import verify_bridge_delete
-from vyos.configverify import verify_diffie_hellman_length
from vyos.ifconfig import VTunIf
+from vyos.pki import load_dh_parameters
+from vyos.pki import load_private_key
+from vyos.pki import wrap_certificate
+from vyos.pki import wrap_crl
+from vyos.pki import wrap_dh_parameters
+from vyos.pki import wrap_openvpn_key
+from vyos.pki import wrap_private_key
from vyos.template import render
from vyos.template import is_ipv4
from vyos.template import is_ipv6
@@ -40,6 +47,7 @@ from vyos.util import call
from vyos.util import chown
from vyos.util import chmod_600
from vyos.util import dict_search
+from vyos.util import dict_search_args
from vyos.validate import is_addr_assigned
from vyos import ConfigError
@@ -49,23 +57,9 @@ airbag.enable()
user = 'openvpn'
group = 'openvpn'
+cfg_dir = '/run/openvpn'
cfg_file = '/run/openvpn/{ifname}.conf'
-def checkCertHeader(header, filename):
- """
- Verify if filename contains specified header.
- Returns True if match is found, False if no match or file is not found
- """
- if not os.path.isfile(filename):
- return False
-
- with open(filename, 'r') as f:
- for line in f:
- if re.match(header, line):
- return True
-
- return False
-
def get_config(config=None):
"""
Retrive CLI config as dictionary. Dictionary can never be empty, as at least the
@@ -76,14 +70,105 @@ def get_config(config=None):
else:
conf = Config()
base = ['interfaces', 'openvpn']
+
+ tmp_pki = conf.get_config_dict(['pki'], key_mangling=('-', '_'),
+ get_first_key=True, no_tag_node_value_mangle=True)
+
openvpn = get_interface_dict(conf, base)
+ if 'deleted' not in openvpn:
+ openvpn['pki'] = tmp_pki
+
openvpn['auth_user_pass_file'] = '/run/openvpn/{ifname}.pw'.format(**openvpn)
openvpn['daemon_user'] = user
openvpn['daemon_group'] = group
return openvpn
+def is_ec_private_key(pki, cert_name):
+ if not pki or 'certificate' not in pki:
+ return False
+ if cert_name not in pki['certificate']:
+ return False
+
+ pki_cert = pki['certificate'][cert_name]
+ if 'private' not in pki_cert or 'key' not in pki_cert['private']:
+ return False
+
+ key = load_private_key(pki_cert['private']['key'])
+ return isinstance(key, ec.EllipticCurvePrivateKey)
+
+def verify_pki(openvpn):
+ pki = openvpn['pki']
+ interface = openvpn['ifname']
+ mode = openvpn['mode']
+ shared_secret_key = dict_search_args(openvpn, 'shared_secret_key')
+ tls = dict_search_args(openvpn, 'tls')
+
+ if not bool(shared_secret_key) ^ bool(tls): # xor check if only one is set
+ raise ConfigError('Must specify only one of "shared-secret-key" and "tls"')
+
+ if mode in ['server', 'client'] and not tls:
+ raise ConfigError('Must specify "tls" for server and client modes')
+
+ if not pki:
+ raise ConfigError('PKI is not configured')
+
+ if shared_secret_key:
+ if not dict_search_args(pki, 'openvpn', 'shared_secret'):
+ raise ConfigError('There are no openvpn shared-secrets in PKI configuration')
+
+ if shared_secret_key not in pki['openvpn']['shared_secret']:
+ raise ConfigError(f'Invalid shared-secret on openvpn interface {interface}')
+
+ if tls:
+ if 'ca_certificate' not in tls:
+ raise ConfigError(f'Must specify "tls ca-certificate" on openvpn interface {interface}')
+
+ if tls['ca_certificate'] not in pki['ca']:
+ raise ConfigError(f'Invalid CA certificate on openvpn interface {interface}')
+
+ if not (mode == 'client' and 'auth_key' in tls):
+ if 'certificate' not in tls:
+ raise ConfigError(f'Missing "tls certificate" on openvpn interface {interface}')
+
+ if 'certificate' in tls:
+ if tls['certificate'] not in pki['certificate']:
+ raise ConfigError(f'Invalid certificate on openvpn interface {interface}')
+
+ if dict_search_args(pki, 'certificate', tls['certificate'], 'private', 'password_protected'):
+ raise ConfigError(f'Cannot use encrypted private key on openvpn interface {interface}')
+
+ if mode == 'server' and 'dh_params' not in tls and not is_ec_private_key(pki, tls['certificate']):
+ raise ConfigError('Must specify "tls dh-params" when not using EC keys in server mode')
+
+ if 'dh_params' in tls:
+ if 'dh' not in pki:
+ raise ConfigError('There are no DH parameters in PKI configuration')
+
+ if tls['dh_params'] not in pki['dh']:
+ raise ConfigError(f'Invalid dh-params on openvpn interface {interface}')
+
+ pki_dh = pki['dh'][tls['dh_params']]
+ dh_params = load_dh_parameters(pki_dh['parameters'])
+ dh_numbers = dh_params.parameter_numbers()
+ dh_bits = dh_numbers.p.bit_length()
+
+ if dh_bits < 2048:
+ raise ConfigError(f'Minimum DH key-size is 2048 bits')
+
+ if 'auth_key' in tls or 'crypt_key' in tls:
+ if not dict_search_args(pki, 'openvpn', 'shared_secret'):
+ raise ConfigError('There are no openvpn shared-secrets in PKI configuration')
+
+ if 'auth_key' in tls:
+ if tls['auth_key'] not in pki['openvpn']['shared_secret']:
+ raise ConfigError(f'Invalid auth-key on openvpn interface {interface}')
+
+ if 'crypt_key' in tls:
+ if tls['crypt_key'] not in pki['openvpn']['shared_secret']:
+ raise ConfigError(f'Invalid crypt-key on openvpn interface {interface}')
+
def verify(openvpn):
if 'deleted' in openvpn:
verify_bridge_delete(openvpn)
@@ -108,8 +193,8 @@ def verify(openvpn):
if openvpn['protocol'] == 'tcp-passive':
raise ConfigError('Protocol "tcp-passive" is not valid in client mode')
- if dict_search('tls.dh_file', openvpn):
- raise ConfigError('Cannot specify "tls dh-file" in client mode')
+ if dict_search('tls.dh_params', openvpn):
+ raise ConfigError('Cannot specify "tls dh-params" in client mode')
#
# OpenVPN site-to-site - VERIFY
@@ -194,11 +279,6 @@ def verify(openvpn):
if 'remote_host' in openvpn:
raise ConfigError('Cannot specify "remote-host" in server mode')
- if 'tls' in openvpn:
- if 'dh_file' not in openvpn['tls']:
- if 'key_file' in openvpn['tls'] and not checkCertHeader('-----BEGIN EC PRIVATE KEY-----', openvpn['tls']['key_file']):
- raise ConfigError('Must specify "tls dh-file" when not using EC keys in server mode')
-
tmp = dict_search('server.subnet', openvpn)
if tmp:
v4_subnets = len([subnet for subnet in tmp if is_ipv4(subnet)])
@@ -306,97 +386,40 @@ def verify(openvpn):
if 'remote_host' not in openvpn:
raise ConfigError('Must specify "remote-host" with "tcp-active"')
- # shared secret and TLS
- if not ('shared_secret_key_file' in openvpn or 'tls' in openvpn):
- raise ConfigError('Must specify one of "shared-secret-key-file" and "tls"')
-
- if {'shared_secret_key_file', 'tls'} <= set(openvpn):
- raise ConfigError('Can only specify one of "shared-secret-key-file" and "tls"')
-
- if openvpn['mode'] in ['client', 'server']:
- if 'tls' not in openvpn:
- raise ConfigError('Must specify "tls" for server and client mode')
-
#
# TLS/encryption
#
- if 'shared_secret_key_file' in openvpn:
+ if 'shared_secret_key' in openvpn:
if dict_search('encryption.cipher', openvpn) in ['aes128gcm', 'aes192gcm', 'aes256gcm']:
- raise ConfigError('GCM encryption with shared-secret-key-file not supported')
-
- file = dict_search('shared_secret_key_file', openvpn)
- if file and not checkCertHeader('-----BEGIN OpenVPN Static key V1-----', file):
- raise ConfigError(f'Specified shared-secret-key-file "{file}" is not valid')
+ raise ConfigError('GCM encryption with shared-secret-key not supported')
if 'tls' in openvpn:
- if 'ca_cert_file' not in openvpn['tls']:
- raise ConfigError('Must specify "tls ca-cert-file"')
-
- if not (openvpn['mode'] == 'client' and 'auth_file' in openvpn['tls']):
- if 'cert_file' not in openvpn['tls']:
- raise ConfigError('Missing "tls cert-file"')
-
- if 'key_file' not in openvpn['tls']:
- raise ConfigError('Missing "tls key-file"')
-
- if {'auth_file', 'crypt_file'} <= set(openvpn['tls']):
- raise ConfigError('TLS auth and crypt are mutually exclusive')
-
- file = dict_search('tls.ca_cert_file', openvpn)
- if file and not checkCertHeader('-----BEGIN CERTIFICATE-----', file):
- raise ConfigError(f'Specified ca-cert-file "{file}" is invalid')
-
- file = dict_search('tls.auth_file', openvpn)
- if file and not checkCertHeader('-----BEGIN OpenVPN Static key V1-----', file):
- raise ConfigError(f'Specified auth-file "{file}" is invalid')
-
- file = dict_search('tls.cert_file', openvpn)
- if file and not checkCertHeader('-----BEGIN CERTIFICATE-----', file):
- raise ConfigError(f'Specified cert-file "{file}" is invalid')
-
- file = dict_search('tls.key_file', openvpn)
- if file and not checkCertHeader('-----BEGIN (?:RSA |EC )?PRIVATE KEY-----', file):
- raise ConfigError(f'Specified key-file "{file}" is not valid')
-
- file = dict_search('tls.crypt_file', openvpn)
- if file and not checkCertHeader('-----BEGIN OpenVPN Static key V1-----', file):
- raise ConfigError(f'Specified TLS crypt-file "{file}" is invalid')
-
- file = dict_search('tls.crl_file', openvpn)
- if file and not checkCertHeader('-----BEGIN X509 CRL-----', file):
- raise ConfigError(f'Specified crl-file "{file} not valid')
-
- file = dict_search('tls.dh_file', openvpn)
- if file and not checkCertHeader('-----BEGIN DH PARAMETERS-----', file):
- raise ConfigError(f'Specified dh-file "{file}" is not valid')
-
- if file and not verify_diffie_hellman_length(file, 2048):
- raise ConfigError(f'Minimum DH key-size is 2048 bits')
+ if {'auth_key', 'crypt_key'} <= set(openvpn['tls']):
+ raise ConfigError('TLS auth and crypt keys are mutually exclusive')
tmp = dict_search('tls.role', openvpn)
if tmp:
if openvpn['mode'] in ['client', 'server']:
- if not dict_search('tls.auth_file', openvpn):
+ if not dict_search('tls.auth_key', openvpn):
raise ConfigError('Cannot specify "tls role" in client-server mode')
if tmp == 'active':
if openvpn['protocol'] == 'tcp-passive':
raise ConfigError('Cannot specify "tcp-passive" when "tls role" is "active"')
- if dict_search('tls.dh_file', openvpn):
- raise ConfigError('Cannot specify "tls dh-file" when "tls role" is "active"')
+ if dict_search('tls.dh_params', openvpn):
+ raise ConfigError('Cannot specify "tls dh-params" when "tls role" is "active"')
elif tmp == 'passive':
if openvpn['protocol'] == 'tcp-active':
raise ConfigError('Cannot specify "tcp-active" when "tls role" is "passive"')
- if not dict_search('tls.dh_file', openvpn):
- raise ConfigError('Must specify "tls dh-file" when "tls role" is "passive"')
+ if not dict_search('tls.dh_params', openvpn):
+ raise ConfigError('Must specify "tls dh-params" when "tls role" is "passive"')
- file = dict_search('tls.key_file', openvpn)
- if file and checkCertHeader('-----BEGIN EC PRIVATE KEY-----', file):
- if dict_search('tls.dh_file', openvpn):
- print('Warning: using dh-file and EC keys simultaneously will ' \
+ if 'certificate' in openvpn['tls'] and is_ec_private_key(openvpn['pki'], openvpn['tls']['certificate']):
+ if 'dh_params' in openvpn['tls']:
+ print('Warning: using dh-params and EC keys simultaneously will ' \
'lead to DH ciphers being used instead of ECDH')
if dict_search('encryption.cipher', openvpn) == 'none':
@@ -404,6 +427,8 @@ def verify(openvpn):
print('No encryption will be performed and data is transmitted in ' \
'plain text over the network!')
+ verify_pki(openvpn)
+
#
# Auth user/pass
#
@@ -419,6 +444,110 @@ def verify(openvpn):
return None
+def generate_pki_files(openvpn):
+ pki = openvpn['pki']
+
+ if not pki:
+ return None
+
+ interface = openvpn['ifname']
+ shared_secret_key = dict_search_args(openvpn, 'shared_secret_key')
+ tls = dict_search_args(openvpn, 'tls')
+
+ files = []
+
+ if shared_secret_key:
+ pki_key = pki['openvpn']['shared_secret'][shared_secret_key]
+ key_path = os.path.join(cfg_dir, f'{interface}_shared.key')
+
+ with open(key_path, 'w') as f:
+ f.write(wrap_openvpn_key(pki_key['key']))
+
+ files.append(key_path)
+
+ if tls:
+ if 'ca_certificate' in tls:
+ cert_name = tls['ca_certificate']
+ pki_ca = pki['ca'][cert_name]
+
+ if 'certificate' in pki_ca:
+ cert_path = os.path.join(cfg_dir, f'{interface}_ca.pem')
+
+ with open(cert_path, 'w') as f:
+ f.write(wrap_certificate(pki_ca['certificate']))
+
+ files.append(cert_path)
+
+ if 'crl' in pki_ca:
+ for crl in pki_ca['crl']:
+ crl_path = os.path.join(cfg_dir, f'{interface}_crl.pem')
+
+ with open(crl_path, 'w') as f:
+ f.write(wrap_crl(crl))
+
+ files.append(crl_path)
+ openvpn['tls']['crl'] = True
+
+ if 'certificate' in tls:
+ cert_name = tls['certificate']
+ pki_cert = pki['certificate'][cert_name]
+
+ if 'certificate' in pki_cert:
+ cert_path = os.path.join(cfg_dir, f'{interface}_cert.pem')
+
+ with open(cert_path, 'w') as f:
+ f.write(wrap_certificate(pki_cert['certificate']))
+
+ files.append(cert_path)
+
+ if 'private' in pki_cert and 'key' in pki_cert['private']:
+ key_path = os.path.join(cfg_dir, f'{interface}_cert.key')
+
+ with open(key_path, 'w') as f:
+ f.write(wrap_private_key(pki_cert['private']['key']))
+
+ files.append(key_path)
+ openvpn['tls']['private_key'] = True
+
+ if 'dh_params' in tls:
+ dh_name = tls['dh_params']
+ pki_dh = pki['dh'][dh_name]
+
+ if 'parameters' in pki_dh:
+ dh_path = os.path.join(cfg_dir, f'{interface}_dh.pem')
+
+ with open(dh_path, 'w') as f:
+ f.write(wrap_dh_parameters(pki_dh['parameters']))
+
+ files.append(dh_path)
+
+ if 'auth_key' in tls:
+ key_name = tls['auth_key']
+ pki_key = pki['openvpn']['shared_secret'][key_name]
+
+ if 'key' in pki_key:
+ key_path = os.path.join(cfg_dir, f'{interface}_auth.key')
+
+ with open(key_path, 'w') as f:
+ f.write(wrap_openvpn_key(pki_key['key']))
+
+ files.append(key_path)
+
+ if 'crypt_key' in tls:
+ key_name = tls['crypt_key']
+ pki_key = pki['openvpn']['shared_secret'][key_name]
+
+ if 'key' in pki_key:
+ key_path = os.path.join(cfg_dir, f'{interface}_crypt.key')
+
+ with open(key_path, 'w') as f:
+ f.write(wrap_openvpn_key(pki_key['key']))
+
+ files.append(key_path)
+
+ return files
+
+
def generate(openvpn):
interface = openvpn['ifname']
directory = os.path.dirname(cfg_file.format(**openvpn))
@@ -438,13 +567,7 @@ def generate(openvpn):
chown(ccd_dir, user, group)
# Fix file permissons for keys
- fix_permissions = []
-
- tmp = dict_search('shared_secret_key_file', openvpn)
- if tmp: fix_permissions.append(openvpn['shared_secret_key_file'])
-
- tmp = dict_search('tls.key_file', openvpn)
- if tmp: fix_permissions.append(tmp)
+ fix_permissions = generate_pki_files(openvpn)
# Generate User/Password authentication file
if 'authentication' in openvpn:
@@ -456,8 +579,9 @@ def generate(openvpn):
os.remove(openvpn['auth_user_pass_file'])
# Generate client specific configuration
- if dict_search('server.client', openvpn):
- for client, client_config in dict_search('server.client', openvpn).items():
+ server_client = dict_search_args(openvpn, 'server', 'client')
+ if server_client:
+ for client, client_config in server_client.items():
client_file = os.path.join(ccd_dir, client)
# Our client need's to know its subnet mask ...
diff --git a/src/conf_mode/interfaces-vti.py b/src/conf_mode/interfaces-vti.py
index 1b38304c1..57950ffea 100755
--- a/src/conf_mode/interfaces-vti.py
+++ b/src/conf_mode/interfaces-vti.py
@@ -45,13 +45,13 @@ def generate(vti):
return None
def apply(vti):
- if vti['ifname'] in interfaces():
- # Always delete the VTI interface in advance
+ # Remove macsec interface
+ if 'deleted' in vti:
VTIIf(**vti).remove()
+ return None
- if 'deleted' not in vti:
- tmp = VTIIf(**vti)
- tmp.update(vti)
+ tmp = VTIIf(**vti)
+ tmp.update(vti)
return None
diff --git a/src/conf_mode/interfaces-vxlan.py b/src/conf_mode/interfaces-vxlan.py
index 8e6247a30..804f2d14f 100755
--- a/src/conf_mode/interfaces-vxlan.py
+++ b/src/conf_mode/interfaces-vxlan.py
@@ -25,7 +25,9 @@ from vyos.configverify import verify_address
from vyos.configverify import verify_bridge_delete
from vyos.configverify import verify_mtu_ipv6
from vyos.configverify import verify_source_interface
-from vyos.ifconfig import VXLANIf, Interface
+from vyos.ifconfig import Interface
+from vyos.ifconfig import VXLANIf
+from vyos.template import is_ipv6
from vyos import ConfigError
from vyos import airbag
airbag.enable()
@@ -65,12 +67,19 @@ def verify(vxlan):
raise ConfigError('Must configure VNI for VXLAN')
if 'source_interface' in vxlan:
- # VXLAN adds a 50 byte overhead - we need to check the underlaying MTU
- # if our configured MTU is at least 50 bytes less
+ # VXLAN adds at least an overhead of 50 byte - we need to check the
+ # underlaying device if our VXLAN package is not going to be fragmented!
+ vxlan_overhead = 50
+ if 'source_address' in vxlan and is_ipv6(vxlan['source_address']):
+ # IPv6 adds an extra 20 bytes overhead because the IPv6 header is 20
+ # bytes larger than the IPv4 header - assuming no extra options are
+ # in use.
+ vxlan_overhead += 20
+
lower_mtu = Interface(vxlan['source_interface']).get_mtu()
- if lower_mtu < (int(vxlan['mtu']) + 50):
- raise ConfigError('VXLAN has a 50 byte overhead, underlaying device ' \
- f'MTU is to small ({lower_mtu} bytes)')
+ if lower_mtu < (int(vxlan['mtu']) + vxlan_overhead):
+ raise ConfigError(f'Underlaying device MTU is to small ({lower_mtu} '\
+ f'bytes) for VXLAN overhead ({vxlan_overhead} bytes!)')
verify_mtu_ipv6(vxlan)
verify_address(vxlan)
diff --git a/src/conf_mode/interfaces-wireguard.py b/src/conf_mode/interfaces-wireguard.py
index 024ab8f59..4c566a5ad 100755
--- a/src/conf_mode/interfaces-wireguard.py
+++ b/src/conf_mode/interfaces-wireguard.py
@@ -46,17 +46,14 @@ def get_config(config=None):
base = ['interfaces', 'wireguard']
wireguard = get_interface_dict(conf, base)
- # Mangle private key - it has a default so its always valid
- wireguard['private_key'] = '/config/auth/wireguard/{private_key}/private.key'.format(**wireguard)
-
# Determine which Wireguard peer has been removed.
# Peers can only be removed with their public key!
dict = {}
tmp = node_changed(conf, ['peer'], key_mangling=('-', '_'))
for peer in (tmp or []):
- pubkey = leaf_node_changed(conf, ['peer', peer, 'pubkey'])
- if pubkey:
- dict = dict_merge({'peer_remove' : {peer : {'pubkey' : pubkey[0]}}}, dict)
+ public_key = leaf_node_changed(conf, ['peer', peer, 'public_key'])
+ if public_key:
+ dict = dict_merge({'peer_remove' : {peer : {'public_key' : public_key[0]}}}, dict)
wireguard.update(dict)
return wireguard
@@ -70,9 +67,8 @@ def verify(wireguard):
verify_address(wireguard)
verify_vrf(wireguard)
- if not os.path.exists(wireguard['private_key']):
- raise ConfigError('Wireguard private-key not found! Execute: ' \
- '"run generate wireguard [default-keypair|named-keypairs]"')
+ if 'private_key' not in wireguard:
+ raise ConfigError('Wireguard private-key not defined')
if 'peer' not in wireguard:
raise ConfigError('At least one Wireguard peer is required!')
@@ -84,7 +80,7 @@ def verify(wireguard):
if 'allowed_ips' not in peer:
raise ConfigError(f'Wireguard allowed-ips required for peer "{tmp}"!')
- if 'pubkey' not in peer:
+ if 'public_key' not in peer:
raise ConfigError(f'Wireguard public-key required for peer "{tmp}"!')
if ('address' in peer and 'port' not in peer) or ('port' in peer and 'address' not in peer):
diff --git a/src/conf_mode/ipsec-settings.py b/src/conf_mode/ipsec-settings.py
deleted file mode 100755
index a373f821f..000000000
--- a/src/conf_mode/ipsec-settings.py
+++ /dev/null
@@ -1,223 +0,0 @@
-#!/usr/bin/env python3
-#
-# Copyright (C) 2018-2020 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 re
-import os
-
-from time import sleep
-from sys import exit
-
-from vyos.config import Config
-from vyos import ConfigError
-from vyos.util import call
-from vyos.template import render
-
-from vyos import airbag
-airbag.enable()
-
-ra_conn_name = "remote-access"
-ipsec_secrets_file = "/etc/ipsec.secrets"
-ipsec_ra_conn_dir = "/etc/ipsec.d/tunnels/"
-ipsec_ra_conn_file = ipsec_ra_conn_dir + ra_conn_name
-ipsec_conf_file = "/etc/ipsec.conf"
-ca_cert_path = "/etc/ipsec.d/cacerts"
-server_cert_path = "/etc/ipsec.d/certs"
-server_key_path = "/etc/ipsec.d/private"
-delim_ipsec_l2tp_begin = "### VyOS L2TP VPN Begin ###"
-delim_ipsec_l2tp_end = "### VyOS L2TP VPN End ###"
-charon_pidfile = "/var/run/charon.pid"
-
-def get_config(config=None):
- if config:
- config = config
- else:
- config = Config()
-
- if config.exists("vpn ipsec ipsec-interfaces interface"):
- data["ipsec_interfaces"] = config.return_values("vpn ipsec ipsec-interfaces interface")
-
- # Init config variables
- data["delim_ipsec_l2tp_begin"] = delim_ipsec_l2tp_begin
- data["delim_ipsec_l2tp_end"] = delim_ipsec_l2tp_end
- data["ipsec_ra_conn_file"] = ipsec_ra_conn_file
- data["ra_conn_name"] = ra_conn_name
- # Get l2tp ipsec settings
- data["ipsec_l2tp"] = False
- conf_ipsec_command = "vpn l2tp remote-access ipsec-settings " #last space is useful
- if config.exists(conf_ipsec_command):
- data["ipsec_l2tp"] = True
-
- # Authentication params
- if config.exists(conf_ipsec_command + "authentication mode"):
- data["ipsec_l2tp_auth_mode"] = config.return_value(conf_ipsec_command + "authentication mode")
- if config.exists(conf_ipsec_command + "authentication pre-shared-secret"):
- data["ipsec_l2tp_secret"] = config.return_value(conf_ipsec_command + "authentication pre-shared-secret")
-
- # mode x509
- if config.exists(conf_ipsec_command + "authentication x509 ca-cert-file"):
- data["ipsec_l2tp_x509_ca_cert_file"] = config.return_value(conf_ipsec_command + "authentication x509 ca-cert-file")
- if config.exists(conf_ipsec_command + "authentication x509 crl-file"):
- data["ipsec_l2tp_x509_crl_file"] = config.return_value(conf_ipsec_command + "authentication x509 crl-file")
- if config.exists(conf_ipsec_command + "authentication x509 server-cert-file"):
- data["ipsec_l2tp_x509_server_cert_file"] = config.return_value(conf_ipsec_command + "authentication x509 server-cert-file")
- data["server_cert_file_copied"] = server_cert_path+"/"+re.search('\w+(?:\.\w+)*$', config.return_value(conf_ipsec_command + "authentication x509 server-cert-file")).group(0)
- if config.exists(conf_ipsec_command + "authentication x509 server-key-file"):
- data["ipsec_l2tp_x509_server_key_file"] = config.return_value(conf_ipsec_command + "authentication x509 server-key-file")
- data["server_key_file_copied"] = server_key_path+"/"+re.search('\w+(?:\.\w+)*$', config.return_value(conf_ipsec_command + "authentication x509 server-key-file")).group(0)
- if config.exists(conf_ipsec_command + "authentication x509 server-key-password"):
- data["ipsec_l2tp_x509_server_key_password"] = config.return_value(conf_ipsec_command + "authentication x509 server-key-password")
-
- # Common l2tp ipsec params
- if config.exists(conf_ipsec_command + "ike-lifetime"):
- data["ipsec_l2tp_ike_lifetime"] = config.return_value(conf_ipsec_command + "ike-lifetime")
- else:
- data["ipsec_l2tp_ike_lifetime"] = "3600"
-
- if config.exists(conf_ipsec_command + "lifetime"):
- data["ipsec_l2tp_lifetime"] = config.return_value(conf_ipsec_command + "lifetime")
- else:
- data["ipsec_l2tp_lifetime"] = "3600"
-
- if config.exists("vpn l2tp remote-access outside-address"):
- data['outside_addr'] = config.return_value('vpn l2tp remote-access outside-address')
-
- return data
-
-def write_ipsec_secrets(c):
- if c.get("ipsec_l2tp_auth_mode") == "pre-shared-secret":
- secret_txt = "{0}\n{1} %any : PSK \"{2}\"\n{3}\n".format(delim_ipsec_l2tp_begin, c['outside_addr'], c['ipsec_l2tp_secret'], delim_ipsec_l2tp_end)
- elif c.get("ipsec_l2tp_auth_mode") == "x509":
- secret_txt = "{0}\n: RSA {1}\n{2}\n".format(delim_ipsec_l2tp_begin, c['server_key_file_copied'], delim_ipsec_l2tp_end)
-
- old_umask = os.umask(0o077)
- with open(ipsec_secrets_file, 'a+') as f:
- f.write(secret_txt)
- os.umask(old_umask)
-
-def write_ipsec_conf(c):
- ipsec_confg_txt = "{0}\ninclude {1}\n{2}\n".format(delim_ipsec_l2tp_begin, ipsec_ra_conn_file, delim_ipsec_l2tp_end)
-
- old_umask = os.umask(0o077)
- with open(ipsec_conf_file, 'a+') as f:
- f.write(ipsec_confg_txt)
- os.umask(old_umask)
-
-### Remove config from file by delimiter
-def remove_confs(delim_begin, delim_end, conf_file):
- call("sed -i '/"+delim_begin+"/,/"+delim_end+"/d' "+conf_file)
-
-
-### Checking certificate storage and notice if certificate not in /config directory
-def check_cert_file_store(cert_name, file_path, dts_path):
- if not re.search('^\/config\/.+', file_path):
- print("Warning: \"" + file_path + "\" lies outside of /config/auth directory. It will not get preserved during image upgrade.")
- #Checking file existence
- if not os.path.isfile(file_path):
- raise ConfigError("L2TP VPN configuration error: Invalid "+cert_name+" \""+file_path+"\"")
- else:
- ### Cpy file to /etc/ipsec.d/certs/ /etc/ipsec.d/cacerts/
- # todo make check
- ret = call('cp -f '+file_path+' '+dts_path)
- if ret:
- raise ConfigError("L2TP VPN configuration error: Cannot copy "+file_path)
-
-def verify(data):
- # l2tp ipsec check
- if data["ipsec_l2tp"]:
- # Checking dependecies for "authentication mode pre-shared-secret"
- if data.get("ipsec_l2tp_auth_mode") == "pre-shared-secret":
- if not data.get("ipsec_l2tp_secret"):
- raise ConfigError("pre-shared-secret required")
- if not data.get("outside_addr"):
- raise ConfigError("outside-address not defined")
-
- # Checking dependecies for "authentication mode x509"
- if data.get("ipsec_l2tp_auth_mode") == "x509":
- if not data.get("ipsec_l2tp_x509_server_key_file"):
- raise ConfigError("L2TP VPN configuration error: \"server-key-file\" not defined.")
- else:
- check_cert_file_store("server-key-file", data['ipsec_l2tp_x509_server_key_file'], server_key_path)
-
- if not data.get("ipsec_l2tp_x509_server_cert_file"):
- raise ConfigError("L2TP VPN configuration error: \"server-cert-file\" not defined.")
- else:
- check_cert_file_store("server-cert-file", data['ipsec_l2tp_x509_server_cert_file'], server_cert_path)
-
- if not data.get("ipsec_l2tp_x509_ca_cert_file"):
- raise ConfigError("L2TP VPN configuration error: \"ca-cert-file\" must be defined for X.509")
- else:
- check_cert_file_store("ca-cert-file", data['ipsec_l2tp_x509_ca_cert_file'], ca_cert_path)
-
- if not data.get('ipsec_interfaces'):
- raise ConfigError("L2TP VPN configuration error: \"vpn ipsec ipsec-interfaces\" must be specified.")
-
-def generate(data):
- if data["ipsec_l2tp"]:
- remove_confs(delim_ipsec_l2tp_begin, delim_ipsec_l2tp_end, ipsec_secrets_file)
- # old_umask = os.umask(0o077)
- # render(ipsec_secrets_file, 'ipsec/ipsec.secrets.tmpl', data)
- # os.umask(old_umask)
- ## Use this method while IPSec CLI handler won't be overwritten to python
- write_ipsec_secrets(data)
-
- old_umask = os.umask(0o077)
-
- # Create tunnels directory if does not exist
- if not os.path.exists(ipsec_ra_conn_dir):
- os.makedirs(ipsec_ra_conn_dir)
-
- render(ipsec_ra_conn_file, 'ipsec/remote-access.tmpl', data)
- os.umask(old_umask)
-
- remove_confs(delim_ipsec_l2tp_begin, delim_ipsec_l2tp_end, ipsec_conf_file)
- # old_umask = os.umask(0o077)
- # render(ipsec_conf_file, 'ipsec/ipsec.conf.tmpl', data)
- # os.umask(old_umask)
- ## Use this method while IPSec CLI handler won't be overwritten to python
- write_ipsec_conf(data)
-
- else:
- if os.path.exists(ipsec_ra_conn_file):
- remove_confs(delim_ipsec_l2tp_begin, delim_ipsec_l2tp_end, ipsec_ra_conn_file)
- remove_confs(delim_ipsec_l2tp_begin, delim_ipsec_l2tp_end, ipsec_secrets_file)
- remove_confs(delim_ipsec_l2tp_begin, delim_ipsec_l2tp_end, ipsec_conf_file)
-
-def restart_ipsec():
- call('ipsec restart >&/dev/null')
- # counter for apply swanctl config
- counter = 10
- while counter <= 10:
- if os.path.exists(charon_pidfile):
- call('swanctl -q >&/dev/null')
- break
- counter -=1
- sleep(1)
- if counter == 0:
- raise ConfigError('VPN configuration error: IPSec is not running.')
-
-def apply(data):
- # Restart IPSec daemon
- restart_ipsec()
-
-if __name__ == '__main__':
- try:
- c = get_config()
- verify(c)
- generate(c)
- apply(c)
- except ConfigError as e:
- print(e)
- exit(1)
diff --git a/src/conf_mode/le_cert.py b/src/conf_mode/le_cert.py
index 755c89966..6e169a3d5 100755
--- a/src/conf_mode/le_cert.py
+++ b/src/conf_mode/le_cert.py
@@ -22,6 +22,7 @@ from vyos.config import Config
from vyos import ConfigError
from vyos.util import cmd
from vyos.util import call
+from vyos.util import is_systemd_service_running
from vyos import airbag
airbag.enable()
@@ -87,8 +88,7 @@ def generate(cert):
# certbot will attempt to reload nginx, even with 'certonly';
# start nginx if not active
- ret = call('systemctl is-active --quiet nginx.service')
- if ret:
+ if not is_systemd_service_running('nginx.service'):
call('systemctl start nginx.service')
request_certbot(cert)
diff --git a/src/conf_mode/protocols_bfd.py b/src/conf_mode/protocols_bfd.py
index dd70d6bab..348bae59f 100755
--- a/src/conf_mode/protocols_bfd.py
+++ b/src/conf_mode/protocols_bfd.py
@@ -102,12 +102,6 @@ def apply(bfd):
frr_cfg.add_before(r'(ip prefix-list .*|route-map .*|line vty)', bfd['new_frr_config'])
frr_cfg.commit_configuration()
- # If FRR config is blank, rerun the blank commit x times due to frr-reload
- # behavior/bug not properly clearing out on one commit.
- if bfd['new_frr_config'] == '':
- for a in range(5):
- frr_cfg.commit_configuration()
-
return None
if __name__ == '__main__':
diff --git a/src/conf_mode/protocols_bgp.py b/src/conf_mode/protocols_bgp.py
index 95f277d74..9ecfd07fe 100755
--- a/src/conf_mode/protocols_bgp.py
+++ b/src/conf_mode/protocols_bgp.py
@@ -57,6 +57,11 @@ def get_config(config=None):
if not conf.exists(base):
bgp.update({'deleted' : ''})
+ if not vrf:
+ # We are running in the default VRF context, thus we can not delete
+ # our main BGP instance if there are dependent BGP VRF instances.
+ bgp['dependent_vrfs'] = conf.get_config_dict(['vrf', 'name'],
+ key_mangling=('-', '_'), get_first_key=True, no_tag_node_value_mangle=True)
return bgp
# We also need some additional information from the config, prefix-lists
@@ -96,6 +101,11 @@ def verify_remote_as(peer_config, bgp_config):
def verify(bgp):
if not bgp or 'deleted' in bgp:
+ if 'dependent_vrfs' in bgp:
+ for vrf, vrf_options in bgp['dependent_vrfs'].items():
+ if dict_search('protocols.bgp', vrf_options) != None:
+ raise ConfigError('Cannot delete default BGP instance, ' \
+ 'dependent VRF instance(s) exist!')
return None
if 'local_as' not in bgp:
@@ -271,15 +281,6 @@ def apply(bgp):
frr_cfg.add_before(r'(ip prefix-list .*|route-map .*|line vty)', bgp['frr_bgpd_config'])
frr_cfg.commit_configuration(bgp_daemon)
- # If FRR config is blank, re-run the blank commit x times due to frr-reload
- # behavior/bug not properly clearing out on one commit.
- if bgp['frr_bgpd_config'] == '':
- for a in range(5):
- frr_cfg.commit_configuration(bgp_daemon)
- if bgp['frr_zebra_config'] == '':
- for a in range(5):
- frr_cfg.commit_configuration(zebra_daemon)
-
# Save configuration to /run/frr/config/frr.conf
frr.save_configuration()
diff --git a/src/conf_mode/protocols_isis.py b/src/conf_mode/protocols_isis.py
index c3a444f16..4cf0312e9 100755
--- a/src/conf_mode/protocols_isis.py
+++ b/src/conf_mode/protocols_isis.py
@@ -113,9 +113,13 @@ def verify(isis):
# Interface MTU must be >= configured lsp-mtu
mtu = Interface(interface).get_mtu()
area_mtu = isis['lsp_mtu']
- if mtu < int(area_mtu):
- raise ConfigError(f'Interface {interface} has MTU {mtu}, minimum ' \
- f'area MTU is {area_mtu}!')
+ # Recommended maximum PDU size = interface MTU - 3 bytes
+ recom_area_mtu = mtu - 3
+ if mtu < int(area_mtu) or int(area_mtu) > recom_area_mtu:
+ raise ConfigError(f'Interface {interface} has MTU {mtu}, ' \
+ f'current area MTU is {area_mtu}! \n' \
+ f'Recommended area lsp-mtu {recom_area_mtu} or less ' \
+ '(calculated on MTU size).')
if 'vrf' in isis:
# If interface specific options are set, we must ensure that the
@@ -149,7 +153,7 @@ def verify(isis):
# If Redistribute set, but level don't set
if 'redistribute' in isis:
proc_level = isis.get('level','').replace('-','_')
- for afi in ['ipv4']:
+ for afi in ['ipv4', 'ipv6']:
if afi not in isis['redistribute']:
continue
@@ -198,7 +202,7 @@ def generate(isis):
isis['protocol'] = 'isis' # required for frr/vrf.route-map.frr.tmpl
isis['frr_zebra_config'] = render_to_string('frr/vrf.route-map.frr.tmpl', isis)
- isis['frr_isisd_config'] = render_to_string('frr/isis.frr.tmpl', isis)
+ isis['frr_isisd_config'] = render_to_string('frr/isisd.frr.tmpl', isis)
return None
def apply(isis):
@@ -232,15 +236,6 @@ def apply(isis):
frr_cfg.add_before(r'(ip prefix-list .*|route-map .*|line vty)', isis['frr_isisd_config'])
frr_cfg.commit_configuration(isis_daemon)
- # If FRR config is blank, rerun the blank commit x times due to frr-reload
- # behavior/bug not properly clearing out on one commit.
- if isis['frr_isisd_config'] == '':
- for a in range(5):
- frr_cfg.commit_configuration(isis_daemon)
- if isis['frr_zebra_config'] == '':
- for a in range(5):
- frr_cfg.commit_configuration(zebra_daemon)
-
# Save configuration to /run/frr/config/frr.conf
frr.save_configuration()
diff --git a/src/conf_mode/protocols_ospf.py b/src/conf_mode/protocols_ospf.py
index 21eb8e447..78c1c82bd 100755
--- a/src/conf_mode/protocols_ospf.py
+++ b/src/conf_mode/protocols_ospf.py
@@ -211,15 +211,6 @@ def apply(ospf):
frr_cfg.add_before(r'(ip prefix-list .*|route-map .*|line vty)', ospf['frr_ospfd_config'])
frr_cfg.commit_configuration(ospf_daemon)
- # If FRR config is blank, rerun the blank commit x times due to frr-reload
- # behavior/bug not properly clearing out on one commit.
- if ospf['frr_ospfd_config'] == '':
- for a in range(5):
- frr_cfg.commit_configuration(ospf_daemon)
- if ospf['frr_zebra_config'] == '':
- for a in range(5):
- frr_cfg.commit_configuration(zebra_daemon)
-
# Save configuration to /run/frr/config/frr.conf
frr.save_configuration()
diff --git a/src/conf_mode/protocols_ospfv3.py b/src/conf_mode/protocols_ospfv3.py
index 1964e9d34..fef0f509b 100755
--- a/src/conf_mode/protocols_ospfv3.py
+++ b/src/conf_mode/protocols_ospfv3.py
@@ -86,12 +86,6 @@ def apply(ospfv3):
frr_cfg.add_before(r'(ip prefix-list .*|route-map .*|line vty)', ospfv3['new_frr_config'])
frr_cfg.commit_configuration(frr_daemon)
- # If FRR config is blank, re-run the blank commit x times due to frr-reload
- # behavior/bug not properly clearing out on one commit.
- if ospfv3['new_frr_config'] == '':
- for a in range(5):
- frr_cfg.commit_configuration(frr_daemon)
-
# Save configuration to /run/frr/config/frr.conf
frr.save_configuration()
diff --git a/src/conf_mode/protocols_rip.py b/src/conf_mode/protocols_rip.py
index 907ac54ac..e56eb1f56 100755
--- a/src/conf_mode/protocols_rip.py
+++ b/src/conf_mode/protocols_rip.py
@@ -117,12 +117,6 @@ def apply(rip):
frr_cfg.add_before(r'(ip prefix-list .*|route-map .*|line vty)', rip['new_frr_config'])
frr_cfg.commit_configuration(rip_daemon)
- # If FRR config is blank, rerun the blank commit x times due to frr-reload
- # behavior/bug not properly clearing out on one commit.
- if rip['new_frr_config'] == '':
- for a in range(5):
- frr_cfg.commit_configuration(rip_daemon)
-
# Save configuration to /run/frr/config/frr.conf
frr.save_configuration()
diff --git a/src/conf_mode/protocols_ripng.py b/src/conf_mode/protocols_ripng.py
index 44c080546..aaec5dacb 100755
--- a/src/conf_mode/protocols_ripng.py
+++ b/src/conf_mode/protocols_ripng.py
@@ -108,12 +108,6 @@ def apply(ripng):
frr_cfg.add_before(r'(ip prefix-list .*|route-map .*|line vty)', ripng['new_frr_config'])
frr_cfg.commit_configuration(frr_daemon)
- # If FRR config is blank, rerun the blank commit x times due to frr-reload
- # behavior/bug not properly clearing out on one commit.
- if ripng['new_frr_config'] == '':
- for a in range(5):
- frr_cfg.commit_configuration(frr_daemon)
-
# Save configuration to /run/frr/config/frr.conf
frr.save_configuration()
diff --git a/src/conf_mode/protocols_rpki.py b/src/conf_mode/protocols_rpki.py
index d8f99efb8..947c8ab7a 100755
--- a/src/conf_mode/protocols_rpki.py
+++ b/src/conf_mode/protocols_rpki.py
@@ -90,12 +90,6 @@ def apply(rpki):
frr_cfg.add_before(r'(ip prefix-list .*|route-map .*|line vty)', rpki['new_frr_config'])
frr_cfg.commit_configuration(frr_daemon)
- # If FRR config is blank, re-run the blank commit x times due to frr-reload
- # behavior/bug not properly clearing out on one commit.
- if rpki['new_frr_config'] == '':
- for a in range(5):
- frr_cfg.commit_configuration(frr_daemon)
-
return None
if __name__ == '__main__':
diff --git a/src/conf_mode/protocols_static.py b/src/conf_mode/protocols_static.py
index 1d45cb71c..338247e30 100755
--- a/src/conf_mode/protocols_static.py
+++ b/src/conf_mode/protocols_static.py
@@ -107,12 +107,6 @@ def apply(static):
frr_cfg.add_before(r'(interface .*|line vty)', static['new_frr_config'])
frr_cfg.commit_configuration(static_daemon)
- # If FRR config is blank, rerun the blank commit x times due to frr-reload
- # behavior/bug not properly clearing out on one commit.
- if static['new_frr_config'] == '':
- for a in range(5):
- frr_cfg.commit_configuration(static_daemon)
-
# Save configuration to /run/frr/config/frr.conf
frr.save_configuration()
diff --git a/src/conf_mode/snmp.py b/src/conf_mode/snmp.py
index 3990e5735..23e45a5b7 100755
--- a/src/conf_mode/snmp.py
+++ b/src/conf_mode/snmp.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2018-2020 VyOS maintainers and contributors
+# Copyright (C) 2018-2021 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
@@ -54,6 +54,7 @@ default_config_data = {
'location' : '',
'description' : '',
'contact' : '',
+ 'route_table': 'False',
'trap_source': '',
'trap_targets': [],
'vyos_user': '',
@@ -186,6 +187,9 @@ def get_config():
snmp['script_ext'].append(extension)
+ if conf.exists('oid-enable route-table'):
+ snmp['route_table'] = True
+
if conf.exists('vrf'):
# Append key to dict but don't place it in the default dictionary.
# This is required to make the override.conf.tmpl work until we
diff --git a/src/conf_mode/system-login.py b/src/conf_mode/system-login.py
index da0fc2a25..f0b92aea8 100755
--- a/src/conf_mode/system-login.py
+++ b/src/conf_mode/system-login.py
@@ -43,12 +43,11 @@ radius_config_file = "/etc/pam_radius_auth.conf"
def get_local_users():
"""Return list of dynamically allocated users (see Debian Policy Manual)"""
local_users = []
- for p in getpwall():
- username = p[0]
- uid = getpwnam(username).pw_uid
+ for s_user in getpwall():
+ uid = getpwnam(s_user.pw_name).pw_uid
if uid in range(1000, 29999):
- if username not in ['radius_user', 'radius_priv_user']:
- local_users.append(username)
+ if s_user.pw_name not in ['radius_user', 'radius_priv_user']:
+ local_users.append(s_user.pw_name)
return local_users
@@ -104,7 +103,14 @@ def verify(login):
raise ConfigError(f'Attempting to delete current user: {cur_user}')
if 'user' in login:
+ system_users = getpwall()
for user, user_config in login['user'].items():
+ # Linux system users range up until UID 1000, we can not create a
+ # VyOS CLI user which already exists as system user
+ for s_user in system_users:
+ if s_user.pw_name == user and s_user.pw_uid < 1000:
+ raise ConfigError(f'User "{user}" can not be created, conflict with local system account!')
+
for pubkey, pubkey_options in (dict_search('authentication.public_keys', user_config) or {}).items():
if 'type' not in pubkey_options:
raise ConfigError(f'Missing type for public-key "{pubkey}"!')
diff --git a/src/conf_mode/system-option.py b/src/conf_mode/system-option.py
index 454611c55..55cf6b142 100755
--- a/src/conf_mode/system-option.py
+++ b/src/conf_mode/system-option.py
@@ -24,6 +24,7 @@ from vyos.config import Config
from vyos.configdict import dict_merge
from vyos.template import render
from vyos.util import cmd
+from vyos.util import is_systemd_service_running
from vyos.validate import is_addr_assigned
from vyos.xml import defaults
from vyos import ConfigError
@@ -114,7 +115,7 @@ def apply(options):
if 'performance' in options:
cmd('systemctl restart tuned.service')
# wait until daemon has started before sending configuration
- while (int(os.system('systemctl is-active --quiet tuned.service')) != 0):
+ while (not is_systemd_service_running('tuned.service')):
sleep(0.250)
cmd('tuned-adm profile network-{performance}'.format(**options))
else:
diff --git a/src/conf_mode/vpn_ipsec.py b/src/conf_mode/vpn_ipsec.py
index 3fab8e868..d3065fc47 100755
--- a/src/conf_mode/vpn_ipsec.py
+++ b/src/conf_mode/vpn_ipsec.py
@@ -33,9 +33,12 @@ from vyos.pki import wrap_crl
from vyos.pki import wrap_public_key
from vyos.pki import wrap_private_key
from vyos.template import ip_from_cidr
+from vyos.template import is_ipv4
+from vyos.template import is_ipv6
from vyos.template import render
from vyos.validate import is_ipv6_link_local
from vyos.util import call
+from vyos.util import dict_search
from vyos.util import dict_search_args
from vyos.util import run
from vyos.xml import defaults
@@ -46,13 +49,14 @@ airbag.enable()
dhcp_wait_attempts = 2
dhcp_wait_sleep = 1
-swanctl_dir = '/etc/swanctl'
-ipsec_conf = '/etc/ipsec.conf'
-ipsec_secrets = '/etc/ipsec.secrets'
-charon_conf = '/etc/strongswan.d/charon.conf'
-charon_dhcp_conf = '/etc/strongswan.d/charon/dhcp.conf'
-interface_conf = '/etc/strongswan.d/interfaces_use.conf'
-swanctl_conf = f'{swanctl_dir}/swanctl.conf'
+swanctl_dir = '/etc/swanctl'
+ipsec_conf = '/etc/ipsec.conf'
+ipsec_secrets = '/etc/ipsec.secrets'
+charon_conf = '/etc/strongswan.d/charon.conf'
+charon_dhcp_conf = '/etc/strongswan.d/charon/dhcp.conf'
+charon_radius_conf = '/etc/strongswan.d/charon/eap-radius.conf'
+interface_conf = '/etc/strongswan.d/interfaces_use.conf'
+swanctl_conf = f'{swanctl_dir}/swanctl.conf'
default_install_routes = 'yes'
@@ -73,6 +77,7 @@ def get_config(config=None):
else:
conf = Config()
base = ['vpn', 'ipsec']
+ l2tp_base = ['vpn', 'l2tp', 'remote-access', 'ipsec-settings']
if not conf.exists(base):
return None
@@ -97,26 +102,51 @@ def get_config(config=None):
ipsec['esp_group'][group])
if 'ike_group' in ipsec:
default_values = defaults(base + ['ike-group'])
+ # proposal is a tag node which may come with individual defaults per node
+ if 'proposal' in default_values:
+ del default_values['proposal']
+
for group in ipsec['ike_group']:
ipsec['ike_group'][group] = dict_merge(default_values,
ipsec['ike_group'][group])
- if 'remote_access' in ipsec:
- default_values = defaults(base + ['remote-access'])
- for rw in ipsec['remote_access']:
- ipsec['remote_access'][rw] = dict_merge(default_values,
- ipsec['remote_access'][rw])
+
+ if 'proposal' in ipsec['ike_group'][group]:
+ default_values = defaults(base + ['ike-group', 'proposal'])
+ for proposal in ipsec['ike_group'][group]['proposal']:
+ ipsec['ike_group'][group]['proposal'][proposal] = dict_merge(default_values,
+ ipsec['ike_group'][group]['proposal'][proposal])
+
+ if 'remote_access' in ipsec and 'connection' in ipsec['remote_access']:
+ default_values = defaults(base + ['remote-access', 'connection'])
+ for rw in ipsec['remote_access']['connection']:
+ ipsec['remote_access']['connection'][rw] = dict_merge(default_values,
+ ipsec['remote_access']['connection'][rw])
+
+ if 'remote_access' in ipsec and 'radius' in ipsec['remote_access'] and 'server' in ipsec['remote_access']['radius']:
+ default_values = defaults(base + ['remote-access', 'radius', 'server'])
+ for server in ipsec['remote_access']['radius']['server']:
+ ipsec['remote_access']['radius']['server'][server] = dict_merge(default_values,
+ ipsec['remote_access']['radius']['server'][server])
ipsec['dhcp_no_address'] = {}
ipsec['install_routes'] = 'no' if conf.exists(base + ["options", "disable-route-autoinstall"]) else default_install_routes
- ipsec['interface_change'] = leaf_node_changed(conf, base + ['ipsec-interfaces',
- 'interface'])
- ipsec['l2tp_exists'] = conf.exists(['vpn', 'l2tp', 'remote-access',
- 'ipsec-settings'])
+ ipsec['interface_change'] = leaf_node_changed(conf, base + ['interface'])
ipsec['nhrp_exists'] = conf.exists(['protocols', 'nhrp', 'tunnel'])
ipsec['pki'] = conf.get_config_dict(['pki'], key_mangling=('-', '_'),
get_first_key=True,
no_tag_node_value_mangle=True)
+ tmp = conf.get_config_dict(l2tp_base, key_mangling=('-', '_'),
+ get_first_key=True,
+ no_tag_node_value_mangle=True)
+ if tmp:
+ ipsec['l2tp'] = tmp
+ l2tp_defaults = defaults(l2tp_base)
+ ipsec['l2tp'] = dict_merge(l2tp_defaults, ipsec['l2tp'])
+ ipsec['l2tp_outside_address'] = conf.return_value(['vpn', 'l2tp', 'remote-access', 'outside-address'])
+ ipsec['l2tp_ike_default'] = 'aes256-sha1-modp1024,3des-sha1-modp1024'
+ ipsec['l2tp_esp_default'] = 'aes256-sha1,3des-sha1'
+
return ipsec
def get_dhcp_address(iface):
@@ -165,14 +195,43 @@ def verify(ipsec):
if not ipsec:
return None
- if 'ipsec_interfaces' in ipsec and 'interface' in ipsec['ipsec_interfaces']:
- interfaces = ipsec['ipsec_interfaces']['interface']
- if isinstance(interfaces, str):
- interfaces = [interfaces]
-
- for ifname in interfaces:
+ if 'interfaces' in ipsec :
+ for ifname in ipsec['interface']:
verify_interface_exists(ifname)
+ if 'l2tp' in ipsec:
+ if 'esp_group' in ipsec['l2tp']:
+ if 'esp_group' not in ipsec or ipsec['l2tp']['esp_group'] not in ipsec['esp_group']:
+ raise ConfigError(f"Invalid esp-group on L2TP remote-access config")
+
+ if 'ike_group' in ipsec['l2tp']:
+ if 'ike_group' not in ipsec or ipsec['l2tp']['ike_group'] not in ipsec['ike_group']:
+ raise ConfigError(f"Invalid ike-group on L2TP remote-access config")
+
+ if 'authentication' not in ipsec['l2tp']:
+ raise ConfigError(f'Missing authentication settings on L2TP remote-access config')
+
+ if 'mode' not in ipsec['l2tp']['authentication']:
+ raise ConfigError(f'Missing authentication mode on L2TP remote-access config')
+
+ if not ipsec['l2tp_outside_address']:
+ raise ConfigError(f'Missing outside-address on L2TP remote-access config')
+
+ if ipsec['l2tp']['authentication']['mode'] == 'pre-shared-secret':
+ if 'pre_shared_secret' not in ipsec['l2tp']['authentication']:
+ raise ConfigError(f'Missing pre shared secret on L2TP remote-access config')
+
+ if ipsec['l2tp']['authentication']['mode'] == 'x509':
+ if 'x509' not in ipsec['l2tp']['authentication']:
+ raise ConfigError(f'Missing x509 settings on L2TP remote-access config')
+
+ x509 = ipsec['l2tp']['authentication']['x509']
+
+ if 'ca_certificate' not in x509 or 'certificate' not in x509:
+ raise ConfigError(f'Missing x509 certificates on L2TP remote-access config')
+
+ verify_pki_x509(ipsec['pki'], x509)
+
if 'profile' in ipsec:
for profile, profile_conf in ipsec['profile'].items():
if 'esp_group' in profile_conf:
@@ -191,35 +250,86 @@ def verify(ipsec):
raise ConfigError(f"Missing authentication on {profile} profile")
if 'remote_access' in ipsec:
- for name, ra_conf in ipsec['remote_access'].items():
- if 'esp_group' in ra_conf:
- if 'esp_group' not in ipsec or ra_conf['esp_group'] not in ipsec['esp_group']:
- raise ConfigError(f"Invalid esp-group on {name} remote-access config")
- else:
- raise ConfigError(f"Missing esp-group on {name} remote-access config")
-
- if 'ike_group' in ra_conf:
- if 'ike_group' not in ipsec or ra_conf['ike_group'] not in ipsec['ike_group']:
- raise ConfigError(f"Invalid ike-group on {name} remote-access config")
- else:
- raise ConfigError(f"Missing ike-group on {name} remote-access config")
-
- if 'authentication' not in ra_conf:
- raise ConfigError(f"Missing authentication on {name} remote-access config")
-
- if ra_conf['authentication']['server_mode'] == 'x509':
- if 'x509' not in ra_conf['authentication']:
- raise ConfigError(f"Missing x509 settings on {name} remote-access config")
-
- x509 = ra_conf['authentication']['x509']
-
- if 'ca_certificate' not in x509 or 'certificate' not in x509:
- raise ConfigError(f"Missing x509 certificates on {name} remote-access config")
-
- verify_pki_x509(ipsec['pki'], x509)
- elif ra_conf['authentication']['server_mode'] == 'pre-shared-secret':
- if 'pre_shared_secret' not in ra_conf['authentication']:
- raise ConfigError(f"Missing pre-shared-key on {name} remote-access config")
+ if 'connection' in ipsec['remote_access']:
+ for name, ra_conf in ipsec['remote_access']['connection'].items():
+ if 'esp_group' in ra_conf:
+ if 'esp_group' not in ipsec or ra_conf['esp_group'] not in ipsec['esp_group']:
+ raise ConfigError(f"Invalid esp-group on {name} remote-access config")
+ else:
+ raise ConfigError(f"Missing esp-group on {name} remote-access config")
+
+ if 'ike_group' in ra_conf:
+ if 'ike_group' not in ipsec or ra_conf['ike_group'] not in ipsec['ike_group']:
+ raise ConfigError(f"Invalid ike-group on {name} remote-access config")
+
+ ike = ra_conf['ike_group']
+ if dict_search(f'ike_group.{ike}.key_exchange', ipsec) != 'ikev2':
+ raise ConfigError('IPSec remote-access connections requires IKEv2!')
+
+ else:
+ raise ConfigError(f"Missing ike-group on {name} remote-access config")
+
+ if 'authentication' not in ra_conf:
+ raise ConfigError(f"Missing authentication on {name} remote-access config")
+
+ if ra_conf['authentication']['server_mode'] == 'x509':
+ if 'x509' not in ra_conf['authentication']:
+ raise ConfigError(f"Missing x509 settings on {name} remote-access config")
+
+ x509 = ra_conf['authentication']['x509']
+
+ if 'ca_certificate' not in x509 or 'certificate' not in x509:
+ raise ConfigError(f"Missing x509 certificates on {name} remote-access config")
+
+ verify_pki_x509(ipsec['pki'], x509)
+ elif ra_conf['authentication']['server_mode'] == 'pre-shared-secret':
+ if 'pre_shared_secret' not in ra_conf['authentication']:
+ raise ConfigError(f"Missing pre-shared-key on {name} remote-access config")
+
+
+ if 'client_mode' in ra_conf['authentication']:
+ if ra_conf['authentication']['client_mode'] == 'eap-radius':
+ if 'radius' not in ipsec['remote_access'] or 'server' not in ipsec['remote_access']['radius'] or len(ipsec['remote_access']['radius']['server']) == 0:
+ raise ConfigError('RADIUS authentication requires at least one server')
+
+ if 'pool' in ra_conf:
+ if 'dhcp' in ra_conf['pool'] and len(ra_conf['pool']) > 1:
+ raise ConfigError(f'Can not use both DHCP and a predefined address pool for "{name}"!')
+
+ for pool in ra_conf['pool']:
+ if pool == 'dhcp':
+ if dict_search('remote_access.dhcp.server', ipsec) == None:
+ raise ConfigError('IPSec DHCP server is not configured!')
+
+ elif 'pool' not in ipsec['remote_access'] or pool not in ipsec['remote_access']['pool']:
+ raise ConfigError(f'Requested pool "{pool}" does not exist!')
+
+ if 'pool' in ipsec['remote_access']:
+ for pool, pool_config in ipsec['remote_access']['pool'].items():
+ if 'prefix' not in pool_config:
+ raise ConfigError(f'Missing madatory prefix option for pool "{pool}"!')
+
+ if 'name_server' in pool_config:
+ if len(pool_config['name_server']) > 2:
+ raise ConfigError(f'Only two name-servers are supported for remote-access pool "{pool}"!')
+
+ for ns in pool_config['name_server']:
+ v4_addr_and_ns = is_ipv4(ns) and not is_ipv4(pool_config['prefix'])
+ v6_addr_and_ns = is_ipv6(ns) and not is_ipv6(pool_config['prefix'])
+ if v4_addr_and_ns or v6_addr_and_ns:
+ raise ConfigError('Must use both IPv4 or IPv6 addresses for pool prefix and name-server adresses!')
+
+ if 'exclude' in pool_config:
+ for exclude in pool_config['exclude']:
+ v4_addr_and_exclude = is_ipv4(exclude) and not is_ipv4(pool_config['prefix'])
+ v6_addr_and_exclude = is_ipv6(exclude) and not is_ipv6(pool_config['prefix'])
+ if v4_addr_and_exclude or v6_addr_and_exclude:
+ raise ConfigError('Must use both IPv4 or IPv6 addresses for pool prefix and exclude prefixes!')
+
+ if 'radius' in ipsec['remote_access'] and 'server' in ipsec['remote_access']['radius']:
+ for server, server_config in ipsec['remote_access']['radius']['server'].items():
+ if 'key' not in server_config:
+ raise ConfigError(f'Missing RADIUS secret key for server "{server}"')
if 'site_to_site' in ipsec and 'peer' in ipsec['site_to_site']:
for peer, peer_conf in ipsec['site_to_site']['peer'].items():
@@ -372,7 +482,8 @@ def generate(ipsec):
cleanup_pki_files()
if not ipsec:
- for config_file in [ipsec_conf, ipsec_secrets, charon_dhcp_conf, interface_conf, swanctl_conf]:
+ for config_file in [ipsec_conf, ipsec_secrets, charon_dhcp_conf,
+ charon_radius_conf, interface_conf, swanctl_conf]:
if os.path.isfile(config_file):
os.unlink(config_file)
render(charon_conf, 'ipsec/charon.tmpl', {'install_routes': default_install_routes})
@@ -389,8 +500,13 @@ def generate(ipsec):
if not os.path.exists(KEY_PATH):
os.mkdir(KEY_PATH, mode=0o700)
- if 'remote_access' in ipsec:
- for rw, rw_conf in ipsec['remote_access'].items():
+ if 'l2tp' in ipsec:
+ if 'authentication' in ipsec['l2tp'] and 'x509' in ipsec['l2tp']['authentication']:
+ generate_pki_files_x509(ipsec['pki'], ipsec['l2tp']['authentication']['x509'])
+
+ if 'remote_access' in ipsec and 'connection' in ipsec['remote_access']:
+ for rw, rw_conf in ipsec['remote_access']['connection'].items():
+
if 'authentication' in rw_conf and 'x509' in rw_conf['authentication']:
generate_pki_files_x509(ipsec['pki'], rw_conf['authentication']['x509'])
@@ -436,17 +552,10 @@ def generate(ipsec):
render(ipsec_secrets, 'ipsec/ipsec.secrets.tmpl', ipsec)
render(charon_conf, 'ipsec/charon.tmpl', ipsec)
render(charon_dhcp_conf, 'ipsec/charon/dhcp.conf.tmpl', ipsec)
+ render(charon_radius_conf, 'ipsec/charon/eap-radius.conf.tmpl', ipsec)
render(interface_conf, 'ipsec/interfaces_use.conf.tmpl', ipsec)
render(swanctl_conf, 'ipsec/swanctl.conf.tmpl', ipsec)
-def resync_l2tp(ipsec):
- if ipsec and not ipsec['l2tp_exists']:
- return
-
- tmp = run('/usr/libexec/vyos/conf_mode/ipsec-settings.py')
- if tmp > 0:
- print('ERROR: failed to reapply L2TP IPSec settings!')
-
def resync_nhrp(ipsec):
if ipsec and not ipsec['nhrp_exists']:
return
@@ -470,17 +579,13 @@ def apply(ipsec):
if not ipsec:
call('sudo ipsec stop')
else:
- args = ''
- if 'auto_update' in ipsec:
- args = '--auto-update ' + ipsec['auto_update']
- call(f'sudo ipsec restart {args}')
+ call('sudo ipsec restart')
call('sudo ipsec rereadall')
call('sudo ipsec reload')
if wait_for_vici_socket():
call('sudo swanctl -q')
- resync_l2tp(ipsec)
resync_nhrp(ipsec)
if __name__ == '__main__':
diff --git a/src/conf_mode/vpn_l2tp.py b/src/conf_mode/vpn_l2tp.py
index e970d2ef5..9c52f77ca 100755
--- a/src/conf_mode/vpn_l2tp.py
+++ b/src/conf_mode/vpn_l2tp.py
@@ -20,7 +20,6 @@ import re
from copy import deepcopy
from stat import S_IRUSR, S_IWUSR, S_IRGRP
from sys import exit
-from time import sleep
from ipaddress import ip_network
diff --git a/src/conf_mode/vpn_openconnect.py b/src/conf_mode/vpn_openconnect.py
index 2986c3458..f6db196dc 100755
--- a/src/conf_mode/vpn_openconnect.py
+++ b/src/conf_mode/vpn_openconnect.py
@@ -19,9 +19,11 @@ from sys import exit
from vyos.config import Config
from vyos.configdict import dict_merge
-from vyos.xml import defaults
+from vyos.pki import wrap_certificate
+from vyos.pki import wrap_private_key
from vyos.template import render
from vyos.util import call
+from vyos.xml import defaults
from vyos import ConfigError
from crypt import crypt, mksalt, METHOD_SHA512
@@ -50,6 +52,10 @@ def get_config():
default_values = defaults(base)
ocserv = dict_merge(default_values, ocserv)
+ if ocserv:
+ ocserv['pki'] = conf.get_config_dict(['pki'], key_mangling=('-', '_'),
+ get_first_key=True, no_tag_node_value_mangle=True)
+
return ocserv
def verify(ocserv):
@@ -72,14 +78,36 @@ def verify(ocserv):
raise ConfigError('openconnect authentication credentials required')
# Check ssl
- if "ssl" in ocserv:
- req_cert = ['cert_file', 'key_file']
- for cert in req_cert:
- if not cert in ocserv["ssl"]:
- raise ConfigError('openconnect ssl {0} required'.format(cert.replace('_', '-')))
- else:
+ if 'ssl' not in ocserv:
raise ConfigError('openconnect ssl required')
+ if not ocserv['pki'] or 'certificate' not in ocserv['pki']:
+ raise ConfigError('PKI not configured')
+
+ ssl = ocserv['ssl']
+ if 'certificate' not in ssl:
+ raise ConfigError('openconnect ssl certificate required')
+
+ cert_name = ssl['certificate']
+
+ if cert_name not in ocserv['pki']['certificate']:
+ raise ConfigError('Invalid openconnect ssl certificate')
+
+ cert = ocserv['pki']['certificate'][cert_name]
+
+ if 'certificate' not in cert:
+ raise ConfigError('Missing certificate in PKI')
+
+ if 'private' not in cert or 'key' not in cert['private']:
+ raise ConfigError('Missing private key in PKI')
+
+ if 'ca_certificate' in ssl:
+ if 'ca' not in ocserv['pki']:
+ raise ConfigError('PKI not configured')
+
+ if ssl['ca_certificate'] not in ocserv['pki']['ca']:
+ raise ConfigError('Invalid openconnect ssl CA certificate')
+
# Check network settings
if "network_settings" in ocserv:
if "push_route" in ocserv["network_settings"]:
@@ -109,6 +137,29 @@ def generate(ocserv):
# Render local users
render(ocserv_passwd, 'ocserv/ocserv_passwd.tmpl', ocserv["authentication"]["local_users"])
+ if "ssl" in ocserv:
+ cert_file_path = os.path.join(cfg_dir, 'cert.pem')
+ cert_key_path = os.path.join(cfg_dir, 'cert.key')
+ ca_cert_file_path = os.path.join(cfg_dir, 'ca.pem')
+
+ if 'certificate' in ocserv['ssl']:
+ cert_name = ocserv['ssl']['certificate']
+ pki_cert = ocserv['pki']['certificate'][cert_name]
+
+ with open(cert_file_path, 'w') as f:
+ f.write(wrap_certificate(pki_cert['certificate']))
+
+ if 'private' in pki_cert and 'key' in pki_cert['private']:
+ with open(cert_key_path, 'w') as f:
+ f.write(wrap_private_key(pki_cert['private']['key']))
+
+ if 'ca_certificate' in ocserv['ssl']:
+ ca_name = ocserv['ssl']['ca_certificate']
+ pki_ca_cert = ocserv['pki']['ca'][ca_name]
+
+ with open(ca_cert_file_path, 'w') as f:
+ f.write(wrap_certificate(pki_ca_cert['certificate']))
+
# Render config
render(ocserv_conf, 'ocserv/ocserv_config.tmpl', ocserv)
diff --git a/src/conf_mode/vpn_sstp.py b/src/conf_mode/vpn_sstp.py
index 47367f125..d1a71a5ad 100755
--- a/src/conf_mode/vpn_sstp.py
+++ b/src/conf_mode/vpn_sstp.py
@@ -21,6 +21,8 @@ from sys import exit
from vyos.config import Config
from vyos.configdict import get_accel_dict
from vyos.configverify import verify_accel_ppp_base_service
+from vyos.pki import wrap_certificate
+from vyos.pki import wrap_private_key
from vyos.template import render
from vyos.util import call
from vyos.util import dict_search
@@ -28,6 +30,7 @@ from vyos import ConfigError
from vyos import airbag
airbag.enable()
+cfg_dir = '/run/accel-pppd'
sstp_conf = '/run/accel-pppd/sstp.conf'
sstp_chap_secrets = '/run/accel-pppd/sstp.chap-secrets'
@@ -42,6 +45,11 @@ def get_config(config=None):
# retrieve common dictionary keys
sstp = get_accel_dict(conf, base, sstp_chap_secrets)
+
+ if sstp:
+ sstp['pki'] = conf.get_config_dict(['pki'], key_mangling=('-', '_'),
+ get_first_key=True, no_tag_node_value_mangle=True)
+
return sstp
def verify(sstp):
@@ -56,31 +64,59 @@ def verify(sstp):
#
# SSL certificate checks
#
- tmp = dict_search('ssl.ca_cert_file', sstp)
- if not tmp:
- raise ConfigError(f'SSL CA certificate file required!')
- else:
- if not os.path.isfile(tmp):
- raise ConfigError(f'SSL CA certificate "{tmp}" does not exist!')
+ if not sstp['pki']:
+ raise ConfigError('PKI is not configured')
- tmp = dict_search('ssl.cert_file', sstp)
- if not tmp:
- raise ConfigError(f'SSL public key file required!')
- else:
- if not os.path.isfile(tmp):
- raise ConfigError(f'SSL public key "{tmp}" does not exist!')
+ if 'ssl' not in sstp:
+ raise ConfigError('SSL missing on SSTP config')
- tmp = dict_search('ssl.key_file', sstp)
- if not tmp:
- raise ConfigError(f'SSL private key file required!')
- else:
- if not os.path.isfile(tmp):
- raise ConfigError(f'SSL private key "{tmp}" does not exist!')
+ ssl = sstp['ssl']
+
+ if 'ca_certificate' not in ssl:
+ raise ConfigError('SSL CA certificate missing on SSTP config')
+
+ if 'certificate' not in ssl:
+ raise ConfigError('SSL certificate missing on SSTP config')
+
+ cert_name = ssl['certificate']
+
+ if ssl['ca_certificate'] not in sstp['pki']['ca']:
+ raise ConfigError('Invalid CA certificate on SSTP config')
+
+ if cert_name not in sstp['pki']['certificate']:
+ raise ConfigError('Invalid certificate on SSTP config')
+
+ pki_cert = sstp['pki']['certificate'][cert_name]
+
+ if 'private' not in pki_cert or 'key' not in pki_cert['private']:
+ raise ConfigError('Missing private key for certificate on SSTP config')
+
+ if 'password_protected' in pki_cert['private']:
+ raise ConfigError('Encrypted private key is not supported on SSTP config')
def generate(sstp):
if not sstp:
return None
+ cert_file_path = os.path.join(cfg_dir, 'sstp-cert.pem')
+ cert_key_path = os.path.join(cfg_dir, 'sstp-cert.key')
+ ca_cert_file_path = os.path.join(cfg_dir, 'sstp-ca.pem')
+
+ cert_name = sstp['ssl']['certificate']
+ pki_cert = sstp['pki']['certificate'][cert_name]
+
+ with open(cert_file_path, 'w') as f:
+ f.write(wrap_certificate(pki_cert['certificate']))
+
+ with open(cert_key_path, 'w') as f:
+ f.write(wrap_private_key(pki_cert['private']['key']))
+
+ ca_cert_name = sstp['ssl']['ca_certificate']
+ pki_ca = sstp['pki']['ca'][ca_cert_name]
+
+ with open(ca_cert_file_path, 'w') as f:
+ f.write(wrap_certificate(pki_ca['certificate']))
+
# accel-cmd reload doesn't work so any change results in a restart of the daemon
render(sstp_conf, 'accel-ppp/sstp.config.tmpl', sstp)
diff --git a/src/conf_mode/vrf.py b/src/conf_mode/vrf.py
index 936561edc..c1cfc1dcb 100755
--- a/src/conf_mode/vrf.py
+++ b/src/conf_mode/vrf.py
@@ -18,6 +18,7 @@ import os
from sys import exit
from json import loads
+from tempfile import NamedTemporaryFile
from vyos.config import Config
from vyos.configdict import node_changed
@@ -28,6 +29,8 @@ from vyos.util import call
from vyos.util import cmd
from vyos.util import dict_search
from vyos.util import get_interface_config
+from vyos.util import popen
+from vyos.util import run
from vyos import ConfigError
from vyos import frr
from vyos import airbag
@@ -125,11 +128,17 @@ def verify(vrf):
return None
+
def generate(vrf):
render(config_file, 'vrf/vrf.conf.tmpl', vrf)
vrf['new_frr_config'] = render_to_string('frr/vrf.frr.tmpl', vrf)
+ # Render nftables zones config
+ vrf['nft_vrf_zones'] = NamedTemporaryFile().name
+ render(vrf['nft_vrf_zones'], 'firewall/nftables-vrf-zones.tmpl', vrf)
+
return None
+
def apply(vrf):
# Documentation
#
@@ -151,8 +160,19 @@ def apply(vrf):
call(f'ip -4 route del vrf {tmp} unreachable default metric 4278198272')
call(f'ip -6 route del vrf {tmp} unreachable default metric 4278198272')
call(f'ip link delete dev {tmp}')
+ # Remove nftables conntrack zone map item
+ nft_del_element = f'delete element inet vrf_zones ct_iface_map {{ "{tmp}" }}'
+ cmd(f'nft {nft_del_element}')
if 'name' in vrf:
+ # Separate VRFs in conntrack table
+ # check if table already exists
+ _, err = popen('nft list table inet vrf_zones')
+ # If not, create a table
+ if err:
+ cmd(f'nft -f {vrf["nft_vrf_zones"]}')
+ os.unlink(vrf['nft_vrf_zones'])
+
for name, config in vrf['name'].items():
table = config['table']
@@ -182,6 +202,9 @@ def apply(vrf):
# reconfiguration.
state = 'down' if 'disable' in config else 'up'
vrf_if.set_admin_state(state)
+ # Add nftables conntrack zone map item
+ nft_add_element = f'add element inet vrf_zones ct_iface_map {{ "{name}" : {table} }}'
+ cmd(f'nft {nft_add_element}')
# Linux routing uses rules to find tables - routing targets are then
# looked up in those tables. If the lookup got a matching route, the
@@ -214,22 +237,25 @@ def apply(vrf):
# clean out l3mdev-table rule if present
if 1000 in [r.get('priority') for r in list_rules() if r.get('priority') == 1000]:
call(f'ip {af} rule del pref 1000')
-
- # add configuration to FRR
- frr_cfg = frr.FRRConfig()
- frr_cfg.load_configuration(frr_daemon)
- frr_cfg.modify_section(f'^vrf [a-zA-Z-]*$', '')
- frr_cfg.add_before(r'(interface .*|line vty)', vrf['new_frr_config'])
- frr_cfg.commit_configuration(frr_daemon)
-
- # If FRR config is blank, rerun the blank commit x times due to frr-reload
- # behavior/bug not properly clearing out on one commit.
- if vrf['new_frr_config'] == '':
- for a in range(5):
- frr_cfg.commit_configuration(frr_daemon)
-
- # Save configuration to /run/frr/config/frr.conf
- frr.save_configuration()
+ # Remove VRF zones table from nftables
+ tmp = run('nft list table inet vrf_zones')
+ if tmp == 0:
+ cmd('nft delete table inet vrf_zones')
+
+ # T3694: Somehow we hit a priority inversion here as we need to remove the
+ # VRF assigned VNI before we can remove a BGP bound VRF instance. Maybe
+ # move this to an individual helper script that set's up the VNI for the
+ # given VRF after any routing protocol.
+ #
+ # # add configuration to FRR
+ # frr_cfg = frr.FRRConfig()
+ # frr_cfg.load_configuration(frr_daemon)
+ # frr_cfg.modify_section(f'^vrf [a-zA-Z-]*$', '')
+ # frr_cfg.add_before(r'(interface .*|line vty)', vrf['new_frr_config'])
+ # frr_cfg.commit_configuration(frr_daemon)
+ #
+ # # Save configuration to /run/frr/config/frr.conf
+ # frr.save_configuration()
return None
diff --git a/src/conf_mode/vyos_cert.py b/src/conf_mode/vyos_cert.py
deleted file mode 100755
index dc7c64684..000000000
--- a/src/conf_mode/vyos_cert.py
+++ /dev/null
@@ -1,147 +0,0 @@
-#!/usr/bin/env python3
-#
-# Copyright (C) 2019 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 sys
-import os
-import tempfile
-import pathlib
-import ssl
-
-import vyos.defaults
-from vyos.config import Config
-from vyos import ConfigError
-from vyos.util import cmd
-
-from vyos import airbag
-airbag.enable()
-
-vyos_conf_scripts_dir = vyos.defaults.directories['conf_mode']
-
-# XXX: this model will need to be extended for tag nodes
-dependencies = [
- 'https.py',
-]
-
-def status_self_signed(cert_data):
-# check existence and expiration date
- path = pathlib.Path(cert_data['conf'])
- if not path.is_file():
- return False
- path = pathlib.Path(cert_data['crt'])
- if not path.is_file():
- return False
- path = pathlib.Path(cert_data['key'])
- if not path.is_file():
- return False
-
- # check if certificate is 1/2 past lifetime, with openssl -checkend
- end_days = int(cert_data['lifetime'])
- end_seconds = int(0.5*60*60*24*end_days)
- checkend_cmd = 'openssl x509 -checkend {end} -noout -in {crt}'.format(end=end_seconds, **cert_data)
- try:
- cmd(checkend_cmd, message='Called process error')
- return True
- except OSError as err:
- if err.errno == 1:
- return False
- print(err)
- # XXX: This seems wrong to continue on failure
- # implicitely returning None
-
-def generate_self_signed(cert_data):
- san_config = None
-
- if ssl.OPENSSL_VERSION_INFO < (1, 1, 1, 0, 0):
- san_config = tempfile.NamedTemporaryFile()
- with open(san_config.name, 'w') as fd:
- fd.write('[req]\n')
- fd.write('distinguished_name=req\n')
- fd.write('[san]\n')
- fd.write('subjectAltName=DNS:vyos\n')
-
- openssl_req_cmd = ('openssl req -x509 -nodes -days {lifetime} '
- '-newkey rsa:4096 -keyout {key} -out {crt} '
- '-subj "/O=Sentrium/OU=VyOS/CN=vyos" '
- '-extensions san -config {san_conf}'
- ''.format(san_conf=san_config.name,
- **cert_data))
-
- else:
- openssl_req_cmd = ('openssl req -x509 -nodes -days {lifetime} '
- '-newkey rsa:4096 -keyout {key} -out {crt} '
- '-subj "/O=Sentrium/OU=VyOS/CN=vyos" '
- '-addext "subjectAltName=DNS:vyos"'
- ''.format(**cert_data))
-
- try:
- cmd(openssl_req_cmd, message='Called process error')
- except OSError as err:
- print(err)
- # XXX: seems wrong to ignore the failure
-
- os.chmod('{key}'.format(**cert_data), 0o400)
-
- with open('{conf}'.format(**cert_data), 'w') as f:
- f.write('ssl_certificate {crt};\n'.format(**cert_data))
- f.write('ssl_certificate_key {key};\n'.format(**cert_data))
-
- if san_config:
- san_config.close()
-
-def get_config(config=None):
- vyos_cert = vyos.defaults.vyos_cert_data
-
- if config:
- conf = config
- else:
- conf = Config()
- if not conf.exists('service https certificates system-generated-certificate'):
- return None
- else:
- conf.set_level('service https certificates system-generated-certificate')
-
- if conf.exists('lifetime'):
- lifetime = conf.return_value('lifetime')
- vyos_cert['lifetime'] = lifetime
-
- return vyos_cert
-
-def verify(vyos_cert):
- return None
-
-def generate(vyos_cert):
- if vyos_cert is None:
- return None
-
- if not status_self_signed(vyos_cert):
- generate_self_signed(vyos_cert)
-
-def apply(vyos_cert):
- for dep in dependencies:
- command = '{0}/{1}'.format(vyos_conf_scripts_dir, dep)
- cmd(command, raising=ConfigError)
-
-if __name__ == '__main__':
- try:
- c = get_config()
- verify(c)
- generate(c)
- apply(c)
- except ConfigError as e:
- print(e)
- sys.exit(1)
diff --git a/src/etc/ipsec.d/vti-up-down b/src/etc/ipsec.d/vti-up-down
index 2b66dd9e6..281c9bf2b 100755
--- a/src/etc/ipsec.d/vti-up-down
+++ b/src/etc/ipsec.d/vti-up-down
@@ -19,7 +19,15 @@
import os
import sys
-from vyos.util import call, get_interface_config, get_interface_address
+from syslog import syslog
+from syslog import openlog
+from syslog import LOG_PID
+from syslog import LOG_INFO
+
+from vyos.configquery import ConfigTreeQuery
+from vyos.util import call
+from vyos.util import get_interface_config
+from vyos.util import get_interface_address
def get_dhcp_address(interface):
addr = get_interface_address(interface)
@@ -35,7 +43,8 @@ if __name__ == '__main__':
interface = sys.argv[1]
dhcp_interface = sys.argv[2]
- print(f'vti-up-down: start: {verb} {connection} {interface}')
+ openlog(ident=f'vti-up-down', logoption=LOG_PID, facility=LOG_INFO)
+ syslog(f'Interface {interface} {verb} {connection}')
if verb in ['up-client', 'up-host']:
call('sudo ip route delete default table 220')
@@ -43,19 +52,24 @@ if __name__ == '__main__':
vti_link = get_interface_config(interface)
if not vti_link:
- print('vti-up-down: interface not found')
+ syslog(f'Interface {interface} not found')
sys.exit(0)
vti_link_up = (vti_link['operstate'] == 'UP' if 'operstate' in vti_link else False)
+ config = ConfigTreeQuery()
+ vti_dict = config.get_config_dict(['interfaces', 'vti', interface],
+ get_first_key=True)
+
if verb in ['up-client', 'up-host']:
if not vti_link_up:
if dhcp_interface != 'no':
local_ip = get_dhcp_address(dhcp_interface)
call(f'sudo ip tunnel change {interface} local {local_ip}')
- call(f'sudo ip link set {interface} up')
+ if 'disable' not in vti_dict:
+ call(f'sudo ip link set {interface} up')
+ else:
+ syslog(f'Interface {interface} is admin down ...')
elif verb in ['down-client', 'down-host']:
if vti_link_up:
call(f'sudo ip link set {interface} down')
-
- print('vti-up-down: finish') \ No newline at end of file
diff --git a/src/etc/sysctl.d/30-vyos-router.conf b/src/etc/sysctl.d/30-vyos-router.conf
index 8265e12dc..e03d3a29c 100644
--- a/src/etc/sysctl.d/30-vyos-router.conf
+++ b/src/etc/sysctl.d/30-vyos-router.conf
@@ -72,6 +72,12 @@ net.ipv4.conf.default.send_redirects=1
# Increase size of buffer for netlink
net.core.rmem_max=2097152
+# Remove IPv4 and IPv6 routes from forward information base when link goes down
+net.ipv4.conf.all.ignore_routes_with_linkdown=1
+net.ipv4.conf.default.ignore_routes_with_linkdown=1
+net.ipv6.conf.all.ignore_routes_with_linkdown=1
+net.ipv6.conf.default.ignore_routes_with_linkdown=1
+
# Enable packet forwarding for IPv6
net.ipv6.conf.all.forwarding=1
@@ -81,6 +87,7 @@ net.ipv6.route.max_size = 262144
# Do not forget IPv6 addresses when a link goes down
net.ipv6.conf.default.keep_addr_on_down=1
net.ipv6.conf.all.keep_addr_on_down=1
+net.ipv6.route.skip_notify_on_dev_down=1
# Default value of 20 seems to interfere with larger OSPF and VRRP setups
net.ipv4.igmp_max_memberships = 512
diff --git a/src/etc/update-motd.d/99-reboot b/src/etc/update-motd.d/99-reboot
new file mode 100755
index 000000000..718be1a7a
--- /dev/null
+++ b/src/etc/update-motd.d/99-reboot
@@ -0,0 +1,7 @@
+#!/bin/vbash
+source /opt/vyatta/etc/functions/script-template
+if [ -f /run/systemd/shutdown/scheduled ]; then
+ echo
+ run show reboot
+fi
+exit
diff --git a/src/etc/vmware-tools/scripts/resume-vm-default.d/ether-resume.py b/src/etc/vmware-tools/scripts/resume-vm-default.d/ether-resume.py
index ec33906ba..4e7fb117c 100755
--- a/src/etc/vmware-tools/scripts/resume-vm-default.d/ether-resume.py
+++ b/src/etc/vmware-tools/scripts/resume-vm-default.d/ether-resume.py
@@ -25,9 +25,8 @@ def get_config():
c = Config()
interfaces = dict()
for intf in c.list_effective_nodes('interfaces ethernet'):
- # skip interfaces that are disabled or is configured for dhcp
+ # skip interfaces that are disabled
check_disable = f'interfaces ethernet {intf} disable'
- check_dhcp = f'interfaces ethernet {intf} address dhcp'
if c.exists_effective(check_disable):
continue
@@ -49,10 +48,10 @@ def apply(config):
# add configured addresses to interface
for addr in addresses:
- if addr == 'dhcp':
- cmd = ['dhclient', intf]
- else:
- cmd = f'ip address add {addr} dev {intf}'
+ # dhcp is handled by netplug
+ if addr in ['dhcp', 'dhcpv6']:
+ continue
+ cmd = f'ip address add {addr} dev {intf}'
syslog.syslog(cmd)
run(cmd)
diff --git a/src/helpers/strip-private.py b/src/helpers/strip-private.py
index 420a039eb..c165d2cba 100755
--- a/src/helpers/strip-private.py
+++ b/src/helpers/strip-private.py
@@ -116,32 +116,33 @@ if __name__ == "__main__":
(True, re.compile(r'pre-shared-secret \S+'), 'pre-shared-secret xxxxxx'),
# Strip OSPF md5-key
(True, re.compile(r'md5-key \S+'), 'md5-key xxxxxx'),
-
+ # Strip WireGuard private-key
+ (True, re.compile(r'private-key \S+'), 'private-key xxxxxx'),
+
# Strip MAC addresses
(args.mac, re.compile(r'([0-9a-fA-F]{2}\:){5}([0-9a-fA-F]{2}((\:{0,1})){3})'), r'XX:XX:XX:XX:XX:\2'),
# Strip host-name, domain-name, and domain-search
(args.hostname, re.compile(r'(host-name|domain-name|domain-search) \S+'), r'\1 xxxxxx'),
-
+
# Strip user-names
(args.username, re.compile(r'(user|username|user-id) \S+'), r'\1 xxxxxx'),
# Strip full-name
(args.username, re.compile(r'(full-name) [ -_A-Z a-z]+'), r'\1 xxxxxx'),
-
+
# Strip DHCP static-mapping and shared network names
(args.dhcp, re.compile(r'(shared-network-name|static-mapping) \S+'), r'\1 xxxxxx'),
-
+
# Strip host/domain names
(args.domain, re.compile(r' (peer|remote-host|local-host|server) ([\w-]+\.)+[\w-]+'), r' \1 xxxxx.tld'),
-
+
# Strip BGP ASNs
(args.asn, re.compile(r'(bgp|remote-as) (\d+)'), r'\1 XXXXXX'),
-
+
# Strip LLDP location parameters
(args.lldp, re.compile(r'(altitude|datum|latitude|longitude|ca-value|country-code) (\S+)'), r'\1 xxxxxx'),
-
+
# Strip SNMP location
(args.snmp, re.compile(r'(location) \S+'), r'\1 xxxxxx'),
]
strip_lines(stripping_rules)
-
diff --git a/src/helpers/vyos-bridge-sync.py b/src/helpers/vyos-bridge-sync.py
deleted file mode 100755
index 097d28d85..000000000
--- a/src/helpers/vyos-bridge-sync.py
+++ /dev/null
@@ -1,51 +0,0 @@
-#!/usr/bin/env python3
-#
-# Copyright (C) 2019 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/>.
-#
-
-# Script is used to synchronize configured bridge interfaces.
-# one can add a non existing interface to a bridge group (e.g. VLAN)
-# but the vlan interface itself does yet not exist. It should be added
-# to the bridge automatically once it's available
-
-import argparse
-from sys import exit
-from time import sleep
-
-from vyos.config import Config
-from vyos.util import cmd, run
-
-
-if __name__ == '__main__':
- parser = argparse.ArgumentParser()
- parser.add_argument('-i', '--interface', action='store', help='Interface name which should be added to bridge it is configured for', required=True)
- args, unknownargs = parser.parse_known_args()
-
- conf = Config()
- if not conf.list_nodes('interfaces bridge'):
- # no bridge interfaces exist .. bail out early
- exit(0)
- else:
- for bridge in conf.list_nodes('interfaces bridge'):
- for member_if in conf.list_nodes('interfaces bridge {} member interface'.format(bridge)):
- if args.interface == member_if:
- command = 'brctl addif "{}" "{}"'.format(bridge, args.interface)
- # let interfaces etc. settle - especially required for OpenVPN bridged interfaces
- sleep(4)
- # XXX: This is ignoring any issue, should be cmd but kept as it
- # XXX: during the migration to not cause any regression
- run(command)
-
- exit(0)
diff --git a/src/migration-scripts/https/2-to-3 b/src/migration-scripts/https/2-to-3
new file mode 100755
index 000000000..fa29fdd18
--- /dev/null
+++ b/src/migration-scripts/https/2-to-3
@@ -0,0 +1,86 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2021 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/>.
+
+# * Migrate system signed certificate to use PKI
+
+import sys
+
+from vyos.configtree import ConfigTree
+from vyos.pki import create_certificate
+from vyos.pki import create_certificate_request
+from vyos.pki import create_private_key
+from vyos.pki import encode_certificate
+from vyos.pki import encode_private_key
+
+if (len(sys.argv) < 2):
+ print("Must specify file name!")
+ sys.exit(1)
+
+file_name = sys.argv[1]
+
+with open(file_name, 'r') as f:
+ config_file = f.read()
+
+config = ConfigTree(config_file)
+
+base = ['service', 'https', 'certificates']
+pki_base = ['pki']
+
+if not config.exists(base + ['system-generated-certificate']):
+ sys.exit(0)
+
+def wrapped_pem_to_config_value(pem):
+ out = []
+ for line in pem.strip().split("\n"):
+ if not line or line.startswith("-----") or line[0] == '#':
+ continue
+ out.append(line)
+ return "".join(out)
+
+if not config.exists(pki_base + ['certificate']):
+ config.set(pki_base + ['certificate'])
+ config.set_tag(pki_base + ['certificate'])
+
+valid_days = 365
+if config.exists(base + ['system-generated-certificate', 'lifetime']):
+ valid_days = int(config.return_value(base + ['system-generated-certificate', 'lifetime']))
+
+key = create_private_key('rsa', 2048)
+subject = {'country': 'GB', 'state': 'N/A', 'locality': 'N/A', 'organization': 'VyOS', 'common_name': 'vyos'}
+cert_req = create_certificate_request(subject, key, ['vyos'])
+cert = create_certificate(cert_req, cert_req, key, valid_days)
+
+if cert:
+ cert_pem = encode_certificate(cert)
+ config.set(pki_base + ['certificate', 'generated_https', 'certificate'], value=wrapped_pem_to_config_value(cert_pem))
+
+if key:
+ key_pem = encode_private_key(key)
+ config.set(pki_base + ['certificate', 'generated_https', 'private', 'key'], value=wrapped_pem_to_config_value(key_pem))
+
+if cert and key:
+ config.set(base + ['certificate'], value='generated_https')
+else:
+ print('Failed to migrate system-generated-certificate from https service')
+
+config.delete(base + ['system-generated-certificate'])
+
+try:
+ with open(file_name, 'w') as f:
+ f.write(config.to_string())
+except OSError as e:
+ print("Failed to save the modified config: {}".format(e))
+ sys.exit(1)
diff --git a/src/migration-scripts/interfaces/19-to-20 b/src/migration-scripts/interfaces/19-to-20
index 06e07572f..e96663e54 100755
--- a/src/migration-scripts/interfaces/19-to-20
+++ b/src/migration-scripts/interfaces/19-to-20
@@ -18,62 +18,6 @@ from sys import argv
from sys import exit
from vyos.configtree import ConfigTree
-def migrate_ospf(config, path, interface):
- path = path + ['ospf']
- if config.exists(path):
- new_base = ['protocols', 'ospf', 'interface']
- config.set(new_base)
- config.set_tag(new_base)
- config.copy(path, new_base + [interface])
- config.delete(path)
-
- # if "ip ospf" was the only setting, we can clean out the empty
- # ip node afterwards
- if len(config.list_nodes(path[:-1])) == 0:
- config.delete(path[:-1])
-
-def migrate_ospfv3(config, path, interface):
- path = path + ['ospfv3']
- if config.exists(path):
- new_base = ['protocols', 'ospfv3', 'interface']
- config.set(new_base)
- config.set_tag(new_base)
- config.copy(path, new_base + [interface])
- config.delete(path)
-
- # if "ipv6 ospfv3" was the only setting, we can clean out the empty
- # ip node afterwards
- if len(config.list_nodes(path[:-1])) == 0:
- config.delete(path[:-1])
-
-def migrate_rip(config, path, interface):
- path = path + ['rip']
- if config.exists(path):
- new_base = ['protocols', 'rip', 'interface']
- config.set(new_base)
- config.set_tag(new_base)
- config.copy(path, new_base + [interface])
- config.delete(path)
-
- # if "ip rip" was the only setting, we can clean out the empty
- # ip node afterwards
- if len(config.list_nodes(path[:-1])) == 0:
- config.delete(path[:-1])
-
-def migrate_ripng(config, path, interface):
- path = path + ['ripng']
- if config.exists(path):
- new_base = ['protocols', 'ripng', 'interface']
- config.set(new_base)
- config.set_tag(new_base)
- config.copy(path, new_base + [interface])
- config.delete(path)
-
- # if "ipv6 ripng" was the only setting, we can clean out the empty
- # ip node afterwards
- if len(config.list_nodes(path[:-1])) == 0:
- config.delete(path[:-1])
-
if __name__ == '__main__':
if (len(argv) < 1):
print("Must specify file name!")
@@ -85,57 +29,29 @@ if __name__ == '__main__':
config = ConfigTree(config_file)
- #
- # Migrate "interface ethernet eth0 ip ospf" to "protocols ospf interface eth0"
- #
- for type in config.list_nodes(['interfaces']):
- for interface in config.list_nodes(['interfaces', type]):
- ip_base = ['interfaces', type, interface, 'ip']
- ipv6_base = ['interfaces', type, interface, 'ipv6']
- migrate_rip(config, ip_base, interface)
- migrate_ripng(config, ipv6_base, interface)
- migrate_ospf(config, ip_base, interface)
- migrate_ospfv3(config, ipv6_base, interface)
-
- vif_path = ['interfaces', type, interface, 'vif']
- if config.exists(vif_path):
- for vif in config.list_nodes(vif_path):
- vif_ip_base = vif_path + [vif, 'ip']
- vif_ipv6_base = vif_path + [vif, 'ipv6']
- ifname = f'{interface}.{vif}'
-
- migrate_rip(config, vif_ip_base, ifname)
- migrate_ripng(config, vif_ipv6_base, ifname)
- migrate_ospf(config, vif_ip_base, ifname)
- migrate_ospfv3(config, vif_ipv6_base, ifname)
-
-
- vif_s_path = ['interfaces', type, interface, 'vif-s']
- if config.exists(vif_s_path):
- for vif_s in config.list_nodes(vif_s_path):
- vif_s_ip_base = vif_s_path + [vif_s, 'ip']
- vif_s_ipv6_base = vif_s_path + [vif_s, 'ipv6']
-
- # vif-c interfaces MUST be migrated before their parent vif-s
- # interface as the migrate_*() functions delete the path!
- vif_c_path = ['interfaces', type, interface, 'vif-s', vif_s, 'vif-c']
- if config.exists(vif_c_path):
- for vif_c in config.list_nodes(vif_c_path):
- vif_c_ip_base = vif_c_path + [vif_c, 'ip']
- vif_c_ipv6_base = vif_c_path + [vif_c, 'ipv6']
- ifname = f'{interface}.{vif_s}.{vif_c}'
-
- migrate_rip(config, vif_c_ip_base, ifname)
- migrate_ripng(config, vif_c_ipv6_base, ifname)
- migrate_ospf(config, vif_c_ip_base, ifname)
- migrate_ospfv3(config, vif_c_ipv6_base, ifname)
-
-
- ifname = f'{interface}.{vif_s}'
- migrate_rip(config, vif_s_ip_base, ifname)
- migrate_ripng(config, vif_s_ipv6_base, ifname)
- migrate_ospf(config, vif_s_ip_base, ifname)
- migrate_ospfv3(config, vif_s_ipv6_base, ifname)
+ for type in ['tunnel', 'l2tpv3']:
+ base = ['interfaces', type]
+ if not config.exists(base):
+ # Nothing to do
+ continue
+
+ for interface in config.list_nodes(base):
+ # Migrate "interface tunnel <tunX> encapsulation gre-bridge" to gretap
+ encap_path = base + [interface, 'encapsulation']
+ if type == 'tunnel' and config.exists(encap_path):
+ tmp = config.return_value(encap_path)
+ if tmp == 'gre-bridge':
+ config.set(encap_path, value='gretap')
+
+ # Migrate "interface tunnel|l2tpv3 <interface> local-ip" to source-address
+ # Migrate "interface tunnel|l2tpv3 <interface> remote-ip" to remote
+ local_ip_path = base + [interface, 'local-ip']
+ if config.exists(local_ip_path):
+ config.rename(local_ip_path, 'source-address')
+
+ remote_ip_path = base + [interface, 'remote-ip']
+ if config.exists(remote_ip_path):
+ config.rename(remote_ip_path, 'remote')
try:
with open(file_name, 'w') as f:
diff --git a/src/migration-scripts/interfaces/20-to-21 b/src/migration-scripts/interfaces/20-to-21
index e96663e54..06e07572f 100755
--- a/src/migration-scripts/interfaces/20-to-21
+++ b/src/migration-scripts/interfaces/20-to-21
@@ -18,6 +18,62 @@ from sys import argv
from sys import exit
from vyos.configtree import ConfigTree
+def migrate_ospf(config, path, interface):
+ path = path + ['ospf']
+ if config.exists(path):
+ new_base = ['protocols', 'ospf', 'interface']
+ config.set(new_base)
+ config.set_tag(new_base)
+ config.copy(path, new_base + [interface])
+ config.delete(path)
+
+ # if "ip ospf" was the only setting, we can clean out the empty
+ # ip node afterwards
+ if len(config.list_nodes(path[:-1])) == 0:
+ config.delete(path[:-1])
+
+def migrate_ospfv3(config, path, interface):
+ path = path + ['ospfv3']
+ if config.exists(path):
+ new_base = ['protocols', 'ospfv3', 'interface']
+ config.set(new_base)
+ config.set_tag(new_base)
+ config.copy(path, new_base + [interface])
+ config.delete(path)
+
+ # if "ipv6 ospfv3" was the only setting, we can clean out the empty
+ # ip node afterwards
+ if len(config.list_nodes(path[:-1])) == 0:
+ config.delete(path[:-1])
+
+def migrate_rip(config, path, interface):
+ path = path + ['rip']
+ if config.exists(path):
+ new_base = ['protocols', 'rip', 'interface']
+ config.set(new_base)
+ config.set_tag(new_base)
+ config.copy(path, new_base + [interface])
+ config.delete(path)
+
+ # if "ip rip" was the only setting, we can clean out the empty
+ # ip node afterwards
+ if len(config.list_nodes(path[:-1])) == 0:
+ config.delete(path[:-1])
+
+def migrate_ripng(config, path, interface):
+ path = path + ['ripng']
+ if config.exists(path):
+ new_base = ['protocols', 'ripng', 'interface']
+ config.set(new_base)
+ config.set_tag(new_base)
+ config.copy(path, new_base + [interface])
+ config.delete(path)
+
+ # if "ipv6 ripng" was the only setting, we can clean out the empty
+ # ip node afterwards
+ if len(config.list_nodes(path[:-1])) == 0:
+ config.delete(path[:-1])
+
if __name__ == '__main__':
if (len(argv) < 1):
print("Must specify file name!")
@@ -29,29 +85,57 @@ if __name__ == '__main__':
config = ConfigTree(config_file)
- for type in ['tunnel', 'l2tpv3']:
- base = ['interfaces', type]
- if not config.exists(base):
- # Nothing to do
- continue
-
- for interface in config.list_nodes(base):
- # Migrate "interface tunnel <tunX> encapsulation gre-bridge" to gretap
- encap_path = base + [interface, 'encapsulation']
- if type == 'tunnel' and config.exists(encap_path):
- tmp = config.return_value(encap_path)
- if tmp == 'gre-bridge':
- config.set(encap_path, value='gretap')
-
- # Migrate "interface tunnel|l2tpv3 <interface> local-ip" to source-address
- # Migrate "interface tunnel|l2tpv3 <interface> remote-ip" to remote
- local_ip_path = base + [interface, 'local-ip']
- if config.exists(local_ip_path):
- config.rename(local_ip_path, 'source-address')
-
- remote_ip_path = base + [interface, 'remote-ip']
- if config.exists(remote_ip_path):
- config.rename(remote_ip_path, 'remote')
+ #
+ # Migrate "interface ethernet eth0 ip ospf" to "protocols ospf interface eth0"
+ #
+ for type in config.list_nodes(['interfaces']):
+ for interface in config.list_nodes(['interfaces', type]):
+ ip_base = ['interfaces', type, interface, 'ip']
+ ipv6_base = ['interfaces', type, interface, 'ipv6']
+ migrate_rip(config, ip_base, interface)
+ migrate_ripng(config, ipv6_base, interface)
+ migrate_ospf(config, ip_base, interface)
+ migrate_ospfv3(config, ipv6_base, interface)
+
+ vif_path = ['interfaces', type, interface, 'vif']
+ if config.exists(vif_path):
+ for vif in config.list_nodes(vif_path):
+ vif_ip_base = vif_path + [vif, 'ip']
+ vif_ipv6_base = vif_path + [vif, 'ipv6']
+ ifname = f'{interface}.{vif}'
+
+ migrate_rip(config, vif_ip_base, ifname)
+ migrate_ripng(config, vif_ipv6_base, ifname)
+ migrate_ospf(config, vif_ip_base, ifname)
+ migrate_ospfv3(config, vif_ipv6_base, ifname)
+
+
+ vif_s_path = ['interfaces', type, interface, 'vif-s']
+ if config.exists(vif_s_path):
+ for vif_s in config.list_nodes(vif_s_path):
+ vif_s_ip_base = vif_s_path + [vif_s, 'ip']
+ vif_s_ipv6_base = vif_s_path + [vif_s, 'ipv6']
+
+ # vif-c interfaces MUST be migrated before their parent vif-s
+ # interface as the migrate_*() functions delete the path!
+ vif_c_path = ['interfaces', type, interface, 'vif-s', vif_s, 'vif-c']
+ if config.exists(vif_c_path):
+ for vif_c in config.list_nodes(vif_c_path):
+ vif_c_ip_base = vif_c_path + [vif_c, 'ip']
+ vif_c_ipv6_base = vif_c_path + [vif_c, 'ipv6']
+ ifname = f'{interface}.{vif_s}.{vif_c}'
+
+ migrate_rip(config, vif_c_ip_base, ifname)
+ migrate_ripng(config, vif_c_ipv6_base, ifname)
+ migrate_ospf(config, vif_c_ip_base, ifname)
+ migrate_ospfv3(config, vif_c_ipv6_base, ifname)
+
+
+ ifname = f'{interface}.{vif_s}'
+ migrate_rip(config, vif_s_ip_base, ifname)
+ migrate_ripng(config, vif_s_ipv6_base, ifname)
+ migrate_ospf(config, vif_s_ip_base, ifname)
+ migrate_ospfv3(config, vif_s_ipv6_base, ifname)
try:
with open(file_name, 'w') as f:
diff --git a/src/migration-scripts/interfaces/22-to-23 b/src/migration-scripts/interfaces/22-to-23
new file mode 100755
index 000000000..93ce9215f
--- /dev/null
+++ b/src/migration-scripts/interfaces/22-to-23
@@ -0,0 +1,369 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2021 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/>.
+
+# Migrate Wireguard to store keys in CLI
+# Migrate EAPoL to PKI configuration
+
+import os
+import sys
+from vyos.configtree import ConfigTree
+from vyos.pki import load_certificate
+from vyos.pki import load_crl
+from vyos.pki import load_dh_parameters
+from vyos.pki import load_private_key
+from vyos.pki import encode_certificate
+from vyos.pki import encode_dh_parameters
+from vyos.pki import encode_private_key
+from vyos.util import run
+
+def wrapped_pem_to_config_value(pem):
+ out = []
+ for line in pem.strip().split("\n"):
+ if not line or line.startswith("-----") or line[0] == '#':
+ continue
+ out.append(line)
+ return "".join(out)
+
+def read_file_for_pki(config_auth_path):
+ full_path = os.path.join(AUTH_DIR, config_auth_path)
+ output = None
+
+ if os.path.isfile(full_path):
+ if not os.access(full_path, os.R_OK):
+ run(f'sudo chmod 644 {full_path}')
+
+ with open(full_path, 'r') as f:
+ output = f.read()
+
+ return output
+
+if (len(sys.argv) < 1):
+ print("Must specify file name!")
+ sys.exit(1)
+
+file_name = sys.argv[1]
+
+with open(file_name, 'r') as f:
+ config_file = f.read()
+
+config = ConfigTree(config_file)
+
+AUTH_DIR = '/config/auth'
+pki_base = ['pki']
+
+# OpenVPN
+base = ['interfaces', 'openvpn']
+
+if config.exists(base):
+ for interface in config.list_nodes(base):
+ x509_base = base + [interface, 'tls']
+ pki_name = f'openvpn_{interface}'
+
+ if config.exists(base + [interface, 'shared-secret-key-file']):
+ if not config.exists(pki_base + ['openvpn', 'shared-secret']):
+ config.set(pki_base + ['openvpn', 'shared-secret'])
+ config.set_tag(pki_base + ['openvpn', 'shared-secret'])
+
+ key_file = config.return_value(base + [interface, 'shared-secret-key-file'])
+ key = read_file_for_pki(key_file)
+ key_pki_name = f'{pki_name}_shared'
+
+ if key:
+ config.set(pki_base + ['openvpn', 'shared-secret', key_pki_name, 'key'], value=wrapped_pem_to_config_value(key))
+ config.set(pki_base + ['openvpn', 'shared-secret', key_pki_name, 'version'], value='1')
+ config.set(base + [interface, 'shared-secret-key'], value=key_pki_name)
+ else:
+ print(f'Failed to migrate shared-secret-key on openvpn interface {interface}')
+
+ config.delete(base + [interface, 'shared-secret-key-file'])
+
+ if not config.exists(base + [interface, 'tls']):
+ continue
+
+ if config.exists(base + [interface, 'tls', 'auth-file']):
+ if not config.exists(pki_base + ['openvpn', 'shared-secret']):
+ config.set(pki_base + ['openvpn', 'shared-secret'])
+ config.set_tag(pki_base + ['openvpn', 'shared-secret'])
+
+ key_file = config.return_value(base + [interface, 'tls', 'auth-file'])
+ key = read_file_for_pki(key_file)
+ key_pki_name = f'{pki_name}_auth'
+
+ if key:
+ config.set(pki_base + ['openvpn', 'shared-secret', key_pki_name, 'key'], value=wrapped_pem_to_config_value(key))
+ config.set(pki_base + ['openvpn', 'shared-secret', key_pki_name, 'version'], value='1')
+ config.set(base + [interface, 'tls', 'auth-key'], value=key_pki_name)
+ else:
+ print(f'Failed to migrate auth-key on openvpn interface {interface}')
+
+ config.delete(base + [interface, 'tls', 'auth-file'])
+
+ if config.exists(base + [interface, 'tls', 'crypt-file']):
+ if not config.exists(pki_base + ['openvpn', 'shared-secret']):
+ config.set(pki_base + ['openvpn', 'shared-secret'])
+ config.set_tag(pki_base + ['openvpn', 'shared-secret'])
+
+ key_file = config.return_value(base + [interface, 'tls', 'crypt-file'])
+ key = read_file_for_pki(key_file)
+ key_pki_name = f'{pki_name}_crypt'
+
+ if key:
+ config.set(pki_base + ['openvpn', 'shared-secret', key_pki_name, 'key'], value=wrapped_pem_to_config_value(key))
+ config.set(pki_base + ['openvpn', 'shared-secret', key_pki_name, 'version'], value='1')
+ config.set(base + [interface, 'tls', 'crypt-key'], value=key_pki_name)
+ else:
+ print(f'Failed to migrate crypt-key on openvpn interface {interface}')
+
+ config.delete(base + [interface, 'tls', 'crypt-file'])
+
+ if config.exists(x509_base + ['ca-cert-file']):
+ if not config.exists(pki_base + ['ca']):
+ config.set(pki_base + ['ca'])
+ config.set_tag(pki_base + ['ca'])
+
+ cert_file = config.return_value(x509_base + ['ca-cert-file'])
+ cert_path = os.path.join(AUTH_DIR, cert_file)
+ cert = None
+
+ if os.path.isfile(cert_path):
+ if not os.access(cert_path, os.R_OK):
+ run(f'sudo chmod 644 {cert_path}')
+
+ with open(cert_path, 'r') as f:
+ cert_data = f.read()
+ cert = load_certificate(cert_data, wrap_tags=False)
+
+ if cert:
+ cert_pem = encode_certificate(cert)
+ config.set(pki_base + ['ca', pki_name, 'certificate'], value=wrapped_pem_to_config_value(cert_pem))
+ config.set(x509_base + ['ca-certificate'], value=pki_name)
+ else:
+ print(f'Failed to migrate CA certificate on openvpn interface {interface}')
+
+ config.delete(x509_base + ['ca-cert-file'])
+
+ if config.exists(x509_base + ['crl-file']):
+ if not config.exists(pki_base + ['ca']):
+ config.set(pki_base + ['ca'])
+ config.set_tag(pki_base + ['ca'])
+
+ crl_file = config.return_value(x509_base + ['crl-file'])
+ crl_path = os.path.join(AUTH_DIR, crl_file)
+ crl = None
+
+ if os.path.isfile(crl_path):
+ if not os.access(crl_path, os.R_OK):
+ run(f'sudo chmod 644 {crl_path}')
+
+ with open(crl_path, 'r') as f:
+ crl_data = f.read()
+ crl = load_crl(crl_data, wrap_tags=False)
+
+ if crl:
+ crl_pem = encode_certificate(crl)
+ config.set(pki_base + ['ca', pki_name, 'crl'], value=wrapped_pem_to_config_value(crl_pem))
+ else:
+ print(f'Failed to migrate CRL on openvpn interface {interface}')
+
+ config.delete(x509_base + ['crl-file'])
+
+ if config.exists(x509_base + ['cert-file']):
+ if not config.exists(pki_base + ['certificate']):
+ config.set(pki_base + ['certificate'])
+ config.set_tag(pki_base + ['certificate'])
+
+ cert_file = config.return_value(x509_base + ['cert-file'])
+ cert_path = os.path.join(AUTH_DIR, cert_file)
+ cert = None
+
+ if os.path.isfile(cert_path):
+ if not os.access(cert_path, os.R_OK):
+ run(f'sudo chmod 644 {cert_path}')
+
+ with open(cert_path, 'r') as f:
+ cert_data = f.read()
+ cert = load_certificate(cert_data, wrap_tags=False)
+
+ if cert:
+ cert_pem = encode_certificate(cert)
+ config.set(pki_base + ['certificate', pki_name, 'certificate'], value=wrapped_pem_to_config_value(cert_pem))
+ config.set(x509_base + ['certificate'], value=pki_name)
+ else:
+ print(f'Failed to migrate certificate on openvpn interface {interface}')
+
+ config.delete(x509_base + ['cert-file'])
+
+ if config.exists(x509_base + ['key-file']):
+ key_file = config.return_value(x509_base + ['key-file'])
+ key_path = os.path.join(AUTH_DIR, key_file)
+ key = None
+
+ if os.path.isfile(key_path):
+ if not os.access(key_path, os.R_OK):
+ run(f'sudo chmod 644 {key_path}')
+
+ with open(key_path, 'r') as f:
+ key_data = f.read()
+ key = load_private_key(key_data, passphrase=None, wrap_tags=False)
+
+ if key:
+ key_pem = encode_private_key(key, passphrase=None)
+ config.set(pki_base + ['certificate', pki_name, 'private', 'key'], value=wrapped_pem_to_config_value(key_pem))
+ else:
+ print(f'Failed to migrate private key on openvpn interface {interface}')
+
+ config.delete(x509_base + ['key-file'])
+
+ if config.exists(x509_base + ['dh-file']):
+ if not config.exists(pki_base + ['dh']):
+ config.set(pki_base + ['dh'])
+ config.set_tag(pki_base + ['dh'])
+
+ dh_file = config.return_value(x509_base + ['dh-file'])
+ dh_path = os.path.join(AUTH_DIR, dh_file)
+ dh = None
+
+ if os.path.isfile(dh_path):
+ if not os.access(dh_path, os.R_OK):
+ run(f'sudo chmod 644 {dh_path}')
+
+ with open(dh_path, 'r') as f:
+ dh_data = f.read()
+ dh = load_dh_parameters(dh_data, wrap_tags=False)
+
+ if dh:
+ dh_pem = encode_dh_parameters(dh)
+ config.set(pki_base + ['dh', pki_name, 'parameters'], value=wrapped_pem_to_config_value(dh_pem))
+ config.set(x509_base + ['dh-params'], value=pki_name)
+ else:
+ print(f'Failed to migrate DH parameters on openvpn interface {interface}')
+
+ config.delete(x509_base + ['dh-file'])
+
+# Wireguard
+base = ['interfaces', 'wireguard']
+
+if config.exists(base):
+ for interface in config.list_nodes(base):
+ private_key_path = base + [interface, 'private-key']
+
+ key_file = 'default'
+ if config.exists(private_key_path):
+ key_file = config.return_value(private_key_path)
+
+ full_key_path = f'/config/auth/wireguard/{key_file}/private.key'
+
+ if not os.path.exists(full_key_path):
+ print(f'Could not find wireguard private key for migration on interface "{interface}"')
+ continue
+
+ with open(full_key_path, 'r') as f:
+ key_data = f.read().strip()
+ config.set(private_key_path, value=key_data)
+
+ for peer in config.list_nodes(base + [interface, 'peer']):
+ config.rename(base + [interface, 'peer', peer, 'pubkey'], 'public-key')
+
+# Ethernet EAPoL
+base = ['interfaces', 'ethernet']
+
+if config.exists(base):
+ for interface in config.list_nodes(base):
+ if not config.exists(base + [interface, 'eapol']):
+ continue
+
+ x509_base = base + [interface, 'eapol']
+ pki_name = f'eapol_{interface}'
+
+ if config.exists(x509_base + ['ca-cert-file']):
+ if not config.exists(pki_base + ['ca']):
+ config.set(pki_base + ['ca'])
+ config.set_tag(pki_base + ['ca'])
+
+ cert_file = config.return_value(x509_base + ['ca-cert-file'])
+ cert_path = os.path.join(AUTH_DIR, cert_file)
+ cert = None
+
+ if os.path.isfile(cert_path):
+ if not os.access(cert_path, os.R_OK):
+ run(f'sudo chmod 644 {cert_path}')
+
+ with open(cert_path, 'r') as f:
+ cert_data = f.read()
+ cert = load_certificate(cert_data, wrap_tags=False)
+
+ if cert:
+ cert_pem = encode_certificate(cert)
+ config.set(pki_base + ['ca', pki_name, 'certificate'], value=wrapped_pem_to_config_value(cert_pem))
+ config.set(x509_base + ['ca-certificate'], value=pki_name)
+ else:
+ print(f'Failed to migrate CA certificate on eapol config for interface {interface}')
+
+ config.delete(x509_base + ['ca-cert-file'])
+
+ if config.exists(x509_base + ['cert-file']):
+ if not config.exists(pki_base + ['certificate']):
+ config.set(pki_base + ['certificate'])
+ config.set_tag(pki_base + ['certificate'])
+
+ cert_file = config.return_value(x509_base + ['cert-file'])
+ cert_path = os.path.join(AUTH_DIR, cert_file)
+ cert = None
+
+ if os.path.isfile(cert_path):
+ if not os.access(cert_path, os.R_OK):
+ run(f'sudo chmod 644 {cert_path}')
+
+ with open(cert_path, 'r') as f:
+ cert_data = f.read()
+ cert = load_certificate(cert_data, wrap_tags=False)
+
+ if cert:
+ cert_pem = encode_certificate(cert)
+ config.set(pki_base + ['certificate', pki_name, 'certificate'], value=wrapped_pem_to_config_value(cert_pem))
+ config.set(x509_base + ['certificate'], value=pki_name)
+ else:
+ print(f'Failed to migrate certificate on eapol config for interface {interface}')
+
+ config.delete(x509_base + ['cert-file'])
+
+ if config.exists(x509_base + ['key-file']):
+ key_file = config.return_value(x509_base + ['key-file'])
+ key_path = os.path.join(AUTH_DIR, key_file)
+ key = None
+
+ if os.path.isfile(key_path):
+ if not os.access(key_path, os.R_OK):
+ run(f'sudo chmod 644 {key_path}')
+
+ with open(key_path, 'r') as f:
+ key_data = f.read()
+ key = load_private_key(key_data, passphrase=None, wrap_tags=False)
+
+ if key:
+ key_pem = encode_private_key(key, passphrase=None)
+ config.set(pki_base + ['certificate', pki_name, 'private', 'key'], value=wrapped_pem_to_config_value(key_pem))
+ else:
+ print(f'Failed to migrate private key on eapol config for interface {interface}')
+
+ config.delete(x509_base + ['key-file'])
+
+try:
+ with open(file_name, 'w') as f:
+ f.write(config.to_string())
+except OSError as e:
+ print("Failed to save the modified config: {}".format(e))
+ sys.exit(1)
diff --git a/src/migration-scripts/ipsec/5-to-6 b/src/migration-scripts/ipsec/5-to-6
index ba5ce0fca..e9adee01b 100755
--- a/src/migration-scripts/ipsec/5-to-6
+++ b/src/migration-scripts/ipsec/5-to-6
@@ -74,6 +74,17 @@ log_mode = log + ['log-modes']
if config.exists(log_mode):
config.rename(log_mode, 'subsystem')
+# Rename "ipsec-interfaces interface" to "interface"
+base_interfaces = base + ['ipsec-interfaces', 'interface']
+if config.exists(base_interfaces):
+ config.copy(base_interfaces, base + ['interface'])
+ config.delete(base_interfaces)
+
+# Remove deprecated "auto-update" option
+tmp = base + ['auto-update']
+if config.exists(tmp):
+ config.delete(tmp)
+
try:
with open(file_name, 'w') as f:
f.write(config.to_string())
diff --git a/src/migration-scripts/l2tp/3-to-4 b/src/migration-scripts/l2tp/3-to-4
new file mode 100755
index 000000000..18eabadec
--- /dev/null
+++ b/src/migration-scripts/l2tp/3-to-4
@@ -0,0 +1,169 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2021 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/>.
+
+# - remove primary/secondary identifier from nameserver
+# - TODO: remove radius server req-limit
+
+import os
+
+from sys import argv
+from sys import exit
+from vyos.configtree import ConfigTree
+from vyos.pki import load_certificate
+from vyos.pki import load_crl
+from vyos.pki import load_private_key
+from vyos.pki import encode_certificate
+from vyos.pki import encode_private_key
+from vyos.util import run
+
+if (len(argv) < 1):
+ print("Must specify file name!")
+ exit(1)
+
+file_name = argv[1]
+
+with open(file_name, 'r') as f:
+ config_file = f.read()
+
+config = ConfigTree(config_file)
+base = ['vpn', 'l2tp', 'remote-access', 'ipsec-settings']
+pki_base = ['pki']
+
+if not config.exists(base):
+ exit(0)
+
+AUTH_DIR = '/config/auth'
+
+def wrapped_pem_to_config_value(pem):
+ return "".join(pem.strip().split("\n")[1:-1])
+
+if not config.exists(base + ['authentication', 'x509']):
+ exit(0)
+
+x509_base = base + ['authentication', 'x509']
+pki_name = 'l2tp_remote_access'
+
+if not config.exists(pki_base + ['ca']):
+ config.set(pki_base + ['ca'])
+ config.set_tag(pki_base + ['ca'])
+
+if not config.exists(pki_base + ['certificate']):
+ config.set(pki_base + ['certificate'])
+ config.set_tag(pki_base + ['certificate'])
+
+if config.exists(x509_base + ['ca-cert-file']):
+ cert_file = config.return_value(x509_base + ['ca-cert-file'])
+ cert_path = os.path.join(AUTH_DIR, cert_file)
+ cert = None
+
+ if os.path.isfile(cert_path):
+ if not os.access(cert_path, os.R_OK):
+ run(f'sudo chmod 644 {cert_path}')
+
+ with open(cert_path, 'r') as f:
+ cert_data = f.read()
+ cert = load_certificate(cert_data, wrap_tags=False)
+
+ if cert:
+ cert_pem = encode_certificate(cert)
+ config.set(pki_base + ['ca', pki_name, 'certificate'], value=wrapped_pem_to_config_value(cert_pem))
+ config.set(x509_base + ['ca-certificate'], value=pki_name)
+ else:
+ print(f'Failed to migrate CA certificate on l2tp remote-access config')
+
+ config.delete(x509_base + ['ca-cert-file'])
+
+if config.exists(x509_base + ['crl-file']):
+ crl_file = config.return_value(x509_base + ['crl-file'])
+ crl_path = os.path.join(AUTH_DIR, crl_file)
+ crl = None
+
+ if os.path.isfile(crl_path):
+ if not os.access(crl_path, os.R_OK):
+ run(f'sudo chmod 644 {crl_path}')
+
+ with open(crl_path, 'r') as f:
+ crl_data = f.read()
+ crl = load_certificate(crl_data, wrap_tags=False)
+
+ if crl:
+ crl_pem = encode_certificate(crl)
+ config.set(pki_base + ['ca', pki_name, 'crl'], value=wrapped_pem_to_config_value(crl_pem))
+ else:
+ print(f'Failed to migrate CRL on l2tp remote-access config')
+
+ config.delete(x509_base + ['crl-file'])
+
+if config.exists(x509_base + ['server-cert-file']):
+ cert_file = config.return_value(x509_base + ['server-cert-file'])
+ cert_path = os.path.join(AUTH_DIR, cert_file)
+ cert = None
+
+ if os.path.isfile(cert_path):
+ if not os.access(cert_path, os.R_OK):
+ run(f'sudo chmod 644 {cert_path}')
+
+ with open(cert_path, 'r') as f:
+ cert_data = f.read()
+ cert = load_certificate(cert_data, wrap_tags=False)
+
+ if cert:
+ cert_pem = encode_certificate(cert)
+ config.set(pki_base + ['certificate', pki_name, 'certificate'], value=wrapped_pem_to_config_value(cert_pem))
+ config.set(x509_base + ['certificate'], value=pki_name)
+ else:
+ print(f'Failed to migrate certificate on l2tp remote-access config')
+
+ config.delete(x509_base + ['server-cert-file'])
+
+if config.exists(x509_base + ['server-key-file']):
+ key_file = config.return_value(x509_base + ['server-key-file'])
+ key_passphrase = None
+
+ if config.exists(x509_base + ['server-key-password']):
+ key_passphrase = config.return_value(x509_base + ['server-key-password'])
+
+ key_path = os.path.join(AUTH_DIR, key_file)
+ key = None
+
+ if os.path.isfile(key_path):
+ if not os.access(key_path, os.R_OK):
+ run(f'sudo chmod 644 {key_path}')
+
+ with open(key_path, 'r') as f:
+ key_data = f.read()
+ key = load_private_key(key_data, passphrase=key_passphrase, wrap_tags=False)
+
+ if key:
+ key_pem = encode_private_key(key, passphrase=key_passphrase)
+ config.set(pki_base + ['certificate', pki_name, 'private', 'key'], value=wrapped_pem_to_config_value(key_pem))
+
+ if key_passphrase:
+ config.set(pki_base + ['certificate', pki_name, 'private', 'password-protected'])
+ config.set(x509_base + ['private-key-passphrase'], value=key_passphrase)
+ else:
+ print(f'Failed to migrate private key on l2tp remote-access config')
+
+ config.delete(x509_base + ['server-key-file'])
+ if config.exists(x509_base + ['server-key-password']):
+ config.delete(x509_base + ['server-key-password'])
+
+try:
+ with open(file_name, 'w') as f:
+ f.write(config.to_string())
+except OSError as e:
+ print("Failed to save the modified config: {}".format(e))
+ exit(1)
diff --git a/src/migration-scripts/openconnect/0-to-1 b/src/migration-scripts/openconnect/0-to-1
new file mode 100755
index 000000000..83cd09143
--- /dev/null
+++ b/src/migration-scripts/openconnect/0-to-1
@@ -0,0 +1,136 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2021 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/>.
+
+# - Update SSL to use PKI configuration
+
+import os
+
+from sys import argv
+from sys import exit
+from vyos.configtree import ConfigTree
+from vyos.pki import load_certificate
+from vyos.pki import load_crl
+from vyos.pki import load_private_key
+from vyos.pki import encode_certificate
+from vyos.pki import encode_private_key
+from vyos.util import run
+
+if (len(argv) < 1):
+ print("Must specify file name!")
+ exit(1)
+
+file_name = argv[1]
+
+with open(file_name, 'r') as f:
+ config_file = f.read()
+
+config = ConfigTree(config_file)
+base = ['vpn', 'openconnect']
+pki_base = ['pki']
+
+if not config.exists(base):
+ exit(0)
+
+AUTH_DIR = '/config/auth'
+
+def wrapped_pem_to_config_value(pem):
+ return "".join(pem.strip().split("\n")[1:-1])
+
+if not config.exists(base + ['ssl']):
+ exit(0)
+
+x509_base = base + ['ssl']
+pki_name = 'openconnect'
+
+if not config.exists(pki_base + ['ca']):
+ config.set(pki_base + ['ca'])
+ config.set_tag(pki_base + ['ca'])
+
+if not config.exists(pki_base + ['certificate']):
+ config.set(pki_base + ['certificate'])
+ config.set_tag(pki_base + ['certificate'])
+
+if config.exists(x509_base + ['ca-cert-file']):
+ cert_file = config.return_value(x509_base + ['ca-cert-file'])
+ cert_path = os.path.join(AUTH_DIR, cert_file)
+ cert = None
+
+ if os.path.isfile(cert_path):
+ if not os.access(cert_path, os.R_OK):
+ run(f'sudo chmod 644 {cert_path}')
+
+ with open(cert_path, 'r') as f:
+ cert_data = f.read()
+ cert = load_certificate(cert_data, wrap_tags=False)
+
+ if cert:
+ cert_pem = encode_certificate(cert)
+ config.set(pki_base + ['ca', pki_name, 'certificate'], value=wrapped_pem_to_config_value(cert_pem))
+ config.set(x509_base + ['ca-certificate'], value=pki_name)
+ else:
+ print(f'Failed to migrate CA certificate on openconnect config')
+
+ config.delete(x509_base + ['ca-cert-file'])
+
+if config.exists(x509_base + ['cert-file']):
+ cert_file = config.return_value(x509_base + ['cert-file'])
+ cert_path = os.path.join(AUTH_DIR, cert_file)
+ cert = None
+
+ if os.path.isfile(cert_path):
+ if not os.access(cert_path, os.R_OK):
+ run(f'sudo chmod 644 {cert_path}')
+
+ with open(cert_path, 'r') as f:
+ cert_data = f.read()
+ cert = load_certificate(cert_data, wrap_tags=False)
+
+ if cert:
+ cert_pem = encode_certificate(cert)
+ config.set(pki_base + ['certificate', pki_name, 'certificate'], value=wrapped_pem_to_config_value(cert_pem))
+ config.set(x509_base + ['certificate'], value=pki_name)
+ else:
+ print(f'Failed to migrate certificate on openconnect config')
+
+ config.delete(x509_base + ['cert-file'])
+
+if config.exists(x509_base + ['key-file']):
+ key_file = config.return_value(x509_base + ['key-file'])
+ key_path = os.path.join(AUTH_DIR, key_file)
+ key = None
+
+ if os.path.isfile(key_path):
+ if not os.access(key_path, os.R_OK):
+ run(f'sudo chmod 644 {key_path}')
+
+ with open(key_path, 'r') as f:
+ key_data = f.read()
+ key = load_private_key(key_data, passphrase=None, wrap_tags=False)
+
+ if key:
+ key_pem = encode_private_key(key, passphrase=None)
+ config.set(pki_base + ['certificate', pki_name, 'private', 'key'], value=wrapped_pem_to_config_value(key_pem))
+ else:
+ print(f'Failed to migrate private key on openconnect config')
+
+ config.delete(x509_base + ['key-file'])
+
+try:
+ with open(file_name, 'w') as f:
+ f.write(config.to_string())
+except OSError as e:
+ print("Failed to save the modified config: {}".format(e))
+ exit(1)
diff --git a/src/migration-scripts/quagga/7-to-8 b/src/migration-scripts/quagga/7-to-8
index 9c277a6f1..15c44924f 100755
--- a/src/migration-scripts/quagga/7-to-8
+++ b/src/migration-scripts/quagga/7-to-8
@@ -14,61 +14,14 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
-# - T2450: drop interface-route and interface-route6 from "protocols static"
+# - T3391: Migrate "maximum-paths" setting from "protocols bgp asn maximum-paths"
+# under the IPv4 address-family tree. Reason is we currently have no way in
+# configuring this for IPv6 address-family. This mimics the FRR configuration.
from sys import argv
from sys import exit
-
from vyos.configtree import ConfigTree
-def migrate_interface_route(config, base, path, route_route6):
- """ Generic migration function which can be called on every instance of
- interface-route, beeing it ipv4, ipv6 or nested under the "static table" nodes.
-
- What we do?
- - Drop 'interface-route' or 'interface-route6' and migrate the route unter the
- 'route' or 'route6' tag node.
- """
- if config.exists(base + path):
- for route in config.list_nodes(base + path):
- interface = config.list_nodes(base + path + [route, 'next-hop-interface'])
-
- tmp = base + path + [route, 'next-hop-interface']
- for interface in config.list_nodes(tmp):
- new_base = base + [route_route6, route, 'interface']
- config.set(new_base)
- config.set_tag(base + [route_route6])
- config.set_tag(new_base)
- config.copy(tmp + [interface], new_base + [interface])
-
- config.delete(base + path)
-
-def migrate_route(config, base, path, route_route6):
- """ Generic migration function which can be called on every instance of
- route, beeing it ipv4, ipv6 or even nested under the static table nodes.
-
- What we do?
- - for consistency reasons rename next-hop-interface to interface
- - for consistency reasons rename next-hop-vrf to vrf
- """
- if config.exists(base + path):
- for route in config.list_nodes(base + path):
- next_hop = base + path + [route, 'next-hop']
- if config.exists(next_hop):
- for gateway in config.list_nodes(next_hop):
- # IPv4 routes calls it next-hop-interface, rename this to
- # interface instead so it's consitent with IPv6
- interface_path = next_hop + [gateway, 'next-hop-interface']
- if config.exists(interface_path):
- config.rename(interface_path, 'interface')
-
- # When VRFs got introduced, I (c-po) named it next-hop-vrf,
- # we can also call it vrf which is simply shorter.
- vrf_path = next_hop + [gateway, 'next-hop-vrf']
- if config.exists(vrf_path):
- config.rename(vrf_path, 'vrf')
-
-
if (len(argv) < 2):
print("Must specify file name!")
exit(1)
@@ -78,41 +31,27 @@ file_name = argv[1]
with open(file_name, 'r') as f:
config_file = f.read()
-base = ['protocols', 'static']
-
+base = ['protocols', 'bgp']
config = ConfigTree(config_file)
+
if not config.exists(base):
# Nothing to do
exit(0)
-# Migrate interface-route into route
-migrate_interface_route(config, base, ['interface-route'], 'route')
-
-# Migrate interface-route6 into route6
-migrate_interface_route(config, base, ['interface-route6'], 'route6')
-
-# Cleanup nodes inside route
-migrate_route(config, base, ['route'], 'route')
-
-# Cleanup nodes inside route6
-migrate_route(config, base, ['route6'], 'route6')
-
-#
-# PBR table cleanup
-table_path = base + ['table']
-if config.exists(table_path):
- for table in config.list_nodes(table_path):
- # Migrate interface-route into route
- migrate_interface_route(config, table_path + [table], ['interface-route'], 'route')
-
- # Migrate interface-route6 into route6
- migrate_interface_route(config, table_path + [table], ['interface-route6'], 'route6')
-
- # Cleanup nodes inside route
- migrate_route(config, table_path + [table], ['route'], 'route')
-
- # Cleanup nodes inside route6
- migrate_route(config, table_path + [table], ['route6'], 'route6')
+# Check if BGP is actually configured and obtain the ASN
+asn_list = config.list_nodes(base)
+if asn_list:
+ # There's always just one BGP node, if any
+ bgp_base = base + [asn_list[0]]
+
+ maximum_paths = bgp_base + ['maximum-paths']
+ if config.exists(maximum_paths):
+ for bgp_type in ['ebgp', 'ibgp']:
+ if config.exists(maximum_paths + [bgp_type]):
+ new_base = bgp_base + ['address-family', 'ipv4-unicast', 'maximum-paths']
+ config.set(new_base)
+ config.copy(maximum_paths + [bgp_type], new_base + [bgp_type])
+ config.delete(maximum_paths)
try:
with open(file_name, 'w') as f:
diff --git a/src/migration-scripts/quagga/8-to-9 b/src/migration-scripts/quagga/8-to-9
index 15c44924f..38507bd3d 100755
--- a/src/migration-scripts/quagga/8-to-9
+++ b/src/migration-scripts/quagga/8-to-9
@@ -14,14 +14,76 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
-# - T3391: Migrate "maximum-paths" setting from "protocols bgp asn maximum-paths"
-# under the IPv4 address-family tree. Reason is we currently have no way in
-# configuring this for IPv6 address-family. This mimics the FRR configuration.
+# - T2450: drop interface-route and interface-route6 from "protocols static"
from sys import argv
from sys import exit
+
from vyos.configtree import ConfigTree
+def migrate_interface_route(config, base, path, route_route6):
+ """ Generic migration function which can be called on every instance of
+ interface-route, beeing it ipv4, ipv6 or nested under the "static table" nodes.
+
+ What we do?
+ - Drop 'interface-route' or 'interface-route6' and migrate the route unter the
+ 'route' or 'route6' tag node.
+ """
+ if config.exists(base + path):
+ for route in config.list_nodes(base + path):
+ interface = config.list_nodes(base + path + [route, 'next-hop-interface'])
+
+ tmp = base + path + [route, 'next-hop-interface']
+ for interface in config.list_nodes(tmp):
+ new_base = base + [route_route6, route, 'interface']
+ config.set(new_base)
+ config.set_tag(base + [route_route6])
+ config.set_tag(new_base)
+ config.copy(tmp + [interface], new_base + [interface])
+
+ config.delete(base + path)
+
+def migrate_route(config, base, path, route_route6):
+ """ Generic migration function which can be called on every instance of
+ route, beeing it ipv4, ipv6 or even nested under the static table nodes.
+
+ What we do?
+ - for consistency reasons rename next-hop-interface to interface
+ - for consistency reasons rename next-hop-vrf to vrf
+ """
+ if config.exists(base + path):
+ for route in config.list_nodes(base + path):
+ next_hop = base + path + [route, 'next-hop']
+ if config.exists(next_hop):
+ for gateway in config.list_nodes(next_hop):
+ # IPv4 routes calls it next-hop-interface, rename this to
+ # interface instead so it's consitent with IPv6
+ interface_path = next_hop + [gateway, 'next-hop-interface']
+ if config.exists(interface_path):
+ config.rename(interface_path, 'interface')
+
+ # When VRFs got introduced, I (c-po) named it next-hop-vrf,
+ # we can also call it vrf which is simply shorter.
+ vrf_path = next_hop + [gateway, 'next-hop-vrf']
+ if config.exists(vrf_path):
+ config.rename(vrf_path, 'vrf')
+
+ next_hop = base + path + [route, 'interface']
+ if config.exists(next_hop):
+ for interface in config.list_nodes(next_hop):
+ # IPv4 routes calls it next-hop-interface, rename this to
+ # interface instead so it's consitent with IPv6
+ interface_path = next_hop + [interface, 'next-hop-interface']
+ if config.exists(interface_path):
+ config.rename(interface_path, 'interface')
+
+ # When VRFs got introduced, I (c-po) named it next-hop-vrf,
+ # we can also call it vrf which is simply shorter.
+ vrf_path = next_hop + [interface, 'next-hop-vrf']
+ if config.exists(vrf_path):
+ config.rename(vrf_path, 'vrf')
+
+
if (len(argv) < 2):
print("Must specify file name!")
exit(1)
@@ -31,27 +93,41 @@ file_name = argv[1]
with open(file_name, 'r') as f:
config_file = f.read()
-base = ['protocols', 'bgp']
-config = ConfigTree(config_file)
+base = ['protocols', 'static']
+config = ConfigTree(config_file)
if not config.exists(base):
# Nothing to do
exit(0)
-# Check if BGP is actually configured and obtain the ASN
-asn_list = config.list_nodes(base)
-if asn_list:
- # There's always just one BGP node, if any
- bgp_base = base + [asn_list[0]]
-
- maximum_paths = bgp_base + ['maximum-paths']
- if config.exists(maximum_paths):
- for bgp_type in ['ebgp', 'ibgp']:
- if config.exists(maximum_paths + [bgp_type]):
- new_base = bgp_base + ['address-family', 'ipv4-unicast', 'maximum-paths']
- config.set(new_base)
- config.copy(maximum_paths + [bgp_type], new_base + [bgp_type])
- config.delete(maximum_paths)
+# Migrate interface-route into route
+migrate_interface_route(config, base, ['interface-route'], 'route')
+
+# Migrate interface-route6 into route6
+migrate_interface_route(config, base, ['interface-route6'], 'route6')
+
+# Cleanup nodes inside route
+migrate_route(config, base, ['route'], 'route')
+
+# Cleanup nodes inside route6
+migrate_route(config, base, ['route6'], 'route6')
+
+#
+# PBR table cleanup
+table_path = base + ['table']
+if config.exists(table_path):
+ for table in config.list_nodes(table_path):
+ # Migrate interface-route into route
+ migrate_interface_route(config, table_path + [table], ['interface-route'], 'route')
+
+ # Migrate interface-route6 into route6
+ migrate_interface_route(config, table_path + [table], ['interface-route6'], 'route6')
+
+ # Cleanup nodes inside route
+ migrate_route(config, table_path + [table], ['route'], 'route')
+
+ # Cleanup nodes inside route6
+ migrate_route(config, table_path + [table], ['route6'], 'route6')
try:
with open(file_name, 'w') as f:
diff --git a/src/migration-scripts/sstp/3-to-4 b/src/migration-scripts/sstp/3-to-4
new file mode 100755
index 000000000..0568f043f
--- /dev/null
+++ b/src/migration-scripts/sstp/3-to-4
@@ -0,0 +1,136 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2021 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/>.
+
+# - Update SSL to use PKI configuration
+
+import os
+
+from sys import argv
+from sys import exit
+from vyos.configtree import ConfigTree
+from vyos.pki import load_certificate
+from vyos.pki import load_crl
+from vyos.pki import load_private_key
+from vyos.pki import encode_certificate
+from vyos.pki import encode_private_key
+from vyos.util import run
+
+if (len(argv) < 1):
+ print("Must specify file name!")
+ exit(1)
+
+file_name = argv[1]
+
+with open(file_name, 'r') as f:
+ config_file = f.read()
+
+config = ConfigTree(config_file)
+base = ['vpn', 'sstp']
+pki_base = ['pki']
+
+if not config.exists(base):
+ exit(0)
+
+AUTH_DIR = '/config/auth'
+
+def wrapped_pem_to_config_value(pem):
+ return "".join(pem.strip().split("\n")[1:-1])
+
+if not config.exists(base + ['ssl']):
+ exit(0)
+
+x509_base = base + ['ssl']
+pki_name = 'sstp'
+
+if not config.exists(pki_base + ['ca']):
+ config.set(pki_base + ['ca'])
+ config.set_tag(pki_base + ['ca'])
+
+if not config.exists(pki_base + ['certificate']):
+ config.set(pki_base + ['certificate'])
+ config.set_tag(pki_base + ['certificate'])
+
+if config.exists(x509_base + ['ca-cert-file']):
+ cert_file = config.return_value(x509_base + ['ca-cert-file'])
+ cert_path = os.path.join(AUTH_DIR, cert_file)
+ cert = None
+
+ if os.path.isfile(cert_path):
+ if not os.access(cert_path, os.R_OK):
+ run(f'sudo chmod 644 {cert_path}')
+
+ with open(cert_path, 'r') as f:
+ cert_data = f.read()
+ cert = load_certificate(cert_data, wrap_tags=False)
+
+ if cert:
+ cert_pem = encode_certificate(cert)
+ config.set(pki_base + ['ca', pki_name, 'certificate'], value=wrapped_pem_to_config_value(cert_pem))
+ config.set(x509_base + ['ca-certificate'], value=pki_name)
+ else:
+ print(f'Failed to migrate CA certificate on sstp config')
+
+ config.delete(x509_base + ['ca-cert-file'])
+
+if config.exists(x509_base + ['cert-file']):
+ cert_file = config.return_value(x509_base + ['cert-file'])
+ cert_path = os.path.join(AUTH_DIR, cert_file)
+ cert = None
+
+ if os.path.isfile(cert_path):
+ if not os.access(cert_path, os.R_OK):
+ run(f'sudo chmod 644 {cert_path}')
+
+ with open(cert_path, 'r') as f:
+ cert_data = f.read()
+ cert = load_certificate(cert_data, wrap_tags=False)
+
+ if cert:
+ cert_pem = encode_certificate(cert)
+ config.set(pki_base + ['certificate', pki_name, 'certificate'], value=wrapped_pem_to_config_value(cert_pem))
+ config.set(x509_base + ['certificate'], value=pki_name)
+ else:
+ print(f'Failed to migrate certificate on sstp config')
+
+ config.delete(x509_base + ['cert-file'])
+
+if config.exists(x509_base + ['key-file']):
+ key_file = config.return_value(x509_base + ['key-file'])
+ key_path = os.path.join(AUTH_DIR, key_file)
+ key = None
+
+ if os.path.isfile(key_path):
+ if not os.access(key_path, os.R_OK):
+ run(f'sudo chmod 644 {key_path}')
+
+ with open(key_path, 'r') as f:
+ key_data = f.read()
+ key = load_private_key(key_data, passphrase=None, wrap_tags=False)
+
+ if key:
+ key_pem = encode_private_key(key, passphrase=None)
+ config.set(pki_base + ['certificate', pki_name, 'private', 'key'], value=wrapped_pem_to_config_value(key_pem))
+ else:
+ print(f'Failed to migrate private key on sstp config')
+
+ config.delete(x509_base + ['key-file'])
+
+try:
+ with open(file_name, 'w') as f:
+ f.write(config.to_string())
+except OSError as e:
+ print("Failed to save the modified config: {}".format(e))
+ exit(1)
diff --git a/src/migration-scripts/vrf/0-to-1 b/src/migration-scripts/vrf/0-to-1
index 29b2fab74..2b41ef3c7 100755
--- a/src/migration-scripts/vrf/0-to-1
+++ b/src/migration-scripts/vrf/0-to-1
@@ -91,6 +91,16 @@ for vrf in config.list_nodes(base):
if config.exists(vrf_path):
config.rename(vrf_path, 'vrf')
+ next_hop = route_path + [route, 'interface']
+ if config.exists(next_hop):
+ for interface in config.list_nodes(next_hop):
+ interface_path = next_hop + [interface, 'next-hop-interface']
+ if config.exists(interface_path):
+ config.rename(interface_path, 'interface')
+ vrf_path = next_hop + [interface, 'next-hop-vrf']
+ if config.exists(vrf_path):
+ config.rename(vrf_path, 'vrf')
+
#
# Cleanup nodes inside route6
#
diff --git a/src/migration-scripts/vrf/1-to-2 b/src/migration-scripts/vrf/1-to-2
index 20128e957..9bc704e02 100755
--- a/src/migration-scripts/vrf/1-to-2
+++ b/src/migration-scripts/vrf/1-to-2
@@ -49,6 +49,7 @@ for vrf in config.list_nodes(base):
new_static_base = vrf_base + [vrf, 'protocols']
config.set(new_static_base)
config.copy(static_base, new_static_base + ['static'])
+ config.set_tag(new_static_base + ['static', 'route'])
# Now delete the old configuration
config.delete(base)
diff --git a/src/migration-scripts/vrf/2-to-3 b/src/migration-scripts/vrf/2-to-3
new file mode 100755
index 000000000..8e0f97141
--- /dev/null
+++ b/src/migration-scripts/vrf/2-to-3
@@ -0,0 +1,144 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2021 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/>.
+
+# Since connection tracking zones are int16, VRFs tables maximum value must
+# be limited to 65535
+# Also, interface names in nftables cannot start from numbers,
+# so VRF name should not start from a number
+
+from sys import argv
+from sys import exit
+from random import randrange
+from random import choice
+from string import ascii_lowercase
+from vyos.configtree import ConfigTree
+import re
+
+
+# Helper function to find all config items with a VRF name
+def _search_vrfs(config_commands, vrf_name):
+ vrf_values = []
+ # Regex to find path of config command with old VRF
+ regex_filter = re.compile(rf'^set (?P<cmd_path>[^\']+vrf) \'{vrf_name}\'$')
+ # Check each command for VRF value
+ for config_command in config_commands:
+ search_result = regex_filter.search(config_command)
+ if search_result:
+ # Append VRF command to a list
+ vrf_values.append(search_result.group('cmd_path').split())
+ if vrf_values:
+ return vrf_values
+ else:
+ return None
+
+
+# Helper function to find all config items with a table number
+def _search_tables(config_commands, table_num):
+ table_items = {'table_tags': [], 'table_values': []}
+ # Regex to find values and nodes with a table number
+ regex_tags = re.compile(rf'^set (?P<cmd_path>[^\']+table {table_num}) ?.*$')
+ regex_values = re.compile(
+ rf'^set (?P<cmd_path>[^\']+table) \'{table_num}\'$')
+ for config_command in config_commands:
+ # Search for tag nodes
+ search_result = regex_tags.search(config_command)
+ if search_result:
+ # Append table node path to a tag nodes list
+ cmd_path = search_result.group('cmd_path').split()
+ if cmd_path not in table_items['table_tags']:
+ table_items['table_tags'].append(cmd_path)
+ # Search for value nodes
+ search_result = regex_values.search(config_command)
+ if search_result:
+ # Append table node path to a value nodes list
+ table_items['table_values'].append(
+ search_result.group('cmd_path').split())
+ return table_items
+
+
+if (len(argv) < 2):
+ print("Must specify file name!")
+ exit(1)
+
+file_name = argv[1]
+
+with open(file_name, 'r') as f:
+ config_file = f.read()
+
+base = ['vrf', 'name']
+config = ConfigTree(config_file)
+
+if not config.exists(base):
+ # Nothing to do
+ exit(0)
+
+# Get a list of all currently used VRFs and tables
+vrfs_current = {}
+for vrf in config.list_nodes(base):
+ vrfs_current[vrf] = int(config.return_value(base + [vrf, 'table']))
+
+# Check VRF names and table numbers
+name_regex = re.compile(r'^\d.*$')
+for vrf_name, vrf_table in vrfs_current.items():
+ # Check table number
+ if vrf_table > 65535:
+ # Find new unused table number
+ vrfs_current[vrf_name] = None
+ while not vrfs_current[vrf_name]:
+ table_random = randrange(100, 65535)
+ if table_random not in vrfs_current.values():
+ vrfs_current[vrf_name] = table_random
+ # Update number to a new one
+ config.set(['vrf', 'name', vrf_name, 'table'],
+ vrfs_current[vrf_name],
+ replace=True)
+ # Check config items with old table number and replace to new one
+ config_commands = config.to_commands().split('\n')
+ table_config_lines = _search_tables(config_commands, vrf_table)
+ # Rename table nodes
+ if table_config_lines.get('table_tags'):
+ for table_config_path in table_config_lines.get('table_tags'):
+ config.rename(table_config_path, f'{vrfs_current[vrf_name]}')
+ # Replace table values
+ if table_config_lines.get('table_values'):
+ for table_config_path in table_config_lines.get('table_values'):
+ config.set(table_config_path,
+ f'{vrfs_current[vrf_name]}',
+ replace=True)
+
+ # Check VRF name
+ if name_regex.match(vrf_name):
+ vrf_name_new = None
+ while not vrf_name_new:
+ vrf_name_rand = f'{choice(ascii_lowercase)}{vrf_name}'[:15]
+ if vrf_name_rand not in vrfs_current:
+ vrf_name_new = vrf_name_rand
+ # Update VRF name to a new one
+ config.rename(['vrf', 'name', vrf_name], vrf_name_new)
+ # Check config items with old VRF name and replace to new one
+ config_commands = config.to_commands().split('\n')
+ vrf_config_lines = _search_vrfs(config_commands, vrf_name)
+ # Rename VRF to a new name
+ if vrf_config_lines:
+ for vrf_value_path in vrf_config_lines:
+ config.set(vrf_value_path, vrf_name_new, replace=True)
+
+try:
+ with open(file_name, 'w') as f:
+ f.write(config.to_string())
+except OSError as e:
+ print("Failed to save the modified config: {}".format(e))
+ exit(1)
diff --git a/src/op_mode/dns_forwarding_statistics.py b/src/op_mode/dns_forwarding_statistics.py
index 1fb61d263..d79b6c024 100755
--- a/src/op_mode/dns_forwarding_statistics.py
+++ b/src/op_mode/dns_forwarding_statistics.py
@@ -11,7 +11,7 @@ PDNS_CMD='/usr/bin/rec_control --socket-dir=/run/powerdns'
OUT_TMPL_SRC = """
DNS forwarding statistics:
-Cache entries: {{ cache_entries -}}
+Cache entries: {{ cache_entries }}
Cache size: {{ cache_size }} kbytes
"""
diff --git a/src/op_mode/ikev2_profile_generator.py b/src/op_mode/ikev2_profile_generator.py
new file mode 100755
index 000000000..d45525431
--- /dev/null
+++ b/src/op_mode/ikev2_profile_generator.py
@@ -0,0 +1,230 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2021 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 argparse
+
+from jinja2 import Template
+from sys import exit
+from socket import getfqdn
+from cryptography.x509.oid import NameOID
+
+from vyos.config import Config
+from vyos.pki import load_certificate
+from vyos.template import render_to_string
+from vyos.util import ask_input
+
+# Apple profiles only support one IKE/ESP encryption cipher and hash, whereas
+# VyOS comes with a multitude of different proposals for a connection.
+#
+# We take all available proposals from the VyOS CLI and ask the user which one
+# he would like to get enabled in his profile - thus there is limited possibility
+# to select a proposal that is not supported on the connection profile.
+#
+# IOS supports IKE-SA encryption algorithms:
+# - DES
+# - 3DES
+# - AES-128
+# - AES-256
+# - AES-128-GCM
+# - AES-256-GCM
+# - ChaCha20Poly1305
+#
+vyos2apple_cipher = {
+ '3des' : '3DES',
+ 'aes128' : 'AES-128',
+ 'aes256' : 'AES-256',
+ 'aes128gcm128' : 'AES-128-GCM',
+ 'aes256gcm128' : 'AES-256-GCM',
+ 'chacha20poly1305' : 'ChaCha20Poly1305',
+}
+
+# Windows supports IKE-SA encryption algorithms:
+# - DES3
+# - AES128
+# - AES192
+# - AES256
+# - GCMAES128
+# - GCMAES192
+# - GCMAES256
+#
+vyos2windows_cipher = {
+ '3des' : 'DES3',
+ 'aes128' : 'AES128',
+ 'aes192' : 'AES192',
+ 'aes256' : 'AES256',
+ 'aes128gcm128' : 'GCMAES128',
+ 'aes192gcm128' : 'GCMAES192',
+ 'aes256gcm128' : 'GCMAES256',
+}
+
+# IOS supports IKE-SA integrity algorithms:
+# - SHA1-96
+# - SHA1-160
+# - SHA2-256
+# - SHA2-384
+# - SHA2-512
+#
+vyos2apple_integrity = {
+ 'sha1' : 'SHA1-96',
+ 'sha1_160' : 'SHA1-160',
+ 'sha256' : 'SHA2-256',
+ 'sha384' : 'SHA2-384',
+ 'sha512' : 'SHA2-512',
+}
+
+# Windows supports IKE-SA integrity algorithms:
+# - SHA1-96
+# - SHA1-160
+# - SHA2-256
+# - SHA2-384
+# - SHA2-512
+#
+vyos2windows_integrity = {
+ 'sha1' : 'SHA196',
+ 'sha256' : 'SHA256',
+ 'aes128gmac' : 'GCMAES128',
+ 'aes192gmac' : 'GCMAES192',
+ 'aes256gmac' : 'GCMAES256',
+}
+
+# IOS 14.2 and later do no support dh-group 1,2 and 5. Supported DH groups would
+# be: 14, 15, 16, 17, 18, 19, 20, 21, 31
+ios_supported_dh_groups = ['14', '15', '16', '17', '18', '19', '20', '21', '31']
+# Windows 10 only allows a limited set of DH groups
+windows_supported_dh_groups = ['1', '2', '14', '24']
+
+parser = argparse.ArgumentParser()
+parser.add_argument('--os', const='all', nargs='?', choices=['ios', 'windows'], help='Operating system used for config generation', required=True)
+parser.add_argument("--connection", action="store", help='IPsec IKEv2 remote-access connection name from CLI', required=True)
+parser.add_argument("--remote", action="store", help='VPN connection remote-address where the client will connect to', required=True)
+parser.add_argument("--profile", action="store", help='IKEv2 profile name used in the profile list on the device')
+parser.add_argument("--name", action="store", help='VPN connection name as seen in the VPN application later')
+args = parser.parse_args()
+
+ipsec_base = ['vpn', 'ipsec']
+config_base = ipsec_base + ['remote-access', 'connection']
+pki_base = ['pki']
+conf = Config()
+if not conf.exists(config_base):
+ exit('IPSec remote-access is not configured!')
+
+profile_name = 'VyOS IKEv2 Profile'
+if args.profile:
+ profile_name = args.profile
+
+vpn_name = 'VyOS IKEv2 VPN'
+if args.name:
+ vpn_name = args.name
+
+conn_base = config_base + [args.connection]
+if not conf.exists(conn_base):
+ exit(f'IPSec remote-access connection "{args.connection}" does not exist!')
+
+data = conf.get_config_dict(conn_base, key_mangling=('-', '_'),
+ get_first_key=True, no_tag_node_value_mangle=True)
+
+data['profile_name'] = profile_name
+data['vpn_name'] = vpn_name
+data['remote'] = args.remote
+# This is a reverse-DNS style unique identifier used to detect duplicate profiles
+tmp = getfqdn().split('.')
+tmp = reversed(tmp)
+data['rfqdn'] = '.'.join(tmp)
+
+pki = conf.get_config_dict(pki_base, get_first_key=True)
+ca_name = data['authentication']['x509']['ca_certificate']
+cert_name = data['authentication']['x509']['certificate']
+
+ca_cert = load_certificate(pki['ca'][ca_name]['certificate'])
+cert = load_certificate(pki['certificate'][cert_name]['certificate'])
+
+data['ca_cn'] = ca_cert.subject.get_attributes_for_oid(NameOID.COMMON_NAME)[0].value
+data['cert_cn'] = cert.subject.get_attributes_for_oid(NameOID.COMMON_NAME)[0].value
+data['ca_cert'] = conf.return_value(pki_base + ['ca', ca_name, 'certificate'])
+
+esp_proposals = conf.get_config_dict(ipsec_base + ['esp-group', data['esp_group'], 'proposal'],
+ key_mangling=('-', '_'), get_first_key=True)
+ike_proposal = conf.get_config_dict(ipsec_base + ['ike-group', data['ike_group'], 'proposal'],
+ key_mangling=('-', '_'), get_first_key=True)
+
+
+# This script works only for Apple iOS/iPadOS and Windows. Both operating systems
+# have different limitations thus we load the limitations based on the operating
+# system used.
+
+vyos2client_cipher = vyos2apple_cipher if args.os == 'ios' else vyos2windows_cipher;
+vyos2client_integrity = vyos2apple_integrity if args.os == 'ios' else vyos2windows_integrity;
+supported_dh_groups = ios_supported_dh_groups if args.os == 'ios' else windows_supported_dh_groups;
+
+# Create a dictionary containing client conform IKE settings
+ike = {}
+count = 1
+for _, proposal in ike_proposal.items():
+ if {'dh_group', 'encryption', 'hash'} <= set(proposal):
+ if (proposal['encryption'] in set(vyos2client_cipher) and
+ proposal['hash'] in set(vyos2client_integrity) and
+ proposal['dh_group'] in set(supported_dh_groups)):
+
+ # We 're-code' from the VyOS IPSec proposals to the Apple naming scheme
+ proposal['encryption'] = vyos2client_cipher[ proposal['encryption'] ]
+ proposal['hash'] = vyos2client_integrity[ proposal['hash'] ]
+
+ ike.update( { str(count) : proposal } )
+ count += 1
+
+# Create a dictionary containing Apple conform ESP settings
+esp = {}
+count = 1
+for _, proposal in esp_proposals.items():
+ if {'encryption', 'hash'} <= set(proposal):
+ if proposal['encryption'] in set(vyos2client_cipher) and proposal['hash'] in set(vyos2client_integrity):
+ # We 're-code' from the VyOS IPSec proposals to the Apple naming scheme
+ proposal['encryption'] = vyos2client_cipher[ proposal['encryption'] ]
+ proposal['hash'] = vyos2client_integrity[ proposal['hash'] ]
+
+ esp.update( { str(count) : proposal } )
+ count += 1
+try:
+ if len(ike) > 1:
+ # Propare the input questions for the user
+ tmp = '\n'
+ for number, options in ike.items():
+ tmp += f'({number}) Encryption {options["encryption"]}, Integrity {options["hash"]}, DH group {options["dh_group"]}\n'
+ tmp += '\nSelect one of the above IKE groups: '
+ data['ike_encryption'] = ike[ ask_input(tmp, valid_responses=list(ike)) ]
+ else:
+ data['ike_encryption'] = ike['1']
+
+ if len(esp) > 1:
+ tmp = '\n'
+ for number, options in esp.items():
+ tmp += f'({number}) Encryption {options["encryption"]}, Integrity {options["hash"]}\n'
+ tmp += '\nSelect one of the above ESP groups: '
+ data['esp_encryption'] = esp[ ask_input(tmp, valid_responses=list(esp)) ]
+ else:
+ data['esp_encryption'] = esp['1']
+
+except KeyboardInterrupt:
+ exit("Interrupted")
+
+print('\n\n==== <snip> ====')
+if args.os == 'ios':
+ print(render_to_string('ipsec/ios_profile.tmpl', data))
+ print('==== </snip> ====\n')
+ print('Save the XML from above to a new file named "vyos.mobileconfig" and E-Mail it to your phone.')
+elif args.os == 'windows':
+ print(render_to_string('ipsec/windows_profile.tmpl', data))
+ print('==== </snip> ====\n')
diff --git a/src/op_mode/ping.py b/src/op_mode/ping.py
index 924a889db..2144ab53c 100755
--- a/src/op_mode/ping.py
+++ b/src/op_mode/ping.py
@@ -51,7 +51,7 @@ options = {
'help': 'Number of seconds before ping exits'
},
'do-not-fragment': {
- 'ping': '{command} -M dont',
+ 'ping': '{command} -M do',
'type': 'noarg',
'help': 'Set DF-bit flag to 1 for no fragmentation'
},
@@ -220,6 +220,8 @@ if __name__ == '__main__':
try:
ip = socket.gethostbyname(host)
+ except UnicodeError:
+ sys.exit(f'ping: Unknown host: {host}')
except socket.gaierror:
ip = host
diff --git a/src/op_mode/pki.py b/src/op_mode/pki.py
index 7dbeb4097..297270cf1 100755
--- a/src/op_mode/pki.py
+++ b/src/op_mode/pki.py
@@ -38,6 +38,8 @@ from vyos.util import cmd
CERT_REQ_END = '-----END CERTIFICATE REQUEST-----'
+auth_dir = '/config/auth'
+
# Helper Functions
def get_default_values():
@@ -215,7 +217,7 @@ def install_wireguard_key(name, private_key, public_key):
print("")
print("Public key for use on peer configuration: " + public_key)
else:
- print("set interfaces wireguard [INTERFACE] peer %s pubkey '%s'" % (name, public_key))
+ print("set interfaces wireguard [INTERFACE] peer %s public-key '%s'" % (name, public_key))
print("")
print("Private key for use on peer configuration: " + private_key)
@@ -230,6 +232,22 @@ def ask_passphrase():
passphrase = ask_input('Enter passphrase:')
return passphrase
+def write_file(filename, contents):
+ full_path = os.path.join(auth_dir, filename)
+ directory = os.path.dirname(full_path)
+
+ if not os.path.exists(directory):
+ print('Failed to write file: directory does not exist')
+ return False
+
+ if os.path.exists(full_path) and not ask_yes_no('Do you want to overwrite the existing file?'):
+ return False
+
+ with open(full_path, 'w') as f:
+ f.write(contents)
+
+ print(f'File written to {full_path}')
+
# Generation functions
def generate_private_key():
@@ -266,7 +284,7 @@ def parse_san_string(san_string):
output.append(value)
return output
-def generate_certificate_request(private_key=None, key_type=None, return_request=False, name=None, install=False, ask_san=True):
+def generate_certificate_request(private_key=None, key_type=None, return_request=False, name=None, install=False, file=False, ask_san=True):
if not private_key:
private_key, key_type = generate_private_key()
@@ -291,14 +309,19 @@ def generate_certificate_request(private_key=None, key_type=None, return_request
passphrase = ask_passphrase()
- if not install:
+ if not install and not file:
print(encode_certificate(cert_req))
print(encode_private_key(private_key, passphrase=passphrase))
return None
- print("Certificate request:")
- print(encode_certificate(cert_req) + "\n")
- install_certificate(name, private_key=private_key, key_type=key_type, key_passphrase=passphrase, is_ca=False)
+ if install:
+ print("Certificate request:")
+ print(encode_certificate(cert_req) + "\n")
+ install_certificate(name, private_key=private_key, key_type=key_type, key_passphrase=passphrase, is_ca=False)
+
+ if file:
+ write_file(f'{name}.csr', encode_certificate(cert_req))
+ write_file(f'{name}.key', encode_private_key(private_key, passphrase=passphrase))
def generate_certificate(cert_req, ca_cert, ca_private_key, is_ca=False, is_sub_ca=False):
valid_days = ask_input('Enter how many days certificate will be valid:', default='365' if not is_ca else '1825', numeric_only=True)
@@ -307,20 +330,25 @@ def generate_certificate(cert_req, ca_cert, ca_private_key, is_ca=False, is_sub_
cert_type = ask_input('Enter certificate type: (client, server)', default='server', valid_responses=['client', 'server'])
return create_certificate(cert_req, ca_cert, ca_private_key, valid_days, cert_type, is_ca, is_sub_ca)
-def generate_ca_certificate(name, install=False):
+def generate_ca_certificate(name, install=False, file=False):
private_key, key_type = generate_private_key()
cert_req = generate_certificate_request(private_key, key_type, return_request=True, ask_san=False)
cert = generate_certificate(cert_req, cert_req, private_key, is_ca=True)
passphrase = ask_passphrase()
- if not install:
+ if not install and not file:
print(encode_certificate(cert))
print(encode_private_key(private_key, passphrase=passphrase))
return None
- install_certificate(name, cert, private_key, key_type, key_passphrase=passphrase, is_ca=True)
+ if install:
+ install_certificate(name, cert, private_key, key_type, key_passphrase=passphrase, is_ca=True)
+
+ if file:
+ write_file(f'{name}.pem', encode_certificate(cert))
+ write_file(f'{name}.key', encode_private_key(private_key, passphrase=passphrase))
-def generate_ca_certificate_sign(name, ca_name, install=False):
+def generate_ca_certificate_sign(name, ca_name, install=False, file=False):
ca_dict = get_config_ca_certificate(ca_name)
if not ca_dict:
@@ -374,14 +402,19 @@ def generate_ca_certificate_sign(name, ca_name, install=False):
cert = generate_certificate(cert_req, ca_cert, ca_private_key, is_ca=True, is_sub_ca=True)
passphrase = ask_passphrase()
- if not install:
+ if not install and not file:
print(encode_certificate(cert))
print(encode_private_key(private_key, passphrase=passphrase))
return None
- install_certificate(name, cert, private_key, key_type, key_passphrase=passphrase, is_ca=True)
+ if install:
+ install_certificate(name, cert, private_key, key_type, key_passphrase=passphrase, is_ca=True)
+
+ if file:
+ write_file(f'{name}.pem', encode_certificate(cert))
+ write_file(f'{name}.key', encode_private_key(private_key, passphrase=passphrase))
-def generate_certificate_sign(name, ca_name, install=False):
+def generate_certificate_sign(name, ca_name, install=False, file=False):
ca_dict = get_config_ca_certificate(ca_name)
if not ca_dict:
@@ -435,27 +468,37 @@ def generate_certificate_sign(name, ca_name, install=False):
cert = generate_certificate(cert_req, ca_cert, ca_private_key, is_ca=False)
passphrase = ask_passphrase()
- if not install:
+ if not install and not file:
print(encode_certificate(cert))
print(encode_private_key(private_key, passphrase=passphrase))
return None
- install_certificate(name, cert, private_key, key_type, key_passphrase=passphrase, is_ca=False)
+ if install:
+ install_certificate(name, cert, private_key, key_type, key_passphrase=passphrase, is_ca=False)
+
+ if file:
+ write_file(f'{name}.pem', encode_certificate(cert))
+ write_file(f'{name}.key', encode_private_key(private_key, passphrase=passphrase))
-def generate_certificate_selfsign(name, install=False):
+def generate_certificate_selfsign(name, install=False, file=False):
private_key, key_type = generate_private_key()
cert_req = generate_certificate_request(private_key, key_type, return_request=True)
cert = generate_certificate(cert_req, cert_req, private_key, is_ca=False)
passphrase = ask_passphrase()
- if not install:
+ if not install and not file:
print(encode_certificate(cert))
print(encode_private_key(private_key, passphrase=passphrase))
return None
- install_certificate(name, cert, private_key=private_key, key_type=key_type, key_passphrase=passphrase, is_ca=False)
+ if install:
+ install_certificate(name, cert, private_key=private_key, key_type=key_type, key_passphrase=passphrase, is_ca=False)
+
+ if file:
+ write_file(f'{name}.pem', encode_certificate(cert))
+ write_file(f'{name}.key', encode_private_key(private_key, passphrase=passphrase))
-def generate_certificate_revocation_list(ca_name, install=False):
+def generate_certificate_revocation_list(ca_name, install=False, file=False):
ca_dict = get_config_ca_certificate(ca_name)
if not ca_dict:
@@ -505,26 +548,35 @@ def generate_certificate_revocation_list(ca_name, install=False):
print("Failed to create CRL")
return None
- if not install:
+ if not install and not file:
print(encode_certificate(crl))
return None
- install_crl(ca_name, crl)
+ if install:
+ install_crl(ca_name, crl)
+
+ if file:
+ write_file(f'{name}.crl', encode_certificate(crl))
-def generate_ssh_keypair(name, install=False):
+def generate_ssh_keypair(name, install=False, file=False):
private_key, key_type = generate_private_key()
public_key = private_key.public_key()
passphrase = ask_passphrase()
- if not install:
+ if not install and not file:
print(encode_public_key(public_key, encoding='OpenSSH', key_format='OpenSSH'))
print("")
print(encode_private_key(private_key, encoding='PEM', key_format='OpenSSH', passphrase=passphrase))
return None
- install_ssh_key(name, public_key, private_key, passphrase)
+ if install:
+ install_ssh_key(name, public_key, private_key, passphrase)
+
+ if file:
+ write_file(f'{name}.pem', encode_public_key(public_key, encoding='OpenSSH', key_format='OpenSSH'))
+ write_file(f'{name}.key', encode_private_key(private_key, encoding='PEM', key_format='OpenSSH', passphrase=passphrase))
-def generate_dh_parameters(name, install=False):
+def generate_dh_parameters(name, install=False, file=False):
bits = ask_input('Enter DH parameters key size:', default=2048, numeric_only=True)
print("Generating parameters...")
@@ -534,49 +586,62 @@ def generate_dh_parameters(name, install=False):
print("Failed to create DH parameters")
return None
- if not install:
+ if not install and not file:
print("DH Parameters:")
print(encode_dh_parameters(dh_params))
- install_dh_parameters(name, dh_params)
+ if install:
+ install_dh_parameters(name, dh_params)
+
+ if file:
+ write_file(f'{name}.pem', encode_dh_parameters(dh_params))
-def generate_keypair(name, install=False):
+def generate_keypair(name, install=False, file=False):
private_key, key_type = generate_private_key()
public_key = private_key.public_key()
passphrase = ask_passphrase()
- if not install:
+ if not install and not file:
print(encode_public_key(public_key))
print("")
print(encode_private_key(private_key, passphrase=passphrase))
return None
- install_keypair(name, key_type, private_key, public_key, passphrase)
+ if install:
+ install_keypair(name, key_type, private_key, public_key, passphrase)
+
+ if file:
+ write_file(f'{name}.pem', encode_public_key(public_key))
+ write_file(f'{name}.key', encode_private_key(private_key, passphrase=passphrase))
-def generate_openvpn_key(name, install=False):
+def generate_openvpn_key(name, install=False, file=False):
result = cmd('openvpn --genkey secret /dev/stdout | grep -o "^[^#]*"')
if not result:
print("Failed to generate OpenVPN key")
return None
- if not install:
+ if not install and not file:
print(result)
return None
- key_lines = result.split("\n")
- key_data = "".join(key_lines[1:-1]) # Remove wrapper tags and line endings
- key_version = '1'
+ if install:
+ key_lines = result.split("\n")
+ key_data = "".join(key_lines[1:-1]) # Remove wrapper tags and line endings
+ key_version = '1'
+
+ version_search = re.search(r'BEGIN OpenVPN Static key V(\d+)', result) # Future-proofing (hopefully)
+ if version_search:
+ key_version = version_search[1]
- version_search = re.search(r'BEGIN OpenVPN Static key V(\d+)', result) # Future-proofing (hopefully)
- if version_search:
- key_version = version_search[1]
+ print("Configure mode commands to install OpenVPN key:")
+ print("set pki openvpn shared-secret %s key '%s'" % (name, key_data))
+ print("set pki openvpn shared-secret %s version '%s'" % (name, key_version))
- print("Configure mode commands to install OpenVPN key:")
- print("set pki openvpn shared-secret %s key '%s'" % (name, key_data))
- print("set pki openvpn shared-secret %s version '%s'" % (name, key_version))
+ if file:
+ write_file(f'{name}.key', result)
-def generate_wireguard_key(name, install=False):
+def generate_wireguard_key(name, install=False, file=False):
private_key = cmd('wg genkey')
public_key = cmd('wg pubkey', input=private_key)
@@ -585,17 +650,26 @@ def generate_wireguard_key(name, install=False):
print("Public key: " + public_key)
return None
- install_wireguard_key(name, private_key, public_key)
+ if install:
+ install_wireguard_key(name, private_key, public_key)
-def generate_wireguard_psk(name, install=False):
+ if file:
+ write_file(f'{name}_public.key', public_key)
+ write_file(f'{name}_private.key', private_key)
+
+def generate_wireguard_psk(name, install=False, file=False):
psk = cmd('wg genpsk')
- if not install:
+ if not install and not file:
print("Pre-shared key:")
print(psk)
return None
- install_wireguard_psk(name, psk)
+ if install:
+ install_wireguard_psk(name, psk)
+
+ if file:
+ write_file(f'{name}.key', psk)
# Show functions
@@ -721,6 +795,7 @@ if __name__ == '__main__':
parser.add_argument('--psk', help='Wireguard pre shared key', required=False)
# Global
+ parser.add_argument('--file', help='Write generated keys into specified filename', action='store_true')
parser.add_argument('--install', help='Install generated keys into running-config', action='store_true')
args = parser.parse_args()
@@ -729,31 +804,31 @@ if __name__ == '__main__':
if args.action == 'generate':
if args.ca:
if args.sign:
- generate_ca_certificate_sign(args.ca, args.sign, args.install)
+ generate_ca_certificate_sign(args.ca, args.sign, install=args.install, file=args.file)
else:
- generate_ca_certificate(args.ca, args.install)
+ generate_ca_certificate(args.ca, install=args.install, file=args.file)
elif args.certificate:
if args.sign:
- generate_certificate_sign(args.certificate, args.sign, args.install)
+ generate_certificate_sign(args.certificate, args.sign, install=args.install, file=args.file)
elif args.self_sign:
- generate_certificate_selfsign(args.certificate, args.install)
+ generate_certificate_selfsign(args.certificate, install=args.install, file=args.file)
else:
generate_certificate_request(name=args.certificate, install=args.install)
elif args.crl:
- generate_certificate_revocation_list(args.crl, args.install)
+ generate_certificate_revocation_list(args.crl, install=args.install, file=args.file)
elif args.ssh:
- generate_ssh_keypair(args.ssh, args.install)
+ generate_ssh_keypair(args.ssh, install=args.install, file=args.file)
elif args.dh:
- generate_dh_parameters(args.dh, args.install)
+ generate_dh_parameters(args.dh, install=args.install, file=args.file)
elif args.keypair:
- generate_keypair(args.keypair, args.install)
+ generate_keypair(args.keypair, install=args.install, file=args.file)
elif args.openvpn:
- generate_openvpn_key(args.openvpn, args.install)
+ generate_openvpn_key(args.openvpn, install=args.install, file=args.file)
elif args.wireguard:
if args.key:
- generate_wireguard_key(args.key, args.install)
+ generate_wireguard_key(args.key, install=args.install, file=args.file)
elif args.psk:
- generate_wireguard_psk(args.psk, args.install)
+ generate_wireguard_psk(args.psk, install=args.install, file=args.file)
elif args.action == 'show':
if args.ca:
show_certificate_authority(None if args.ca == 'all' else args.ca)
diff --git a/src/op_mode/show_dhcp.py b/src/op_mode/show_dhcp.py
index ff1e3cc56..4df275e04 100755
--- a/src/op_mode/show_dhcp.py
+++ b/src/op_mode/show_dhcp.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2018-2020 VyOS maintainers and contributors
+# Copyright (C) 2018-2021 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
@@ -27,8 +27,7 @@ from datetime import datetime
from isc_dhcp_leases import Lease, IscDhcpLeases
from vyos.config import Config
-from vyos.util import call
-
+from vyos.util import is_systemd_service_running
lease_file = "/config/dhcpd.leases"
pool_key = "shared-networkname"
@@ -217,7 +216,7 @@ if __name__ == '__main__':
exit(0)
# if dhcp server is down, inactive leases may still be shown as active, so warn the user.
- if call('systemctl -q is-active isc-dhcp-server.service') != 0:
+ if not is_systemd_service_running('isc-dhcp-server.service'):
print("WARNING: DHCP server is configured but not started. Data may be stale.")
if args.leases:
diff --git a/src/op_mode/show_dhcpv6.py b/src/op_mode/show_dhcpv6.py
index f70f04298..1f987ff7b 100755
--- a/src/op_mode/show_dhcpv6.py
+++ b/src/op_mode/show_dhcpv6.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2018-2020 VyOS maintainers and contributors
+# Copyright (C) 2018-2021 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
@@ -27,7 +27,7 @@ from datetime import datetime
from isc_dhcp_leases import Lease, IscDhcpLeases
from vyos.config import Config
-from vyos.util import call
+from vyos.util import is_systemd_service_running
lease_file = "/config/dhcpdv6.leases"
pool_key = "shared-networkname"
@@ -202,7 +202,7 @@ if __name__ == '__main__':
exit(0)
# if dhcp server is down, inactive leases may still be shown as active, so warn the user.
- if call('systemctl -q is-active isc-dhcp-server6.service') != 0:
+ if not is_systemd_service_running('isc-dhcp-server6.service'):
print("WARNING: DHCPv6 server is configured but not started. Data may be stale.")
if args.leases:
diff --git a/src/op_mode/show_vrf.py b/src/op_mode/show_vrf.py
index 94358c6e4..3c7a90205 100755
--- a/src/op_mode/show_vrf.py
+++ b/src/op_mode/show_vrf.py
@@ -20,12 +20,11 @@ from json import loads
from vyos.util import cmd
-vrf_out_tmpl = """
-VRF name state mac address flags interfaces
+vrf_out_tmpl = """VRF name state mac address flags interfaces
-------- ----- ----------- ----- ----------
-{% for v in vrf %}
+{%- for v in vrf %}
{{"%-16s"|format(v.ifname)}} {{ "%-8s"|format(v.operstate | lower())}} {{"%-17s"|format(v.address | lower())}} {{ v.flags|join(',')|lower()}} {{v.members|join(',')|lower()}}
-{% endfor %}
+{%- endfor %}
"""
diff --git a/src/op_mode/wireguard.py b/src/op_mode/wireguard.py
deleted file mode 100755
index e08bc983a..000000000
--- a/src/op_mode/wireguard.py
+++ /dev/null
@@ -1,159 +0,0 @@
-#!/usr/bin/env python3
-#
-# Copyright (C) 2018-2020 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 argparse
-import os
-import sys
-import shutil
-import syslog as sl
-import re
-
-from vyos.config import Config
-from vyos.ifconfig import WireGuardIf
-from vyos.util import cmd
-from vyos.util import run
-from vyos.util import check_kmod
-from vyos import ConfigError
-
-dir = r'/config/auth/wireguard'
-psk = dir + '/preshared.key'
-
-k_mod = 'wireguard'
-
-def generate_keypair(pk, pub):
- """ generates a keypair which is stored in /config/auth/wireguard """
- old_umask = os.umask(0o027)
- if run(f'wg genkey | tee {pk} | wg pubkey > {pub}') != 0:
- raise ConfigError("wireguard key-pair generation failed")
- else:
- sl.syslog(
- sl.LOG_NOTICE, "new keypair wireguard key generated in " + dir)
- os.umask(old_umask)
-
-
-def genkey(location):
- """ helper function to check, regenerate the keypair """
- pk = "{}/private.key".format(location)
- pub = "{}/public.key".format(location)
- old_umask = os.umask(0o027)
- if os.path.exists(pk) and os.path.exists(pub):
- try:
- choice = input(
- "You already have a wireguard key-pair, do you want to re-generate? [y/n] ")
- if choice == 'y' or choice == 'Y':
- generate_keypair(pk, pub)
- except KeyboardInterrupt:
- sys.exit(0)
- else:
- """ if keypair is bing executed from a running iso """
- if not os.path.exists(location):
- run(f'sudo mkdir -p {location}')
- run(f'sudo chgrp vyattacfg {location}')
- run(f'sudo chmod 750 {location}')
- generate_keypair(pk, pub)
- os.umask(old_umask)
-
-
-def showkey(key):
- """ helper function to show privkey or pubkey """
- if os.path.exists(key):
- print (open(key).read().strip())
- else:
- print ("{} not found".format(key))
-
-
-def genpsk():
- """
- generates a preshared key and shows it on stdout,
- it's stored only in the cli config
- """
-
- psk = cmd('wg genpsk')
- print(psk)
-
-def list_key_dirs():
- """ lists all dirs under /config/auth/wireguard """
- if os.path.exists(dir):
- nks = next(os.walk(dir))[1]
- for nk in nks:
- print (nk)
-
-def del_key_dir(kname):
- """ deletes /config/auth/wireguard/<kname> """
- kdir = "{0}/{1}".format(dir,kname)
- if not os.path.isdir(kdir):
- print ("named keypair {} not found".format(kname))
- return 1
- shutil.rmtree(kdir)
-
-
-if __name__ == '__main__':
- check_kmod(k_mod)
- parser = argparse.ArgumentParser(description='wireguard key management')
- parser.add_argument(
- '--genkey', action="store_true", help='generate key-pair')
- parser.add_argument(
- '--showpub', action="store_true", help='shows public key')
- parser.add_argument(
- '--showpriv', action="store_true", help='shows private key')
- parser.add_argument(
- '--genpsk', action="store_true", help='generates preshared-key')
- parser.add_argument(
- '--location', action="store", help='key location within {}'.format(dir))
- parser.add_argument(
- '--listkdir', action="store_true", help='lists named keydirectories')
- parser.add_argument(
- '--delkdir', action="store_true", help='removes named keydirectories')
- parser.add_argument(
- '--showinterface', action="store", help='shows interface details')
- args = parser.parse_args()
-
- try:
- if args.genkey:
- if args.location:
- genkey("{0}/{1}".format(dir, args.location))
- else:
- genkey("{}/default".format(dir))
- if args.showpub:
- if args.location:
- showkey("{0}/{1}/public.key".format(dir, args.location))
- else:
- showkey("{}/default/public.key".format(dir))
- if args.showpriv:
- if args.location:
- showkey("{0}/{1}/private.key".format(dir, args.location))
- else:
- showkey("{}/default/private.key".format(dir))
- if args.genpsk:
- genpsk()
- if args.listkdir:
- list_key_dirs()
- if args.showinterface:
- try:
- intf = WireGuardIf(args.showinterface, create=False, debug=False)
- print(intf.operational.show_interface())
- # the interface does not exists
- except Exception:
- pass
- if args.delkdir:
- if args.location:
- del_key_dir(args.location)
- else:
- del_key_dir("default")
-
- except ConfigError as e:
- print(e)
- sys.exit(1)
diff --git a/src/op_mode/wireguard_client.py b/src/op_mode/wireguard_client.py
index 7a620a01e..7661254da 100755
--- a/src/op_mode/wireguard_client.py
+++ b/src/op_mode/wireguard_client.py
@@ -38,7 +38,7 @@ To enable this configuration on a VyOS router you can use the following commands
{% for addr in address if address is defined %}
set interfaces wireguard {{ interface }} peer {{ name }} allowed-ips '{{ addr }}'
{% endfor %}
-set interfaces wireguard {{ interface }} peer {{ name }} pubkey '{{ pubkey }}'
+set interfaces wireguard {{ interface }} peer {{ name }} public-key '{{ pubkey }}'
"""
client_config = """
diff --git a/src/services/api/graphql/README.graphql b/src/services/api/graphql/README.graphql
new file mode 100644
index 000000000..a04138010
--- /dev/null
+++ b/src/services/api/graphql/README.graphql
@@ -0,0 +1,116 @@
+
+Example using GraphQL mutations to configure a DHCP server:
+
+This assumes that the http-api is running:
+
+'set service https api'
+
+One can configure an address on an interface, and configure the DHCP server
+to run with that address as default router by requesting these 'mutations'
+in the GraphQL playground:
+
+mutation {
+ createInterfaceEthernet (data: {interface: "eth1",
+ address: "192.168.0.1/24",
+ description: "BOB"}) {
+ success
+ errors
+ data {
+ address
+ }
+ }
+}
+
+mutation {
+ createDhcpServer(data: {sharedNetworkName: "BOB",
+ subnet: "192.168.0.0/24",
+ defaultRouter: "192.168.0.1",
+ dnsServer: "192.168.0.1",
+ domainName: "vyos.net",
+ lease: 86400,
+ range: 0,
+ start: "192.168.0.9",
+ stop: "192.168.0.254",
+ dnsForwardingAllowFrom: "192.168.0.0/24",
+ dnsForwardingCacheSize: 0,
+ dnsForwardingListenAddress: "192.168.0.1"}) {
+ success
+ errors
+ data {
+ defaultRouter
+ }
+ }
+}
+
+The GraphQL playground will be found at:
+
+https://{{ host_address }}/graphql
+
+An equivalent curl command to the first example above would be:
+
+curl -k 'https://192.168.100.168/graphql' -H 'Content-Type: application/json' --data-binary '{"query": "mutation {createInterfaceEthernet (data: {interface: \"eth1\", address: \"192.168.0.1/24\", description: \"BOB\"}) {success errors data {address}}}"}'
+
+Note that the 'mutation' term is prefaced by 'query' in the curl command.
+
+What's here:
+
+services
+├── api
+│   └── graphql
+│   ├── graphql
+│   │   ├── directives.py
+│   │   ├── __init__.py
+│   │   ├── mutations.py
+│   │   └── schema
+│   │   ├── dhcp_server.graphql
+│   │   ├── interface_ethernet.graphql
+│   │   └── schema.graphql
+│   ├── recipes
+│   │   ├── dhcp_server.py
+│   │   ├── __init__.py
+│   │   ├── interface_ethernet.py
+│   │   ├── recipe.py
+│   │   └── templates
+│   │   ├── dhcp_server.tmpl
+│   │   └── interface_ethernet.tmpl
+│   └── state.py
+├── vyos-configd
+├── vyos-hostsd
+└── vyos-http-api-server
+
+The GraphQL library that we are using, Ariadne, advertises itself as a
+'schema-first' implementation: define the schema; define resolvers
+(handlers) for declared Query and Mutation types (Subscription types are not
+currently used).
+
+In the current approach to a high-level API, we consider the
+Jinja2-templated collection of configuration mode 'set'/'delete' commands as
+the Ur-data; the GraphQL schema is produced from those files, located in
+'api/graphql/recipes/templates'.
+
+Resolvers for the schema Mutation fields are dynamically generated using a
+'directive' added to the respective schema field. The directive,
+'@generate', is handled by the class 'DataDirective' in
+'api/graphql/graphql/directives.py', which calls the 'make_resolver' function in
+'api/graphql/graphql/mutations.py'; the produced resolver calls the appropriate
+wrapper in 'api/graphql/recipes', with base class doing the (overridable)
+configuration steps of calling all defined 'set'/'delete' commands.
+
+Integrating the above with vyos-http-api-server is ~10 lines of code.
+
+What needs to be done:
+
+• automate generation of schema and wrappers from templated configuration
+commands
+
+• investigate whether the subclassing provided by the named wrappers in
+'api/graphql/recipes' is sufficient for use cases which need to modify data
+
+• encapsulate the manipulation of 'canonical names' which transforms the
+prefixed camel-case schema names to various snake-case file/function names
+
+• consider mechanism for migration of templates: offline vs. on-the-fly
+
+• define the naming convention for those schema fields that refer to
+configuration mode parameters: e.g. how much of the path is needed as prefix
+to uniquely define the term
diff --git a/src/services/api/graphql/graphql/__init__.py b/src/services/api/graphql/graphql/__init__.py
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/src/services/api/graphql/graphql/__init__.py
diff --git a/src/services/api/graphql/graphql/directives.py b/src/services/api/graphql/graphql/directives.py
new file mode 100644
index 000000000..651421c35
--- /dev/null
+++ b/src/services/api/graphql/graphql/directives.py
@@ -0,0 +1,17 @@
+from ariadne import SchemaDirectiveVisitor, ObjectType
+from . mutations import make_resolver
+
+class DataDirective(SchemaDirectiveVisitor):
+ """
+ Class providing implementation of 'generate' directive in schema.
+
+ """
+ def visit_field_definition(self, field, object_type):
+ name = f'{field.type}'
+ # field.type contains the return value of the mutation; trim value
+ # to produce canonical name
+ name = name.replace('Result', '', 1)
+
+ func = make_resolver(name)
+ field.resolve = func
+ return field
diff --git a/src/services/api/graphql/graphql/mutations.py b/src/services/api/graphql/graphql/mutations.py
new file mode 100644
index 000000000..98c665c9a
--- /dev/null
+++ b/src/services/api/graphql/graphql/mutations.py
@@ -0,0 +1,60 @@
+
+from importlib import import_module
+from typing import Any, Dict
+from ariadne import ObjectType, convert_kwargs_to_snake_case, convert_camel_case_to_snake
+from graphql import GraphQLResolveInfo
+from makefun import with_signature
+
+from .. import state
+
+mutation = ObjectType("Mutation")
+
+def make_resolver(mutation_name):
+ """Dynamically generate a resolver for the mutation named in the
+ schema by 'mutation_name'.
+
+ Dynamic generation is provided using the package 'makefun' (via the
+ decorator 'with_signature'), which provides signature-preserving
+ function wrappers; it provides several improvements over, say,
+ functools.wraps.
+
+ :raise Exception:
+ encapsulating ConfigErrors, or internal errors
+ """
+ class_name = mutation_name.replace('create', '', 1).replace('delete', '', 1)
+ func_base_name = convert_camel_case_to_snake(class_name)
+ resolver_name = f'resolve_create_{func_base_name}'
+ func_sig = '(obj: Any, info: GraphQLResolveInfo, data: Dict)'
+
+ @mutation.field(mutation_name)
+ @convert_kwargs_to_snake_case
+ @with_signature(func_sig, func_name=resolver_name)
+ async def func_impl(*args, **kwargs):
+ try:
+ if 'data' not in kwargs:
+ return {
+ "success": False,
+ "errors": ['missing data']
+ }
+
+ data = kwargs['data']
+ session = state.settings['app'].state.vyos_session
+
+ mod = import_module(f'api.graphql.recipes.{func_base_name}')
+ klass = getattr(mod, class_name)
+ k = klass(session, data)
+ k.configure()
+
+ return {
+ "success": True,
+ "data": data
+ }
+ except Exception as error:
+ return {
+ "success": False,
+ "errors": [str(error)]
+ }
+
+ return func_impl
+
+
diff --git a/src/services/api/graphql/graphql/schema/dhcp_server.graphql b/src/services/api/graphql/graphql/schema/dhcp_server.graphql
new file mode 100644
index 000000000..a7ee75d40
--- /dev/null
+++ b/src/services/api/graphql/graphql/schema/dhcp_server.graphql
@@ -0,0 +1,35 @@
+input dhcpServerConfigInput {
+ sharedNetworkName: String
+ subnet: String
+ defaultRouter: String
+ dnsServer: String
+ domainName: String
+ lease: Int
+ range: Int
+ start: String
+ stop: String
+ dnsForwardingAllowFrom: String
+ dnsForwardingCacheSize: Int
+ dnsForwardingListenAddress: String
+}
+
+type dhcpServerConfig {
+ sharedNetworkName: String
+ subnet: String
+ defaultRouter: String
+ dnsServer: String
+ domainName: String
+ lease: Int
+ range: Int
+ start: String
+ stop: String
+ dnsForwardingAllowFrom: String
+ dnsForwardingCacheSize: Int
+ dnsForwardingListenAddress: String
+}
+
+type createDhcpServerResult {
+ data: dhcpServerConfig
+ success: Boolean!
+ errors: [String]
+}
diff --git a/src/services/api/graphql/graphql/schema/interface_ethernet.graphql b/src/services/api/graphql/graphql/schema/interface_ethernet.graphql
new file mode 100644
index 000000000..fdcf97bad
--- /dev/null
+++ b/src/services/api/graphql/graphql/schema/interface_ethernet.graphql
@@ -0,0 +1,18 @@
+input interfaceEthernetConfigInput {
+ interface: String
+ address: String
+ replace: Boolean = true
+ description: String
+}
+
+type interfaceEthernetConfig {
+ interface: String
+ address: String
+ description: String
+}
+
+type createInterfaceEthernetResult {
+ data: interfaceEthernetConfig
+ success: Boolean!
+ errors: [String]
+}
diff --git a/src/services/api/graphql/graphql/schema/schema.graphql b/src/services/api/graphql/graphql/schema/schema.graphql
new file mode 100644
index 000000000..8a5e17962
--- /dev/null
+++ b/src/services/api/graphql/graphql/schema/schema.graphql
@@ -0,0 +1,15 @@
+schema {
+ query: Query
+ mutation: Mutation
+}
+
+type Query {
+ _dummy: String
+}
+
+directive @generate on FIELD_DEFINITION
+
+type Mutation {
+ createDhcpServer(data: dhcpServerConfigInput) : createDhcpServerResult @generate
+ createInterfaceEthernet(data: interfaceEthernetConfigInput) : createInterfaceEthernetResult @generate
+}
diff --git a/src/services/api/graphql/recipes/__init__.py b/src/services/api/graphql/recipes/__init__.py
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/src/services/api/graphql/recipes/__init__.py
diff --git a/src/services/api/graphql/recipes/dhcp_server.py b/src/services/api/graphql/recipes/dhcp_server.py
new file mode 100644
index 000000000..3edb3028e
--- /dev/null
+++ b/src/services/api/graphql/recipes/dhcp_server.py
@@ -0,0 +1,13 @@
+
+from . recipe import Recipe
+
+class DhcpServer(Recipe):
+ def __init__(self, session, command_file):
+ super().__init__(session, command_file)
+
+ # Define any custom processing of parameters here by overriding
+ # configure:
+ #
+ # def configure(self):
+ # self.data = transform_data(self.data)
+ # super().configure()
diff --git a/src/services/api/graphql/recipes/interface_ethernet.py b/src/services/api/graphql/recipes/interface_ethernet.py
new file mode 100644
index 000000000..f88f5924f
--- /dev/null
+++ b/src/services/api/graphql/recipes/interface_ethernet.py
@@ -0,0 +1,13 @@
+
+from . recipe import Recipe
+
+class InterfaceEthernet(Recipe):
+ def __init__(self, session, command_file):
+ super().__init__(session, command_file)
+
+ # Define any custom processing of parameters here by overriding
+ # configure:
+ #
+ # def configure(self):
+ # self.data = transform_data(self.data)
+ # super().configure()
diff --git a/src/services/api/graphql/recipes/recipe.py b/src/services/api/graphql/recipes/recipe.py
new file mode 100644
index 000000000..8fbb9e0bf
--- /dev/null
+++ b/src/services/api/graphql/recipes/recipe.py
@@ -0,0 +1,49 @@
+from ariadne import convert_camel_case_to_snake
+import vyos.defaults
+from vyos.template import render
+
+class Recipe(object):
+ def __init__(self, session, data):
+ self._session = session
+ self.data = data
+ self._name = convert_camel_case_to_snake(type(self).__name__)
+
+ @property
+ def data(self):
+ return self.__data
+
+ @data.setter
+ def data(self, data):
+ if isinstance(data, dict):
+ self.__data = data
+ else:
+ raise ValueError("data must be of type dict")
+
+ def configure(self):
+ session = self._session
+ data = self.data
+ func_base_name = self._name
+
+ tmpl_file = f'{func_base_name}.tmpl'
+ cmd_file = f'/tmp/{func_base_name}.cmds'
+ tmpl_dir = vyos.defaults.directories['api_templates']
+
+ try:
+ render(cmd_file, tmpl_file, data, location=tmpl_dir)
+ commands = []
+ with open(cmd_file) as f:
+ lines = f.readlines()
+ for line in lines:
+ commands.append(line.split())
+ for cmd in commands:
+ if cmd[0] == 'set':
+ session.set(cmd[1:])
+ elif cmd[0] == 'delete':
+ session.delete(cmd[1:])
+ else:
+ raise ValueError('Operation must be "set" or "delete"')
+ session.commit()
+ except Exception as error:
+ raise error
+
+
diff --git a/src/services/api/graphql/recipes/templates/dhcp_server.tmpl b/src/services/api/graphql/recipes/templates/dhcp_server.tmpl
new file mode 100644
index 000000000..629ce83c1
--- /dev/null
+++ b/src/services/api/graphql/recipes/templates/dhcp_server.tmpl
@@ -0,0 +1,9 @@
+set service dhcp-server shared-network-name {{ shared_network_name }} subnet {{ subnet }} default-router {{ default_router }}
+set service dhcp-server shared-network-name {{ shared_network_name }} subnet {{ subnet }} dns-server {{ dns_server }}
+set service dhcp-server shared-network-name {{ shared_network_name }} subnet {{ subnet }} domain-name {{ domain_name }}
+set service dhcp-server shared-network-name {{ shared_network_name }} subnet {{ subnet }} lease {{ lease }}
+set service dhcp-server shared-network-name {{ shared_network_name }} subnet {{ subnet }} range {{ range }} start {{ start }}
+set service dhcp-server shared-network-name {{ shared_network_name }} subnet {{ subnet }} range {{ range }} stop {{ stop }}
+set service dns forwarding allow-from {{ dns_forwarding_allow_from }}
+set service dns forwarding cache-size {{ dns_forwarding_cache_size }}
+set service dns forwarding listen-address {{ dns_forwarding_listen_address }}
diff --git a/src/services/api/graphql/recipes/templates/interface_ethernet.tmpl b/src/services/api/graphql/recipes/templates/interface_ethernet.tmpl
new file mode 100644
index 000000000..d9d7ed691
--- /dev/null
+++ b/src/services/api/graphql/recipes/templates/interface_ethernet.tmpl
@@ -0,0 +1,5 @@
+{% if replace %}
+delete interfaces ethernet {{ interface }} address
+{% endif %}
+set interfaces ethernet {{ interface }} address {{ address }}
+set interfaces ethernet {{ interface }} description {{ description }}
diff --git a/src/services/api/graphql/state.py b/src/services/api/graphql/state.py
new file mode 100644
index 000000000..63db9f4ef
--- /dev/null
+++ b/src/services/api/graphql/state.py
@@ -0,0 +1,4 @@
+
+def init():
+ global settings
+ settings = {}
diff --git a/src/services/vyos-configd b/src/services/vyos-configd
index 6f770b696..670b6e66a 100755
--- a/src/services/vyos-configd
+++ b/src/services/vyos-configd
@@ -133,8 +133,7 @@ def explicit_print(path, mode, msg):
logger.critical("error explicit_print")
def run_script(script, config, args) -> int:
- if args:
- script.argv = args
+ script.argv = args
config.set_level([])
try:
c = script.get_config(config)
@@ -208,7 +207,7 @@ def process_node_data(config, data) -> int:
return R_ERROR_DAEMON
script_name = None
- args = None
+ args = []
res = re.match(r'^(VYOS_TAGNODE_VALUE=[^/]+)?.*\/([^/]+).py(.*)', data)
if res.group(1):
@@ -221,7 +220,7 @@ def process_node_data(config, data) -> int:
return R_ERROR_DAEMON
if res.group(3):
args = res.group(3).split()
- args.insert(0, f'{script_name}.py')
+ args.insert(0, f'{script_name}.py')
if script_name not in include_set:
return R_PASS
diff --git a/src/services/vyos-http-api-server b/src/services/vyos-http-api-server
index cbf321dc8..cb4ce4072 100755
--- a/src/services/vyos-http-api-server
+++ b/src/services/vyos-http-api-server
@@ -36,10 +36,16 @@ from starlette.datastructures import FormData, MutableHeaders
from starlette.formparsers import FormParser, MultiPartParser
from multipart.multipart import parse_options_header
+from ariadne import make_executable_schema, load_schema_from_path, snake_case_fallback_resolvers
+from ariadne.asgi import GraphQL
+
import vyos.config
+import vyos.defaults
from vyos.configsession import ConfigSession, ConfigSessionError
+import api.graphql.state
+
DEFAULT_CONFIG_FILE = '/etc/vyos/http-api.conf'
CFG_GROUP = 'vyattacfg'
@@ -603,6 +609,25 @@ def show_op(data: ShowModel):
return success(res)
+###
+# GraphQL integration
+###
+
+api.graphql.state.init()
+
+from api.graphql.graphql.mutations import mutation
+from api.graphql.graphql.directives import DataDirective
+
+api_schema_dir = vyos.defaults.directories['api_schema']
+
+type_defs = load_schema_from_path(api_schema_dir)
+
+schema = make_executable_schema(type_defs, mutation, snake_case_fallback_resolvers, directives={"generate": DataDirective})
+
+app.add_route('/graphql', GraphQL(schema, debug=True))
+
+###
+
if __name__ == '__main__':
# systemd's user and group options don't work, do it by hand here,
# else no one else will be able to commit
@@ -626,6 +651,8 @@ if __name__ == '__main__':
app.state.vyos_debug = True if server_config['debug'] == 'true' else False
app.state.vyos_strict = True if server_config['strict'] == 'true' else False
+ api.graphql.state.settings['app'] = app
+
try:
uvicorn.run(app, host=server_config["listen_address"],
port=int(server_config["port"]),
diff --git a/src/systemd/isc-dhcp-server.service b/src/systemd/isc-dhcp-server.service
index 9aa70a7cc..a7d86e69c 100644
--- a/src/systemd/isc-dhcp-server.service
+++ b/src/systemd/isc-dhcp-server.service
@@ -14,10 +14,10 @@ Environment=PID_FILE=/run/dhcp-server/dhcpd.pid CONFIG_FILE=/run/dhcp-server/dhc
PIDFile=/run/dhcp-server/dhcpd.pid
ExecStartPre=/bin/sh -ec '\
touch ${LEASE_FILE}; \
-chown dhcpd:nogroup ${LEASE_FILE}* ; \
+chown dhcpd:vyattacfg ${LEASE_FILE}* ; \
chmod 664 ${LEASE_FILE}* ; \
-/usr/sbin/dhcpd -4 -t -T -q -user dhcpd -group nogroup -pf ${PID_FILE} -cf ${CONFIG_FILE} -lf ${LEASE_FILE} '
-ExecStart=/usr/sbin/dhcpd -4 -q -user dhcpd -group nogroup -pf ${PID_FILE} -cf ${CONFIG_FILE} -lf ${LEASE_FILE}
+/usr/sbin/dhcpd -4 -t -T -q -user dhcpd -group vyattacfg -pf ${PID_FILE} -cf ${CONFIG_FILE} -lf ${LEASE_FILE} '
+ExecStart=/usr/sbin/dhcpd -4 -q -user dhcpd -group vyattacfg -pf ${PID_FILE} -cf ${CONFIG_FILE} -lf ${LEASE_FILE}
Restart=always
[Install]
diff --git a/src/validators/vrf-name b/src/validators/vrf-name
index c78a80776..29167c635 100755
--- a/src/validators/vrf-name
+++ b/src/validators/vrf-name
@@ -33,8 +33,8 @@ if __name__ == '__main__':
if vrf == "lo":
exit(1)
- pattern = "^(?!(bond|br|dum|eth|lan|eno|ens|enp|enx|gnv|ipoe|l2tp|l2tpeth|" \
- "vtun|ppp|pppoe|peth|tun|vti|vxlan|wg|wlan|wwan)\d+(\.\d+(v.+)?)?$).*$"
+ pattern = r'^(?!(bond|br|dum|eth|lan|eno|ens|enp|enx|gnv|ipoe|l2tp|l2tpeth|\
+ vtun|ppp|pppoe|peth|tun|vti|vxlan|wg|wlan|wwan|\d)\d*(\.\d+)?(v.+)?).*$'
if not re.match(pattern, vrf):
exit(1)