diff --git a/data/templates/openvpn/server.conf.j2 b/data/templates/openvpn/server.conf.j2 index bc3a6127f..408103558 100644 --- a/data/templates/openvpn/server.conf.j2 +++ b/data/templates/openvpn/server.conf.j2 @@ -1,224 +1,224 @@ ### Autogenerated by interfaces_openvpn.py ### # # See https://community.openvpn.net/openvpn/wiki/Openvpn24ManPage # for individual keyword definition # # {{ description if description is vyos_defined }} # verb 3 dev-type {{ device_type }} dev {{ ifname }} persist-key {% if protocol is vyos_defined('tcp-active') %} proto tcp-client {% elif protocol is vyos_defined('tcp-passive') %} proto tcp-server {% else %} proto udp {% endif %} {% if local_host is vyos_defined %} local {{ local_host }} {% endif %} {% if mode is vyos_defined('server') and protocol is vyos_defined('udp') and local_host is not vyos_defined %} multihome {% endif %} {% if local_port is vyos_defined %} lport {{ local_port }} {% endif %} {% if remote_port is vyos_defined %} rport {{ remote_port }} {% endif %} {% if remote_host is vyos_defined %} {% for remote in remote_host %} remote {{ remote }} {% endfor %} {% endif %} {% if shared_secret_key is vyos_defined %} secret /run/openvpn/{{ ifname }}_shared.key {% endif %} {% if persistent_tunnel is vyos_defined %} persist-tun {% endif %} {% if replace_default_route.local is vyos_defined %} push "redirect-gateway local def1" {% elif replace_default_route is vyos_defined %} push "redirect-gateway def1" {% endif %} {% if use_lzo_compression is vyos_defined %} compress lzo {% endif %} {% if offload.dco is not vyos_defined %} disable-dco {% endif %} {% if mode is vyos_defined('client') %} # # OpenVPN Client mode # client nobind {% elif mode is vyos_defined('server') %} # # OpenVPN Server mode # mode server tls-server {% if server is vyos_defined %} {% if server.subnet is vyos_defined %} {% if server.topology is vyos_defined('point-to-point') %} topology p2p {% elif server.topology is vyos_defined %} topology {{ server.topology }} {% endif %} {% for subnet in server.subnet %} {% if subnet | is_ipv4 %} server {{ subnet | address_from_cidr }} {{ subnet | netmask_from_cidr }} {{ 'nopool' if server.client_ip_pool is vyos_defined and server.client_ip_pool.disable is not vyos_defined else '' }} {# First ip address is used as gateway. It's allows to use metrics #} {% if server.push_route is vyos_defined %} {% for route, route_config in server.push_route.items() %} {% if route | is_ipv4 %} push "route {{ route | address_from_cidr }} {{ route | netmask_from_cidr }} {{ 'vpn_gateway' ~ ' ' ~ route_config.metric if route_config.metric is vyos_defined }}" {% elif route | is_ipv6 %} push "route-ipv6 {{ route }}" {% endif %} {% endfor %} {% endif %} {% elif subnet | is_ipv6 %} server-ipv6 {{ subnet }} {% endif %} {% endfor %} {% endif %} {% if server.bridge is vyos_defined and server.bridge.disable is not vyos_defined %} server-bridge {{ server.bridge.gateway }} {{ server.bridge.subnet_mask }} {{ server.bridge.start }} {{ server.bridge.stop if server.bridge.stop is vyos_defined }} {% endif %} {% if server.client_ip_pool is vyos_defined and server.client_ip_pool.disable is not vyos_defined %} ifconfig-pool {{ server.client_ip_pool.start }} {{ server.client_ip_pool.stop }} {{ server.client_ip_pool.subnet_mask if server.client_ip_pool.subnet_mask is vyos_defined }} {% endif %} {% if server.max_connections is vyos_defined %} max-clients {{ server.max_connections }} {% endif %} {% if server.client is vyos_defined %} client-config-dir /run/openvpn/ccd/{{ ifname }} {% endif %} {% endif %} keepalive {{ keep_alive.interval }} {{ keep_alive.interval | int * keep_alive.failure_count | int }} management /run/openvpn/openvpn-mgmt-intf unix {% if server is vyos_defined %} {% if server.reject_unconfigured_clients is vyos_defined %} ccd-exclusive {% endif %} {% if server.name_server is vyos_defined %} {% for nameserver in server.name_server %} {% if nameserver | is_ipv4 %} push "dhcp-option DNS {{ nameserver }}" {% elif nameserver | is_ipv6 %} push "dhcp-option DNS6 {{ nameserver }}" {% endif %} {% endfor %} {% endif %} {% if server.domain_name is vyos_defined %} push "dhcp-option DOMAIN {{ server.domain_name }}" {% endif %} {% if server.mfa.totp is vyos_defined %} {% set totp_config = server.mfa.totp %} plugin "{{ plugin_dir }}/openvpn-otp.so" "otp_secrets=/config/auth/openvpn/{{ ifname }}-otp-secrets otp_slop={{ totp_config.slop }} totp_t0={{ totp_config.drift }} totp_step={{ totp_config.step }} totp_digits={{ totp_config.digits }} password_is_cr={{ '1' if totp_config.challenge == 'enable' else '0' }}" {% endif %} {% endif %} {% else %} # # OpenVPN site-2-site mode # ping {{ keep_alive.interval }} ping-restart {{ keep_alive.failure_count }} {% if device_type == 'tap' %} {% if local_address is vyos_defined %} {% for laddr, laddr_conf in local_address.items() if laddr | is_ipv4 %} {% if laddr_conf.subnet_mask is vyos_defined %} ifconfig {{ laddr }} {{ laddr_conf.subnet_mask }} {% endif %} {% endfor %} {% endif %} {% else %} {% for laddr in local_address if laddr | is_ipv4 %} {% for raddr in remote_address if raddr | is_ipv4 %} ifconfig {{ laddr }} {{ raddr }} {% endfor %} {% endfor %} {% for laddr in local_address if laddr | is_ipv6 %} {% for raddr in remote_address if raddr | is_ipv6 %} ifconfig-ipv6 {{ laddr }} {{ raddr }} {% endfor %} {% endfor %} {% endif %} {% endif %} {% if tls is vyos_defined %} # TLS options {% if tls.ca_certificate is vyos_defined %} ca /run/openvpn/{{ ifname }}_ca.pem {% endif %} {% if tls.certificate is vyos_defined %} cert /run/openvpn/{{ ifname }}_cert.pem {% endif %} {% if tls.private_key is vyos_defined %} key /run/openvpn/{{ ifname }}_cert.key {% endif %} {% if tls.crypt_key is vyos_defined %} tls-crypt /run/openvpn/{{ ifname }}_crypt.key {% endif %} {% if tls.crl is vyos_defined %} crl-verify /run/openvpn/{{ ifname }}_crl.pem {% endif %} {% if tls.tls_version_min is vyos_defined %} tls-version-min {{ tls.tls_version_min }} {% endif %} {% if tls.dh_params is vyos_defined %} dh /run/openvpn/{{ ifname }}_dh.pem {% else %} dh none {% endif %} {% if tls.auth_key is vyos_defined %} {% if mode == 'client' %} tls-auth /run/openvpn/{{ ifname }}_auth.key 1 {% elif mode == 'server' %} tls-auth /run/openvpn/{{ ifname }}_auth.key 0 {% endif %} {% endif %} {% if tls.role is vyos_defined('active') %} tls-client {% elif tls.role is vyos_defined('passive') %} tls-server {% endif %} {% if tls.peer_fingerprint is vyos_defined %} <peer-fingerprint> {% for fp in tls.peer_fingerprint %} {{ fp }} {% endfor %} </peer-fingerprint> {% endif %} {% endif %} # Encryption options {% if encryption is vyos_defined %} {% if encryption.cipher is vyos_defined %} cipher {{ encryption.cipher | openvpn_cipher }} {% endif %} -{% if encryption.ncp_ciphers is vyos_defined %} -data-ciphers {{ encryption.ncp_ciphers | openvpn_ncp_ciphers }} +{% if encryption.data_ciphers is vyos_defined %} +data-ciphers {{ encryption.data_ciphers | openvpn_data_ciphers }} {% endif %} {% endif %} providers default {% if hash is vyos_defined %} auth {{ hash }} {% endif %} {% if authentication is vyos_defined %} auth-user-pass {{ auth_user_pass_file }} auth-retry nointeract {% endif %} diff --git a/interface-definitions/include/version/openvpn-version.xml.i b/interface-definitions/include/version/openvpn-version.xml.i index e03ad55c0..67ef21983 100644 --- a/interface-definitions/include/version/openvpn-version.xml.i +++ b/interface-definitions/include/version/openvpn-version.xml.i @@ -1,3 +1,3 @@ <!-- include start from include/version/openvpn-version.xml.i --> -<syntaxVersion component='openvpn' version='3'></syntaxVersion> +<syntaxVersion component='openvpn' version='4'></syntaxVersion> <!-- include end --> diff --git a/interface-definitions/interfaces_openvpn.xml.in b/interface-definitions/interfaces_openvpn.xml.in index 2d880feef..3563caef2 100644 --- a/interface-definitions/interfaces_openvpn.xml.in +++ b/interface-definitions/interfaces_openvpn.xml.in @@ -1,832 +1,832 @@ <?xml version="1.0"?> <interfaceDefinition> <node name="interfaces"> <children> <tagNode name="openvpn" owner="${vyos_conf_scripts_dir}/interfaces_openvpn.py"> <properties> <help>OpenVPN Tunnel Interface</help> <priority>460</priority> <constraint> <regex>vtun[0-9]+</regex> </constraint> <constraintErrorMessage>OpenVPN tunnel interface must be named vtunN</constraintErrorMessage> <valueHelp> <format>vtunN</format> <description>OpenVPN interface name</description> </valueHelp> </properties> <children> #include <include/interface/authentication.xml.i> #include <include/generic-description.xml.i> <leafNode name="device-type"> <properties> <help>OpenVPN interface device-type</help> <completionHelp> <list>tun tap</list> </completionHelp> <valueHelp> <format>tun</format> <description>TUN device, required for OSI layer 3</description> </valueHelp> <valueHelp> <format>tap</format> <description>TAP device, required for OSI layer 2</description> </valueHelp> <constraint> <regex>(tun|tap)</regex> </constraint> </properties> <defaultValue>tun</defaultValue> </leafNode> #include <include/interface/disable.xml.i> <node name="encryption"> <properties> <help>Data Encryption settings</help> </properties> <children> <leafNode name="cipher"> <properties> <help>Standard Data Encryption Algorithm</help> <completionHelp> <list>none 3des aes128 aes128gcm aes192 aes192gcm aes256 aes256gcm</list> </completionHelp> <valueHelp> <format>none</format> <description>Disable encryption</description> </valueHelp> <valueHelp> <format>3des</format> <description>DES algorithm with triple encryption</description> </valueHelp> <valueHelp> <format>aes128</format> <description>AES algorithm with 128-bit key CBC</description> </valueHelp> <valueHelp> <format>aes128gcm</format> <description>AES algorithm with 128-bit key GCM</description> </valueHelp> <valueHelp> <format>aes192</format> <description>AES algorithm with 192-bit key CBC</description> </valueHelp> <valueHelp> <format>aes192gcm</format> <description>AES algorithm with 192-bit key GCM</description> </valueHelp> <valueHelp> <format>aes256</format> <description>AES algorithm with 256-bit key CBC</description> </valueHelp> <valueHelp> <format>aes256gcm</format> <description>AES algorithm with 256-bit key GCM</description> </valueHelp> <constraint> <regex>(none|3des|aes128|aes128gcm|aes192|aes192gcm|aes256|aes256gcm)</regex> </constraint> </properties> </leafNode> - <leafNode name="ncp-ciphers"> + <leafNode name="data-ciphers"> <properties> <help>Cipher negotiation list for use in server or client mode</help> <completionHelp> <list>none 3des aes128 aes128gcm aes192 aes192gcm aes256 aes256gcm</list> </completionHelp> <valueHelp> <format>none</format> <description>Disable encryption</description> </valueHelp> <valueHelp> <format>3des</format> <description>DES algorithm with triple encryption</description> </valueHelp> <valueHelp> <format>aes128</format> <description>AES algorithm with 128-bit key CBC</description> </valueHelp> <valueHelp> <format>aes128gcm</format> <description>AES algorithm with 128-bit key GCM</description> </valueHelp> <valueHelp> <format>aes192</format> <description>AES algorithm with 192-bit key CBC</description> </valueHelp> <valueHelp> <format>aes192gcm</format> <description>AES algorithm with 192-bit key GCM</description> </valueHelp> <valueHelp> <format>aes256</format> <description>AES algorithm with 256-bit key CBC</description> </valueHelp> <valueHelp> <format>aes256gcm</format> <description>AES algorithm with 256-bit key GCM</description> </valueHelp> <constraint> <regex>(none|3des|aes128|aes128gcm|aes192|aes192gcm|aes256|aes256gcm)</regex> </constraint> <multi/> </properties> </leafNode> </children> </node> #include <include/interface/ipv4-options.xml.i> #include <include/interface/ipv6-options.xml.i> #include <include/interface/mirror.xml.i> <leafNode name="hash"> <properties> <help>Hashing Algorithm</help> <completionHelp> <list>md5 sha1 sha256 sha384 sha512</list> </completionHelp> <valueHelp> <format>md5</format> <description>MD5 algorithm</description> </valueHelp> <valueHelp> <format>sha1</format> <description>SHA-1 algorithm</description> </valueHelp> <valueHelp> <format>sha256</format> <description>SHA-256 algorithm</description> </valueHelp> <valueHelp> <format>sha384</format> <description>SHA-384 algorithm</description> </valueHelp> <valueHelp> <format>sha512</format> <description>SHA-512 algorithm</description> </valueHelp> <constraint> <regex>(md5|sha1|sha256|sha384|sha512)</regex> </constraint> </properties> </leafNode> <node name="keep-alive"> <properties> <help>Keepalive helper options</help> </properties> <children> <leafNode name="failure-count"> <properties> <help>Maximum number of keepalive packet failures</help> <valueHelp> <format>u32:0-1000</format> <description>Maximum number of keepalive packet failures</description> </valueHelp> <constraint> <validator name="numeric" argument="--range 0-1000"/> </constraint> </properties> <defaultValue>60</defaultValue> </leafNode> <leafNode name="interval"> <properties> <help>Keepalive packet interval in seconds</help> <valueHelp> <format>u32:0-600</format> <description>Keepalive packet interval (seconds)</description> </valueHelp> <constraint> <validator name="numeric" argument="--range 0-600"/> </constraint> </properties> <defaultValue>10</defaultValue> </leafNode> </children> </node> <tagNode name="local-address"> <properties> <help>Local IP address of tunnel (IPv4 or IPv6)</help> <constraint> <validator name="ip-address"/> </constraint> </properties> <children> <leafNode name="subnet-mask"> <properties> <help>Subnet-mask for local IP address of tunnel (IPv4 only)</help> <constraint> <validator name="ipv4-address"/> </constraint> </properties> </leafNode> </children> </tagNode> <leafNode name="local-host"> <properties> <help>Local IP address to accept connections (all if not set)</help> <valueHelp> <format>ipv4</format> <description>Local IPv4 address</description> </valueHelp> <valueHelp> <format>ipv6</format> <description>Local IPv6 address</description> </valueHelp> <constraint> <validator name="ip-address"/> </constraint> </properties> </leafNode> <leafNode name="local-port"> <properties> <help>Local port number to accept connections</help> <valueHelp> <format>u32:1-65535</format> <description>Numeric IP port</description> </valueHelp> <constraint> <validator name="numeric" argument="--range 1-65535"/> </constraint> </properties> </leafNode> <leafNode name="mode"> <properties> <help>OpenVPN mode of operation</help> <completionHelp> <list>site-to-site client server</list> </completionHelp> <valueHelp> <format>site-to-site</format> <description>Site-to-site mode</description> </valueHelp> <valueHelp> <format>client</format> <description>Client in client-server mode</description> </valueHelp> <valueHelp> <format>server</format> <description>Server in client-server mode</description> </valueHelp> <constraint> <regex>(site-to-site|client|server)</regex> </constraint> </properties> </leafNode> <node name="offload"> <properties> <help>Configurable offload options</help> </properties> <children> <leafNode name="dco"> <properties> <help>Enable data channel offload on this interface</help> <valueless/> </properties> </leafNode> </children> </node> <leafNode name="openvpn-option"> <properties> <help>Additional OpenVPN options. You must use the syntax of openvpn.conf in this text-field. Using this without proper knowledge may result in a crashed OpenVPN server. Check system log to look for errors.</help> <multi/> </properties> </leafNode> <leafNode name="persistent-tunnel"> <properties> <help>Do not close and reopen interface (TUN/TAP device) on client restarts</help> <valueless/> </properties> </leafNode> <leafNode name="protocol"> <properties> <help>OpenVPN communication protocol</help> <completionHelp> <list>udp tcp-passive tcp-active</list> </completionHelp> <valueHelp> <format>udp</format> <description>UDP</description> </valueHelp> <valueHelp> <format>tcp-passive</format> <description>TCP and accepts connections passively</description> </valueHelp> <valueHelp> <format>tcp-active</format> <description>TCP and initiates connections actively</description> </valueHelp> <constraint> <regex>(udp|tcp-passive|tcp-active)</regex> </constraint> </properties> <defaultValue>udp</defaultValue> </leafNode> <leafNode name="remote-address"> <properties> <help>IP address of remote end of tunnel</help> <valueHelp> <format>ipv4</format> <description>Remote end IPv4 address</description> </valueHelp> <valueHelp> <format>ipv6</format> <description>Remote end IPv6 address</description> </valueHelp> <constraint> <validator name="ip-address"/> </constraint> <multi/> </properties> </leafNode> <leafNode name="remote-host"> <properties> <help>Remote host to connect to (dynamic if not set)</help> <valueHelp> <format>ipv4</format> <description>IPv4 address of remote host</description> </valueHelp> <valueHelp> <format>ipv6</format> <description>IPv6 address of remote host</description> </valueHelp> <valueHelp> <format>txt</format> <description>Hostname of remote host</description> </valueHelp> <multi/> </properties> </leafNode> <leafNode name="remote-port"> <properties> <help>Remote port number to connect to</help> <valueHelp> <format>u32:1-65535</format> <description>Numeric IP port</description> </valueHelp> <constraint> <validator name="numeric" argument="--range 1-65535"/> </constraint> </properties> </leafNode> <node name="replace-default-route"> <properties> <help>OpenVPN tunnel to be used as the default route</help> </properties> <children> <leafNode name="local"> <properties> <help>Tunnel endpoints are on the same subnet</help> </properties> </leafNode> </children> </node> <node name="server"> <properties> <help>Server-mode options</help> </properties> <children> <tagNode name="client"> <properties> <help>Client-specific settings</help> <valueHelp> <format>name</format> <description>Client common-name in the certificate</description> </valueHelp> </properties> <children> #include <include/generic-disable-node.xml.i> <leafNode name="ip"> <properties> <help>IP address of the client</help> <valueHelp> <format>ipv4</format> <description>Client IPv4 address</description> </valueHelp> <valueHelp> <format>ipv6</format> <description>Client IPv6 address</description> </valueHelp> <constraint> <validator name="ip-address"/> </constraint> <multi/> </properties> </leafNode> <leafNode name="push-route"> <properties> <help>Route to be pushed to the client</help> <valueHelp> <format>ipv4net</format> <description>IPv4 network and prefix length</description> </valueHelp> <valueHelp> <format>ipv6net</format> <description>IPv6 network and prefix length</description> </valueHelp> <constraint> <validator name="ip-prefix"/> </constraint> <multi/> </properties> </leafNode> <leafNode name="subnet"> <properties> <help>Subnet belonging to the client (iroute)</help> <valueHelp> <format>ipv4net</format> <description>IPv4 network and prefix length belonging to the client</description> </valueHelp> <valueHelp> <format>ipv6net</format> <description>IPv6 network and prefix length belonging to the client</description> </valueHelp> <constraint> <validator name="ip-prefix"/> </constraint> <multi/> </properties> </leafNode> </children> </tagNode> <node name="bridge"> <properties> <help>Used with TAP device (layer 2)</help> </properties> <children> #include <include/generic-disable-node.xml.i> <leafNode name="start"> <properties> <help>First IP address in the pool</help> <constraint> <validator name="ipv4-address"/> </constraint> <valueHelp> <format>ipv4</format> <description>IPv4 address</description> </valueHelp> </properties> </leafNode> <leafNode name="stop"> <properties> <help>Last IP address in the pool</help> <constraint> <validator name="ipv4-address"/> </constraint> <valueHelp> <format>ipv4</format> <description>IPv4 address</description> </valueHelp> </properties> </leafNode> <leafNode name="subnet-mask"> <properties> <help>Subnet mask pushed to dynamic clients.</help> <constraint> <validator name="ipv4-address"/> </constraint> <valueHelp> <format>ipv4</format> <description>IPv4 subnet mask</description> </valueHelp> </properties> </leafNode> <leafNode name="gateway"> <properties> <help>Gateway IP address</help> <constraint> <validator name="ipv4-address"/> </constraint> <valueHelp> <format>ipv4</format> <description>IPv4 address</description> </valueHelp> </properties> </leafNode> </children> </node> <node name="client-ip-pool"> <properties> <help>Pool of client IPv4 addresses</help> </properties> <children> #include <include/generic-disable-node.xml.i> <leafNode name="start"> <properties> <help>First IP address in the pool</help> <constraint> <validator name="ipv4-address"/> </constraint> <valueHelp> <format>ipv4</format> <description>IPv4 address</description> </valueHelp> </properties> </leafNode> <leafNode name="stop"> <properties> <help>Last IP address in the pool</help> <constraint> <validator name="ipv4-address"/> </constraint> <valueHelp> <format>ipv4</format> <description>IPv4 address</description> </valueHelp> </properties> </leafNode> <leafNode name="subnet-mask"> <properties> <help>Subnet mask pushed to dynamic clients. If not set the server subnet mask will be used. Only used with topology subnet or device type tap. Not used with bridged interfaces.</help> <constraint> <validator name="ipv4-address"/> </constraint> <valueHelp> <format>ipv4</format> <description>IPv4 subnet mask</description> </valueHelp> </properties> </leafNode> </children> </node> <node name="client-ipv6-pool"> <properties> <help>Pool of client IPv6 addresses</help> </properties> <children> <leafNode name="base"> <properties> <help>Client IPv6 pool base address with optional prefix length</help> <valueHelp> <format>ipv6net</format> <description>Client IPv6 pool base address with optional prefix length (defaults: base = server subnet + 0x1000, prefix length = server prefix length)</description> </valueHelp> <constraint> <validator name="ipv6"/> </constraint> </properties> </leafNode> #include <include/generic-disable-node.xml.i> </children> </node> <leafNode name="domain-name"> <properties> <help>DNS suffix to be pushed to all clients</help> <valueHelp> <format>txt</format> <description>Domain Name Server suffix</description> </valueHelp> </properties> </leafNode> <leafNode name="max-connections"> <properties> <help>Number of maximum client connections</help> <valueHelp> <format>u32:1-4096</format> <description>Number of concurrent clients</description> </valueHelp> <constraint> <validator name="numeric" argument="--range 1-4096"/> </constraint> </properties> </leafNode> #include <include/name-server-ipv4-ipv6.xml.i> <tagNode name="push-route"> <properties> <help>Route to be pushed to all clients</help> <valueHelp> <format>ipv4net</format> <description>IPv4 network and prefix length</description> </valueHelp> <valueHelp> <format>ipv6net</format> <description>IPv6 network and prefix length</description> </valueHelp> <constraint> <validator name="ip-prefix"/> </constraint> </properties> <children> <leafNode name="metric"> <properties> <help>Set metric for this route</help> <valueHelp> <format>u32:0-4294967295</format> <description>Metric for this route</description> </valueHelp> <constraint> <validator name="numeric" argument="--range 0-4294967295"/> </constraint> </properties> <defaultValue>0</defaultValue> </leafNode> </children> </tagNode> <leafNode name="reject-unconfigured-clients"> <properties> <help>Reject connections from clients that are not explicitly configured</help> <valueless/> </properties> </leafNode> <leafNode name="subnet"> <properties> <help>Server-mode subnet (from which client IPs are allocated)</help> <valueHelp> <format>ipv4net</format> <description>IPv4 network and prefix length</description> </valueHelp> <valueHelp> <format>ipv6net</format> <description>IPv6 network and prefix length</description> </valueHelp> <constraint> <validator name="ip-prefix"/> </constraint> <multi/> </properties> </leafNode> <leafNode name="topology"> <properties> <help>Topology for clients</help> <completionHelp> <list>subnet point-to-point net30</list> </completionHelp> <valueHelp> <format>subnet</format> <description>Subnet topology (recommended)</description> </valueHelp> <valueHelp> <format>point-to-point</format> <description>Point-to-point topology</description> </valueHelp> <valueHelp> <format>net30</format> <description>net30 topology (deprecated)</description> </valueHelp> <constraint> <regex>(subnet|point-to-point|net30)</regex> </constraint> </properties> <defaultValue>subnet</defaultValue> </leafNode> <node name="mfa"> <properties> <help>multi-factor authentication</help> </properties> <children> <node name="totp"> <properties> <help>Time-based one-time passwords</help> </properties> <children> <leafNode name="slop"> <properties> <help>Maximum allowed clock slop in seconds</help> <valueHelp> <format>1-65535</format> <description>Seconds</description> </valueHelp> <constraint> <validator name="numeric" argument="--range 1-65535"/> </constraint> </properties> <defaultValue>180</defaultValue> </leafNode> <leafNode name="drift"> <properties> <help>Time drift in seconds</help> <valueHelp> <format>1-65535</format> <description>Seconds</description> </valueHelp> <constraint> <validator name="numeric" argument="--range 1-65535"/> </constraint> </properties> <defaultValue>0</defaultValue> </leafNode> <leafNode name="step"> <properties> <help>Step value for totp in seconds</help> <valueHelp> <format>1-65535</format> <description>Seconds</description> </valueHelp> <constraint> <validator name="numeric" argument="--range 1-65535"/> </constraint> </properties> <defaultValue>30</defaultValue> </leafNode> <leafNode name="digits"> <properties> <help>Number of digits to use for totp hash</help> <valueHelp> <format>1-65535</format> <description>Digits</description> </valueHelp> <constraint> <validator name="numeric" argument="--range 1-65535"/> </constraint> </properties> <defaultValue>6</defaultValue> </leafNode> <leafNode name="challenge"> <properties> <help>Expect password as result of a challenge response protocol</help> <completionHelp> <list>disable enable</list> </completionHelp> <valueHelp> <format>disable</format> <description>Disable challenge-response</description> </valueHelp> <valueHelp> <format>enable</format> <description>Enable chalenge-response</description> </valueHelp> <constraint> <regex>(disable|enable)</regex> </constraint> </properties> <defaultValue>enable</defaultValue> </leafNode> </children> </node> </children> </node> </children> </node> <leafNode name="shared-secret-key"> <properties> <help>Secret key shared with remote end of tunnel</help> <completionHelp> <path>pki openvpn shared-secret</path> </completionHelp> </properties> </leafNode> <node name="tls"> <properties> <help>Transport Layer Security (TLS) options</help> </properties> <children> <leafNode name="auth-key"> <properties> <help>TLS shared secret key for tls-auth</help> <completionHelp> <path>pki openvpn shared-secret</path> </completionHelp> </properties> </leafNode> #include <include/pki/certificate.xml.i> #include <include/pki/ca-certificate-multi.xml.i> #include <include/pki/dh-params.xml.i> <leafNode name="crypt-key"> <properties> <help>Static key to use to authenticate control channel</help> <completionHelp> <path>pki openvpn shared-secret</path> </completionHelp> </properties> </leafNode> <leafNode name="peer-fingerprint"> <properties> <multi/> <help>Peer certificate SHA256 fingerprint</help> <constraint> <regex>[0-9a-fA-F]{2}:([0-9a-fA-F]{2}:){30}[0-9a-fA-F]{2}</regex> </constraint> <constraintErrorMessage>Peer certificate fingerprint must be a colon-separated SHA256 hex digest</constraintErrorMessage> </properties> </leafNode> #include <include/tls-version-min.xml.i> <leafNode name="role"> <properties> <help>TLS negotiation role</help> <completionHelp> <list>active passive</list> </completionHelp> <valueHelp> <format>active</format> <description>Initiate TLS negotiation actively</description> </valueHelp> <valueHelp> <format>passive</format> <description>Wait for incoming TLS connection</description> </valueHelp> <constraint> <regex>(active|passive)</regex> </constraint> </properties> </leafNode> </children> </node> <leafNode name="use-lzo-compression"> <properties> <help>Use fast LZO compression on this TUN/TAP interface</help> <valueless/> </properties> </leafNode> #include <include/interface/redirect.xml.i> #include <include/interface/vrf.xml.i> </children> </tagNode> </children> </node> </interfaceDefinition> diff --git a/python/vyos/template.py b/python/vyos/template.py index 768c43387..3ba5c9012 100755 --- a/python/vyos/template.py +++ b/python/vyos/template.py @@ -1,990 +1,990 @@ # Copyright 2019-2024 VyOS maintainers and contributors <maintainers@vyos.io> # # This library is free software; you can redistribute it and/or # modify it under the terms of the GNU Lesser General Public # License as published by the Free Software Foundation; either # version 2.1 of the License, or (at your option) any later version. # # This library is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public # License along with this library. If not, see <http://www.gnu.org/licenses/>. import functools import os from jinja2 import Environment from jinja2 import FileSystemLoader from jinja2 import ChainableUndefined from vyos.defaults import directories from vyos.utils.dict import dict_search_args from vyos.utils.file import makedir from vyos.utils.permission import chmod from vyos.utils.permission import chown # We use a mutable global variable for the default template directory # to make it possible to call scripts from this repository # outside of live VyOS systems. # If something (like the image build scripts) # want to call a script, they can modify the default location # to the repository path. DEFAULT_TEMPLATE_DIR = directories["templates"] # Holds template filters registered via register_filter() _FILTERS = {} _TESTS = {} # reuse Environments with identical settings to improve performance @functools.lru_cache(maxsize=2) def _get_environment(location=None): from os import getenv if location is None: loc_loader=FileSystemLoader(DEFAULT_TEMPLATE_DIR) 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=loc_loader, trim_blocks=True, undefined=ChainableUndefined, extensions=['jinja2.ext.loopcontrols'] ) env.filters.update(_FILTERS) env.tests.update(_TESTS) return env def register_filter(name, func=None): """Register a function to be available as filter in templates under given name. It can also be used as a decorator, see below in this module for examples. :raise RuntimeError: when trying to register a filter after a template has been rendered already :raise ValueError: when trying to register a name which was taken already """ if func is None: return functools.partial(register_filter, name) if _get_environment.cache_info().currsize: raise RuntimeError( "Filters can only be registered before rendering the first template" ) if name in _FILTERS: raise ValueError(f"A filter with name {name!r} was registered already") _FILTERS[name] = func return func def register_test(name, func=None): """Register a function to be available as test in templates under given name. It can also be used as a decorator, see below in this module for examples. :raise RuntimeError: when trying to register a test after a template has been rendered already :raise ValueError: when trying to register a name which was taken already """ if func is None: return functools.partial(register_test, name) if _get_environment.cache_info().currsize: raise RuntimeError( "Tests can only be registered before rendering the first template" ) if name in _TESTS: raise ValueError(f"A test with name {name!r} was registered already") _TESTS[name] = func return func 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 :param content: the dictionary of variables to put into rendering context :param formater: if given, it has to be a callable the rendered string is passed through The parsed template files are cached, so rendering the same file multiple times does not cause as too much overhead. If used everywhere, it could be changed to load the template from Python environment variables from an importable Python module generated when the Debian package is build (recovering the load time and overhead caused by having the file out of the code). """ template = _get_environment(location).get_template(template) rendered = template.render(content) if formater is not None: rendered = formater(rendered) return rendered def render( destination, template, content, formater=None, permission=None, user=None, group=None, location=None, ): """Render a template from the template directory to a file, raise on any errors. :param destination: path to the file to save the rendered template in :param permission: permission bitmask to set for the output file :param user: user to own the output file :param group: group to own the output file All other parameters are as for :func:`render_to_string`. """ # Create the directory if it does not exist folder = os.path.dirname(destination) makedir(folder, user, group) # 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, location) # Write to file with open(destination, "w") as file: chmod(file.fileno(), permission) chown(file.fileno(), user, group) file.write(rendered) ################################## # Custom template filters follow # ################################## @register_filter('force_to_list') def force_to_list(value): """ Convert scalars to single-item lists and leave lists untouched """ if isinstance(value, list): return value else: return [value] @register_filter('seconds_to_human') def seconds_to_human(seconds, separator=""): """ Convert seconds to human-readable values like 1d6h15m23s """ from vyos.utils.convert import seconds_to_human return seconds_to_human(seconds, separator=separator) @register_filter('bytes_to_human') def bytes_to_human(bytes, initial_exponent=0, precision=2): """ Convert bytes to human-readable values like 1.44M """ from vyos.utils.convert import bytes_to_human return bytes_to_human(bytes, initial_exponent=initial_exponent, precision=precision) @register_filter('human_to_bytes') def human_to_bytes(value): """ Convert a data amount with a unit suffix to bytes, like 2K to 2048 """ from vyos.utils.convert import human_to_bytes return human_to_bytes(value) @register_filter('ip_from_cidr') def ip_from_cidr(prefix): """ Take an IPv4/IPv6 CIDR host and strip cidr mask. Example: 192.0.2.1/24 -> 192.0.2.1, 2001:db8::1/64 -> 2001:db8::1 """ from ipaddress import ip_interface return str(ip_interface(prefix).ip) @register_filter('address_from_cidr') def address_from_cidr(prefix): """ Take an IPv4/IPv6 CIDR prefix and convert the network to an "address". Example: 192.0.2.0/24 -> 192.0.2.0, 2001:db8::/48 -> 2001:db8:: """ from ipaddress import ip_network return str(ip_network(prefix).network_address) @register_filter('bracketize_ipv6') def bracketize_ipv6(address): """ Place a passed IPv6 address into [] brackets, do nothing for IPv4 """ if is_ipv6(address): return f'[{address}]' return address @register_filter('dot_colon_to_dash') def dot_colon_to_dash(text): """ Replace dot and colon to dash for string Example: 192.0.2.1 => 192-0-2-1, 2001:db8::1 => 2001-db8--1 """ text = text.replace(":", "-") text = text.replace(".", "-") return text @register_filter('generate_uuid4') def generate_uuid4(text): """ Generate random unique ID Example: % uuid4() UUID('958ddf6a-ef14-4e81-8cfb-afb12456d1c5') """ from uuid import uuid4 return uuid4() @register_filter('netmask_from_cidr') def netmask_from_cidr(prefix): """ Take CIDR prefix and convert the prefix length to a "subnet mask". Example: - 192.0.2.0/24 -> 255.255.255.0 - 2001:db8::/48 -> ffff:ffff:ffff:: """ from ipaddress import ip_network return str(ip_network(prefix).netmask) @register_filter('netmask_from_ipv4') def netmask_from_ipv4(address): """ Take IP address and search all attached interface IP addresses for the given one. After address has been found, return the associated netmask. Example: - 172.18.201.10 -> 255.255.255.128 """ from netifaces import interfaces from netifaces import ifaddresses from netifaces import AF_INET for interface in interfaces(): tmp = ifaddresses(interface) if AF_INET in tmp: for af_addr in tmp[AF_INET]: if 'addr' in af_addr: if af_addr['addr'] == address: return af_addr['netmask'] raise ValueError @register_filter('is_ip_network') def is_ip_network(addr): """ Take IP(v4/v6) address and validate if the passed argument is a network or a host address. Example: - 192.0.2.0 -> False - 192.0.2.10/24 -> False - 192.0.2.0/24 -> True - 2001:db8:: -> False - 2001:db8::100 -> False - 2001:db8::/48 -> True - 2001:db8:1000::/64 -> True """ try: from ipaddress import ip_network # input variables must contain a / to indicate its CIDR notation if len(addr.split('/')) != 2: raise ValueError() ip_network(addr) return True except: return False @register_filter('network_from_ipv4') def network_from_ipv4(address): """ Take IP address and search all attached interface IP addresses for the given one. After address has been found, return the associated network address. Example: - 172.18.201.10 has mask 255.255.255.128 -> network is 172.18.201.0 """ netmask = netmask_from_ipv4(address) from ipaddress import ip_interface cidr_prefix = ip_interface(f'{address}/{netmask}').network return address_from_cidr(cidr_prefix) @register_filter('is_interface') def is_interface(interface): """ Check if parameter is a valid local interface name """ from vyos.utils.network import interface_exists return interface_exists(interface) @register_filter('is_ip') def is_ip(addr): """ Check addr if it is an IPv4 or IPv6 address """ return is_ipv4(addr) or is_ipv6(addr) @register_filter('is_ipv4') def is_ipv4(text): """ Filter IP address, return True on IPv4 address, False otherwise """ from ipaddress import ip_interface try: return ip_interface(text).version == 4 except: return False @register_filter('is_ipv6') def is_ipv6(text): """ Filter IP address, return True on IPv6 address, False otherwise """ from ipaddress import ip_interface try: return ip_interface(text).version == 6 except: return False @register_filter('first_host_address') def first_host_address(prefix): """ Return first usable (host) IP address from given prefix. Example: - 10.0.0.0/24 -> 10.0.0.1 - 2001:db8::/64 -> 2001:db8:: """ from ipaddress import ip_interface tmp = ip_interface(prefix).network return str(tmp.network_address +1) @register_filter('last_host_address') def last_host_address(text): """ Return first usable IP address from given prefix. Example: - 10.0.0.0/24 -> 10.0.0.254 - 2001:db8::/64 -> 2001:db8::ffff:ffff:ffff:ffff """ from ipaddress import ip_interface from ipaddress import IPv4Network from ipaddress import IPv6Network addr = ip_interface(text) if addr.version == 4: return str(IPv4Network(addr).broadcast_address - 1) return str(IPv6Network(addr).broadcast_address) @register_filter('inc_ip') def inc_ip(address, increment): """ Increment given IP address by 'increment' Example (inc by 2): - 10.0.0.0/24 -> 10.0.0.2 - 2001:db8::/64 -> 2001:db8::2 """ from ipaddress import ip_interface return str(ip_interface(address).ip + int(increment)) @register_filter('dec_ip') def dec_ip(address, decrement): """ Decrement given IP address by 'decrement' Example (inc by 2): - 10.0.0.0/24 -> 10.0.0.2 - 2001:db8::/64 -> 2001:db8::2 """ from ipaddress import ip_interface return str(ip_interface(address).ip - int(decrement)) @register_filter('compare_netmask') def compare_netmask(netmask1, netmask2): """ Compare two IP netmask if they have the exact same size. compare_netmask('10.0.0.0/8', '20.0.0.0/8') -> True compare_netmask('10.0.0.0/8', '20.0.0.0/16') -> False """ from ipaddress import ip_network try: return ip_network(netmask1).netmask == ip_network(netmask2).netmask except: return False @register_filter('isc_static_route') def isc_static_route(subnet, router): # https://ercpe.de/blog/pushing-static-routes-with-isc-dhcp-server # Option format is: # <netmask>, <network-byte1>, <network-byte2>, <network-byte3>, <router-byte1>, <router-byte2>, <router-byte3> # where bytes with the value 0 are omitted. from ipaddress import ip_network net = ip_network(subnet) # add netmask string = str(net.prefixlen) + ',' # add network bytes if net.prefixlen: width = net.prefixlen // 8 if net.prefixlen % 8: width += 1 string += ','.join(map(str,tuple(net.network_address.packed)[:width])) + ',' # add router bytes string += ','.join(router.split('.')) return string @register_filter('is_file') def is_file(filename): if os.path.exists(filename): return os.path.isfile(filename) return False @register_filter('get_dhcp_router') def get_dhcp_router(interface): """ Static routes can point to a router received by a DHCP reply. This helper is used to get the current default router from the DHCP reply. Returns False of no router is found, returns the IP address as string if a router is found. """ lease_file = directories['isc_dhclient_dir'] + f'/dhclient_{interface}.leases' if not os.path.exists(lease_file): return None from vyos.utils.file import read_file for line in read_file(lease_file).splitlines(): if 'option routers' in line: (_, _, address) = line.split() return address.rstrip(';') @register_filter('natural_sort') def natural_sort(iterable): import re from jinja2.runtime import Undefined if isinstance(iterable, Undefined) or iterable is None: return list() def convert(text): return int(text) if text.isdigit() else text.lower() def alphanum_key(key): return [convert(c) for c in re.split('([0-9]+)', str(key))] return sorted(iterable, key=alphanum_key) @register_filter('get_ipv4') def get_ipv4(interface): """ Get interface IPv4 addresses""" from vyos.ifconfig import Interface return Interface(interface).get_addr_v4() @register_filter('get_ipv6') def get_ipv6(interface): """ Get interface IPv6 addresses""" from vyos.ifconfig import Interface return Interface(interface).get_addr_v6() @register_filter('get_ip') def get_ip(interface): """ Get interface IP addresses""" from vyos.ifconfig import Interface return Interface(interface).get_addr() def get_first_ike_dh_group(ike_group): if ike_group and 'proposal' in ike_group: for priority, proposal in ike_group['proposal'].items(): if 'dh_group' in proposal: return 'dh-group' + proposal['dh_group'] return 'dh-group2' # Fallback on dh-group2 @register_filter('get_esp_ike_cipher') def get_esp_ike_cipher(group_config, ike_group=None): pfs_lut = { 'dh-group1' : 'modp768', 'dh-group2' : 'modp1024', 'dh-group5' : 'modp1536', 'dh-group14' : 'modp2048', 'dh-group15' : 'modp3072', 'dh-group16' : 'modp4096', 'dh-group17' : 'modp6144', 'dh-group18' : 'modp8192', 'dh-group19' : 'ecp256', 'dh-group20' : 'ecp384', 'dh-group21' : 'ecp521', 'dh-group22' : 'modp1024s160', 'dh-group23' : 'modp2048s224', 'dh-group24' : 'modp2048s256', 'dh-group25' : 'ecp192', 'dh-group26' : 'ecp224', 'dh-group27' : 'ecp224bp', 'dh-group28' : 'ecp256bp', 'dh-group29' : 'ecp384bp', 'dh-group30' : 'ecp512bp', 'dh-group31' : 'curve25519', 'dh-group32' : 'curve448' } ciphers = [] if 'proposal' in group_config: for priority, proposal in group_config['proposal'].items(): # both encryption and hash need to be specified for a proposal if not {'encryption', 'hash'} <= set(proposal): continue tmp = '{encryption}-{hash}'.format(**proposal) if 'prf' in proposal: tmp += '-' + proposal['prf'] if 'dh_group' in proposal: tmp += '-' + pfs_lut[ 'dh-group' + proposal['dh_group'] ] elif 'pfs' in group_config and group_config['pfs'] != 'disable': group = group_config['pfs'] if group_config['pfs'] == 'enable': group = get_first_ike_dh_group(ike_group) tmp += '-' + pfs_lut[group] ciphers.append(tmp) return ciphers @register_filter('get_uuid') def get_uuid(seed): """ Get interface IP addresses""" if seed: from hashlib import md5 from uuid import UUID tmp = md5() tmp.update(seed.encode('utf-8')) return str(UUID(tmp.hexdigest())) else: 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): +@register_filter('openvpn_data_ciphers') +def get_openvpn_data_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() @register_filter('snmp_auth_oid') def snmp_auth_oid(type): if type not in ['md5', 'sha', 'aes', 'des', 'none']: raise ValueError() OIDs = { 'md5' : '.1.3.6.1.6.3.10.1.1.2', 'sha' : '.1.3.6.1.6.3.10.1.1.3', 'aes' : '.1.3.6.1.6.3.10.1.2.4', 'des' : '.1.3.6.1.6.3.10.1.2.2', 'none': '.1.3.6.1.6.3.10.1.2.1' } return OIDs[type] @register_filter('nft_action') def nft_action(vyos_action): if vyos_action == 'accept': return 'return' return vyos_action @register_filter('nft_rule') def nft_rule(rule_conf, fw_hook, fw_name, rule_id, ip_name='ip'): from vyos.firewall import parse_rule return parse_rule(rule_conf, fw_hook, fw_name, rule_id, ip_name) @register_filter('nft_default_rule') def nft_default_rule(fw_conf, fw_name, family): output = ['counter'] default_action = fw_conf['default_action'] #family = 'ipv6' if ipv6 else 'ipv4' if 'default_log' in fw_conf: action_suffix = default_action[:1].upper() output.append(f'log prefix "[{family}-{fw_name[:19]}-default-{action_suffix}]"') #output.append(nft_action(default_action)) output.append(f'{default_action}') if 'default_jump_target' in fw_conf: target = fw_conf['default_jump_target'] def_suffix = '6' if family == 'ipv6' else '' output.append(f'NAME{def_suffix}_{target}') output.append(f'comment "{fw_name} default-action {default_action}"') return " ".join(output) @register_filter('nft_state_policy') def nft_state_policy(conf, state): out = [f'ct state {state}'] if 'log' in conf: log_state = state[:3].upper() log_action = (conf['action'] if 'action' in conf else 'accept')[:1].upper() out.append(f'log prefix "[STATE-POLICY-{log_state}-{log_action}]"') if 'log_level' in conf: log_level = conf['log_level'] out.append(f'level {log_level}') out.append('counter') if 'action' in conf: out.append(conf['action']) return " ".join(out) @register_filter('nft_intra_zone_action') def nft_intra_zone_action(zone_conf, ipv6=False): if 'intra_zone_filtering' in zone_conf: intra_zone = zone_conf['intra_zone_filtering'] fw_name = 'ipv6_name' if ipv6 else 'name' name_prefix = 'NAME6_' if ipv6 else 'NAME_' if 'action' in intra_zone: if intra_zone['action'] == 'accept': return 'return' return intra_zone['action'] elif dict_search_args(intra_zone, 'firewall', fw_name): name = dict_search_args(intra_zone, 'firewall', fw_name) return f'jump {name_prefix}{name}' return 'return' @register_filter('nft_nested_group') def nft_nested_group(out_list, includes, groups, key): if not vyos_defined(out_list): out_list = [] def add_includes(name): if key in groups[name]: for item in groups[name][key]: if item in out_list: continue out_list.append(item) if 'include' in groups[name]: for name_inc in groups[name]['include']: add_includes(name_inc) for name in includes: add_includes(name) return out_list @register_filter('nat_rule') def nat_rule(rule_conf, rule_id, nat_type, ipv6=False): from vyos.nat import parse_nat_rule return parse_nat_rule(rule_conf, rule_id, nat_type, ipv6) @register_filter('nat_static_rule') def nat_static_rule(rule_conf, rule_id, nat_type): from vyos.nat import parse_nat_static_rule return parse_nat_static_rule(rule_conf, rule_id, nat_type) @register_filter('conntrack_rule') def conntrack_rule(rule_conf, rule_id, action, ipv6=False): ip_prefix = 'ip6' if ipv6 else 'ip' def_suffix = '6' if ipv6 else '' output = [] if 'inbound_interface' in rule_conf: ifname = rule_conf['inbound_interface'] if ifname != 'any': output.append(f'iifname {ifname}') if 'protocol' in rule_conf: if action != 'timeout': proto = rule_conf['protocol'] else: for protocol, protocol_config in rule_conf['protocol'].items(): proto = protocol if proto != 'all': output.append(f'meta l4proto {proto}') tcp_flags = dict_search_args(rule_conf, 'tcp', 'flags') if tcp_flags and action != 'timeout': from vyos.firewall import parse_tcp_flags output.append(parse_tcp_flags(tcp_flags)) for side in ['source', 'destination']: if side in rule_conf: side_conf = rule_conf[side] prefix = side[0] if 'address' in side_conf: address = side_conf['address'] operator = '' if address[0] == '!': operator = '!=' address = address[1:] output.append(f'{ip_prefix} {prefix}addr {operator} {address}') if 'port' in side_conf: port = side_conf['port'] operator = '' if port[0] == '!': operator = '!=' port = port[1:] output.append(f'th {prefix}port {operator} {port}') if 'group' in side_conf: group = side_conf['group'] if 'address_group' in group: group_name = group['address_group'] operator = '' if group_name[0] == '!': operator = '!=' group_name = group_name[1:] output.append(f'{ip_prefix} {prefix}addr {operator} @A{def_suffix}_{group_name}') # Generate firewall group domain-group elif 'domain_group' in group: group_name = group['domain_group'] operator = '' if group_name[0] == '!': operator = '!=' group_name = group_name[1:] output.append(f'{ip_prefix} {prefix}addr {operator} @D_{group_name}') elif 'network_group' in group: group_name = group['network_group'] operator = '' if group_name[0] == '!': operator = '!=' group_name = group_name[1:] output.append(f'{ip_prefix} {prefix}addr {operator} @N{def_suffix}_{group_name}') if 'port_group' in group: group_name = group['port_group'] if proto == 'tcp_udp': proto = 'th' operator = '' if group_name[0] == '!': operator = '!=' group_name = group_name[1:] output.append(f'{proto} {prefix}port {operator} @P_{group_name}') if action == 'ignore': output.append('counter notrack') output.append(f'comment "ignore-{rule_id}"') else: output.append(f'counter ct timeout set ct-timeout-{rule_id}') output.append(f'comment "timeout-{rule_id}"') return " ".join(output) @register_filter('conntrack_ct_policy') def conntrack_ct_policy(protocol_conf): output = [] for item in protocol_conf: item_value = protocol_conf[item] output.append(f'{item}: {item_value}') return ", ".join(output) @register_filter('range_to_regex') def range_to_regex(num_range): """Convert range of numbers or list of ranges to regex % range_to_regex('11-12') '(1[1-2])' % range_to_regex(['11-12', '14-15']) '(1[1-2]|1[4-5])' """ from vyos.range_regex import range_to_regex if isinstance(num_range, list): data = [] for entry in num_range: if '-' not in entry: data.append(entry) else: data.append(range_to_regex(entry)) return f'({"|".join(data)})' if '-' not in num_range: return num_range regex = range_to_regex(num_range) return f'({regex})' @register_filter('kea_address_json') def kea_address_json(addresses): from json import dumps from vyos.utils.network import is_addr_assigned out = [] for address in addresses: ifname = is_addr_assigned(address, return_ifname=True, include_vrf=True) if not ifname: continue out.append(f'{ifname}/{address}') return dumps(out) @register_filter('kea_high_availability_json') def kea_high_availability_json(config): from json import dumps source_addr = config['source_address'] remote_addr = config['remote'] ha_mode = 'hot-standby' if config['mode'] == 'active-passive' else 'load-balancing' ha_role = config['status'] if ha_role == 'primary': peer1_role = 'primary' peer2_role = 'standby' if ha_mode == 'hot-standby' else 'secondary' else: peer1_role = 'standby' if ha_mode == 'hot-standby' else 'secondary' peer2_role = 'primary' data = { 'this-server-name': os.uname()[1], 'mode': ha_mode, 'heartbeat-delay': 10000, 'max-response-delay': 10000, 'max-ack-delay': 5000, 'max-unacked-clients': 0, 'peers': [ { 'name': os.uname()[1], 'url': f'http://{source_addr}:647/', 'role': peer1_role, 'auto-failover': True }, { 'name': config['name'], 'url': f'http://{remote_addr}:647/', 'role': peer2_role, 'auto-failover': True }] } if 'ca_cert_file' in config: data['trust-anchor'] = config['ca_cert_file'] if 'cert_file' in config: data['cert-file'] = config['cert_file'] if 'cert_key_file' in config: data['key-file'] = config['cert_key_file'] return dumps(data) @register_filter('kea_shared_network_json') def kea_shared_network_json(shared_networks): from vyos.kea import kea_parse_options from vyos.kea import kea_parse_subnet from json import dumps out = [] for name, config in shared_networks.items(): if 'disable' in config: continue network = { 'name': name, 'authoritative': ('authoritative' in config), 'subnet4': [] } if 'option' in config: network['option-data'] = kea_parse_options(config['option']) if 'bootfile_name' in config['option']: network['boot-file-name'] = config['option']['bootfile_name'] if 'bootfile_server' in config['option']: network['next-server'] = config['option']['bootfile_server'] if 'subnet' in config: for subnet, subnet_config in config['subnet'].items(): if 'disable' in subnet_config: continue network['subnet4'].append(kea_parse_subnet(subnet, subnet_config)) out.append(network) return dumps(out, indent=4) @register_filter('kea6_shared_network_json') def kea6_shared_network_json(shared_networks): from vyos.kea import kea6_parse_options from vyos.kea import kea6_parse_subnet from json import dumps out = [] for name, config in shared_networks.items(): if 'disable' in config: continue network = { 'name': name, 'subnet6': [] } if 'common_options' in config: network['option-data'] = kea6_parse_options(config['common_options']) if 'interface' in config: network['interface'] = config['interface'] if 'subnet' in config: for subnet, subnet_config in config['subnet'].items(): network['subnet6'].append(kea6_parse_subnet(subnet, subnet_config)) out.append(network) return dumps(out, indent=4) @register_test('vyos_defined') def vyos_defined(value, test_value=None, var_type=None): """ Jinja2 plugin to test if a variable is defined and not none - vyos_defined will test value if defined and is not none and return true or false. If test_value is supplied, the value must also pass == test_value to return true. If var_type is supplied, the value must also be of the specified class/type Examples: 1. Test if var is defined and not none: {% if foo is vyos_defined %} ... {% endif %} 2. Test if variable is defined, not none and has value "something" {% if bar is vyos_defined("something") %} ... {% endif %} Parameters ---------- value : any Value to test from ansible test_value : any, optional Value to test in addition of defined and not none, by default None var_type : ['float', 'int', 'str', 'list', 'dict', 'tuple', 'bool'], optional Type or Class to test for Returns ------- boolean True if variable matches criteria, False in other cases. Implementation inspired and re-used from https://github.com/aristanetworks/ansible-avd/ """ from jinja2 import Undefined if isinstance(value, Undefined) or value is None: # Invalid value - return false return False elif test_value is not None and value != test_value: # Valid value but not matching the optional argument return False elif str(var_type).lower() in ['float', 'int', 'str', 'list', 'dict', 'tuple', 'bool'] and str(var_type).lower() != type(value).__name__: # Invalid class - return false return False else: # Valid value and is matching optional argument if provided - return true return True diff --git a/smoketest/config-tests/dialup-router-medium-vpn b/smoketest/config-tests/dialup-router-medium-vpn index 8c221707f..ab3b1dac5 100644 --- a/smoketest/config-tests/dialup-router-medium-vpn +++ b/smoketest/config-tests/dialup-router-medium-vpn @@ -1,317 +1,317 @@ set firewall global-options all-ping 'enable' set firewall global-options broadcast-ping 'disable' set firewall global-options ip-src-route 'disable' set firewall global-options ipv6-receive-redirects 'disable' set firewall global-options ipv6-src-route 'disable' set firewall global-options log-martians 'enable' set firewall global-options receive-redirects 'disable' set firewall global-options send-redirects 'enable' set firewall global-options source-validation 'disable' set firewall global-options syn-cookies 'disable' set firewall global-options twa-hazards-protection 'enable' set firewall ipv4 name test_tcp_flags rule 1 action 'drop' set firewall ipv4 name test_tcp_flags rule 1 protocol 'tcp' set firewall ipv4 name test_tcp_flags rule 1 tcp flags ack set firewall ipv4 name test_tcp_flags rule 1 tcp flags not fin set firewall ipv4 name test_tcp_flags rule 1 tcp flags not rst set firewall ipv4 name test_tcp_flags rule 1 tcp flags syn set high-availability vrrp group LAN address 192.168.0.1/24 set high-availability vrrp group LAN hello-source-address '192.168.0.250' set high-availability vrrp group LAN interface 'eth1' set high-availability vrrp group LAN peer-address '192.168.0.251' set high-availability vrrp group LAN priority '200' set high-availability vrrp group LAN vrid '1' set high-availability vrrp sync-group failover-group member 'LAN' set interfaces ethernet eth0 duplex 'auto' set interfaces ethernet eth0 mtu '9000' set interfaces ethernet eth0 offload gro set interfaces ethernet eth0 offload gso set interfaces ethernet eth0 offload sg set interfaces ethernet eth0 offload tso set interfaces ethernet eth0 speed 'auto' set interfaces ethernet eth1 address '192.168.0.250/24' set interfaces ethernet eth1 duplex 'auto' set interfaces ethernet eth1 ip source-validation 'strict' set interfaces ethernet eth1 mtu '9000' set interfaces ethernet eth1 offload gro set interfaces ethernet eth1 offload gso set interfaces ethernet eth1 offload sg set interfaces ethernet eth1 offload tso set interfaces ethernet eth1 speed 'auto' set interfaces loopback lo -set interfaces openvpn vtun0 encryption ncp-ciphers 'aes256' +set interfaces openvpn vtun0 encryption data-ciphers 'aes256' set interfaces openvpn vtun0 hash 'sha512' set interfaces openvpn vtun0 ip adjust-mss '1380' set interfaces openvpn vtun0 ip source-validation 'strict' set interfaces openvpn vtun0 keep-alive failure-count '3' set interfaces openvpn vtun0 keep-alive interval '30' set interfaces openvpn vtun0 mode 'client' set interfaces openvpn vtun0 openvpn-option 'comp-lzo adaptive' set interfaces openvpn vtun0 openvpn-option 'fast-io' set interfaces openvpn vtun0 openvpn-option 'persist-key' set interfaces openvpn vtun0 openvpn-option 'reneg-sec 86400' set interfaces openvpn vtun0 persistent-tunnel set interfaces openvpn vtun0 remote-host '192.0.2.10' set interfaces openvpn vtun0 tls auth-key 'openvpn_vtun0_auth' set interfaces openvpn vtun0 tls ca-certificate 'openvpn_vtun0_1' set interfaces openvpn vtun0 tls ca-certificate 'openvpn_vtun0_2' set interfaces openvpn vtun0 tls certificate 'openvpn_vtun0' set interfaces openvpn vtun1 authentication password 'vyos1' set interfaces openvpn vtun1 authentication username 'vyos1' -set interfaces openvpn vtun1 encryption ncp-ciphers 'aes256' +set interfaces openvpn vtun1 encryption data-ciphers 'aes256' set interfaces openvpn vtun1 hash 'sha1' set interfaces openvpn vtun1 ip adjust-mss '1380' set interfaces openvpn vtun1 keep-alive failure-count '3' set interfaces openvpn vtun1 keep-alive interval '30' set interfaces openvpn vtun1 mode 'client' set interfaces openvpn vtun1 openvpn-option 'comp-lzo adaptive' set interfaces openvpn vtun1 openvpn-option 'tun-mtu 1500' set interfaces openvpn vtun1 openvpn-option 'tun-mtu-extra 32' set interfaces openvpn vtun1 openvpn-option 'mssfix 1300' set interfaces openvpn vtun1 openvpn-option 'persist-key' set interfaces openvpn vtun1 openvpn-option 'mute 10' set interfaces openvpn vtun1 openvpn-option 'route-nopull' set interfaces openvpn vtun1 openvpn-option 'fast-io' set interfaces openvpn vtun1 openvpn-option 'reneg-sec 86400' set interfaces openvpn vtun1 persistent-tunnel set interfaces openvpn vtun1 protocol 'udp' set interfaces openvpn vtun1 remote-host '01.foo.com' set interfaces openvpn vtun1 remote-port '1194' set interfaces openvpn vtun1 tls auth-key 'openvpn_vtun1_auth' set interfaces openvpn vtun1 tls ca-certificate 'openvpn_vtun1_1' set interfaces openvpn vtun1 tls ca-certificate 'openvpn_vtun1_2' set interfaces openvpn vtun2 authentication password 'vyos2' set interfaces openvpn vtun2 authentication username 'vyos2' set interfaces openvpn vtun2 disable -set interfaces openvpn vtun2 encryption ncp-ciphers 'aes256' +set interfaces openvpn vtun2 encryption data-ciphers 'aes256' set interfaces openvpn vtun2 hash 'sha512' set interfaces openvpn vtun2 ip adjust-mss '1380' set interfaces openvpn vtun2 keep-alive failure-count '3' set interfaces openvpn vtun2 keep-alive interval '30' set interfaces openvpn vtun2 mode 'client' set interfaces openvpn vtun2 openvpn-option 'tun-mtu 1500' set interfaces openvpn vtun2 openvpn-option 'tun-mtu-extra 32' set interfaces openvpn vtun2 openvpn-option 'mssfix 1300' set interfaces openvpn vtun2 openvpn-option 'persist-key' set interfaces openvpn vtun2 openvpn-option 'mute 10' set interfaces openvpn vtun2 openvpn-option 'route-nopull' set interfaces openvpn vtun2 openvpn-option 'fast-io' set interfaces openvpn vtun2 openvpn-option 'remote-random' set interfaces openvpn vtun2 openvpn-option 'reneg-sec 86400' set interfaces openvpn vtun2 persistent-tunnel set interfaces openvpn vtun2 protocol 'udp' set interfaces openvpn vtun2 remote-host '01.myvpn.com' set interfaces openvpn vtun2 remote-host '02.myvpn.com' set interfaces openvpn vtun2 remote-host '03.myvpn.com' set interfaces openvpn vtun2 remote-port '1194' set interfaces openvpn vtun2 tls auth-key 'openvpn_vtun2_auth' set interfaces openvpn vtun2 tls ca-certificate 'openvpn_vtun2_1' set interfaces pppoe pppoe0 authentication password 'password' set interfaces pppoe pppoe0 authentication username 'vyos' set interfaces pppoe pppoe0 mtu '1500' set interfaces pppoe pppoe0 source-interface 'eth0' set interfaces wireguard wg0 address '192.168.10.1/24' set interfaces wireguard wg0 ip adjust-mss '1380' set interfaces wireguard wg0 peer blue allowed-ips '192.168.10.3/32' set interfaces wireguard wg0 peer blue persistent-keepalive '20' set interfaces wireguard wg0 peer blue preshared-key 'ztFDOY9UyaDvn8N3X97SFMDwIfv7EEfuUIPP2yab6UI=' set interfaces wireguard wg0 peer blue public-key 'G4pZishpMRrLmd96Kr6V7LIuNGdcUb81gWaYZ+FWkG0=' set interfaces wireguard wg0 peer green allowed-ips '192.168.10.21/32' set interfaces wireguard wg0 peer green persistent-keepalive '25' set interfaces wireguard wg0 peer green preshared-key 'LQ9qmlTh9G4nZu4UgElxRUwg7JB/qoV799aADJOijnY=' set interfaces wireguard wg0 peer green public-key '5iQUD3VoCDBTPXAPHOwUJ0p7xzKGHEY/wQmgvBVmaFI=' set interfaces wireguard wg0 peer pink allowed-ips '192.168.10.14/32' set interfaces wireguard wg0 peer pink allowed-ips '192.168.10.16/32' set interfaces wireguard wg0 peer pink persistent-keepalive '25' set interfaces wireguard wg0 peer pink preshared-key 'Qi9Odyx0/5itLPN5C5bEy3uMX+tmdl15QbakxpKlWqQ=' set interfaces wireguard wg0 peer pink public-key 'i4qNPmxyy9EETL4tIoZOLKJF4p7IlVmpAE15gglnAk4=' set interfaces wireguard wg0 peer red allowed-ips '192.168.10.4/32' set interfaces wireguard wg0 peer red persistent-keepalive '20' set interfaces wireguard wg0 peer red preshared-key 'CumyXX7osvUT9AwnS+m2TEfCaL0Ptc2LfuZ78Sujuk8=' set interfaces wireguard wg0 peer red public-key 'ALGWvMJCKpHF2tVH3hEIHqUe9iFfAmZATUUok/WQzks=' set interfaces wireguard wg0 port '7777' set interfaces wireguard wg1 address '10.89.90.2/30' set interfaces wireguard wg1 ip adjust-mss '1380' set interfaces wireguard wg1 peer sam address '192.0.2.45' set interfaces wireguard wg1 peer sam allowed-ips '10.1.1.0/24' set interfaces wireguard wg1 peer sam allowed-ips '10.89.90.1/32' set interfaces wireguard wg1 peer sam persistent-keepalive '20' set interfaces wireguard wg1 peer sam port '1200' set interfaces wireguard wg1 peer sam preshared-key 'XpFtzx2Z+nR8pBv9/sSf7I94OkZkVYTz0AeU5Q/QQUE=' set interfaces wireguard wg1 peer sam public-key 'v5zfKGvH6W/lfDXJ0en96lvKo1gfFxMUWxe02+Fj5BU=' set interfaces wireguard wg1 port '7778' set nat destination rule 50 destination port '49371' set nat destination rule 50 inbound-interface name 'pppoe0' set nat destination rule 50 protocol 'tcp_udp' set nat destination rule 50 translation address '192.168.0.5' set nat destination rule 51 destination port '58050-58051' set nat destination rule 51 inbound-interface name 'pppoe0' set nat destination rule 51 protocol 'tcp' set nat destination rule 51 translation address '192.168.0.5' set nat destination rule 52 destination port '22067-22070' set nat destination rule 52 inbound-interface name 'pppoe0' set nat destination rule 52 protocol 'tcp' set nat destination rule 52 translation address '192.168.0.5' set nat destination rule 53 destination port '34342' set nat destination rule 53 inbound-interface name 'pppoe0' set nat destination rule 53 protocol 'tcp_udp' set nat destination rule 53 translation address '192.168.0.121' set nat destination rule 54 destination port '45459' set nat destination rule 54 inbound-interface name 'pppoe0' set nat destination rule 54 protocol 'tcp_udp' set nat destination rule 54 translation address '192.168.0.120' set nat destination rule 55 destination port '22' set nat destination rule 55 inbound-interface name 'pppoe0' set nat destination rule 55 protocol 'tcp' set nat destination rule 55 translation address '192.168.0.5' set nat destination rule 56 destination port '8920' set nat destination rule 56 inbound-interface name 'pppoe0' set nat destination rule 56 protocol 'tcp' set nat destination rule 56 translation address '192.168.0.5' set nat destination rule 60 destination port '80,443' set nat destination rule 60 inbound-interface name 'pppoe0' set nat destination rule 60 protocol 'tcp' set nat destination rule 60 translation address '192.168.0.5' set nat destination rule 70 destination port '5001' set nat destination rule 70 inbound-interface name 'pppoe0' set nat destination rule 70 protocol 'tcp' set nat destination rule 70 translation address '192.168.0.5' set nat destination rule 80 destination port '25' set nat destination rule 80 inbound-interface name 'pppoe0' set nat destination rule 80 protocol 'tcp' set nat destination rule 80 translation address '192.168.0.5' set nat destination rule 90 destination port '8123' set nat destination rule 90 inbound-interface name 'pppoe0' set nat destination rule 90 protocol 'tcp' set nat destination rule 90 translation address '192.168.0.7' set nat destination rule 91 destination port '1880' set nat destination rule 91 inbound-interface name 'pppoe0' set nat destination rule 91 protocol 'tcp' set nat destination rule 91 translation address '192.168.0.7' set nat destination rule 500 destination address '!192.168.0.0/24' set nat destination rule 500 destination port '53' set nat destination rule 500 inbound-interface name 'eth1' set nat destination rule 500 protocol 'tcp_udp' set nat destination rule 500 source address '!192.168.0.1-192.168.0.5' set nat destination rule 500 translation address '192.168.0.1' set nat source rule 1000 outbound-interface name 'pppoe0' set nat source rule 1000 translation address 'masquerade' set nat source rule 2000 outbound-interface name 'vtun0' set nat source rule 2000 source address '192.168.0.0/16' set nat source rule 2000 translation address 'masquerade' set nat source rule 3000 outbound-interface name 'vtun1' set nat source rule 3000 translation address 'masquerade' set policy prefix-list user1-routes rule 1 action 'permit' set policy prefix-list user1-routes rule 1 prefix '192.168.0.0/24' set policy prefix-list user2-routes rule 1 action 'permit' set policy prefix-list user2-routes rule 1 prefix '10.1.1.0/24' set policy route LAN-POLICY-BASED-ROUTING interface 'eth1' set policy route LAN-POLICY-BASED-ROUTING rule 10 destination set policy route LAN-POLICY-BASED-ROUTING rule 10 disable set policy route LAN-POLICY-BASED-ROUTING rule 10 set table '10' set policy route LAN-POLICY-BASED-ROUTING rule 10 source address '192.168.0.119/32' set policy route LAN-POLICY-BASED-ROUTING rule 20 destination set policy route LAN-POLICY-BASED-ROUTING rule 20 set table '100' set policy route LAN-POLICY-BASED-ROUTING rule 20 source address '192.168.0.240' set policy route-map rm-static-to-bgp rule 10 action 'permit' set policy route-map rm-static-to-bgp rule 10 match ip address prefix-list 'user1-routes' set policy route-map rm-static-to-bgp rule 100 action 'deny' set policy route6 LAN6-POLICY-BASED-ROUTING interface 'eth1' set policy route6 LAN6-POLICY-BASED-ROUTING rule 10 destination set policy route6 LAN6-POLICY-BASED-ROUTING rule 10 disable set policy route6 LAN6-POLICY-BASED-ROUTING rule 10 set table '10' set policy route6 LAN6-POLICY-BASED-ROUTING rule 10 source address '2002::1' set policy route6 LAN6-POLICY-BASED-ROUTING rule 20 destination set policy route6 LAN6-POLICY-BASED-ROUTING rule 20 set table '100' set policy route6 LAN6-POLICY-BASED-ROUTING rule 20 source address '2008::f' set protocols bgp address-family ipv4-unicast redistribute connected route-map 'rm-static-to-bgp' set protocols bgp neighbor 10.89.90.1 address-family ipv4-unicast nexthop-self set protocols bgp neighbor 10.89.90.1 address-family ipv4-unicast prefix-list export 'user1-routes' set protocols bgp neighbor 10.89.90.1 address-family ipv4-unicast prefix-list import 'user2-routes' set protocols bgp neighbor 10.89.90.1 address-family ipv4-unicast soft-reconfiguration inbound set protocols bgp neighbor 10.89.90.1 password 'ericandre2020' set protocols bgp neighbor 10.89.90.1 remote-as '64589' set protocols bgp parameters log-neighbor-changes set protocols bgp parameters router-id '10.89.90.2' set protocols bgp system-as '64590' set protocols static route 100.64.160.23/32 interface pppoe0 set protocols static route 100.64.165.25/32 interface pppoe0 set protocols static route 100.64.165.26/32 interface pppoe0 set protocols static route 100.64.198.0/24 interface vtun0 set protocols static table 10 route 0.0.0.0/0 interface vtun1 set protocols static table 100 route 0.0.0.0/0 next-hop 192.168.10.5 set service conntrack-sync accept-protocol 'tcp' set service conntrack-sync accept-protocol 'udp' set service conntrack-sync accept-protocol 'icmp' set service conntrack-sync disable-external-cache set service conntrack-sync event-listen-queue-size '8' set service conntrack-sync expect-sync 'all' set service conntrack-sync failover-mechanism vrrp sync-group 'failover-group' set service conntrack-sync interface eth1 peer '192.168.0.251' set service conntrack-sync sync-queue-size '8' set service dhcp-server high-availability name 'DHCP02' set service dhcp-server high-availability remote '192.168.0.251' set service dhcp-server high-availability source-address '192.168.0.250' set service dhcp-server high-availability status 'primary' set service dhcp-server shared-network-name LAN authoritative set service dhcp-server shared-network-name LAN subnet 192.168.0.0/24 lease '86400' set service dhcp-server shared-network-name LAN subnet 192.168.0.0/24 option default-router '192.168.0.1' set service dhcp-server shared-network-name LAN subnet 192.168.0.0/24 option domain-name 'vyos.net' set service dhcp-server shared-network-name LAN subnet 192.168.0.0/24 option domain-search 'vyos.net' set service dhcp-server shared-network-name LAN subnet 192.168.0.0/24 option name-server '192.168.0.1' set service dhcp-server shared-network-name LAN subnet 192.168.0.0/24 range LANDynamic start '192.168.0.200' set service dhcp-server shared-network-name LAN subnet 192.168.0.0/24 range LANDynamic stop '192.168.0.240' set service dhcp-server shared-network-name LAN subnet 192.168.0.0/24 static-mapping Audio ip-address '192.168.0.107' set service dhcp-server shared-network-name LAN subnet 192.168.0.0/24 static-mapping Audio mac '00:50:01:dc:91:14' set service dhcp-server shared-network-name LAN subnet 192.168.0.0/24 static-mapping IPTV ip-address '192.168.0.104' set service dhcp-server shared-network-name LAN subnet 192.168.0.0/24 static-mapping IPTV mac '00:50:01:31:b5:f6' set service dhcp-server shared-network-name LAN subnet 192.168.0.0/24 static-mapping McPrintus ip-address '192.168.0.60' set service dhcp-server shared-network-name LAN subnet 192.168.0.0/24 static-mapping McPrintus mac '00:50:01:58:ac:95' set service dhcp-server shared-network-name LAN subnet 192.168.0.0/24 static-mapping Mobile01 ip-address '192.168.0.109' set service dhcp-server shared-network-name LAN subnet 192.168.0.0/24 static-mapping Mobile01 mac '00:50:01:bc:ac:51' set service dhcp-server shared-network-name LAN subnet 192.168.0.0/24 static-mapping camera1 ip-address '192.168.0.11' set service dhcp-server shared-network-name LAN subnet 192.168.0.0/24 static-mapping camera1 mac '00:50:01:70:b9:4d' set service dhcp-server shared-network-name LAN subnet 192.168.0.0/24 static-mapping camera2 ip-address '192.168.0.12' set service dhcp-server shared-network-name LAN subnet 192.168.0.0/24 static-mapping camera2 mac '00:50:01:70:b7:4f' set service dhcp-server shared-network-name LAN subnet 192.168.0.0/24 static-mapping pearTV ip-address '192.168.0.101' set service dhcp-server shared-network-name LAN subnet 192.168.0.0/24 static-mapping pearTV mac '00:50:01:ba:62:79' set service dhcp-server shared-network-name LAN subnet 192.168.0.0/24 static-mapping sand ip-address '192.168.0.110' set service dhcp-server shared-network-name LAN subnet 192.168.0.0/24 static-mapping sand mac '00:50:01:af:c5:d2' set service dhcp-server shared-network-name LAN subnet 192.168.0.0/24 subnet-id '1' set service dns forwarding allow-from '192.168.0.0/16' set service dns forwarding cache-size '8192' set service dns forwarding dnssec 'off' set service dns forwarding listen-address '192.168.0.1' set service dns forwarding name-server 100.64.0.1 set service dns forwarding name-server 100.64.0.2 set service ntp allow-client address '192.168.0.0/16' set service ntp server nz.pool.ntp.org prefer set service snmp community AwesomeCommunity authorization 'ro' set service snmp community AwesomeCommunity client '127.0.0.1' set service snmp community AwesomeCommunity network '192.168.0.0/24' set service ssh access-control allow user 'vyos' set service ssh client-keepalive-interval '60' set service ssh listen-address '192.168.0.1' set service ssh listen-address '192.168.10.1' set service ssh listen-address '192.168.0.250' set system config-management commit-revisions '100' set system console device ttyS0 speed '115200' set system host-name 'vyos' set system ip arp table-size '1024' set system name-server '192.168.0.1' set system name-server 'pppoe0' set system option ctrl-alt-delete 'ignore' set system option reboot-on-panic set system option startup-beep set system static-host-mapping host-name host60.vyos.net inet '192.168.0.60' set system static-host-mapping host-name host104.vyos.net inet '192.168.0.104' set system static-host-mapping host-name host107.vyos.net inet '192.168.0.107' set system static-host-mapping host-name host109.vyos.net inet '192.168.0.109' set system sysctl parameter net.core.default_qdisc value 'fq' set system sysctl parameter net.ipv4.tcp_congestion_control value 'bbr' set system syslog global facility all level 'info' set system syslog host 192.168.0.252 facility all level 'debug' set system syslog host 192.168.0.252 protocol 'udp' set system task-scheduler task Update-Blacklists executable path '/config/scripts/vyos-foo-update.script' set system task-scheduler task Update-Blacklists interval '3h' set system time-zone 'Pacific/Auckland' diff --git a/smoketest/scripts/cli/test_interfaces_openvpn.py b/smoketest/scripts/cli/test_interfaces_openvpn.py index d24ce831c..d8a091aaa 100755 --- a/smoketest/scripts/cli/test_interfaces_openvpn.py +++ b/smoketest/scripts/cli/test_interfaces_openvpn.py @@ -1,682 +1,682 @@ #!/usr/bin/env python3 # # Copyright (C) 2020-2024 VyOS maintainers and contributors # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 or later as # published by the Free Software Foundation. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see <http://www.gnu.org/licenses/>. import os import unittest from glob import glob from ipaddress import IPv4Network from netifaces import interfaces from base_vyostest_shim import VyOSUnitTestSHIM from vyos.configsession import ConfigSessionError from vyos.utils.process import cmd from vyos.utils.process import process_named_running from vyos.utils.file import read_file from vyos.template import address_from_cidr from vyos.template import inc_ip from vyos.template import last_host_address from vyos.template import netmask_from_cidr PROCESS_NAME = 'openvpn' base_path = ['interfaces', 'openvpn'] cert_data = """ MIICFDCCAbugAwIBAgIUfMbIsB/ozMXijYgUYG80T1ry+mcwCgYIKoZIzj0EAwIw WTELMAkGA1UEBhMCR0IxEzARBgNVBAgMClNvbWUtU3RhdGUxEjAQBgNVBAcMCVNv bWUtQ2l0eTENMAsGA1UECgwEVnlPUzESMBAGA1UEAwwJVnlPUyBUZXN0MB4XDTIx MDcyMDEyNDUxMloXDTI2MDcxOTEyNDUxMlowWTELMAkGA1UEBhMCR0IxEzARBgNV BAgMClNvbWUtU3RhdGUxEjAQBgNVBAcMCVNvbWUtQ2l0eTENMAsGA1UECgwEVnlP UzESMBAGA1UEAwwJVnlPUyBUZXN0MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE 01HrLcNttqq4/PtoMua8rMWEkOdBu7vP94xzDO7A8C92ls1v86eePy4QllKCzIw3 QxBIoCuH2peGRfWgPRdFsKNhMF8wDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8E BAMCAYYwHQYDVR0lBBYwFAYIKwYBBQUHAwIGCCsGAQUFBwMBMB0GA1UdDgQWBBSu +JnU5ZC4mkuEpqg2+Mk4K79oeDAKBggqhkjOPQQDAgNHADBEAiBEFdzQ/Bc3Lftz ngrY605UhA6UprHhAogKgROv7iR4QgIgEFUxTtW3xXJcnUPWhhUFhyZoqfn8dE93 +dm/LDnp7C0= """ key_data = """ MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgPLpD0Ohhoq0g4nhx 2KMIuze7ucKUt/lBEB2wc03IxXyhRANCAATTUestw222qrj8+2gy5rysxYSQ50G7 u8/3jHMM7sDwL3aWzW/zp54/LhCWUoLMjDdDEEigK4fal4ZF9aA9F0Ww """ dh_data = """ MIIBCAKCAQEApzGAPcQlLJiOyfGZgl1qxNgufXkdpjG7lMaOrO4TGr1giFe3jIFO FxJNC/G9Dn+KSukaWssVVR+Jwr/JesZFPawihS03wC7cZsccykNRIjiteqJDwYJZ UHieOxyCuCeY4pqOUCl1uswRGjLvIFtwynpnXKKuz2YtjNifma90PEgv/vVWKix+ Q0TAbdbzJzO5xp8UVn9DuYfSr10k3LbDqDM7w5ezHZxFk24S5pN/yoOpdbxB8TS6 7q3IYXxR3F+RseKu4J3AvkxXSP1j7COXddPpLnvbJT/SW8NrjuC/n0eKGvmeyqNv 108Y89jnT79MxMMRQk66iwlsd1m4pa/OYwIBAg== """ ovpn_key_data = """ 443f2a710ac411c36894b2531e62c4550b079b8f3f08997f4be57c64abfdaaa4 31d2396b01ecec3a2c0618959e8186d99f489742d25673ffb3268841ebb2e704 2a2daabe584e79d51d2b1d7409bf8840f7e42efa3e660a521719b04ee88b9043 e6315ae12da7c9abd55f67eeed71a9ee8c6e163b5d2661fc332cf90cb45658b4 adf892f79537d37d3a3d90da283ce885adf325ffd2b5be92067cdf0345c7712c 9d36b642c170351b6d9ce9f6230c7a2617b0c181121bce7d5373404fb68e6521 0b36e6d40ef2769cf8990503859f6f2db3c85ba74420430a6250d6a74ca51ece 4b85124bfdfec0c8a530cefa7350378d81a4539f74bed832a902ae4798142e4a """ remote_port = '1194' protocol = 'udp' path = [] interface = '' remote_host = '' vrf_name = 'orange' dummy_if = 'dum1301' def get_vrf(interface): for upper in glob(f'/sys/class/net/{interface}/upper*'): # an upper interface could be named: upper_bond0.1000.1100, thus # we need top drop the upper_ prefix tmp = os.path.basename(upper) tmp = tmp.replace('upper_', '') return tmp class TestInterfacesOpenVPN(VyOSUnitTestSHIM.TestCase): @classmethod def setUpClass(cls): super(TestInterfacesOpenVPN, cls).setUpClass() cls.cli_set(cls, ['interfaces', 'dummy', dummy_if, 'address', '192.0.2.1/32']) cls.cli_set(cls, ['vrf', 'name', vrf_name, 'table', '12345']) cls.cli_set(cls, ['pki', 'ca', 'ovpn_test', 'certificate', cert_data.replace('\n','')]) cls.cli_set(cls, ['pki', 'certificate', 'ovpn_test', 'certificate', cert_data.replace('\n','')]) cls.cli_set(cls, ['pki', 'certificate', 'ovpn_test', 'private', 'key', key_data.replace('\n','')]) cls.cli_set(cls, ['pki', 'dh', 'ovpn_test', 'parameters', dh_data.replace('\n','')]) cls.cli_set(cls, ['pki', 'openvpn', 'shared-secret', 'ovpn_test', 'key', ovpn_key_data.replace('\n','')]) @classmethod def tearDownClass(cls): cls.cli_delete(cls, ['interfaces', 'dummy', dummy_if]) cls.cli_delete(cls, ['vrf']) super(TestInterfacesOpenVPN, cls).tearDownClass() def tearDown(self): self.cli_delete(base_path) self.cli_commit() def test_openvpn_client_verify(self): # Create OpenVPN client interface and test verify() steps. interface = 'vtun2000' path = base_path + [interface] self.cli_set(path + ['mode', 'client']) - self.cli_set(path + ['encryption', 'ncp-ciphers', 'aes192gcm']) + self.cli_set(path + ['encryption', 'data-ciphers', 'aes192gcm']) # check validate() - cannot specify local-port in client mode self.cli_set(path + ['local-port', '5000']) with self.assertRaises(ConfigSessionError): self.cli_commit() self.cli_delete(path + ['local-port']) # check validate() - cannot specify local-host in client mode self.cli_set(path + ['local-host', '127.0.0.1']) with self.assertRaises(ConfigSessionError): self.cli_commit() self.cli_delete(path + ['local-host']) # check validate() - cannot specify protocol tcp-passive in client mode self.cli_set(path + ['protocol', 'tcp-passive']) with self.assertRaises(ConfigSessionError): self.cli_commit() self.cli_delete(path + ['protocol']) # check validate() - remote-host must be set in client mode with self.assertRaises(ConfigSessionError): self.cli_commit() self.cli_set(path + ['remote-host', '192.0.9.9']) # 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" and "tls" with self.assertRaises(ConfigSessionError): self.cli_commit() self.cli_set(path + ['shared-secret-key', 'ovpn_test']) # 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', 'ovpn_test']) # check validate() - cannot specify "encryption cipher" in client mode self.cli_set(path + ['encryption', 'cipher', 'aes192gcm']) with self.assertRaises(ConfigSessionError): self.cli_commit() self.cli_delete(path + ['encryption', 'cipher']) 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']) with self.assertRaises(ConfigSessionError): self.cli_commit() self.cli_set(path + ['authentication', 'password', 'vyos']) # client commit must pass self.cli_commit() self.assertTrue(process_named_running(PROCESS_NAME)) self.assertIn(interface, interfaces()) def test_openvpn_client_interfaces(self): # Create OpenVPN client interfaces connecting to different # server IP addresses. Validate configuration afterwards. num_range = range(10, 15) for ii in num_range: interface = f'vtun{ii}' remote_host = f'192.0.2.{ii}' path = base_path + [interface] auth_hash = 'sha1' self.cli_set(path + ['device-type', 'tun']) - self.cli_set(path + ['encryption', 'ncp-ciphers', 'aes256']) + self.cli_set(path + ['encryption', 'data-ciphers', 'aes256']) self.cli_set(path + ['hash', auth_hash]) self.cli_set(path + ['mode', 'client']) self.cli_set(path + ['persistent-tunnel']) 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-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']) self.cli_commit() for ii in num_range: interface = f'vtun{ii}' remote_host = f'192.0.2.{ii}' config_file = f'/run/openvpn/{interface}.conf' pw_file = f'/run/openvpn/{interface}.pw' config = read_file(config_file) self.assertIn(f'dev {interface}', config) self.assertIn(f'dev-type tun', config) self.assertIn(f'persist-key', config) self.assertIn(f'proto {protocol}', config) self.assertIn(f'rport {remote_port}', config) self.assertIn(f'remote {remote_host}', config) self.assertIn(f'persist-tun', config) self.assertIn(f'auth {auth_hash}', config) self.assertIn(f'data-ciphers AES-256-CBC', config) # TLS options 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) self.assertIn(interface, interfaces()) pw = cmd(f'sudo cat {pw_file}') self.assertIn(f'{interface}user', pw) self.assertIn(f'{interface}secretpw', pw) # check that no interface remained after deleting them self.cli_delete(base_path) self.cli_commit() for ii in num_range: interface = f'vtun{ii}' self.assertNotIn(interface, interfaces()) def test_openvpn_server_verify(self): # Create one OpenVPN server interface and check required verify() stages interface = 'vtun5000' path = base_path + [interface] # check validate() - must speciy operating mode self.cli_set(path) with self.assertRaises(ConfigSessionError): self.cli_commit() self.cli_set(path + ['mode', 'server']) # check validate() - cannot specify protocol tcp-active in server mode self.cli_set(path + ['protocol', 'tcp-active']) with self.assertRaises(ConfigSessionError): self.cli_commit() self.cli_delete(path + ['protocol']) # check validate() - cannot specify local-port in client mode self.cli_set(path + ['remote-port', '5000']) with self.assertRaises(ConfigSessionError): self.cli_commit() self.cli_delete(path + ['remote-port']) # check validate() - cannot specify local-host in client mode self.cli_set(path + ['remote-host', '127.0.0.1']) with self.assertRaises(ConfigSessionError): self.cli_commit() self.cli_delete(path + ['remote-host']) # 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-params', 'ovpn_test']) # check validate() - must specify "server subnet" or add interface to # bridge in server mode with self.assertRaises(ConfigSessionError): self.cli_commit() # check validate() - server client-ip-pool is too large # [100.64.0.4 -> 100.127.255.251 = 4194295], maximum is 65536 addresses. self.cli_set(path + ['server', 'subnet', '100.64.0.0/10']) with self.assertRaises(ConfigSessionError): self.cli_commit() # check validate() - cannot specify more than 1 IPv4 and 1 IPv6 server subnet self.cli_set(path + ['server', 'subnet', '100.64.0.0/20']) with self.assertRaises(ConfigSessionError): self.cli_commit() self.cli_delete(path + ['server', 'subnet', '100.64.0.0/10']) # check validate() - must specify "tls ca-certificate" with self.assertRaises(ConfigSessionError): self.cli_commit() self.cli_set(path + ['tls', 'ca-certificate', 'ovpn_test']) # check validate() - must specify "tls certificate" with self.assertRaises(ConfigSessionError): self.cli_commit() 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']) with self.assertRaises(ConfigSessionError): self.cli_commit() # check validate() - cannot specify "tls role" in client-server mode' self.cli_set(path + ['tls', 'auth-key', 'ovpn_test']) with self.assertRaises(ConfigSessionError): self.cli_commit() # check validate() - cannot specify "tcp-passive" when "tls role" is "active" self.cli_set(path + ['protocol', 'tcp-passive']) with self.assertRaises(ConfigSessionError): self.cli_commit() self.cli_delete(path + ['protocol']) # 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-params']) # check validate() - cannot specify "encryption cipher" in server mode self.cli_set(path + ['encryption', 'cipher', 'aes256']) with self.assertRaises(ConfigSessionError): self.cli_commit() self.cli_delete(path + ['encryption', 'cipher']) # Now test the other path with tls role passive self.cli_set(path + ['tls', 'role', 'passive']) # check validate() - cannot specify "tcp-active" when "tls role" is "passive" self.cli_set(path + ['protocol', 'tcp-active']) with self.assertRaises(ConfigSessionError): self.cli_commit() self.cli_delete(path + ['protocol']) self.cli_set(path + ['tls', 'dh-params', 'ovpn_test']) self.cli_commit() self.assertTrue(process_named_running(PROCESS_NAME)) self.assertIn(interface, interfaces()) def test_openvpn_server_subnet_topology(self): # Create OpenVPN server interfaces using different client subnets. # Validate configuration afterwards. auth_hash = 'sha256' num_range = range(20, 25) port = '' client1_routes = ['10.0.0.0/8', '172.16.0.0/12', '192.168.0.0/16'] for ii in num_range: interface = f'vtun{ii}' subnet = f'192.0.{ii}.0/24' client_ip = inc_ip(subnet, '5') path = base_path + [interface] port = str(2000 + ii) self.cli_set(path + ['device-type', 'tun']) - self.cli_set(path + ['encryption', 'ncp-ciphers', 'aes192']) + self.cli_set(path + ['encryption', 'data-ciphers', 'aes192']) self.cli_set(path + ['hash', auth_hash]) self.cli_set(path + ['mode', 'server']) self.cli_set(path + ['local-port', port]) self.cli_set(path + ['server', 'mfa', 'totp']) self.cli_set(path + ['server', 'subnet', subnet]) self.cli_set(path + ['server', 'topology', 'subnet']) self.cli_set(path + ['keep-alive', 'failure-count', '5']) self.cli_set(path + ['keep-alive', 'interval', '5']) # clients self.cli_set(path + ['server', 'client', 'client1', 'ip', client_ip]) for route in client1_routes: self.cli_set(path + ['server', 'client', 'client1', 'subnet', route]) self.cli_set(path + ['replace-default-route']) 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() for ii in num_range: interface = f'vtun{ii}' plugin = f'plugin "/usr/lib/openvpn/openvpn-otp.so" "otp_secrets=/config/auth/openvpn/{interface}-otp-secrets otp_slop=180 totp_t0=0 totp_step=30 totp_digits=6 password_is_cr=1"' subnet = f'192.0.{ii}.0/24' start_addr = inc_ip(subnet, '2') stop_addr = last_host_address(subnet) client_ip = inc_ip(subnet, '5') client_netmask = netmask_from_cidr(subnet) port = str(2000 + ii) config_file = f'/run/openvpn/{interface}.conf' client_config_file = f'/run/openvpn/ccd/{interface}/client1' config = read_file(config_file) self.assertIn(f'dev {interface}', config) self.assertIn(f'dev-type tun', config) self.assertIn(f'persist-key', config) self.assertIn(f'proto udp', config) # default protocol self.assertIn(f'auth {auth_hash}', config) self.assertIn(f'data-ciphers 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'{plugin}', config) self.assertIn(f'keepalive 5 25', config) # TLS options 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 network = IPv4Network(subnet).network_address self.assertIn(f'server {network} {netmask}', config) # Verify client client_config = read_file(client_config_file) self.assertIn(f'ifconfig-push {client_ip} {client_netmask}', client_config) for route in client1_routes: self.assertIn('iroute {} {}'.format(address_from_cidr(route), netmask_from_cidr(route)), client_config) self.assertTrue(process_named_running(PROCESS_NAME)) self.assertEqual(get_vrf(interface), vrf_name) self.assertIn(interface, interfaces()) # check that no interface remained after deleting them self.cli_delete(base_path) self.cli_commit() for ii in num_range: interface = f'vtun{ii}' self.assertNotIn(interface, interfaces()) def test_openvpn_site2site_verify(self): # Create one OpenVPN site2site interface and check required # verify() stages interface = 'vtun5000' path = base_path + [interface] self.cli_set(path + ['mode', 'site-to-site']) - # check validate() - encryption ncp-ciphers cannot be specified in site-to-site mode - self.cli_set(path + ['encryption', 'ncp-ciphers', 'aes192gcm']) + # check validate() - cipher negotiation cannot be enabled in site-to-site mode + self.cli_set(path + ['encryption', 'data-ciphers', 'aes192gcm']) with self.assertRaises(ConfigSessionError): self.cli_commit() self.cli_delete(path + ['encryption']) # check validate() - must specify "local-address" or add interface to bridge with self.assertRaises(ConfigSessionError): self.cli_commit() self.cli_set(path + ['local-address', '10.0.0.1']) self.cli_set(path + ['local-address', '2001:db8:1::1']) # check validate() - cannot specify more than 1 IPv4 local-address self.cli_set(path + ['local-address', '10.0.0.2']) with self.assertRaises(ConfigSessionError): self.cli_commit() self.cli_delete(path + ['local-address', '10.0.0.2']) # check validate() - cannot specify more than 1 IPv6 local-address self.cli_set(path + ['local-address', '2001:db8:1::2']) with self.assertRaises(ConfigSessionError): self.cli_commit() self.cli_delete(path + ['local-address', '2001:db8:1::2']) # check validate() - IPv4 "local-address" requires IPv4 "remote-address" # or IPv4 "local-address subnet" with self.assertRaises(ConfigSessionError): self.cli_commit() self.cli_set(path + ['remote-address', '192.168.0.1']) self.cli_set(path + ['remote-address', '2001:db8:ffff::1']) # check validate() - Cannot specify more than 1 IPv4 "remote-address" self.cli_set(path + ['remote-address', '192.168.0.2']) with self.assertRaises(ConfigSessionError): self.cli_commit() self.cli_delete(path + ['remote-address', '192.168.0.2']) # check validate() - Cannot specify more than 1 IPv6 "remote-address" self.cli_set(path + ['remote-address', '2001:db8:ffff::2']) with self.assertRaises(ConfigSessionError): self.cli_commit() self.cli_delete(path + ['remote-address', '2001:db8:ffff::2']) # 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', 'ovpn_test']) self.cli_commit() def test_openvpn_options(self): # Ensure OpenVPN process restart on openvpn-option CLI node change interface = 'vtun5001' path = base_path + [interface] encryption_cipher = 'aes256' self.cli_set(path + ['mode', 'site-to-site']) self.cli_set(path + ['local-address', '10.0.0.2']) self.cli_set(path + ['remote-address', '192.168.0.3']) self.cli_set(path + ['shared-secret-key', 'ovpn_test']) self.cli_set(path + ['encryption', 'cipher', encryption_cipher]) self.cli_commit() # Now verify the OpenVPN "raw" option passing. Once an openvpn-option is # added, modified or deleted from the CLI, OpenVPN daemon must be restarted cur_pid = process_named_running('openvpn') self.cli_set(path + ['openvpn-option', '--persist-tun']) self.cli_commit() # PID must be different as OpenVPN Must be restarted new_pid = process_named_running('openvpn') self.assertNotEqual(cur_pid, new_pid) cur_pid = new_pid self.cli_set(path + ['openvpn-option', '--persist-key']) self.cli_commit() # PID must be different as OpenVPN Must be restarted new_pid = process_named_running('openvpn') self.assertNotEqual(cur_pid, new_pid) cur_pid = new_pid self.cli_delete(path + ['openvpn-option']) self.cli_commit() # PID must be different as OpenVPN Must be restarted new_pid = process_named_running('openvpn') self.assertNotEqual(cur_pid, new_pid) cur_pid = new_pid def test_openvpn_site2site_interfaces_tun(self): # Create two OpenVPN site-to-site interfaces num_range = range(30, 35) port = '' local_address = '' remote_address = '' encryption_cipher = 'aes256' for ii in num_range: interface = f'vtun{ii}' local_address = f'192.0.{ii}.1' local_address_subnet = '255.255.255.252' remote_address = f'172.16.{ii}.1' path = base_path + [interface] port = str(3000 + ii) self.cli_set(path + ['local-address', local_address]) # even numbers use tun type, odd numbers use tap type if ii % 2 == 0: self.cli_set(path + ['device-type', 'tun']) else: self.cli_set(path + ['device-type', 'tap']) self.cli_set(path + ['local-address', local_address, 'subnet-mask', local_address_subnet]) 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', 'ovpn_test']) self.cli_set(path + ['remote-address', remote_address]) self.cli_set(path + ['encryption', 'cipher', encryption_cipher]) self.cli_set(path + ['vrf', vrf_name]) self.cli_commit() for ii in num_range: interface = f'vtun{ii}' local_address = f'192.0.{ii}.1' remote_address = f'172.16.{ii}.1' port = str(3000 + ii) config_file = f'/run/openvpn/{interface}.conf' config = read_file(config_file) # even numbers use tun type, odd numbers use tap type if ii % 2 == 0: self.assertIn(f'dev-type tun', config) self.assertIn(f'ifconfig {local_address} {remote_address}', config) else: self.assertIn(f'dev-type tap', config) self.assertIn(f'ifconfig {local_address} {local_address_subnet}', config) self.assertIn(f'dev {interface}', config) self.assertIn(f'secret /run/openvpn/{interface}_shared.key', config) self.assertIn(f'lport {port}', config) self.assertIn(f'rport {port}', config) self.assertTrue(process_named_running(PROCESS_NAME)) self.assertEqual(get_vrf(interface), vrf_name) self.assertIn(interface, interfaces()) # check that no interface remained after deleting them self.cli_delete(base_path) self.cli_commit() for ii in num_range: interface = f'vtun{ii}' self.assertNotIn(interface, interfaces()) def test_openvpn_server_server_bridge(self): # Create OpenVPN server interface using bridge. # Validate configuration afterwards. br_if = 'br0' vtun_if = 'vtun5010' auth_hash = 'sha256' path = base_path + [vtun_if] start_subnet = "192.168.0.100" stop_subnet = "192.168.0.200" mask_subnet = "255.255.255.0" gw_subnet = "192.168.0.1" self.cli_set(['interfaces', 'bridge', br_if, 'member', 'interface', vtun_if]) self.cli_set(path + ['device-type', 'tap']) self.cli_set(path + ['encryption', 'data-ciphers', 'aes192']) self.cli_set(path + ['hash', auth_hash]) self.cli_set(path + ['mode', 'server']) self.cli_set(path + ['server', 'bridge', 'gateway', gw_subnet]) self.cli_set(path + ['server', 'bridge', 'start', start_subnet]) self.cli_set(path + ['server', 'bridge', 'stop', stop_subnet]) self.cli_set(path + ['server', 'bridge', 'subnet-mask', mask_subnet]) self.cli_set(path + ['keep-alive', 'failure-count', '5']) self.cli_set(path + ['keep-alive', 'interval', '5']) 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_commit() config_file = f'/run/openvpn/{vtun_if}.conf' config = read_file(config_file) self.assertIn(f'dev {vtun_if}', config) self.assertIn(f'dev-type tap', config) self.assertIn(f'proto udp', config) # default protocol self.assertIn(f'auth {auth_hash}', config) self.assertIn(f'data-ciphers AES-192-CBC', config) self.assertIn(f'mode server', config) self.assertIn(f'server-bridge {gw_subnet} {mask_subnet} {start_subnet} {stop_subnet}', config) self.assertIn(f'keepalive 5 25', config) # TLS options self.assertIn(f'ca /run/openvpn/{vtun_if}_ca.pem', config) self.assertIn(f'cert /run/openvpn/{vtun_if}_cert.pem', config) self.assertIn(f'key /run/openvpn/{vtun_if}_cert.key', config) self.assertIn(f'dh /run/openvpn/{vtun_if}_dh.pem', config) # check that no interface remained after deleting them self.cli_delete(['interfaces', 'bridge', br_if, 'member', 'interface', vtun_if]) self.cli_delete(base_path) self.cli_commit() if __name__ == '__main__': unittest.main(verbosity=2) diff --git a/src/conf_mode/interfaces_openvpn.py b/src/conf_mode/interfaces_openvpn.py index d2665d9e5..9105ce1f8 100755 --- a/src/conf_mode/interfaces_openvpn.py +++ b/src/conf_mode/interfaces_openvpn.py @@ -1,771 +1,771 @@ #!/usr/bin/env python3 # # Copyright (C) 2019-2024 VyOS maintainers and contributors # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 or later as # published by the Free Software Foundation. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see <http://www.gnu.org/licenses/>. import os import re from cryptography.hazmat.primitives.asymmetric import ec from glob import glob from sys import exit from ipaddress import IPv4Address from ipaddress import IPv4Network from ipaddress import IPv6Address from ipaddress import IPv6Network from ipaddress import summarize_address_range from secrets import SystemRandom from shutil import rmtree from vyos.base import DeprecationWarning from vyos.config import Config from vyos.configdict import get_interface_dict from vyos.configdict import is_node_changed from vyos.configverify import verify_vrf from vyos.configverify import verify_bridge_delete from vyos.configverify import verify_mirror_redirect from vyos.configverify import verify_bond_bridge_member from vyos.ifconfig import VTunIf from vyos.pki import load_dh_parameters from vyos.pki import load_private_key from vyos.pki import sort_ca_chain from vyos.pki import verify_ca_chain 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 from vyos.utils.dict import dict_search from vyos.utils.dict import dict_search_args from vyos.utils.list import is_list_equal from vyos.utils.file import makedir from vyos.utils.file import read_file from vyos.utils.file import write_file from vyos.utils.kernel import check_kmod from vyos.utils.kernel import unload_kmod from vyos.utils.process import call from vyos.utils.permission import chown from vyos.utils.process import cmd from vyos.utils.network import is_addr_assigned from vyos.utils.network import interface_exists from vyos import ConfigError from vyos import airbag airbag.enable() user = 'openvpn' group = 'openvpn' cfg_dir = '/run/openvpn' cfg_file = '/run/openvpn/{ifname}.conf' otp_path = '/config/auth/openvpn' otp_file = '/config/auth/openvpn/{ifname}-otp-secrets' secret_chars = list('ABCDEFGHIJKLMNOPQRSTUVWXYZ234567') service_file = '/run/systemd/system/openvpn@{ifname}.service.d/20-override.conf' def get_config(config=None): """ Retrive CLI config as dictionary. Dictionary can never be empty, as at least the interface name will be added or a deleted flag """ if config: conf = config else: conf = Config() base = ['interfaces', 'openvpn'] ifname, openvpn = get_interface_dict(conf, base, with_pki=True) openvpn['auth_user_pass_file'] = '/run/openvpn/{ifname}.pw'.format(**openvpn) if 'deleted' in openvpn: return openvpn if is_node_changed(conf, base + [ifname, 'openvpn-option']): openvpn.update({'restart_required': {}}) if is_node_changed(conf, base + [ifname, 'enable-dco']): openvpn.update({'restart_required': {}}) # We have to get the dict using 'get_config_dict' instead of 'get_interface_dict' # as 'get_interface_dict' merges the defaults in, so we can not check for defaults in there. tmp = conf.get_config_dict(base + [openvpn['ifname']], get_first_key=True) # We have to cleanup the config dict, as default values could enable features # which are not explicitly enabled on the CLI. Example: server mfa totp # originate comes with defaults, which will enable the # totp plugin, even when not set via CLI so we # need to check this first and drop those keys if dict_search('server.mfa.totp', tmp) == None: del openvpn['server']['mfa'] # OpenVPN Data-Channel-Offload (DCO) is a Kernel module. If loaded it applies to all # OpenVPN interfaces. Check if DCO is used by any other interface instance. tmp = conf.get_config_dict(base, key_mangling=('-', '_'), get_first_key=True) for interface, interface_config in tmp.items(): # If one interface has DCO configured, enable it. No need to further check # all other OpenVPN interfaces. We must use a dedicated key to indicate # the Kernel module must be loaded or not. The per interface "offload.dco" # key is required per OpenVPN interface instance. if dict_search('offload.dco', interface_config) != None: openvpn['module_load_dco'] = {} break 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 PSK settings are correct, warn about its deprecation DeprecationWarning('OpenVPN shared-secret support will be removed in future '\ 'VyOS versions. Please migrate your site-to-site tunnels to '\ 'TLS. You can use self-signed certificates with peer fingerprint '\ 'verification, consult the documentation for details.') if tls: if mode == 'site-to-site': # XXX: site-to-site with PSKs is the only mode that can work without TLS, # so 'tls role' is not mandatory for it, # but we need to check that if it uses peer certificate fingerprints rather than PSKs, # then the TLS role is set if ('shared_secret_key' not in tls) and ('role' not in tls): raise ConfigError('"tls role" is required for site-to-site OpenVPN with TLS') if (mode in ['server', 'client']) and ('ca_certificate' not in tls): raise ConfigError(f'Must specify "tls ca-certificate" on openvpn interface {interface},\ it is required in server and client modes') else: if ('ca_certificate' not in tls) and ('peer_fingerprint' not in tls): raise ConfigError('Either "tls ca-certificate" or "tls peer-fingerprint" is required\ on openvpn interface {interface} in site-to-site mode') if 'ca_certificate' in tls: for ca_name in tls['ca_certificate']: if ca_name not in pki['ca']: raise ConfigError(f'Invalid CA certificate on openvpn interface {interface}') if len(tls['ca_certificate']) > 1: sorted_chain = sort_ca_chain(tls['ca_certificate'], pki['ca']) if not verify_ca_chain(sorted_chain, pki['ca']): raise ConfigError(f'CA certificates are not a valid chain') if mode != 'client' and 'auth_key' not 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') is not None: raise ConfigError(f'Cannot use encrypted private key on openvpn interface {interface}') if 'dh_params' in tls: if 'dh' not in pki: raise ConfigError(f'pki dh is not configured') proposed_dh = tls['dh_params'] if proposed_dh not in pki['dh'].keys(): raise ConfigError(f"pki dh '{proposed_dh}' is not configured") 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) return None if 'mode' not in openvpn: raise ConfigError('Must specify OpenVPN operation mode!') # # OpenVPN client mode - VERIFY # if openvpn['mode'] == 'client': if 'local_port' in openvpn: raise ConfigError('Cannot specify "local-port" in client mode') if 'local_host' in openvpn: raise ConfigError('Cannot specify "local-host" in client mode') if 'remote_host' not in openvpn: raise ConfigError('Must specify "remote-host" in client mode') if openvpn['protocol'] == 'tcp-passive': raise ConfigError('Protocol "tcp-passive" is not valid 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 # elif openvpn['mode'] == 'site-to-site': if 'local_address' not in openvpn and 'is_bridge_member' not in openvpn: raise ConfigError('Must specify "local-address" or add interface to bridge') if 'local_address' in openvpn: if len([addr for addr in openvpn['local_address'] if is_ipv4(addr)]) > 1: raise ConfigError('Only one IPv4 local-address can be specified') if len([addr for addr in openvpn['local_address'] if is_ipv6(addr)]) > 1: raise ConfigError('Only one IPv6 local-address can be specified') if openvpn['device_type'] == 'tun': if 'remote_address' not in openvpn: raise ConfigError('Must specify "remote-address"') if 'remote_address' in openvpn: if len([addr for addr in openvpn['remote_address'] if is_ipv4(addr)]) > 1: raise ConfigError('Only one IPv4 remote-address can be specified') if len([addr for addr in openvpn['remote_address'] if is_ipv6(addr)]) > 1: raise ConfigError('Only one IPv6 remote-address can be specified') if not 'local_address' in openvpn: raise ConfigError('"remote-address" requires "local-address"') v4loAddr = [addr for addr in openvpn['local_address'] if is_ipv4(addr)] v4remAddr = [addr for addr in openvpn['remote_address'] if is_ipv4(addr)] if v4loAddr and not v4remAddr: raise ConfigError('IPv4 "local-address" requires IPv4 "remote-address"') elif v4remAddr and not v4loAddr: raise ConfigError('IPv4 "remote-address" requires IPv4 "local-address"') v6remAddr = [addr for addr in openvpn['remote_address'] if is_ipv6(addr)] v6loAddr = [addr for addr in openvpn['local_address'] if is_ipv6(addr)] if v6loAddr and not v6remAddr: raise ConfigError('IPv6 "local-address" requires IPv6 "remote-address"') elif v6remAddr and not v6loAddr: raise ConfigError('IPv6 "remote-address" requires IPv6 "local-address"') if is_list_equal(v4loAddr, v4remAddr) or is_list_equal(v6loAddr, v6remAddr): raise ConfigError('"local-address" and "remote-address" cannot be the same') if dict_search('local_host', openvpn) in dict_search('local_address', openvpn): raise ConfigError('"local-address" cannot be the same as "local-host"') if dict_search('remote_host', openvpn) in dict_search('remote_address', openvpn): raise ConfigError('"remote-address" and "remote-host" can not be the same') if openvpn['device_type'] == 'tap' and 'local_address' in openvpn: # we can only have one local_address, this is ensured above v4addr = None for laddr in openvpn['local_address']: if is_ipv4(laddr): v4addr = laddr break if v4addr in openvpn['local_address'] and 'subnet_mask' not in openvpn['local_address'][v4addr]: raise ConfigError('Must specify IPv4 "subnet-mask" for local-address') - if dict_search('encryption.ncp_ciphers', openvpn): - raise ConfigError('NCP ciphers can only be used in client or server mode') + if dict_search('encryption.data_ciphers', openvpn): + raise ConfigError('Cipher negotiation can only be used in client or server mode') else: # checks for client-server or site-to-site bridged if 'local_address' in openvpn or 'remote_address' in openvpn: raise ConfigError('Cannot specify "local-address" or "remote-address" ' \ 'in client/server or bridge mode') # # OpenVPN server mode - VERIFY # if openvpn['mode'] == 'server': if openvpn['protocol'] == 'tcp-active': raise ConfigError('Protocol "tcp-active" is not valid in server mode') if dict_search('authentication.username', openvpn) or dict_search('authentication.password', openvpn): raise ConfigError('Cannot specify "authentication" in server mode') if 'remote_port' in openvpn: raise ConfigError('Cannot specify "remote-port" in server mode') if 'remote_host' in openvpn: raise ConfigError('Cannot specify "remote-host" in server mode') tmp = dict_search('server.subnet', openvpn) if tmp: v4_subnets = len([subnet for subnet in tmp if is_ipv4(subnet)]) v6_subnets = len([subnet for subnet in tmp if is_ipv6(subnet)]) if v4_subnets > 1: raise ConfigError('Cannot specify more than 1 IPv4 server subnet') if v6_subnets > 1: raise ConfigError('Cannot specify more than 1 IPv6 server subnet') for subnet in tmp: if is_ipv4(subnet): subnet = IPv4Network(subnet) if openvpn['device_type'] == 'tun' and subnet.prefixlen > 29: raise ConfigError('Server subnets smaller than /29 with device type "tun" are not supported') elif openvpn['device_type'] == 'tap' and subnet.prefixlen > 30: raise ConfigError('Server subnets smaller than /30 with device type "tap" are not supported') for client in (dict_search('client', openvpn) or []): if client['ip'] and not IPv4Address(client['ip'][0]) in subnet: raise ConfigError(f'Client "{client["name"]}" IP {client["ip"][0]} not in server subnet {subnet}') else: if 'is_bridge_member' not in openvpn: raise ConfigError('Must specify "server subnet" or add interface to bridge in server mode') if hasattr(dict_search('server.client', openvpn), '__iter__'): for client_k, client_v in dict_search('server.client', openvpn).items(): if (client_v.get('ip') and len(client_v['ip']) > 1) or (client_v.get('ipv6_ip') and len(client_v['ipv6_ip']) > 1): raise ConfigError(f'Server client "{client_k}": cannot specify more than 1 IPv4 and 1 IPv6 IP') if dict_search('server.bridge', openvpn): # check if server bridge is a tap interfaces if not openvpn['device_type'] == 'tap' and dict_search('server.bridge', openvpn): raise ConfigError('Must specify "device-type tap" with server bridge mode') elif not (dict_search('server.bridge.start', openvpn) and dict_search('server.bridge.stop', openvpn)): raise ConfigError('Server bridge requires both start and stop addresses') else: v4PoolStart = IPv4Address(dict_search('server.bridge.start', openvpn)) v4PoolStop = IPv4Address(dict_search('server.bridge.stop', openvpn)) if v4PoolStart > v4PoolStop: raise ConfigError(f'Server bridge start address {v4PoolStart} is larger than stop address {v4PoolStop}') v4PoolSize = int(v4PoolStop) - int(v4PoolStart) if v4PoolSize >= 65536: raise ConfigError(f'Server bridge is too large [{v4PoolStart} -> {v4PoolStop} = {v4PoolSize}], maximum is 65536 addresses.') if dict_search('server.client_ip_pool', openvpn): if not (dict_search('server.client_ip_pool.start', openvpn) and dict_search('server.client_ip_pool.stop', openvpn)): raise ConfigError('Server client-ip-pool requires both start and stop addresses') else: v4PoolStart = IPv4Address(dict_search('server.client_ip_pool.start', openvpn)) v4PoolStop = IPv4Address(dict_search('server.client_ip_pool.stop', openvpn)) if v4PoolStart > v4PoolStop: raise ConfigError(f'Server client-ip-pool start address {v4PoolStart} is larger than stop address {v4PoolStop}') v4PoolSize = int(v4PoolStop) - int(v4PoolStart) if v4PoolSize >= 65536: raise ConfigError(f'Server client-ip-pool is too large [{v4PoolStart} -> {v4PoolStop} = {v4PoolSize}], maximum is 65536 addresses.') v4PoolNets = list(summarize_address_range(v4PoolStart, v4PoolStop)) for client in (dict_search('client', openvpn) or []): if client['ip']: for v4PoolNet in v4PoolNets: if IPv4Address(client['ip'][0]) in v4PoolNet: print(f'Warning: Client "{client["name"]}" IP {client["ip"][0]} is in server IP pool, it is not reserved for this client.') # configuring a client_ip_pool will set 'server ... nopool' which is currently incompatible with 'server-ipv6' (probably to be fixed upstream) for subnet in (dict_search('server.subnet', openvpn) or []): if is_ipv6(subnet): raise ConfigError(f'Setting client-ip-pool is incompatible having an IPv6 server subnet.') for subnet in (dict_search('server.subnet', openvpn) or []): if is_ipv6(subnet): tmp = dict_search('client_ipv6_pool.base', openvpn) if tmp: if not dict_search('server.client_ip_pool', openvpn): raise ConfigError('IPv6 server pool requires an IPv4 server pool') if int(tmp.split('/')[1]) >= 112: raise ConfigError('IPv6 server pool must be larger than /112') # # todo - weird logic # v6PoolStart = IPv6Address(tmp) v6PoolStop = IPv6Network((v6PoolStart, openvpn['server_ipv6_pool_prefixlen']), strict=False)[-1] # don't remove the parentheses, it's a 2-tuple v6PoolSize = int(v6PoolStop) - int(v6PoolStart) if int(openvpn['server_ipv6_pool_prefixlen']) > 96 else 65536 if v6PoolSize < v4PoolSize: raise ConfigError(f'IPv6 server pool must be at least as large as the IPv4 pool (current sizes: IPv6={v6PoolSize} IPv4={v4PoolSize})') v6PoolNets = list(summarize_address_range(v6PoolStart, v6PoolStop)) for client in (dict_search('client', openvpn) or []): if client['ipv6_ip']: for v6PoolNet in v6PoolNets: if IPv6Address(client['ipv6_ip'][0]) in v6PoolNet: print(f'Warning: Client "{client["name"]}" IP {client["ipv6_ip"][0]} is in server IP pool, it is not reserved for this client.') if 'topology' in openvpn['server']: if openvpn['server']['topology'] == 'net30': DeprecationWarning('Topology net30 is deprecated '\ 'and will be removed in future VyOS versions. '\ 'Switch to "subnet" or "p2p"' ) # add mfa users to the file the mfa plugin uses if dict_search('server.mfa.totp', openvpn): user_data = '' if not os.path.isfile(otp_file.format(**openvpn)): write_file(otp_file.format(**openvpn), user_data, user=user, group=group, mode=0o644) ovpn_users = read_file(otp_file.format(**openvpn)) for client in (dict_search('server.client', openvpn) or []): exists = None for ovpn_user in ovpn_users.split('\n'): if re.search('^' + client + ' ', ovpn_user): user_data += f'{ovpn_user}\n' exists = 'true' if not exists: random = SystemRandom() totp_secret = ''.join(random.choice(secret_chars) for _ in range(16)) user_data += f'{client} otp totp:sha1:base32:{totp_secret}::xxx *\n' write_file(otp_file.format(**openvpn), user_data, user=user, group=group, mode=0o644) else: # checks for both client and site-to-site go here if dict_search('server.reject_unconfigured_clients', openvpn): raise ConfigError('Option reject-unconfigured-clients only supported in server mode') if 'replace_default_route' in openvpn and 'remote_host' not in openvpn: raise ConfigError('Cannot set "replace-default-route" without "remote-host"') # # OpenVPN common verification section # not depending on any operation mode # # verify specified IP address is present on any interface on this system if 'local_host' in openvpn: if not is_addr_assigned(openvpn['local_host']): print('local-host IP address "{local_host}" not assigned' \ ' to any interface'.format(**openvpn)) # TCP active if openvpn['protocol'] == 'tcp-active': if 'local_port' in openvpn: raise ConfigError('Cannot specify "local-port" with "tcp-active"') if 'remote_host' not in openvpn: raise ConfigError('Must specify "remote-host" with "tcp-active"') # # TLS/encryption # if 'shared_secret_key' in openvpn: if dict_search('encryption.cipher', openvpn) in ['aes128gcm', 'aes192gcm', 'aes256gcm']: raise ConfigError('GCM encryption with shared-secret-key not supported') if 'tls' in openvpn: 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_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_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 '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): raise ConfigError('"encryption cipher" option is deprecated for TLS mode. ' - 'Use "encryption ncp-ciphers" instead') + 'Use "encryption data-ciphers" instead') if dict_search('encryption.cipher', openvpn) == 'none': print('Warning: "encryption none" was specified!') print('No encryption will be performed and data is transmitted in ' \ 'plain text over the network!') verify_pki(openvpn) # # Auth user/pass # if (dict_search('authentication.username', openvpn) and not dict_search('authentication.password', openvpn)): raise ConfigError('Password for authentication is missing') if (dict_search('authentication.password', openvpn) and not dict_search('authentication.username', openvpn)): raise ConfigError('Username for authentication is missing') verify_vrf(openvpn) verify_bond_bridge_member(openvpn) verify_mirror_redirect(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') if shared_secret_key: pki_key = pki['openvpn']['shared_secret'][shared_secret_key] key_path = os.path.join(cfg_dir, f'{interface}_shared.key') write_file(key_path, wrap_openvpn_key(pki_key['key']), user=user, group=group) if tls: if 'ca_certificate' in tls: cert_path = os.path.join(cfg_dir, f'{interface}_ca.pem') crl_path = os.path.join(cfg_dir, f'{interface}_crl.pem') if os.path.exists(cert_path): os.unlink(cert_path) if os.path.exists(crl_path): os.unlink(crl_path) for cert_name in sort_ca_chain(tls['ca_certificate'], pki['ca']): pki_ca = pki['ca'][cert_name] if 'certificate' in pki_ca: write_file(cert_path, wrap_certificate(pki_ca['certificate']) + "\n", user=user, group=group, mode=0o600, append=True) if 'crl' in pki_ca: for crl in pki_ca['crl']: write_file(crl_path, wrap_crl(crl) + "\n", user=user, group=group, mode=0o600, append=True) 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') write_file(cert_path, wrap_certificate(pki_cert['certificate']), user=user, group=group, mode=0o600) if 'private' in pki_cert and 'key' in pki_cert['private']: key_path = os.path.join(cfg_dir, f'{interface}_cert.key') write_file(key_path, wrap_private_key(pki_cert['private']['key']), user=user, group=group, mode=0o600) 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') write_file(dh_path, wrap_dh_parameters(pki_dh['parameters']), user=user, group=group, mode=0o600) 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') write_file(key_path, wrap_openvpn_key(pki_key['key']), user=user, group=group, mode=0o600) 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') write_file(key_path, wrap_openvpn_key(pki_key['key']), user=user, group=group, mode=0o600) def generate(openvpn): if 'deleted' in openvpn: # remove totp secrets file if totp is not configured if os.path.isfile(otp_file.format(**openvpn)): os.remove(otp_file.format(**openvpn)) return None if 'disable' in openvpn: return None interface = openvpn['ifname'] directory = os.path.dirname(cfg_file.format(**openvpn)) openvpn['plugin_dir'] = '/usr/lib/openvpn' # create base config directory on demand makedir(directory, user, group) # enforce proper permissions on /run/openvpn chown(directory, user, group) # we can't know in advance which clients have been removed, # thus all client configs will be removed and re-added on demand ccd_dir = os.path.join(directory, 'ccd', interface) if os.path.isdir(ccd_dir): rmtree(ccd_dir, ignore_errors=True) # Remove systemd directories with overrides service_dir = os.path.dirname(service_file.format(**openvpn)) if os.path.isdir(service_dir): rmtree(service_dir, ignore_errors=True) # create client config directory on demand makedir(ccd_dir, user, group) # Fix file permissons for keys generate_pki_files(openvpn) # Generate User/Password authentication file if 'authentication' in openvpn: render(openvpn['auth_user_pass_file'], 'openvpn/auth.pw.j2', openvpn, user=user, group=group, permission=0o600) else: # delete old auth file if present if os.path.isfile(openvpn['auth_user_pass_file']): os.remove(openvpn['auth_user_pass_file']) # Generate client specific configuration 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 ... client_config['server_subnet'] = dict_search('server.subnet', openvpn) render(client_file, 'openvpn/client.conf.j2', client_config, user=user, group=group) # we need to support quoting of raw parameters from OpenVPN CLI # see https://vyos.dev/T1632 render(cfg_file.format(**openvpn), 'openvpn/server.conf.j2', openvpn, formater=lambda _: _.replace(""", '"'), user=user, group=group) # Render 20-override.conf for OpenVPN service render(service_file.format(**openvpn), 'openvpn/service-override.conf.j2', openvpn, formater=lambda _: _.replace(""", '"'), user=user, group=group) # Reload systemd services config to apply an override call(f'systemctl daemon-reload') return None def apply(openvpn): interface = openvpn['ifname'] # Do some cleanup when OpenVPN is disabled/deleted if 'deleted' in openvpn or 'disable' in openvpn: call(f'systemctl stop openvpn@{interface}.service') for cleanup_file in glob(f'/run/openvpn/{interface}.*'): if os.path.isfile(cleanup_file): os.unlink(cleanup_file) if interface_exists(interface): VTunIf(interface).remove() # dynamically load/unload DCO Kernel extension if requested dco_module = 'ovpn_dco_v2' if 'module_load_dco' in openvpn: check_kmod(dco_module) else: unload_kmod(dco_module) # Now bail out early if interface is disabled or got deleted if 'deleted' in openvpn or 'disable' in openvpn: return None # verify specified IP address is present on any interface on this system # Allow to bind service to nonlocal address, if it virtaual-vrrp address # or if address will be assign later if 'local_host' in openvpn: if not is_addr_assigned(openvpn['local_host']): cmd('sysctl -w net.ipv4.ip_nonlocal_bind=1') # No matching OpenVPN process running - maybe it got killed or none # existed - nevertheless, spawn new OpenVPN process action = 'reload-or-restart' if 'restart_required' in openvpn: action = 'restart' call(f'systemctl {action} openvpn@{interface}.service') o = VTunIf(**openvpn) o.update(openvpn) return None 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/migration-scripts/openvpn/3-to-4 b/src/migration-scripts/openvpn/3-to-4 new file mode 100644 index 000000000..d3c76c7d3 --- /dev/null +++ b/src/migration-scripts/openvpn/3-to-4 @@ -0,0 +1,30 @@ +#!/usr/bin/env python3 +# Copyright 2024 VyOS maintainers and contributors <maintainers@vyos.io> +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with this library. If not, see <http://www.gnu.org/licenses/>. +# Renames ncp-ciphers option to data-ciphers + +from vyos.configtree import ConfigTree + +def migrate(config: ConfigTree) -> None: + if not config.exists(['interfaces', 'openvpn']): + # Nothing to do + return + + ovpn_intfs = config.list_nodes(['interfaces', 'openvpn']) + for i in ovpn_intfs: + #Rename 'encryption ncp-ciphers' with 'encryption data-ciphers' + ncp_cipher_path = ['interfaces', 'openvpn', i, 'encryption', 'ncp-ciphers'] + if config.exists(ncp_cipher_path): + config.rename(ncp_cipher_path, 'data-ciphers')