diff options
102 files changed, 1255 insertions, 556 deletions
diff --git a/data/op-mode-standardized.json b/data/op-mode-standardized.json index 90ef804f3..db13eeb5a 100644 --- a/data/op-mode-standardized.json +++ b/data/op-mode-standardized.json @@ -6,6 +6,7 @@ "memory.py", "nat.py", "neighbor.py", +"openconnect.py", "route.py", "ipsec.py", "version.py", diff --git a/data/templates/firewall/nftables-nat66.j2 b/data/templates/firewall/nftables-nat66.j2 index 003b138b2..2fe04b4ff 100644 --- a/data/templates/firewall/nftables-nat66.j2 +++ b/data/templates/firewall/nftables-nat66.j2 @@ -3,8 +3,10 @@ {% macro nptv6_rule(rule,config, chain) %} {% set comment = '' %} {% set base_log = '' %} -{% set src_prefix = 'ip6 saddr ' ~ config.source.prefix if config.source.prefix is vyos_defined %} -{% set dest_address = 'ip6 daddr ' ~ config.destination.address if config.destination.address is vyos_defined %} +{% set dst_prefix = 'ip6 daddr ' ~ config.destination.prefix.replace('!','!= ') if config.destination.prefix is vyos_defined %} +{% set src_prefix = 'ip6 saddr ' ~ config.source.prefix.replace('!','!= ') if config.source.prefix is vyos_defined %} +{% set source_address = 'ip6 saddr ' ~ config.source.address.replace('!','!= ') if config.source.address is vyos_defined %} +{% set dest_address = 'ip6 daddr ' ~ config.destination.address.replace('!','!= ') if config.destination.address is vyos_defined %} {% if chain is vyos_defined('PREROUTING') %} {% set comment = 'DST-NAT66-' ~ rule %} {% set base_log = '[NAT66-DST-' ~ rule %} @@ -52,9 +54,19 @@ {% if src_prefix is vyos_defined %} {% set output = output ~ ' ' ~ src_prefix %} {% endif %} +{% if dst_prefix is vyos_defined %} +{% set output = output ~ ' ' ~ dst_prefix %} +{% endif %} +{% if source_address is vyos_defined %} +{% set output = output ~ ' ' ~ source_address %} +{% endif %} {% if dest_address is vyos_defined %} {% set output = output ~ ' ' ~ dest_address %} {% endif %} +{% if config.exclude is vyos_defined %} +{# rule has been marked as 'exclude' thus we simply return here #} +{% set trns_address = 'return' %} +{% endif %} {% if trns_address is vyos_defined %} {% set output = output ~ ' ' ~ trns_address %} {% endif %} diff --git a/data/templates/firewall/upnpd.conf.j2 b/data/templates/firewall/upnpd.conf.j2 index 27573cbf9..e964fc696 100644 --- a/data/templates/firewall/upnpd.conf.j2 +++ b/data/templates/firewall/upnpd.conf.j2 @@ -71,7 +71,7 @@ min_lifetime={{ pcp_lifetime.min }} {% if friendly_name is vyos_defined %} # Name of this service, default is "`uname -s` router" -friendly_name= {{ friendly_name }} +friendly_name={{ friendly_name }} {% endif %} # Manufacturer name, default is "`uname -s`" @@ -117,7 +117,10 @@ clean_ruleset_threshold=10 clean_ruleset_interval=600 # Anchor name in pf (default is miniupnpd) -anchor=VyOS +# Something wrong with this option "anchor", comment it out +# vyos@r14# miniupnpd -vv -f /run/upnp/miniupnp.conf +# invalid option in file /run/upnp/miniupnp.conf line 74 : anchor=VyOS +#anchor=VyOS uuid={{ uuid }} @@ -129,7 +132,7 @@ lease_file=/config/upnp.leases #serial=12345678 #model_number=1 -{% if rules is vyos_defined %} +{% if rule is vyos_defined %} # UPnP permission rules # (allow|deny) (external port range) IP/mask (internal port range) # A port range is <min port>-<max port> or <port> if there is only @@ -142,9 +145,9 @@ lease_file=/config/upnp.leases # modify the IP ranges to match their own internal networks, and # also consider implementing network-specific restrictions # CAUTION: failure to enforce any rules may permit insecure requests to be made! -{% for rule, config in rules.items() %} -{% if config.disable is vyos_defined %} -{{ config.action }} {{ config.external_port_range }} {{ config.ip }} {{ config.internal_port_range }} +{% for rule, config in rule.items() %} +{% if config.disable is not vyos_defined %} +{{ config.action }} {{ config.external_port_range }} {{ config.ip }}{{ '/32' if '/' not in config.ip else '' }} {{ config.internal_port_range }} {% endif %} {% endfor %} {% endif %} diff --git a/data/templates/frr/bgpd.frr.j2 b/data/templates/frr/bgpd.frr.j2 index 7029f39af..808e9dbe7 100644 --- a/data/templates/frr/bgpd.frr.j2 +++ b/data/templates/frr/bgpd.frr.j2 @@ -38,6 +38,9 @@ {% if config.disable_capability_negotiation is vyos_defined %} neighbor {{ neighbor }} dont-capability-negotiate {% endif %} +{% if config.disable_connected_check is vyos_defined %} + neighbor {{ neighbor }} disable-connected-check +{% endif %} {% if config.ebgp_multihop is vyos_defined %} neighbor {{ neighbor }} ebgp-multihop {{ config.ebgp_multihop }} {% endif %} @@ -231,7 +234,7 @@ {% endif %} {% endmacro %} ! -router bgp {{ local_as }} {{ 'vrf ' ~ vrf if vrf is vyos_defined }} +router bgp {{ system_as }} {{ 'vrf ' ~ vrf if vrf is vyos_defined }} {% if parameters.ebgp_requires_policy is vyos_defined %} bgp ebgp-requires-policy {% else %} diff --git a/data/templates/ipsec/swanctl/profile.j2 b/data/templates/ipsec/swanctl/profile.j2 index d4f417378..8519a84f8 100644 --- a/data/templates/ipsec/swanctl/profile.j2 +++ b/data/templates/ipsec/swanctl/profile.j2 @@ -9,6 +9,10 @@ version = {{ ike.key_exchange[4:] if ike.key_exchange is vyos_defined else "0" }} rekey_time = {{ ike.lifetime }}s keyingtries = 0 +{% if ike.dead_peer_detection is vyos_defined %} + dpd_timeout = {{ ike.dead_peer_detection.timeout }} + dpd_delay = {{ ike.dead_peer_detection.interval }} +{% endif %} {% if profile_conf.authentication.mode is vyos_defined('pre-shared-secret') %} local { auth = psk diff --git a/data/templates/macsec/wpa_supplicant.conf.j2 b/data/templates/macsec/wpa_supplicant.conf.j2 index d2529c50d..1f7ba16f4 100644 --- a/data/templates/macsec/wpa_supplicant.conf.j2 +++ b/data/templates/macsec/wpa_supplicant.conf.j2 @@ -47,6 +47,12 @@ network={ # 1: Integrity only macsec_integ_only={{ '0' if security.encrypt is vyos_defined else '1' }} + # macsec_csindex: IEEE 802.1X/MACsec cipher suite + # 0 = GCM-AES-128 + # 1 = GCM-AES-256 +{# security.cipher is a mandatory key #} + macsec_csindex={{ '1' if security.cipher is vyos_defined('gcm-aes-256') else '0' }} + {% if security.encrypt is vyos_defined %} # mka_cak, mka_ckn, and mka_priority: IEEE 802.1X/MACsec pre-shared key mode # This allows to configure MACsec with a pre-shared key using a (CAK,CKN) pair. @@ -62,12 +68,6 @@ network={ # mka_priority (Priority of MKA Actor) is in 0..255 range with 255 being # default priority mka_priority={{ security.mka.priority }} - - # macsec_csindex: IEEE 802.1X/MACsec cipher suite - # 0 = GCM-AES-128 - # 1 = GCM-AES-256 -{# security.cipher is a mandatory key #} - macsec_csindex={{ '1' if security.cipher is vyos_defined('gcm-aes-256') else '0' }} {% endif %} {% if security.replay_window is vyos_defined %} diff --git a/data/templates/monitoring/override.conf.j2 b/data/templates/monitoring/override.conf.j2 deleted file mode 100644 index 9f1b4ebec..000000000 --- a/data/templates/monitoring/override.conf.j2 +++ /dev/null @@ -1,7 +0,0 @@ -[Unit] -After=vyos-router.service -ConditionPathExists=/run/telegraf/vyos-telegraf.conf -[Service] -Environment=INFLUX_TOKEN={{ influxdb.authentication.token }} -CapabilityBoundingSet=CAP_NET_RAW CAP_NET_ADMIN CAP_SYS_ADMIN -AmbientCapabilities=CAP_NET_RAW CAP_NET_ADMIN diff --git a/data/templates/ocserv/ocserv_config.j2 b/data/templates/ocserv/ocserv_config.j2 index 8418a2185..e0cad5181 100644 --- a/data/templates/ocserv/ocserv_config.j2 +++ b/data/templates/ocserv/ocserv_config.j2 @@ -56,36 +56,32 @@ ban-reset-time = 300 # The name to use for the tun device device = sslvpn -# An alternative way of specifying the network: -{% if network_settings %} # DNS settings -{% if network_settings.name_server is string %} -dns = {{ network_settings.name_server }} -{% else %} -{% for dns in network_settings.name_server %} +{% if network_settings.name_server is vyos_defined %} +{% for dns in network_settings.name_server %} dns = {{ dns }} -{% endfor %} -{% endif %} +{% endfor %} +{% endif %} + # IPv4 network pool -{% if network_settings.client_ip_settings %} -{% if network_settings.client_ip_settings.subnet %} +{% if network_settings.client_ip_settings.subnet is vyos_defined %} ipv4-network = {{ network_settings.client_ip_settings.subnet }} -{% endif %} -{% endif %} +{% endif %} + # IPv6 network pool -{% if network_settings.client_ipv6_pool %} -{% if network_settings.client_ipv6_pool.prefix %} +{% if network_settings.client_ipv6_pool.prefix is vyos_defined %} ipv6-network = {{ network_settings.client_ipv6_pool.prefix }} ipv6-subnet-prefix = {{ network_settings.client_ipv6_pool.mask }} -{% endif %} -{% endif %} {% endif %} -{% if network_settings.push_route is string %} -route = {{ network_settings.push_route }} -{% else %} +{% if network_settings.push_route is vyos_defined %} {% for route in network_settings.push_route %} route = {{ route }} {% endfor %} {% endif %} +{% if network_settings.split_dns is vyos_defined %} +{% for tmp in network_settings.split_dns %} +split-dns = {{ tmp }} +{% endfor %} +{% endif %} diff --git a/data/templates/telegraf/override.conf.j2 b/data/templates/telegraf/override.conf.j2 new file mode 100644 index 000000000..d30bb19de --- /dev/null +++ b/data/templates/telegraf/override.conf.j2 @@ -0,0 +1,15 @@ +{% set vrf_command = 'ip vrf exec ' ~ vrf ~ ' ' if vrf is vyos_defined else '' %} +[Unit] +After= +After=vyos-router.service +ConditionPathExists=/run/telegraf/telegraf.conf + +[Service] +ExecStart= +ExecStart={{ vrf_command }}/usr/bin/telegraf --config /run/telegraf/telegraf.conf --config-directory /etc/telegraf/telegraf.d --pidfile /run/telegraf/telegraf.pid +PIDFile=/run/telegraf/telegraf.pid +EnvironmentFile= +Environment=INFLUX_TOKEN={{ influxdb.authentication.token }} +CapabilityBoundingSet=CAP_NET_RAW CAP_NET_ADMIN CAP_SYS_ADMIN CAP_BPF CAP_DAC_OVERRIDE +AmbientCapabilities=CAP_NET_RAW CAP_NET_ADMIN + diff --git a/data/templates/monitoring/syslog_telegraf.j2 b/data/templates/telegraf/syslog_telegraf.j2 index cdcbd92a4..cdcbd92a4 100644 --- a/data/templates/monitoring/syslog_telegraf.j2 +++ b/data/templates/telegraf/syslog_telegraf.j2 diff --git a/data/templates/monitoring/telegraf.j2 b/data/templates/telegraf/telegraf.j2 index 6b395692b..6b395692b 100644 --- a/data/templates/monitoring/telegraf.j2 +++ b/data/templates/telegraf/telegraf.j2 diff --git a/debian/control b/debian/control index 6a6ccf602..0db098be6 100644 --- a/debian/control +++ b/debian/control @@ -59,7 +59,7 @@ Depends: frr-rpki-rtrlib, frr-snmp, grc, - hostapd (>= 0.6.8), + hostapd, hvinfo, igmpproxy, ipaddrcheck, diff --git a/interface-definitions/include/accel-ppp/radius-additions.xml.i b/interface-definitions/include/accel-ppp/radius-additions.xml.i index 441c9dda5..15ff5165f 100644 --- a/interface-definitions/include/accel-ppp/radius-additions.xml.i +++ b/interface-definitions/include/accel-ppp/radius-additions.xml.i @@ -89,18 +89,7 @@ <defaultValue>3</defaultValue> </leafNode> #include <include/radius-nas-identifier.xml.i> - <leafNode name="nas-ip-address"> - <properties> - <help>NAS-IP-Address attribute sent to RADIUS</help> - <constraint> - <validator name="ipv4-address"/> - </constraint> - <valueHelp> - <format>ipv4</format> - <description>NAS-IP-Address attribute</description> - </valueHelp> - </properties> - </leafNode> + #include <include/radius-nas-ip-address.xml.i> <leafNode name="preallocate-vif"> <properties> <help>Enable attribute NAS-Port-Id in Access-Request</help> diff --git a/interface-definitions/include/bgp/protocol-common-config.xml.i b/interface-definitions/include/bgp/protocol-common-config.xml.i index c1b465e43..d2bcea62a 100644 --- a/interface-definitions/include/bgp/protocol-common-config.xml.i +++ b/interface-definitions/include/bgp/protocol-common-config.xml.i @@ -896,7 +896,7 @@ </tagNode> </children> </node> -<leafNode name="local-as"> +<leafNode name="system-as"> <properties> <help>Autonomous System Number (ASN)</help> <valueHelp> diff --git a/interface-definitions/include/firewall/tcp-flags.xml.i b/interface-definitions/include/firewall/tcp-flags.xml.i index b99896687..5a7b5a8d3 100644 --- a/interface-definitions/include/firewall/tcp-flags.xml.i +++ b/interface-definitions/include/firewall/tcp-flags.xml.i @@ -114,6 +114,23 @@ </node> </children> </node> + <leafNode name="mss"> + <properties> + <help>Maximum segment size (MSS)</help> + <valueHelp> + <format>u32:1-16384</format> + <description>Maximum segment size</description> + </valueHelp> + <valueHelp> + <format><min>-<max></format> + <description>TCP MSS range (use '-' as delimiter)</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 1-16384"/> + <validator name="range" argument="--min=1 --max=16384"/> + </constraint> + </properties> + </leafNode> </children> </node> <!-- include end --> diff --git a/interface-definitions/include/nat-exclude.xml.i b/interface-definitions/include/nat-exclude.xml.i new file mode 100644 index 000000000..4d53cf844 --- /dev/null +++ b/interface-definitions/include/nat-exclude.xml.i @@ -0,0 +1,8 @@ +<!-- include start from nat-exclude.xml.i --> +<leafNode name="exclude"> + <properties> + <help>Exclude packets matching this rule from NAT</help> + <valueless/> + </properties> +</leafNode> +<!-- include end --> diff --git a/interface-definitions/include/nat-rule.xml.i b/interface-definitions/include/nat-rule.xml.i index bdb86ed9b..84941aa6a 100644 --- a/interface-definitions/include/nat-rule.xml.i +++ b/interface-definitions/include/nat-rule.xml.i @@ -23,12 +23,7 @@ </children> </node> #include <include/generic-disable-node.xml.i> - <leafNode name="exclude"> - <properties> - <help>Exclude packets matching this rule from NAT</help> - <valueless/> - </properties> - </leafNode> + #include <include/nat-exclude.xml.i> <leafNode name="log"> <properties> <help>NAT rule logging</help> diff --git a/interface-definitions/include/radius-nas-ip-address.xml.i b/interface-definitions/include/radius-nas-ip-address.xml.i new file mode 100644 index 000000000..8d0a3fd6a --- /dev/null +++ b/interface-definitions/include/radius-nas-ip-address.xml.i @@ -0,0 +1,14 @@ +<!-- include start from radius-nas-ip-address.xml.i --> +<leafNode name="nas-ip-address"> + <properties> + <help>NAS-IP-Address attribute sent to RADIUS</help> + <constraint> + <validator name="ipv4-address"/> + </constraint> + <valueHelp> + <format>ipv4</format> + <description>NAS-IP-Address attribute</description> + </valueHelp> + </properties> +</leafNode> +<!-- include end --> diff --git a/interface-definitions/include/version/bgp-version.xml.i b/interface-definitions/include/version/bgp-version.xml.i index 15bc5abd4..ced49e729 100644 --- a/interface-definitions/include/version/bgp-version.xml.i +++ b/interface-definitions/include/version/bgp-version.xml.i @@ -1,3 +1,3 @@ <!-- include start from include/version/bgp-version.xml.i --> -<syntaxVersion component='bgp' version='2'></syntaxVersion> +<syntaxVersion component='bgp' version='3'></syntaxVersion> <!-- include end --> diff --git a/interface-definitions/nat66.xml.in b/interface-definitions/nat66.xml.in index b47f653c6..bde1a6f8d 100644 --- a/interface-definitions/nat66.xml.in +++ b/interface-definitions/nat66.xml.in @@ -35,6 +35,7 @@ <valueless/> </properties> </leafNode> + #include <include/nat-exclude.xml.i> <leafNode name="log"> <properties> <help>NAT66 rule logging</help> @@ -49,6 +50,30 @@ </completionHelp> </properties> </leafNode> + <node name="destination"> + <properties> + <help>IPv6 destination prefix options</help> + </properties> + <children> + <leafNode name="prefix"> + <properties> + <help>IPv6 prefix to be translated</help> + <valueHelp> + <format>ipv6net</format> + <description>IPv6 prefix</description> + </valueHelp> + <valueHelp> + <format>!ipv6net</format> + <description>Match everything except the specified IPv6 prefix</description> + </valueHelp> + <constraint> + <validator name="ipv6-prefix"/> + <validator name="ipv6-prefix-exclude"/> + </constraint> + </properties> + </leafNode> + </children> + </node> <node name="source"> <properties> <help>IPv6 source prefix options</help> @@ -61,8 +86,13 @@ <format>ipv6net</format> <description>IPv6 prefix</description> </valueHelp> + <valueHelp> + <format>!ipv6net</format> + <description>Match everything except the specified IPv6 prefix</description> + </valueHelp> <constraint> <validator name="ipv6-prefix"/> + <validator name="ipv6-prefix-exclude"/> </constraint> </properties> </leafNode> @@ -133,6 +163,7 @@ <valueless/> </properties> </leafNode> + #include <include/nat-exclude.xml.i> <leafNode name="log"> <properties> <help>NAT66 rule logging</help> @@ -164,9 +195,53 @@ <format>ipv6net</format> <description>IPv6 prefix</description> </valueHelp> + <valueHelp> + <format>!ipv6</format> + <description>Match everything except the specified IPv6 address</description> + </valueHelp> + <valueHelp> + <format>!ipv6net</format> + <description>Match everything except the specified IPv6 prefix</description> + </valueHelp> + <constraint> + <validator name="ipv6-address"/> + <validator name="ipv6-prefix"/> + <validator name="ipv6-address-exclude"/> + <validator name="ipv6-prefix-exclude"/> + </constraint> + </properties> + </leafNode> + </children> + </node> + <node name="source"> + <properties> + <help>IPv6 source prefix options</help> + </properties> + <children> + <leafNode name="address"> + <properties> + <help>IPv6 address or prefix to be translated</help> + <valueHelp> + <format>ipv6</format> + <description>IPv6 address</description> + </valueHelp> + <valueHelp> + <format>ipv6net</format> + <description>IPv6 prefix</description> + </valueHelp> + <valueHelp> + <format>!ipv6</format> + <description>Match everything except the specified IPv6 address</description> + </valueHelp> + <valueHelp> + <format>!ipv6net</format> + <description>Match everything except the specified IPv6 prefix</description> + </valueHelp> <constraint> <validator name="ipv6-address"/> <validator name="ipv6-prefix"/> + <validator name="ipv6-address-exclude"/> + <validator name="ipv6-prefix-exclude"/> </constraint> </properties> </leafNode> diff --git a/interface-definitions/policy.xml.in b/interface-definitions/policy.xml.in index cc1de609d..e794c4b90 100644 --- a/interface-definitions/policy.xml.in +++ b/interface-definitions/policy.xml.in @@ -392,7 +392,7 @@ <description>Prefix to match against</description> </valueHelp> <constraint> - <validator name="ip-prefix"/> + <validator name="ipv4-prefix"/> </constraint> </properties> </leafNode> diff --git a/interface-definitions/service-monitoring-telegraf.xml.in b/interface-definitions/service-monitoring-telegraf.xml.in index 36f40a539..68215dba4 100644 --- a/interface-definitions/service-monitoring-telegraf.xml.in +++ b/interface-definitions/service-monitoring-telegraf.xml.in @@ -10,7 +10,7 @@ <children> <node name="telegraf" owner="${vyos_conf_scripts_dir}/service_monitoring_telegraf.py"> <properties> - <help>Telegraf monitoring</help> + <help>Telegraf metric collector</help> </properties> <children> <node name="influxdb"> @@ -228,27 +228,7 @@ </constraint> </properties> </leafNode> - <leafNode name="listen-address"> - <properties> - <help>Local IP addresses to listen on</help> - <completionHelp> - <script>${vyos_completion_dir}/list_local_ips.sh --both</script> - </completionHelp> - <valueHelp> - <format>ipv4</format> - <description>IPv4 address to listen for incoming connections</description> - </valueHelp> - <valueHelp> - <format>ipv6</format> - <description>IPv6 address to listen for incoming connections</description> - </valueHelp> - <constraint> - <validator name="ipv4-address"/> - <validator name="ipv6-address"/> - <validator name="ipv6-link-local"/> - </constraint> - </properties> - </leafNode> + #include <include/listen-address.xml.i> <leafNode name="metric-version"> <properties> <help>Metric version control mapping from Telegraf to Prometheus format</help> @@ -291,21 +271,10 @@ </leafNode> </children> </node> - <leafNode name="url"> - <properties> - <help>Remote URL</help> - <valueHelp> - <format>url</format> - <description>Remote URL to Splunk collector</description> - </valueHelp> - <constraint> - <regex>^(http(s?):\/\/.*):(\d*)\/?(.*)</regex> - </constraint> - <constraintErrorMessage>Incorrect URL format</constraintErrorMessage> - </properties> - </leafNode> + #include <include/monitoring/url.xml.i> </children> </node> + #include <include/interface/vrf.xml.i> </children> </node> </children> diff --git a/interface-definitions/service-upnp.xml.in b/interface-definitions/service-upnp.xml.in index a129b7260..ec23d87df 100644 --- a/interface-definitions/service-upnp.xml.in +++ b/interface-definitions/service-upnp.xml.in @@ -103,19 +103,19 @@ </valueHelp> <valueHelp> <format>ipv4</format> - <description>IP address to listen for incoming connections</description> + <description>IPv4 address to listen for incoming connections</description> </valueHelp> <valueHelp> - <format>ipv4-prefix</format> - <description>IP prefix to listen for incoming connections</description> + <format>ipv4net</format> + <description>IPv4 prefix to listen for incoming connections</description> </valueHelp> <valueHelp> <format>ipv6</format> - <description>IP address to listen for incoming connections</description> + <description>IPv6 address to listen for incoming connections</description> </valueHelp> <valueHelp> - <format>ipv6-prefix</format> - <description>IP prefix to listen for incoming connections</description> + <format>ipv6net</format> + <description>IPv6 prefix to listen for incoming connections</description> </valueHelp> <multi/> <constraint> @@ -197,10 +197,15 @@ <help>The IP to which this rule applies (REQUIRE)</help> <valueHelp> <format>ipv4</format> + <description>The IPv4 address to which this rule applies</description> + </valueHelp> + <valueHelp> + <format>ipv4net</format> <description>The IPv4 to which this rule applies</description> </valueHelp> <constraint> - <validator name="ipv4-address" /> + <validator name="ipv4-address"/> + <validator name="ipv4-host"/> </constraint> </properties> </leafNode> diff --git a/interface-definitions/system-proxy.xml.in b/interface-definitions/system-proxy.xml.in index 1c06b347f..8fb6bfae5 100644 --- a/interface-definitions/system-proxy.xml.in +++ b/interface-definitions/system-proxy.xml.in @@ -11,7 +11,7 @@ <properties> <help>Proxy URL</help> <constraint> - <regex>http:\/\/[a-z0-9\.]+</regex> + <regex>http(s)?:\/\/[a-z0-9-\.]+</regex> </constraint> </properties> </leafNode> diff --git a/interface-definitions/vpn-l2tp.xml.in b/interface-definitions/vpn-l2tp.xml.in index f734283e7..fd70a76dc 100644 --- a/interface-definitions/vpn-l2tp.xml.in +++ b/interface-definitions/vpn-l2tp.xml.in @@ -215,6 +215,7 @@ </properties> </leafNode> #include <include/radius-nas-identifier.xml.i> + #include <include/radius-nas-ip-address.xml.i> <node name="dae-server"> <properties> <help>IPv4 address and port to bind Dynamic Authorization Extension server (DM/CoA)</help> diff --git a/interface-definitions/vpn-openconnect.xml.in b/interface-definitions/vpn-openconnect.xml.in index 21b47125d..6309863c5 100644 --- a/interface-definitions/vpn-openconnect.xml.in +++ b/interface-definitions/vpn-openconnect.xml.in @@ -265,6 +265,19 @@ </children> </node> #include <include/name-server-ipv4-ipv6.xml.i> + <leafNode name="split-dns"> + <properties> + <help>Domains over which the provided DNS should be used</help> + <valueHelp> + <format>txt</format> + <description>Client prefix length</description> + </valueHelp> + <constraint> + <validator name="fqdn"/> + </constraint> + <multi/> + </properties> + </leafNode> </children> </node> </children> diff --git a/op-mode-definitions/container.xml.in b/op-mode-definitions/container.xml.in index a7048e5ed..97a087ce2 100644 --- a/op-mode-definitions/container.xml.in +++ b/op-mode-definitions/container.xml.in @@ -149,7 +149,7 @@ <path>container name</path> </completionHelp> </properties> - <command>sudo ${vyos_op_scripts_dir}/container.py restart name="$3"</command> + <command>sudo ${vyos_op_scripts_dir}/container.py restart --name="$3"</command> </tagNode> </children> </node> diff --git a/op-mode-definitions/monitor-log.xml.in b/op-mode-definitions/monitor-log.xml.in index 8a02e1f08..975d20465 100644 --- a/op-mode-definitions/monitor-log.xml.in +++ b/op-mode-definitions/monitor-log.xml.in @@ -224,6 +224,43 @@ </properties> <command>journalctl --no-hostname --boot --follow --unit ssh.service</command> </leafNode> + <node name="vpn"> + <properties> + <help>Show log for Virtual Private Network (VPN)</help> + </properties> + <children> + <leafNode name="all"> + <properties> + <help>Monitor last lines of ALL VPNs</help> + </properties> + <command>journalctl --no-hostname --boot --follow --unit strongswan-starter.service --unit accel-ppp@*.service</command> + </leafNode> + <leafNode name="ipsec"> + <properties> + <help>Monitor last lines of IPSec</help> + </properties> + <command>journalctl --no-hostname --boot --follow --unit strongswan-starter.service</command> + </leafNode> + <leafNode name="l2tp"> + <properties> + <help>Monitor last lines of L2TP</help> + </properties> + <command>journalctl --no-hostname --boot --follow --unit accel-ppp@l2tp.service</command> + </leafNode> + <leafNode name="pptp"> + <properties> + <help>Monitor last lines of PPTP</help> + </properties> + <command>journalctl --no-hostname --boot --follow --unit accel-ppp@pptp.service</command> + </leafNode> + <leafNode name="sstp"> + <properties> + <help>Monitor last lines of SSTP</help> + </properties> + <command>journalctl --no-hostname --boot --follow --unit accel-ppp@sstp.service</command> + </leafNode> + </children> + </node> </children> </node> </children> diff --git a/op-mode-definitions/nat.xml.in b/op-mode-definitions/nat.xml.in index 7148c1128..dbc06b930 100644 --- a/op-mode-definitions/nat.xml.in +++ b/op-mode-definitions/nat.xml.in @@ -16,13 +16,13 @@ <properties> <help>Show configured source NAT rules</help> </properties> - <command>${vyos_op_scripts_dir}/nat.py show_rules --direction source</command> + <command>${vyos_op_scripts_dir}/nat.py show_rules --direction source --family inet</command> </node> <node name="statistics"> <properties> <help>Show statistics for configured source NAT rules</help> </properties> - <command>${vyos_op_scripts_dir}/nat.py show_statistics --direction source</command> + <command>${vyos_op_scripts_dir}/nat.py show_statistics --direction source --family inet</command> </node> <node name="translations"> <properties> @@ -58,7 +58,7 @@ <properties> <help>Show configured destination NAT rules</help> </properties> - <command>${vyos_op_scripts_dir}/nat.py show_rules --direction destination</command> + <command>${vyos_op_scripts_dir}/nat.py show_rules --direction destination --family inet</command> </node> <node name="statistics"> <properties> diff --git a/op-mode-definitions/nat66.xml.in b/op-mode-definitions/nat66.xml.in index 1ec46eb11..aba2d6add 100644 --- a/op-mode-definitions/nat66.xml.in +++ b/op-mode-definitions/nat66.xml.in @@ -16,7 +16,7 @@ <properties> <help>Show configured source NAT66 rules</help> </properties> - <command>${vyos_op_scripts_dir}/show_nat66_rules.py --source</command> + <command>${vyos_op_scripts_dir}/nat.py show_rules --direction source --family inet6</command> </node> <node name="statistics"> <properties> @@ -58,7 +58,7 @@ <properties> <help>Show configured destination NAT66 rules</help> </properties> - <command>${vyos_op_scripts_dir}/show_nat66_rules.py --destination</command> + <command>${vyos_op_scripts_dir}/nat.py show_rules --direction destination --family inet6</command> </node> <node name="statistics"> <properties> diff --git a/op-mode-definitions/openconnect.xml.in b/op-mode-definitions/openconnect.xml.in index 9343637c0..88e1f9f15 100644 --- a/op-mode-definitions/openconnect.xml.in +++ b/op-mode-definitions/openconnect.xml.in @@ -11,7 +11,7 @@ <properties> <help>Show active OpenConnect server sessions</help> </properties> - <command>${vyos_op_scripts_dir}/openconnect-control.py --action="show_sessions"</command> + <command>${vyos_op_scripts_dir}/openconnect.py show_sessions</command> </leafNode> <tagNode name="user"> <properties> diff --git a/op-mode-definitions/show-conntrack.xml.in b/op-mode-definitions/show-conntrack.xml.in index 8d921e6a5..4cdcffcdb 100644 --- a/op-mode-definitions/show-conntrack.xml.in +++ b/op-mode-definitions/show-conntrack.xml.in @@ -7,6 +7,12 @@ <help>Show conntrack tables entries</help> </properties> <children> + <node name="statistics"> + <properties> + <help>Show conntrack statistics</help> + </properties> + <command>sudo ${vyos_op_scripts_dir}/conntrack.py show_statistics</command> + </node> <node name="table"> <properties> <help>Show conntrack entries for table</help> diff --git a/op-mode-definitions/show-log.xml.in b/op-mode-definitions/show-log.xml.in index 24a1b5f3e..ebd198215 100644 --- a/op-mode-definitions/show-log.xml.in +++ b/op-mode-definitions/show-log.xml.in @@ -380,19 +380,19 @@ <properties> <help>Show log for ALL</help> </properties> - <command>cat $(printf "%s\n" /var/log/messages* | sort -nr) | grep -e charon -e accel -e pptpd -e ppp</command> + <command>journalctl --no-hostname --boot --unit strongswan-starter.service --unit accel-ppp@*.service</command> </leafNode> <leafNode name="ipsec"> <properties> <help>Show log for IPSec</help> </properties> - <command>cat $(printf "%s\n" /var/log/messages* | sort -nr) | grep -e charon</command> + <command>journalctl --no-hostname --boot --unit strongswan-starter.service</command> </leafNode> <leafNode name="l2tp"> <properties> <help>Show log for L2TP</help> </properties> - <command>cat $(printf "%s\n" /var/log/messages* | sort -nr) | grep -e remote-access-aaa-win -e remote-access-zzz-mac -e accel-l2tp -e ppp</command> + <command>journalctl --no-hostname --boot --unit accel-ppp@l2tp.service</command> </leafNode> <leafNode name="pptp"> <properties> diff --git a/python/vyos/configdict.py b/python/vyos/configdict.py index 8f822a97d..912bc94f2 100644 --- a/python/vyos/configdict.py +++ b/python/vyos/configdict.py @@ -295,11 +295,18 @@ def is_source_interface(conf, interface, intftype=None): """ ret_val = None intftypes = ['macsec', 'pppoe', 'pseudo-ethernet', 'tunnel', 'vxlan'] - if intftype not in intftypes + [None]: + if not intftype: + intftype = intftypes + + if isinstance(intftype, str): + intftype = [intftype] + elif not isinstance(intftype, list): + raise ValueError(f'Interface type "{type(intftype)}" must be either str or list!') + + if not all(x in intftypes for x in intftype): raise ValueError(f'unknown interface type "{intftype}" or it can not ' 'have a source-interface') - intftype = intftypes if intftype == None else [intftype] for it in intftype: base = ['interfaces', it] for intf in conf.list_nodes(base): diff --git a/python/vyos/configverify.py b/python/vyos/configverify.py index 2ab3cb408..447ec795c 100644 --- a/python/vyos/configverify.py +++ b/python/vyos/configverify.py @@ -295,6 +295,12 @@ def verify_source_interface(config): raise ConfigError(f'Invalid source-interface "{src_ifname}". Interface ' f'is already a member of bond "{bond_name}"!') + if 'is_source_interface' in config: + tmp = config['is_source_interface'] + src_ifname = config['source_interface'] + raise ConfigError(f'Can not use source-interface "{src_ifname}", it already ' \ + f'belongs to interface "{tmp}"!') + def verify_dhcpv6(config): """ Common helper function used by interface implementations to perform diff --git a/python/vyos/defaults.py b/python/vyos/defaults.py index 09ae73eac..6894fc4da 100644 --- a/python/vyos/defaults.py +++ b/python/vyos/defaults.py @@ -26,7 +26,7 @@ directories = { "templates": "/usr/share/vyos/templates/", "certbot": "/config/auth/letsencrypt", "api_schema": "/usr/libexec/vyos/services/api/graphql/graphql/schema/", - "api_templates": "/usr/libexec/vyos/services/api/graphql/recipes/templates/", + "api_templates": "/usr/libexec/vyos/services/api/graphql/session/templates/", "vyos_udev_dir": "/run/udev/vyos" } diff --git a/python/vyos/firewall.py b/python/vyos/firewall.py index 3e2de4c3f..663c4394a 100644 --- a/python/vyos/firewall.py +++ b/python/vyos/firewall.py @@ -297,6 +297,11 @@ def parse_rule(rule_conf, fw_name, rule_id, ip_name): if tcp_flags: output.append(parse_tcp_flags(tcp_flags)) + # TCP MSS + tcp_mss = dict_search_args(rule_conf, 'tcp', 'mss') + if tcp_mss: + output.append(f'tcp option maxseg size {tcp_mss}') + output.append('counter') if 'set' in rule_conf: diff --git a/python/vyos/ifconfig/bridge.py b/python/vyos/ifconfig/bridge.py index 758967fbc..aa818bc5f 100644 --- a/python/vyos/ifconfig/bridge.py +++ b/python/vyos/ifconfig/bridge.py @@ -295,8 +295,24 @@ class BridgeIf(Interface): self.del_port(member) # enable/disable Vlan Filter - vlan_filter = '1' if 'enable_vlan' in config else '0' - self.set_vlan_filter(vlan_filter) + tmp = '1' if 'enable_vlan' in config else '0' + self.set_vlan_filter(tmp) + + # add VLAN interfaces to local 'parent' bridge to allow forwarding + if 'enable_vlan' in config: + for vlan in config.get('vif_remove', {}): + # Remove old VLANs from the bridge + cmd = f'bridge vlan del dev {self.ifname} vid {vlan} self' + self._cmd(cmd) + + for vlan in config.get('vif', {}): + cmd = f'bridge vlan add dev {self.ifname} vid {vlan} self' + self._cmd(cmd) + + # VLAN of bridge parent interface is always 1. VLAN 1 is the default + # VLAN for all unlabeled packets + cmd = f'bridge vlan add dev {self.ifname} vid 1 pvid untagged self' + self._cmd(cmd) tmp = dict_search('member.interface', config) if tmp: diff --git a/python/vyos/ifconfig/section.py b/python/vyos/ifconfig/section.py index 91f667b65..5e98cd510 100644 --- a/python/vyos/ifconfig/section.py +++ b/python/vyos/ifconfig/section.py @@ -88,7 +88,7 @@ class Section: raise ValueError(f'No type found for interface name: {name}') @classmethod - def _intf_under_section (cls,section=''): + def _intf_under_section (cls,section='',vlan=True): """ return a generator with the name of the configured interface which are under a section @@ -103,6 +103,9 @@ class Section: if section and ifsection != section: continue + if vlan == False and '.' in ifname: + continue + yield ifname @classmethod @@ -135,13 +138,14 @@ class Section: return l @classmethod - def interfaces(cls, section=''): + def interfaces(cls, section='', vlan=True): """ return a list of the name of the configured interface which are under a section - if no section is provided, then it returns all configured interfaces + if no section is provided, then it returns all configured interfaces. + If vlan is True, also Vlan subinterfaces will be returned """ - return cls._sort_interfaces(cls._intf_under_section(section)) + return cls._sort_interfaces(cls._intf_under_section(section, vlan)) @classmethod def _intf_with_feature(cls, feature=''): diff --git a/python/vyos/opmode.py b/python/vyos/opmode.py index 0af4359c6..628f7b3a2 100644 --- a/python/vyos/opmode.py +++ b/python/vyos/opmode.py @@ -18,6 +18,33 @@ import sys import typing +class Error(Exception): + """ Any error that makes requested operation impossible to complete + for reasons unrelated to the user input or script logic. + """ + pass + +class UnconfiguredSubsystem(Error): + """ Requested operation is valid, but cannot be completed + because corresponding subsystem is not configured and running. + """ + pass + +class DataUnavailable(Error): + """ Requested operation is valid, but cannot be completed + because data for it is not available. + This error MAY be treated as temporary because such issues + are often caused by transient events such as service restarts. + """ + pass + +class PermissionDenied(Error): + """ Requested operation is valid, but the caller has no permission + to perform it. + """ + pass + + def _is_op_mode_function_name(name): if re.match(r"^(show|clear|reset|restart)", name): return True diff --git a/python/vyos/util.py b/python/vyos/util.py index 8df9ef7d6..325b630bc 100644 --- a/python/vyos/util.py +++ b/python/vyos/util.py @@ -471,6 +471,29 @@ def process_named_running(name): return p.pid return None +def is_listen_port_bind_service(port: int, service: str) -> bool: + """Check if listen port bound to expected program name + :param port: Bind port + :param service: Program name + :return: bool + + Example: + % is_listen_port_bind_service(443, 'nginx') + True + % is_listen_port_bind_service(443, 'ocservr-main') + False + """ + from psutil import net_connections as connections + from psutil import Process as process + for connection in connections(): + addr = connection.laddr + pid = connection.pid + pid_name = process(pid).name() + pid_port = addr.port + if service == pid_name and port == pid_port: + return True + return False + def seconds_to_human(s, separator=""): """ Converts number of seconds passed to a human-readable interval such as 1w4d18h35m59s diff --git a/smoketest/configs/pki-misc b/smoketest/configs/pki-misc index c90226a2a..4db795565 100644 --- a/smoketest/configs/pki-misc +++ b/smoketest/configs/pki-misc @@ -59,6 +59,10 @@ vpn { } mode local } + listen-ports { + tcp 4443 + udp 4443 + } network-settings { client-ip-settings { subnet 192.168.160.0/24 diff --git a/smoketest/scripts/cli/test_firewall.py b/smoketest/scripts/cli/test_firewall.py index 4de90e1ec..684a07681 100755 --- a/smoketest/scripts/cli/test_firewall.py +++ b/smoketest/scripts/cli/test_firewall.py @@ -177,6 +177,7 @@ class TestFirewall(VyOSUnitTestSHIM.TestCase): self.verify_nftables(nftables_search, 'ip filter') def test_basic_rules(self): + mss_range = '501-1460' self.cli_set(['firewall', 'name', 'smoketest', 'default-action', 'drop']) self.cli_set(['firewall', 'name', 'smoketest', 'enable-default-log']) self.cli_set(['firewall', 'name', 'smoketest', 'rule', '1', 'action', 'accept']) @@ -203,6 +204,10 @@ class TestFirewall(VyOSUnitTestSHIM.TestCase): self.cli_set(['firewall', 'name', 'smoketest', 'rule', '4', 'destination', 'port', '22']) self.cli_set(['firewall', 'name', 'smoketest', 'rule', '4', 'recent', 'count', '10']) self.cli_set(['firewall', 'name', 'smoketest', 'rule', '4', 'recent', 'time', 'minute']) + self.cli_set(['firewall', 'name', 'smoketest', 'rule', '5', 'action', 'accept']) + self.cli_set(['firewall', 'name', 'smoketest', 'rule', '5', 'protocol', 'tcp']) + self.cli_set(['firewall', 'name', 'smoketest', 'rule', '5', 'tcp', 'flags', 'syn']) + self.cli_set(['firewall', 'name', 'smoketest', 'rule', '5', 'tcp', 'mss', mss_range]) self.cli_set(['interfaces', 'ethernet', 'eth0', 'firewall', 'in', 'name', 'smoketest']) @@ -214,7 +219,8 @@ class TestFirewall(VyOSUnitTestSHIM.TestCase): ['tcp flags & (syn | ack) == syn', 'tcp dport { 8888 }', 'log prefix "[smoketest-2-R]" level err', 'ip ttl > 102', 'reject'], ['tcp dport { 22 }', 'limit rate 5/minute', 'return'], ['log prefix "[smoketest-default-D]"','smoketest default-action', 'drop'], - ['tcp dport { 22 }', 'add @RECENT_smoketest_4 { ip saddr limit rate over 10/minute burst 10 packets }', 'drop'] + ['tcp dport { 22 }', 'add @RECENT_smoketest_4 { ip saddr limit rate over 10/minute burst 10 packets }', 'drop'], + [f'tcp flags & syn == syn tcp option maxseg size {mss_range}'] ] self.verify_nftables(nftables_search, 'ip filter') diff --git a/smoketest/scripts/cli/test_interfaces_bridge.py b/smoketest/scripts/cli/test_interfaces_bridge.py index 8f711af20..6d7af78eb 100755 --- a/smoketest/scripts/cli/test_interfaces_bridge.py +++ b/smoketest/scripts/cli/test_interfaces_bridge.py @@ -19,6 +19,7 @@ import json import unittest from base_interfaces_test import BasicInterfaceTest +from copy import deepcopy from glob import glob from netifaces import interfaces @@ -224,85 +225,78 @@ class BridgeInterfaceTest(BasicInterfaceTest.TestCase): super().test_vif_8021q_mtu_limits() def test_bridge_vlan_filter(self): - def _verify_members() -> None: - # check member interfaces are added on the bridge - for interface in self._interfaces: - bridge_members = [] - for tmp in glob(f'/sys/class/net/{interface}/lower_*'): - bridge_members.append(os.path.basename(tmp).replace('lower_', '')) - - # We can not use assertListEqual() b/c the position of the interface - # names within the list is not fixed - self.assertEqual(len(self._members), len(bridge_members)) - for member in self._members: - self.assertIn(member, bridge_members) - - def _check_vlan_filter() -> None: - for interface in self._interfaces: - tmp = cmd(f'bridge -j vlan show dev {interface}') - tmp = json.loads(tmp) - self.assertIsNotNone(tmp) - - for interface_status in tmp: - ifname = interface_status['ifname'] - for interface in self._members: - vlan_success = 0; - if interface == ifname: - vlans_status = interface_status['vlans'] - for vlan_status in vlans_status: - vlan_id = vlan_status['vlan'] - flag_num = 0 - if 'flags' in vlan_status: - flags = vlan_status['flags'] - for flag in flags: - flag_num = flag_num +1 - if vlan_id == 2: - if flag_num == 0: - vlan_success = vlan_success + 1 - else: - for id in range(4,10): - if vlan_id == id: - if flag_num == 0: - vlan_success = vlan_success + 1 - if vlan_id >= 101: - if flag_num == 2: - vlan_success = vlan_success + 1 - self.assertGreaterEqual(vlan_success, 7) - - vif_vlan = 2 + vifs = ['10', '20', '30', '40'] + native_vlan = '20' + # Add member interface to bridge and set VLAN filter for interface in self._interfaces: base = self._base_path + [interface] self.cli_set(base + ['enable-vlan']) self.cli_set(base + ['address', '192.0.2.1/24']) - self.cli_set(base + ['vif', str(vif_vlan), 'address', '192.0.3.1/24']) - self.cli_set(base + ['vif', str(vif_vlan), 'mtu', self._mtu]) - vlan_id = 101 - allowed_vlan = 2 - allowed_vlan_range = '4-9' - # assign members to bridge interface + for vif in vifs: + self.cli_set(base + ['vif', vif, 'address', f'192.0.{vif}.1/24']) + self.cli_set(base + ['vif', vif, 'mtu', self._mtu]) + for member in self._members: base_member = base + ['member', 'interface', member] - self.cli_set(base_member + ['allowed-vlan', str(allowed_vlan)]) - self.cli_set(base_member + ['allowed-vlan', allowed_vlan_range]) - self.cli_set(base_member + ['native-vlan', str(vlan_id)]) - vlan_id += 1 + self.cli_set(base_member + ['native-vlan', native_vlan]) + for vif in vifs: + self.cli_set(base_member + ['allowed-vlan', vif]) # commit config self.cli_commit() + def _verify_members(interface, members) -> None: + # check member interfaces are added on the bridge + bridge_members = [] + for tmp in glob(f'/sys/class/net/{interface}/lower_*'): + bridge_members.append(os.path.basename(tmp).replace('lower_', '')) + + self.assertListEqual(sorted(members), sorted(bridge_members)) + + def _check_vlan_filter(interface, vifs) -> None: + configured_vlan_ids = [] + + bridge_json = cmd(f'bridge -j vlan show dev {interface}') + bridge_json = json.loads(bridge_json) + self.assertIsNotNone(bridge_json) + + for tmp in bridge_json: + self.assertIn('vlans', tmp) + + for vlan in tmp['vlans']: + self.assertIn('vlan', vlan) + configured_vlan_ids.append(str(vlan['vlan'])) + + # Verify native VLAN ID has 'PVID' flag set on individual member ports + if not interface.startswith('br') and str(vlan['vlan']) == native_vlan: + self.assertIn('flags', vlan) + self.assertIn('PVID', vlan['flags']) + + self.assertListEqual(sorted(configured_vlan_ids), sorted(vifs)) + # Verify correct setting of VLAN filter function for interface in self._interfaces: tmp = read_file(f'/sys/class/net/{interface}/bridge/vlan_filtering') self.assertEqual(tmp, '1') - # Execute the program to obtain status information and verify proper - # VLAN filter setup - _check_vlan_filter() + # Obtain status information and verify proper VLAN filter setup. + # First check if all members are present, second check if all VLANs + # are assigned on the parend bridge interface, third verify all the + # VLANs are properly setup on the downstream "member" ports + for interface in self._interfaces: + # check member interfaces are added on the bridge + _verify_members(interface, self._members) - # check member interfaces are added on the bridge - _verify_members() + # Check if all VLAN ids are properly set up. Bridge interface always + # has native VLAN 1 + tmp = deepcopy(vifs) + tmp.append('1') + _check_vlan_filter(interface, tmp) + + for member in self._members: + _check_vlan_filter(member, vifs) # change member interface description to trigger config update, # VLANs must still exist (T4565) @@ -313,12 +307,22 @@ class BridgeInterfaceTest(BasicInterfaceTest.TestCase): # commit config self.cli_commit() - # check member interfaces are added on the bridge - _verify_members() + # Obtain status information and verify proper VLAN filter setup. + # First check if all members are present, second check if all VLANs + # are assigned on the parend bridge interface, third verify all the + # VLANs are properly setup on the downstream "member" ports + for interface in self._interfaces: + # check member interfaces are added on the bridge + _verify_members(interface, self._members) + + # Check if all VLAN ids are properly set up. Bridge interface always + # has native VLAN 1 + tmp = deepcopy(vifs) + tmp.append('1') + _check_vlan_filter(interface, tmp) - # Execute the program to obtain status information and verify proper - # VLAN filter setup - _check_vlan_filter() + for member in self._members: + _check_vlan_filter(member, vifs) # delete all members for interface in self._interfaces: @@ -337,7 +341,6 @@ class BridgeInterfaceTest(BasicInterfaceTest.TestCase): for member in self._members: self.assertNotIn(member, bridge_members) - def test_bridge_vif_members(self): # T2945: ensure that VIFs are not dropped from bridge vifs = ['300', '400'] diff --git a/smoketest/scripts/cli/test_interfaces_macsec.py b/smoketest/scripts/cli/test_interfaces_macsec.py index ca70e2c18..f141cc6d3 100755 --- a/smoketest/scripts/cli/test_interfaces_macsec.py +++ b/smoketest/scripts/cli/test_interfaces_macsec.py @@ -28,6 +28,8 @@ from vyos.util import read_file from vyos.util import get_interface_config from vyos.util import process_named_running +PROCESS_NAME = 'wpa_supplicant' + def get_config_value(interface, key): tmp = read_file(f'/run/wpa_supplicant/{interface}.conf') tmp = re.findall(r'\n?{}=(.*)'.format(key), tmp) @@ -55,6 +57,10 @@ class MACsecInterfaceTest(BasicInterfaceTest.TestCase): # call base-classes classmethod super(MACsecInterfaceTest, cls).setUpClass() + def tearDown(self): + super().tearDown() + self.assertFalse(process_named_running(PROCESS_NAME)) + def test_macsec_encryption(self): # MACsec can be operating in authentication and encryption mode - both # using different mandatory settings, lets test encryption as the basic @@ -96,32 +102,29 @@ class MACsecInterfaceTest(BasicInterfaceTest.TestCase): self.cli_commit() tmp = get_config_value(src_interface, 'macsec_integ_only') - self.assertTrue("0" in tmp) + self.assertIn("0", tmp) tmp = get_config_value(src_interface, 'mka_cak') - self.assertTrue(mak_cak in tmp) + self.assertIn(mak_cak, tmp) tmp = get_config_value(src_interface, 'mka_ckn') - self.assertTrue(mak_ckn in tmp) - - # check that we use the new macsec_csindex option (T4537) - tmp = get_config_value(src_interface, 'macsec_csindex') - self.assertTrue("1" in tmp) + self.assertIn(mak_ckn, tmp) # check that the default priority of 255 is programmed tmp = get_config_value(src_interface, 'mka_priority') - self.assertTrue("255" in tmp) + self.assertIn("255", tmp) tmp = get_config_value(src_interface, 'macsec_replay_window') - self.assertTrue(replay_window in tmp) + self.assertIn(replay_window, tmp) tmp = read_file(f'/sys/class/net/{interface}/mtu') self.assertEqual(tmp, '1460') - # Check for running process - self.assertTrue(process_named_running('wpa_supplicant')) + # Check for running process + self.assertTrue(process_named_running(PROCESS_NAME)) def test_macsec_gcm_aes_128(self): + src_interface = 'eth0' interface = 'macsec1' cipher = 'gcm-aes-128' self.cli_set(self._base_path + [interface]) @@ -129,7 +132,7 @@ class MACsecInterfaceTest(BasicInterfaceTest.TestCase): # check validate() - source interface is mandatory with self.assertRaises(ConfigSessionError): self.cli_commit() - self.cli_set(self._base_path + [interface, 'source-interface', 'eth0']) + self.cli_set(self._base_path + [interface, 'source-interface', src_interface]) # check validate() - cipher is mandatory with self.assertRaises(ConfigSessionError): @@ -142,7 +145,15 @@ class MACsecInterfaceTest(BasicInterfaceTest.TestCase): self.assertIn(interface, interfaces()) self.assertEqual(cipher, get_cipher(interface)) + # check that we use the new macsec_csindex option (T4537) + tmp = get_config_value(src_interface, 'macsec_csindex') + self.assertIn("0", tmp) + + # Check for running process + self.assertTrue(process_named_running(PROCESS_NAME)) + def test_macsec_gcm_aes_256(self): + src_interface = 'eth0' interface = 'macsec4' cipher = 'gcm-aes-256' self.cli_set(self._base_path + [interface]) @@ -150,7 +161,7 @@ class MACsecInterfaceTest(BasicInterfaceTest.TestCase): # check validate() - source interface is mandatory with self.assertRaises(ConfigSessionError): self.cli_commit() - self.cli_set(self._base_path + [interface, 'source-interface', 'eth0']) + self.cli_set(self._base_path + [interface, 'source-interface', src_interface]) # check validate() - cipher is mandatory with self.assertRaises(ConfigSessionError): @@ -162,6 +173,13 @@ class MACsecInterfaceTest(BasicInterfaceTest.TestCase): self.assertIn(interface, interfaces()) self.assertEqual(cipher, get_cipher(interface)) + # check that we use the new macsec_csindex option (T4537) + tmp = get_config_value(src_interface, 'macsec_csindex') + self.assertIn("1", tmp) + + # Check for running process + self.assertTrue(process_named_running(PROCESS_NAME)) + def test_macsec_source_interface(self): # Ensure source-interface can bot be part of any other bond or bridge @@ -190,6 +208,9 @@ class MACsecInterfaceTest(BasicInterfaceTest.TestCase): self.cli_commit() self.assertIn(interface, interfaces()) + # Check for running process + self.assertTrue(process_named_running(PROCESS_NAME)) + if __name__ == '__main__': - unittest.main(verbosity=2) + unittest.main(verbosity=2, failfast=True) diff --git a/smoketest/scripts/cli/test_nat66.py b/smoketest/scripts/cli/test_nat66.py index aac6a30f9..4b5625569 100755 --- a/smoketest/scripts/cli/test_nat66.py +++ b/smoketest/scripts/cli/test_nat66.py @@ -42,6 +42,17 @@ class TestNAT66(VyOSUnitTestSHIM.TestCase): self.cli_delete(base_path) self.cli_commit() + def verify_nftables(self, nftables_search, table, inverse=False, args=''): + nftables_output = cmd(f'sudo nft {args} list table {table}') + + for search in nftables_search: + matched = False + for line in nftables_output.split("\n"): + if all(item in line for item in search): + matched = True + break + self.assertTrue(not matched if inverse else matched, msg=search) + def test_source_nat66(self): source_prefix = 'fc00::/64' translation_prefix = 'fc01::/64' @@ -49,29 +60,23 @@ class TestNAT66(VyOSUnitTestSHIM.TestCase): self.cli_set(src_path + ['rule', '1', 'source', 'prefix', source_prefix]) self.cli_set(src_path + ['rule', '1', 'translation', 'address', translation_prefix]) - # check validate() - outbound-interface must be defined - self.cli_commit() + self.cli_set(src_path + ['rule', '2', 'outbound-interface', 'eth1']) + self.cli_set(src_path + ['rule', '2', 'source', 'prefix', source_prefix]) + self.cli_set(src_path + ['rule', '2', 'translation', 'address', 'masquerade']) - tmp = cmd('sudo nft -j list table ip6 nat') - data_json = jmespath.search('nftables[?rule].rule[?chain]', json.loads(tmp)) + self.cli_set(src_path + ['rule', '3', 'outbound-interface', 'eth1']) + self.cli_set(src_path + ['rule', '3', 'source', 'prefix', source_prefix]) + self.cli_set(src_path + ['rule', '3', 'exclude']) - for idx in range(0, len(data_json)): - data = data_json[idx] + self.cli_commit() - self.assertEqual(data['chain'], 'POSTROUTING') - self.assertEqual(data['family'], 'ip6') - self.assertEqual(data['table'], 'nat') + nftables_search = [ + ['oifname "eth1"', 'ip6 saddr fc00::/64', 'snat prefix to fc01::/64'], + ['oifname "eth1"', 'ip6 saddr fc00::/64', 'masquerade'], + ['oifname "eth1"', 'ip6 saddr fc00::/64', 'return'] + ] - iface = dict_search('match.right', data['expr'][0]) - address = dict_search('match.right.prefix.addr', data['expr'][2]) - mask = dict_search('match.right.prefix.len', data['expr'][2]) - translation_address = dict_search('snat.addr.prefix.addr', data['expr'][3]) - translation_mask = dict_search('snat.addr.prefix.len', data['expr'][3]) - - self.assertEqual(iface, 'eth1') - # check for translation address - self.assertEqual(f'{translation_address}/{translation_mask}', translation_prefix) - self.assertEqual(f'{address}/{mask}', source_prefix) + self.verify_nftables(nftables_search, 'ip6 nat') def test_source_nat66_address(self): source_prefix = 'fc00::/64' @@ -106,28 +111,25 @@ class TestNAT66(VyOSUnitTestSHIM.TestCase): def test_destination_nat66(self): destination_address = 'fc00::1' translation_address = 'fc01::1' + source_address = 'fc02::1' self.cli_set(dst_path + ['rule', '1', 'inbound-interface', 'eth1']) self.cli_set(dst_path + ['rule', '1', 'destination', 'address', destination_address]) self.cli_set(dst_path + ['rule', '1', 'translation', 'address', translation_address]) + self.cli_set(dst_path + ['rule', '2', 'inbound-interface', 'eth1']) + self.cli_set(dst_path + ['rule', '2', 'destination', 'address', destination_address]) + self.cli_set(dst_path + ['rule', '2', 'source', 'address', source_address]) + self.cli_set(dst_path + ['rule', '2', 'exclude']) + # check validate() - outbound-interface must be defined self.cli_commit() - tmp = cmd('sudo nft -j list table ip6 nat') - data_json = jmespath.search('nftables[?rule].rule[?chain]', json.loads(tmp)) - - for idx in range(0, len(data_json)): - data = data_json[idx] - - self.assertEqual(data['chain'], 'PREROUTING') - self.assertEqual(data['family'], 'ip6') - self.assertEqual(data['table'], 'nat') - - iface = dict_search('match.right', data['expr'][0]) - dnat_addr = dict_search('dnat.addr', data['expr'][3]) + nftables_search = [ + ['iifname "eth1"', 'ip6 daddr fc00::1', 'dnat to fc01::1'], + ['iifname "eth1"', 'ip6 saddr fc02::1', 'ip6 daddr fc00::1', 'return'] + ] - self.assertEqual(dnat_addr, translation_address) - self.assertEqual(iface, 'eth1') + self.verify_nftables(nftables_search, 'ip6 nat') def test_destination_nat66_prefix(self): destination_prefix = 'fc00::/64' diff --git a/smoketest/scripts/cli/test_protocols_bgp.py b/smoketest/scripts/cli/test_protocols_bgp.py index 009dbc803..6196ffe60 100755 --- a/smoketest/scripts/cli/test_protocols_bgp.py +++ b/smoketest/scripts/cli/test_protocols_bgp.py @@ -46,7 +46,7 @@ neighbor_config = { 'shutdown' : '', 'cap_over' : '', 'ttl_security' : '5', - 'local_as' : '300', + 'system_as' : '300', 'route_map_in' : route_map_in, 'route_map_out' : route_map_out, 'no_send_comm_ext' : '', @@ -87,7 +87,7 @@ neighbor_config = { 'shutdown' : '', 'cap_over' : '', 'ttl_security' : '5', - 'local_as' : '300', + 'system_as' : '300', 'solo' : '', 'route_map_in' : route_map_in, 'route_map_out' : route_map_out, @@ -105,7 +105,8 @@ neighbor_config = { 'pfx_list_out' : prefix_list_out6, 'no_send_comm_ext' : '', 'peer_group' : 'foo-bar_baz', - 'graceful_rst_hlp' : '' + 'graceful_rst_hlp' : '', + 'disable_conn_chk' : '', }, } @@ -120,6 +121,7 @@ peer_group_config = { 'shutdown' : '', 'cap_over' : '', 'ttl_security' : '5', + 'disable_conn_chk' : '', }, 'bar' : { 'remote_as' : '111', @@ -131,7 +133,7 @@ peer_group_config = { 'remote_as' : '200', 'shutdown' : '', 'no_cap_nego' : '', - 'local_as' : '300', + 'system_as' : '300', 'pfx_list_in' : prefix_list_in, 'pfx_list_out' : prefix_list_out, 'no_send_comm_ext' : '', @@ -177,7 +179,7 @@ class TestProtocolsBGP(VyOSUnitTestSHIM.TestCase): cls.cli_delete(cls, ['policy']) def setUp(self): - self.cli_set(base_path + ['local-as', ASN]) + self.cli_set(base_path + ['system-as', ASN]) def tearDown(self): self.cli_delete(['vrf']) @@ -251,6 +253,9 @@ class TestProtocolsBGP(VyOSUnitTestSHIM.TestCase): self.assertIn(f' neighbor {peer} graceful-restart-disable', frrconfig) if 'graceful_rst_hlp' in peer_config: self.assertIn(f' neighbor {peer} graceful-restart-helper', frrconfig) + if 'disable_conn_chk' in peer_config: + self.assertIn(f' neighbor {peer} disable-connected-check', frrconfig) + def test_bgp_01_simple(self): router_id = '127.0.0.1' @@ -266,12 +271,12 @@ class TestProtocolsBGP(VyOSUnitTestSHIM.TestCase): self.cli_set(base_path + ['parameters', 'router-id', router_id]) self.cli_set(base_path + ['parameters', 'log-neighbor-changes']) - # Local AS number MUST be defined - as this is set in setUp() we remove + # System AS number MUST be defined - as this is set in setUp() we remove # this once for testing of the proper error - self.cli_delete(base_path + ['local-as']) + self.cli_delete(base_path + ['system-as']) with self.assertRaises(ConfigSessionError): self.cli_commit() - self.cli_set(base_path + ['local-as', ASN]) + self.cli_set(base_path + ['system-as', ASN]) # Default local preference (higher = more preferred, default value is 100) self.cli_set(base_path + ['parameters', 'default', 'local-pref', local_pref]) @@ -400,6 +405,8 @@ class TestProtocolsBGP(VyOSUnitTestSHIM.TestCase): self.cli_set(base_path + ['neighbor', peer, 'graceful-restart', 'disable']) if 'graceful_rst_hlp' in peer_config: self.cli_set(base_path + ['neighbor', peer, 'graceful-restart', 'restart-helper']) + if 'disable_conn_chk' in peer_config: + self.cli_set(base_path + ['neighbor', peer, 'disable-connected-check']) # Conditional advertisement if 'advertise_map' in peer_config: @@ -488,6 +495,8 @@ class TestProtocolsBGP(VyOSUnitTestSHIM.TestCase): self.cli_set(base_path + ['peer-group', peer_group, 'graceful-restart', 'disable']) if 'graceful_rst_hlp' in config: self.cli_set(base_path + ['peer-group', peer_group, 'graceful-restart', 'restart-helper']) + if 'disable_conn_chk' in config: + self.cli_set(base_path + ['peer-group', peer_group, 'disable-connected-check']) # Conditional advertisement if 'advertise_map' in config: @@ -760,13 +769,13 @@ class TestProtocolsBGP(VyOSUnitTestSHIM.TestCase): # templates and Jinja2 FRR template. table = '1000' - self.cli_set(base_path + ['local-as', ASN]) + self.cli_set(base_path + ['system-as', ASN]) # testing only one AFI is sufficient as it's generic code for vrf in vrfs: vrf_base = ['vrf', 'name', vrf] self.cli_set(vrf_base + ['table', table]) - self.cli_set(vrf_base + ['protocols', 'bgp', 'local-as', ASN]) + self.cli_set(vrf_base + ['protocols', 'bgp', 'system-as', ASN]) self.cli_set(vrf_base + ['protocols', 'bgp', 'parameters', 'router-id', router_id]) self.cli_set(vrf_base + ['protocols', 'bgp', 'route-map', route_map_in]) table = str(int(table) + 1000) @@ -804,7 +813,7 @@ class TestProtocolsBGP(VyOSUnitTestSHIM.TestCase): confed_id = str(int(ASN) + 1) confed_asns = '10 20 30 40' - self.cli_set(base_path + ['local-as', ASN]) + self.cli_set(base_path + ['system-as', ASN]) self.cli_set(base_path + ['parameters', 'router-id', router_id]) self.cli_set(base_path + ['parameters', 'confederation', 'identifier', confed_id]) for asn in confed_asns.split(): @@ -825,7 +834,7 @@ class TestProtocolsBGP(VyOSUnitTestSHIM.TestCase): remote_asn = str(int(ASN) + 10) interface = 'eth0' - self.cli_set(base_path + ['local-as', ASN]) + self.cli_set(base_path + ['system-as', ASN]) self.cli_set(base_path + ['neighbor', interface, 'address-family', 'ipv6-unicast']) self.cli_set(base_path + ['neighbor', interface, 'interface', 'v6only', 'remote-as', remote_asn]) @@ -850,7 +859,7 @@ class TestProtocolsBGP(VyOSUnitTestSHIM.TestCase): rt_export = f'{neighbor}:1002 1.2.3.4:567' rt_import = f'{neighbor}:1003 500:100' - self.cli_set(base_path + ['local-as', ASN]) + self.cli_set(base_path + ['system-as', ASN]) # testing only one AFI is sufficient as it's generic code for afi in ['ipv4-unicast', 'ipv6-unicast']: self.cli_set(base_path + ['address-family', afi, 'export', 'vpn']) @@ -889,7 +898,7 @@ class TestProtocolsBGP(VyOSUnitTestSHIM.TestCase): peer_group = 'bar' interface = 'eth0' - self.cli_set(base_path + ['local-as', ASN]) + self.cli_set(base_path + ['system-as', ASN]) self.cli_set(base_path + ['neighbor', neighbor, 'remote-as', remote_asn]) self.cli_set(base_path + ['neighbor', neighbor, 'peer-group', peer_group]) self.cli_set(base_path + ['peer-group', peer_group, 'remote-as', remote_asn]) @@ -929,7 +938,7 @@ class TestProtocolsBGP(VyOSUnitTestSHIM.TestCase): remote_asn = '500' local_asn = '400' - self.cli_set(base_path + ['local-as', ASN]) + self.cli_set(base_path + ['system-as', ASN]) self.cli_set(base_path + ['neighbor', neighbor, 'remote-as', ASN]) self.cli_set(base_path + ['neighbor', neighbor, 'local-as', local_asn]) diff --git a/smoketest/scripts/cli/test_service_monitoring_telegraf.py b/smoketest/scripts/cli/test_service_monitoring_telegraf.py index 1c8cc9759..c1c4044e6 100755 --- a/smoketest/scripts/cli/test_service_monitoring_telegraf.py +++ b/smoketest/scripts/cli/test_service_monitoring_telegraf.py @@ -24,7 +24,7 @@ from vyos.util import process_named_running from vyos.util import read_file PROCESS_NAME = 'telegraf' -TELEGRAF_CONF = '/run/telegraf/vyos-telegraf.conf' +TELEGRAF_CONF = '/run/telegraf/telegraf.conf' base_path = ['service', 'monitoring', 'telegraf'] org = 'log@in.local' token = 'GuRJc12tIzfjnYdKRAIYbxdWd2aTpOT9PVYNddzDnFV4HkAcD7u7-kndTFXjGuXzJN6TTxmrvPODB4mnFcseDV==' diff --git a/smoketest/scripts/cli/test_vpn_openconnect.py b/smoketest/scripts/cli/test_vpn_openconnect.py index bda279342..8572d6d66 100755 --- a/smoketest/scripts/cli/test_vpn_openconnect.py +++ b/smoketest/scripts/cli/test_vpn_openconnect.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2020 VyOS maintainers and contributors +# Copyright (C) 2020-2022 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 @@ -19,6 +19,7 @@ import unittest from base_vyostest_shim import VyOSUnitTestSHIM from vyos.util import process_named_running +from vyos.util import read_file OCSERV_CONF = '/run/ocserv/ocserv.conf' base_path = ['vpn', 'openconnect'] @@ -46,36 +47,84 @@ MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgPLpD0Ohhoq0g4nhx u8/3jHMM7sDwL3aWzW/zp54/LhCWUoLMjDdDEEigK4fal4ZF9aA9F0Ww """ -class TestVpnOpenconnect(VyOSUnitTestSHIM.TestCase): +PROCESS_NAME = 'ocserv-main' +config_file = '/run/ocserv/ocserv.conf' +auth_file = '/run/ocserv/ocpasswd' +otp_file = '/run/ocserv/users.oath' + +class TestVPNOpenConnect(VyOSUnitTestSHIM.TestCase): + @classmethod + def setUpClass(cls): + super(TestVPNOpenConnect, cls).setUpClass() + + # ensure we can also run this test on a live system - so lets clean + # out the current configuration :) + cls.cli_delete(cls, base_path) + + cls.cli_set(cls, pki_path + ['ca', 'openconnect', 'certificate', cert_data.replace('\n','')]) + cls.cli_set(cls, pki_path + ['certificate', 'openconnect', 'certificate', cert_data.replace('\n','')]) + cls.cli_set(cls, pki_path + ['certificate', 'openconnect', 'private', 'key', key_data.replace('\n','')]) + + @classmethod + def tearDownClass(cls): + cls.cli_delete(cls, pki_path) + super(TestVPNOpenConnect, cls).tearDownClass() + def tearDown(self): - # Delete vpn openconnect configuration - self.cli_delete(pki_path) + self.assertTrue(process_named_running(PROCESS_NAME)) + self.cli_delete(base_path) self.cli_commit() - def test_vpn(self): + self.assertFalse(process_named_running(PROCESS_NAME)) + + def test_ocserv(self): user = 'vyos_user' password = 'vyos_pass' otp = '37500000026900000000200000000000' - - self.cli_delete(pki_path) - self.cli_delete(base_path) - - self.cli_set(pki_path + ['ca', 'openconnect', 'certificate', cert_data.replace('\n','')]) - self.cli_set(pki_path + ['certificate', 'openconnect', 'certificate', cert_data.replace('\n','')]) - self.cli_set(pki_path + ['certificate', 'openconnect', 'private', 'key', key_data.replace('\n','')]) + v4_subnet = '192.0.2.0/24' + v6_prefix = '2001:db8:1000::/64' + v6_len = '126' + name_server = ['1.2.3.4', '1.2.3.5', '2001:db8::1'] + split_dns = ['vyos.net', 'vyos.io'] self.cli_set(base_path + ['authentication', 'local-users', 'username', user, 'password', password]) self.cli_set(base_path + ['authentication', 'local-users', 'username', user, 'otp', 'key', otp]) self.cli_set(base_path + ['authentication', 'mode', 'local', 'password-otp']) - self.cli_set(base_path + ['network-settings', 'client-ip-settings', 'subnet', '192.0.2.0/24']) + + self.cli_set(base_path + ['network-settings', 'client-ip-settings', 'subnet', v4_subnet]) + self.cli_set(base_path + ['network-settings', 'client-ipv6-pool', 'prefix', v6_prefix]) + self.cli_set(base_path + ['network-settings', 'client-ipv6-pool', 'mask', v6_len]) + + for ns in name_server: + self.cli_set(base_path + ['network-settings', 'name-server', ns]) + for domain in split_dns: + self.cli_set(base_path + ['network-settings', 'split-dns', domain]) + self.cli_set(base_path + ['ssl', 'ca-certificate', 'openconnect']) self.cli_set(base_path + ['ssl', 'certificate', 'openconnect']) self.cli_commit() - # Check for running process - self.assertTrue(process_named_running('ocserv-main')) + # Verify configuration + daemon_config = read_file(config_file) + + # authentication mode local password-otp + self.assertIn(f'auth = "plain[passwd=/run/ocserv/ocpasswd,otp=/run/ocserv/users.oath]"', daemon_config) + self.assertIn(f'ipv4-network = {v4_subnet}', daemon_config) + self.assertIn(f'ipv6-network = {v6_prefix}', daemon_config) + self.assertIn(f'ipv6-subnet-prefix = {v6_len}', daemon_config) + + for ns in name_server: + self.assertIn(f'dns = {ns}', daemon_config) + for domain in split_dns: + self.assertIn(f'split-dns = {domain}', daemon_config) + + auth_config = read_file(auth_file) + self.assertIn(f'{user}:*:$', auth_config) + + otp_config = read_file(otp_file) + self.assertIn(f'HOTP/T30/6 {user} - {otp}', otp_config) if __name__ == '__main__': unittest.main(verbosity=2) diff --git a/src/conf_mode/arp.py b/src/conf_mode/arp.py index 1cd8f5451..7dc5206e0 100755 --- a/src/conf_mode/arp.py +++ b/src/conf_mode/arp.py @@ -61,7 +61,7 @@ def apply(arp): continue for address, address_config in interface_config['address'].items(): mac = address_config['mac'] - call(f'ip neigh add {address} lladdr {mac} dev {interface}') + call(f'ip neigh replace {address} lladdr {mac} dev {interface}') if __name__ == '__main__': try: diff --git a/src/conf_mode/https.py b/src/conf_mode/https.py index 3057357fc..7cd7ea42e 100755 --- a/src/conf_mode/https.py +++ b/src/conf_mode/https.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2019-2021 VyOS maintainers and contributors +# Copyright (C) 2019-2022 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 @@ -29,6 +29,8 @@ from vyos.pki import wrap_certificate from vyos.pki import wrap_private_key from vyos.template import render from vyos.util import call +from vyos.util import check_port_availability +from vyos.util import is_listen_port_bind_service from vyos.util import write_file from vyos import airbag @@ -107,6 +109,31 @@ def verify(https): raise ConfigError("At least one 'virtual-host <id> server-name' " "matching the 'certbot domain-name' is required.") + server_block_list = [] + + # organize by vhosts + vhost_dict = https.get('virtual-host', {}) + + if not vhost_dict: + # no specified virtual hosts (server blocks); use default + server_block_list.append(default_server_block) + else: + for vhost in list(vhost_dict): + server_block = deepcopy(default_server_block) + data = vhost_dict.get(vhost, {}) + server_block['address'] = data.get('listen-address', '*') + server_block['port'] = data.get('listen-port', '443') + server_block_list.append(server_block) + + for entry in server_block_list: + _address = entry.get('address') + _address = '0.0.0.0' if _address == '*' else _address + _port = entry.get('port') + proto = 'tcp' + if check_port_availability(_address, int(_port), proto) is not True and \ + not is_listen_port_bind_service(int(_port), 'nginx'): + raise ConfigError(f'"{proto}" port "{_port}" is used by another service') + verify_vrf(https) return None diff --git a/src/conf_mode/interfaces-ethernet.py b/src/conf_mode/interfaces-ethernet.py index 30e7a2af7..e02841831 100755 --- a/src/conf_mode/interfaces-ethernet.py +++ b/src/conf_mode/interfaces-ethernet.py @@ -153,11 +153,20 @@ def verify(ethernet): return None def generate(ethernet): - if 'eapol' in ethernet: - render(wpa_suppl_conf.format(**ethernet), - 'ethernet/wpa_supplicant.conf.j2', ethernet) + # render real configuration file once + wpa_supplicant_conf = wpa_suppl_conf.format(**ethernet) + + if 'deleted' in ethernet: + # delete configuration on interface removal + if os.path.isfile(wpa_supplicant_conf): + os.unlink(wpa_supplicant_conf) + return None + if 'eapol' in ethernet: ifname = ethernet['ifname'] + + render(wpa_supplicant_conf, 'ethernet/wpa_supplicant.conf.j2', ethernet) + cert_file_path = os.path.join(cfg_dir, f'{ifname}_cert.pem') cert_key_path = os.path.join(cfg_dir, f'{ifname}_cert.key') @@ -184,10 +193,6 @@ def generate(ethernet): write_file(ca_cert_file_path, '\n'.join(encode_certificate(c) for c in ca_full_chain)) - else: - # delete configuration on interface removal - if os.path.isfile(wpa_suppl_conf.format(**ethernet)): - os.unlink(wpa_suppl_conf.format(**ethernet)) return None @@ -203,9 +208,9 @@ def apply(ethernet): else: e.update(ethernet) if 'eapol' in ethernet: - eapol_action='restart' + eapol_action='reload-or-restart' - call(f'systemctl {eapol_action} wpa_supplicant-macsec@{ifname}') + call(f'systemctl {eapol_action} wpa_supplicant-wired@{ifname}') if __name__ == '__main__': try: diff --git a/src/conf_mode/interfaces-macsec.py b/src/conf_mode/interfaces-macsec.py index 870049a88..649ea8d50 100755 --- a/src/conf_mode/interfaces-macsec.py +++ b/src/conf_mode/interfaces-macsec.py @@ -67,7 +67,7 @@ def get_config(config=None): macsec.update({'shutdown_required': {}}) if 'source_interface' in macsec: - tmp = is_source_interface(conf, macsec['source_interface'], 'macsec') + tmp = is_source_interface(conf, macsec['source_interface'], ['macsec', 'pseudo-ethernet']) if tmp and tmp != ifname: macsec.update({'is_source_interface' : tmp}) return macsec @@ -102,12 +102,6 @@ def verify(macsec): # gcm-aes-128 requires a 128bit long key - 64 characters (string) = 32byte = 256bit raise ConfigError('gcm-aes-128 requires a 256bit long key!') - if 'is_source_interface' in macsec: - tmp = macsec['is_source_interface'] - src_ifname = macsec['source_interface'] - raise ConfigError(f'Can not use source-interface "{src_ifname}", it already ' \ - f'belongs to interface "{tmp}"!') - if 'source_interface' in macsec: # MACsec adds a 40 byte overhead (32 byte MACsec + 8 bytes VLAN 802.1ad # and 802.1q) - we need to check the underlaying MTU if our configured diff --git a/src/conf_mode/interfaces-pseudo-ethernet.py b/src/conf_mode/interfaces-pseudo-ethernet.py index f26a50a0e..20f2b1975 100755 --- a/src/conf_mode/interfaces-pseudo-ethernet.py +++ b/src/conf_mode/interfaces-pseudo-ethernet.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2019-2020 VyOS maintainers and contributors +# Copyright (C) 2019-2022 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 @@ -19,6 +19,7 @@ from sys import exit from vyos.config import Config from vyos.configdict import get_interface_dict from vyos.configdict import is_node_changed +from vyos.configdict import is_source_interface from vyos.configverify import verify_vrf from vyos.configverify import verify_address from vyos.configverify import verify_bridge_delete @@ -51,6 +52,10 @@ def get_config(config=None): if 'source_interface' in peth: _, peth['parent'] = get_interface_dict(conf, ['interfaces', 'ethernet'], peth['source_interface']) + # test if source-interface is maybe already used by another interface + tmp = is_source_interface(conf, peth['source_interface'], ['macsec']) + if tmp and tmp != ifname: peth.update({'is_source_interface' : tmp}) + return peth def verify(peth): diff --git a/src/conf_mode/nat66.py b/src/conf_mode/nat66.py index 0972151a0..f64102d88 100755 --- a/src/conf_mode/nat66.py +++ b/src/conf_mode/nat66.py @@ -125,7 +125,8 @@ def verify(nat): if addr != 'masquerade' and not is_ipv6(addr): raise ConfigError(f'IPv6 address {addr} is not a valid address') else: - raise ConfigError(f'{err_msg} translation address not specified') + if 'exclude' not in config: + raise ConfigError(f'{err_msg} translation address not specified') prefix = dict_search('source.prefix', config) if prefix != None: diff --git a/src/conf_mode/ntp.py b/src/conf_mode/ntp.py index 5490a794d..0ecb4d736 100755 --- a/src/conf_mode/ntp.py +++ b/src/conf_mode/ntp.py @@ -17,6 +17,7 @@ import os from vyos.config import Config +from vyos.configdict import is_node_changed from vyos.configverify import verify_vrf from vyos.configverify import verify_interface_exists from vyos.util import call @@ -40,6 +41,10 @@ def get_config(config=None): ntp = conf.get_config_dict(base, key_mangling=('-', '_'), get_first_key=True) ntp['config_file'] = config_file + + tmp = is_node_changed(conf, base + ['vrf']) + if tmp: ntp.update({'restart_required': {}}) + return ntp def verify(ntp): @@ -78,19 +83,25 @@ def generate(ntp): return None def apply(ntp): + systemd_service = 'ntp.service' + # Reload systemd manager configuration + call('systemctl daemon-reload') + if not ntp: # NTP support is removed in the commit - call('systemctl stop ntp.service') + call(f'systemctl stop {systemd_service}') if os.path.exists(config_file): os.unlink(config_file) if os.path.isfile(systemd_override): os.unlink(systemd_override) + return - # Reload systemd manager configuration - call('systemctl daemon-reload') - if ntp: - call('systemctl restart ntp.service') + # we need to restart the service if e.g. the VRF name changed + systemd_action = 'reload-or-restart' + if 'restart_required' in ntp: + systemd_action = 'restart' + call(f'systemctl {systemd_action} {systemd_service}') return None if __name__ == '__main__': diff --git a/src/conf_mode/protocols_bgp.py b/src/conf_mode/protocols_bgp.py index 7d3687094..87456f00b 100755 --- a/src/conf_mode/protocols_bgp.py +++ b/src/conf_mode/protocols_bgp.py @@ -121,8 +121,8 @@ def verify(bgp): 'dependent VRF instance(s) exist!') return None - if 'local_as' not in bgp: - raise ConfigError('BGP local-as number must be defined!') + if 'system_as' not in bgp: + raise ConfigError('BGP system-as number must be defined!') # Common verification for both peer-group and neighbor statements for neighbor in ['neighbor', 'peer_group']: @@ -147,8 +147,8 @@ def verify(bgp): # Neighbor local-as override can not be the same as the local-as # we use for this BGP instane! asn = list(peer_config['local_as'].keys())[0] - if asn == bgp['local_as']: - raise ConfigError('Cannot have local-as same as BGP AS number') + if asn == bgp['system_as']: + raise ConfigError('Cannot have local-as same as system-as number') # Neighbor AS specified for local-as and remote-as can not be the same if dict_search('remote_as', peer_config) == asn: @@ -216,7 +216,7 @@ def verify(bgp): # Local-AS allowed only for EBGP peers if 'local_as' in peer_config: remote_as = verify_remote_as(peer_config, bgp) - if remote_as == bgp['local_as']: + if remote_as == bgp['system_as']: raise ConfigError(f'local-as configured for "{peer}", allowed only for eBGP peers!') for afi in ['ipv4_unicast', 'ipv4_multicast', 'ipv4_labeled_unicast', 'ipv4_flowspec', @@ -279,12 +279,12 @@ def verify(bgp): verify_route_map(afi_config['route_map'][tmp], bgp) if 'route_reflector_client' in afi_config: - if 'remote_as' in peer_config and peer_config['remote_as'] != 'internal' and peer_config['remote_as'] != bgp['local_as']: + if 'remote_as' in peer_config and peer_config['remote_as'] != 'internal' and peer_config['remote_as'] != bgp['system_as']: raise ConfigError('route-reflector-client only supported for iBGP peers') else: if 'peer_group' in peer_config: peer_group_as = dict_search(f'peer_group.{peer_group}.remote_as', bgp) - if peer_group_as != None and peer_group_as != 'internal' and peer_group_as != bgp['local_as']: + if peer_group_as != None and peer_group_as != 'internal' and peer_group_as != bgp['system_as']: raise ConfigError('route-reflector-client only supported for iBGP peers') # Throw an error if a peer group is not configured for allow range diff --git a/src/conf_mode/service_monitoring_telegraf.py b/src/conf_mode/service_monitoring_telegraf.py index 62f5e1ddf..18b32edab 100755 --- a/src/conf_mode/service_monitoring_telegraf.py +++ b/src/conf_mode/service_monitoring_telegraf.py @@ -22,6 +22,8 @@ from shutil import rmtree from vyos.config import Config from vyos.configdict import dict_merge +from vyos.configdict import is_node_changed +from vyos.configverify import verify_vrf from vyos.ifconfig import Section from vyos.template import render from vyos.util import call @@ -32,20 +34,14 @@ from vyos import ConfigError from vyos import airbag airbag.enable() - -base_dir = '/run/telegraf' cache_dir = f'/etc/telegraf/.cache' -config_telegraf = f'{base_dir}/vyos-telegraf.conf' +config_telegraf = f'/run/telegraf/telegraf.conf' custom_scripts_dir = '/etc/telegraf/custom_scripts' syslog_telegraf = '/etc/rsyslog.d/50-telegraf.conf' -systemd_telegraf_service = '/etc/systemd/system/vyos-telegraf.service' -systemd_telegraf_override_dir = '/etc/systemd/system/vyos-telegraf.service.d' -systemd_override = f'{systemd_telegraf_override_dir}/10-override.conf' - +systemd_override = '/etc/systemd/system/telegraf.service.d/10-override.conf' def get_interfaces(type='', vlan=True): """ - Get interfaces get_interfaces() ['dum0', 'eth0', 'eth1', 'eth1.5', 'lo', 'tun0'] @@ -62,9 +58,7 @@ def get_interfaces(type='', vlan=True): return interfaces def get_nft_filter_chains(): - """ - Get nft chains for table filter - """ + """ Get nft chains for table filter """ nft = cmd('nft --json list table ip filter') nft = json.loads(nft) chain_list = [] @@ -78,7 +72,6 @@ def get_nft_filter_chains(): def get_config(config=None): - if config: conf = config else: @@ -87,8 +80,12 @@ def get_config(config=None): if not conf.exists(base): return None - monitoring = conf.get_config_dict(base, key_mangling=('-', '_'), get_first_key=True, - no_tag_node_value_mangle=True) + monitoring = conf.get_config_dict(base, key_mangling=('-', '_'), + get_first_key=True, + no_tag_node_value_mangle=True) + + tmp = is_node_changed(conf, base + ['vrf']) + if tmp: monitoring.update({'restart_required': {}}) # We have gathered the dict representation of the CLI, but there are default # options which we need to update into the dictionary retrived. @@ -131,6 +128,8 @@ def verify(monitoring): if not monitoring: return None + verify_vrf(monitoring) + # Verify influxdb if 'influxdb' in monitoring: if 'authentication' not in monitoring['influxdb'] or \ @@ -173,7 +172,7 @@ def verify(monitoring): def generate(monitoring): if not monitoring: # Delete config and systemd files - config_files = [config_telegraf, systemd_telegraf_service, systemd_override, syslog_telegraf] + config_files = [config_telegraf, systemd_override, syslog_telegraf] for file in config_files: if os.path.isfile(file): os.unlink(file) @@ -190,33 +189,34 @@ def generate(monitoring): chown(cache_dir, 'telegraf', 'telegraf') - # Create systemd override dir - if not os.path.exists(systemd_telegraf_override_dir): - os.mkdir(systemd_telegraf_override_dir) - # Create custome scripts dir if not os.path.exists(custom_scripts_dir): os.mkdir(custom_scripts_dir) # Render telegraf configuration and systemd override - render(config_telegraf, 'monitoring/telegraf.j2', monitoring) - render(systemd_telegraf_service, 'monitoring/systemd_vyos_telegraf_service.j2', monitoring) - render(systemd_override, 'monitoring/override.conf.j2', monitoring, permission=0o640) - render(syslog_telegraf, 'monitoring/syslog_telegraf.j2', monitoring) - - chown(base_dir, 'telegraf', 'telegraf') + render(config_telegraf, 'telegraf/telegraf.j2', monitoring, user='telegraf', group='telegraf') + render(systemd_override, 'telegraf/override.conf.j2', monitoring) + render(syslog_telegraf, 'telegraf/syslog_telegraf.j2', monitoring) return None def apply(monitoring): # Reload systemd manager configuration + systemd_service = 'telegraf.service' call('systemctl daemon-reload') - if monitoring: - call('systemctl restart vyos-telegraf.service') - else: - call('systemctl stop vyos-telegraf.service') + if not monitoring: + call(f'systemctl stop {systemd_service}') + return + + # we need to restart the service if e.g. the VRF name changed + systemd_action = 'reload-or-restart' + if 'restart_required' in monitoring: + systemd_action = 'restart' + + call(f'systemctl {systemd_action} {systemd_service}') + # Telegraf include custom rsyslog config changes - call('systemctl restart rsyslog') + call('systemctl reload-or-restart rsyslog') if __name__ == '__main__': try: diff --git a/src/conf_mode/service_upnp.py b/src/conf_mode/service_upnp.py index 36f3e18a7..c798fd515 100755 --- a/src/conf_mode/service_upnp.py +++ b/src/conf_mode/service_upnp.py @@ -24,8 +24,6 @@ from ipaddress import IPv6Network from vyos.config import Config from vyos.configdict import dict_merge -from vyos.configdict import get_interface_dict -from vyos.configverify import verify_vrf from vyos.util import call from vyos.template import render from vyos.template import is_ipv4 @@ -113,19 +111,28 @@ def verify(upnpd): listen_dev = [] system_addrs_cidr = get_all_interface_addr(True, [], [netifaces.AF_INET, netifaces.AF_INET6]) system_addrs = get_all_interface_addr(False, [], [netifaces.AF_INET, netifaces.AF_INET6]) + if 'listen' not in upnpd: + raise ConfigError(f'Listen address or interface is required!') for listen_if_or_addr in upnpd['listen']: if listen_if_or_addr not in netifaces.interfaces(): listen_dev.append(listen_if_or_addr) - if (listen_if_or_addr not in system_addrs) and (listen_if_or_addr not in system_addrs_cidr) and (listen_if_or_addr not in netifaces.interfaces()): + if (listen_if_or_addr not in system_addrs) and (listen_if_or_addr not in system_addrs_cidr) and \ + (listen_if_or_addr not in netifaces.interfaces()): if is_ipv4(listen_if_or_addr) and IPv4Network(listen_if_or_addr).is_multicast: - raise ConfigError(f'The address "{listen_if_or_addr}" is an address that is not allowed to listen on. It is not an interface address nor a multicast address!') + raise ConfigError(f'The address "{listen_if_or_addr}" is an address that is not allowed' + f'to listen on. It is not an interface address nor a multicast address!') if is_ipv6(listen_if_or_addr) and IPv6Network(listen_if_or_addr).is_multicast: - raise ConfigError(f'The address "{listen_if_or_addr}" is an address that is not allowed to listen on. It is not an interface address nor a multicast address!') + raise ConfigError(f'The address "{listen_if_or_addr}" is an address that is not allowed' + f'to listen on. It is not an interface address nor a multicast address!') system_listening_dev_addrs_cidr = get_all_interface_addr(True, listen_dev, [netifaces.AF_INET6]) system_listening_dev_addrs = get_all_interface_addr(False, listen_dev, [netifaces.AF_INET6]) for listen_if_or_addr in upnpd['listen']: - if listen_if_or_addr not in netifaces.interfaces() and (listen_if_or_addr not in system_listening_dev_addrs_cidr) and (listen_if_or_addr not in system_listening_dev_addrs) and is_ipv6(listen_if_or_addr) and (not IPv6Network(listen_if_or_addr).is_multicast): + if listen_if_or_addr not in netifaces.interfaces() and \ + (listen_if_or_addr not in system_listening_dev_addrs_cidr) and \ + (listen_if_or_addr not in system_listening_dev_addrs) and \ + is_ipv6(listen_if_or_addr) and \ + (not IPv6Network(listen_if_or_addr).is_multicast): raise ConfigError(f'{listen_if_or_addr} must listen on the interface of the network card') def generate(upnpd): diff --git a/src/conf_mode/ssh.py b/src/conf_mode/ssh.py index 28669694b..2bbd7142a 100755 --- a/src/conf_mode/ssh.py +++ b/src/conf_mode/ssh.py @@ -22,6 +22,7 @@ from syslog import LOG_INFO from vyos.config import Config from vyos.configdict import dict_merge +from vyos.configdict import is_node_changed from vyos.configverify import verify_vrf from vyos.util import call from vyos.template import render @@ -50,6 +51,10 @@ def get_config(config=None): return None ssh = conf.get_config_dict(base, key_mangling=('-', '_'), get_first_key=True) + + tmp = is_node_changed(conf, base + ['vrf']) + if tmp: ssh.update({'restart_required': {}}) + # We have gathered the dict representation of the CLI, but there are default # options which we need to update into the dictionary retrived. default_values = defaults(base) @@ -104,17 +109,25 @@ def generate(ssh): return None def apply(ssh): + systemd_service_ssh = 'ssh.service' + systemd_service_sshguard = 'sshguard.service' if not ssh: # SSH access is removed in the commit - call('systemctl stop ssh.service') - call('systemctl stop sshguard.service') + call(f'systemctl stop {systemd_service_ssh}') + call(f'systemctl stop {systemd_service_sshguard}') return None + if 'dynamic_protection' not in ssh: - call('systemctl stop sshguard.service') + call(f'systemctl stop {systemd_service_sshguard}') else: - call('systemctl restart sshguard.service') + call(f'systemctl reload-or-restart {systemd_service_sshguard}') + + # we need to restart the service if e.g. the VRF name changed + systemd_action = 'reload-or-restart' + if 'restart_required' in ssh: + systemd_action = 'restart' - call('systemctl restart ssh.service') + call(f'systemctl {systemd_action} {systemd_service_ssh}') return None if __name__ == '__main__': diff --git a/src/conf_mode/vpn_ipsec.py b/src/conf_mode/vpn_ipsec.py index bad9cfbd8..5ca32d23e 100755 --- a/src/conf_mode/vpn_ipsec.py +++ b/src/conf_mode/vpn_ipsec.py @@ -595,13 +595,11 @@ def wait_for_vici_socket(timeout=5, sleep_interval=0.1): sleep(sleep_interval) def apply(ipsec): + systemd_service = 'strongswan-starter.service' if not ipsec: - call('sudo ipsec stop') + call(f'systemctl stop {systemd_service}') else: - call('sudo ipsec restart') - call('sudo ipsec rereadall') - call('sudo ipsec reload') - + call(f'systemctl reload-or-restart {systemd_service}') if wait_for_vici_socket(): call('sudo swanctl -q') diff --git a/src/conf_mode/vpn_openconnect.py b/src/conf_mode/vpn_openconnect.py index 8e0e30bbf..240546817 100755 --- a/src/conf_mode/vpn_openconnect.py +++ b/src/conf_mode/vpn_openconnect.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2018-2020 VyOS maintainers and contributors +# Copyright (C) 2018-2022 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 @@ -23,7 +23,9 @@ from vyos.pki import wrap_certificate from vyos.pki import wrap_private_key from vyos.template import render from vyos.util import call +from vyos.util import check_port_availability from vyos.util import is_systemd_service_running +from vyos.util import is_listen_port_bind_service from vyos.util import dict_search from vyos.xml import defaults from vyos import ConfigError @@ -75,6 +77,12 @@ def get_config(): def verify(ocserv): if ocserv is None: return None + # Check if listen-ports not binded other services + # It can be only listen by 'ocserv-main' + for proto, port in ocserv.get('listen_ports').items(): + if check_port_availability('0.0.0.0', int(port), proto) is not True and \ + not is_listen_port_bind_service(int(port), 'ocserv-main'): + raise ConfigError(f'"{proto}" port "{port}" is used by another service') # Check authentication if "authentication" in ocserv: if "mode" in ocserv["authentication"]: diff --git a/src/conf_mode/vpn_sstp.py b/src/conf_mode/vpn_sstp.py index 23e5162ba..2949ab290 100755 --- a/src/conf_mode/vpn_sstp.py +++ b/src/conf_mode/vpn_sstp.py @@ -26,7 +26,9 @@ from vyos.pki import wrap_certificate from vyos.pki import wrap_private_key from vyos.template import render from vyos.util import call +from vyos.util import check_port_availability from vyos.util import dict_search +from vyos.util import is_listen_port_bind_service from vyos.util import write_file from vyos import ConfigError from vyos import airbag @@ -62,6 +64,12 @@ def verify(sstp): if not sstp: return None + port = sstp.get('port') + proto = 'tcp' + if check_port_availability('0.0.0.0', int(port), proto) is not True and \ + not is_listen_port_bind_service(int(port), 'accel-pppd'): + raise ConfigError(f'"{proto}" port "{port}" is used by another service') + verify_accel_ppp_base_service(sstp) if 'client_ip_pool' not in sstp and 'client_ipv6_pool' not in sstp: diff --git a/src/etc/opennhrp/opennhrp-script.py b/src/etc/opennhrp/opennhrp-script.py index 8274e6564..a5293c97e 100755 --- a/src/etc/opennhrp/opennhrp-script.py +++ b/src/etc/opennhrp/opennhrp-script.py @@ -18,44 +18,120 @@ import os import re import sys import vici + from json import loads +from pathlib import Path +from vyos.logger import getLogger from vyos.util import cmd from vyos.util import process_named_running -NHRP_CONFIG = "/run/opennhrp/opennhrp.conf" +NHRP_CONFIG: str = '/run/opennhrp/opennhrp.conf' + + +def vici_get_ipsec_uniqueid(conn: str, src_nbma: str, + dst_nbma: str) -> list[str]: + """ Find and return IKE SAs by src nbma and dst nbma + + Args: + conn (str): a connection name + src_nbma (str): an IP address of NBMA source + dst_nbma (str): an IP address of NBMA destination + + Returns: + list: a list of IKE connections that match a criteria + """ + if not conn or not src_nbma or not dst_nbma: + logger.error( + f'Incomplete input data for resolving IKE unique ids: ' + f'conn: {conn}, src_nbma: {src_nbma}, dst_nbma: {dst_nbma}') + return [] + + try: + logger.info( + f'Resolving IKE unique ids for: conn: {conn}, ' + f'src_nbma: {src_nbma}, dst_nbma: {dst_nbma}') + session: vici.Session = vici.Session() + list_ikeid: list[str] = [] + list_sa = session.list_sas({'ike': conn}) + for sa in list_sa: + if sa[conn]['local-host'].decode('ascii') == src_nbma \ + and sa[conn]['remote-host'].decode('ascii') == dst_nbma: + list_ikeid.append(sa[conn]['uniqueid'].decode('ascii')) + return list_ikeid + except Exception as err: + logger.error(f'Unable to find unique ids for IKE: {err}') + return [] + + +def vici_ike_terminate(list_ikeid: list[str]) -> bool: + """Terminating IKE SAs by list of IKE IDs + + Args: + list_ikeid (list[str]): a list of IKE ids to terminate + + Returns: + bool: result of termination action + """ + if not list: + logger.warning('An empty list for termination was provided') + return False + + try: + session = vici.Session() + for ikeid in list_ikeid: + logger.info(f'Terminating IKE SA with id {ikeid}') + session.terminate({'ike-id': ikeid, 'timeout': '-1'}) + return True + except Exception as err: + logger.error(f'Failed to terminate SA for IKE ids {list_ikeid}: {err}') + return False + +def parse_type_ipsec(interface: str) -> tuple[str, str]: + """Get DMVPN Type and NHRP Profile from the configuration -def parse_type_ipsec(interface): - with open(NHRP_CONFIG, 'r') as f: - lines = f.readlines() - match = rf'^interface {interface} #(hub|spoke)(?:\s([\w-]+))?$' - for line in lines: - m = re.match(match, line) - if m: - return m[1], m[2] - return None, None + Args: + interface (str): a name of interface + + Returns: + tuple[str, str]: `peer_type` and `profile_name` + """ + if not interface: + logger.error('Cannot find peer type - no input provided') + return '', '' + + config_file: str = Path(NHRP_CONFIG).read_text() + regex: str = rf'^interface {interface} #(?P<peer_type>hub|spoke) ?(?P<profile_name>[^\n]*)$' + match = re.search(regex, config_file, re.M) + if match: + return match.groupdict()['peer_type'], match.groupdict()[ + 'profile_name'] + return '', '' def add_peer_route(nbma_src: str, nbma_dst: str, mtu: str) -> None: """Add a route to a NBMA peer Args: - nmba_src (str): a local IP address + nbma_src (str): a local IP address nbma_dst (str): a remote IP address mtu (str): a MTU for a route """ + logger.info(f'Adding route from {nbma_src} to {nbma_dst} with MTU {mtu}') # Find routes to a peer - route_get_cmd = f'sudo ip -j route get {nbma_dst} from {nbma_src}' + route_get_cmd: str = f'sudo ip --json route get {nbma_dst} from {nbma_src}' try: route_info_data = loads(cmd(route_get_cmd)) except Exception as err: - print(f'Unable to find a route to {nbma_dst}: {err}') + logger.error(f'Unable to find a route to {nbma_dst}: {err}') + return # Check if an output has an expected format if not isinstance(route_info_data, list): - print(f'Garbage returned from the "{route_get_cmd}" command: \ - {route_info_data}') + logger.error( + f'Garbage returned from the "{route_get_cmd}" ' + f'command: {route_info_data}') return # Add static routes to a peer @@ -76,104 +152,217 @@ def add_peer_route(nbma_src: str, nbma_dst: str, mtu: str) -> None: try: cmd(route_add_cmd) except Exception as err: - print(f'Unable to add a route using command "{route_add_cmd}": \ - {err}') + logger.error( + f'Unable to add a route using command "{route_add_cmd}": ' + f'{err}') -def vici_initiate(conn, child_sa, src_addr, dest_addr): - try: - session = vici.Session() - logs = session.initiate({ - 'ike': conn, - 'child': child_sa, - 'timeout': '-1', - 'my-host': src_addr, - 'other-host': dest_addr - }) - for log in logs: - message = log['msg'].decode('ascii') - print('INIT LOG:', message) - return True - except: - return None +def vici_initiate(conn: str, child_sa: str, src_addr: str, + dest_addr: str) -> bool: + """Initiate IKE SA connection with specific peer + Args: + conn (str): an IKE connection name + child_sa (str): a child SA profile name + src_addr (str): NBMA local address + dest_addr (str): NBMA address of a peer -def vici_terminate(conn, child_sa, src_addr, dest_addr): + Returns: + bool: a result of initiation command + """ + logger.info( + f'Trying to initiate connection. Name: {conn}, child sa: {child_sa}, ' + f'src_addr: {src_addr}, dst_addr: {dest_addr}') try: session = vici.Session() - logs = session.terminate({ + session.initiate({ 'ike': conn, 'child': child_sa, 'timeout': '-1', 'my-host': src_addr, 'other-host': dest_addr }) - for log in logs: - message = log['msg'].decode('ascii') - print('TERM LOG:', message) return True - except: - return None + except Exception as err: + logger.error(f'Unable to initiate connection {err}') + return False + + +def vici_terminate(conn: str, src_addr: str, dest_addr: str) -> None: + """Find and terminate IKE SAs by local NBMA and remote NBMA addresses + + Args: + conn (str): IKE connection name + src_addr (str): NBMA local address + dest_addr (str): NBMA address of a peer + """ + logger.info( + f'Terminating IKE connection {conn} between {src_addr} ' + f'and {dest_addr}') + ikeid_list: list[str] = vici_get_ipsec_uniqueid(conn, src_addr, dest_addr) -def iface_up(interface): - cmd(f'sudo ip route flush proto 42 dev {interface}') - cmd(f'sudo ip neigh flush dev {interface}') + if not ikeid_list: + logger.warning( + f'No active sessions found for IKE profile {conn}, ' + f'local NBMA {src_addr}, remote NBMA {dest_addr}') + else: + vici_ike_terminate(ikeid_list) -def peer_up(dmvpn_type, conn): - # src_addr = os.getenv('NHRP_SRCADDR') +def iface_up(interface: str) -> None: + """Proceed tunnel interface UP event + + Args: + interface (str): an interface name + """ + if not interface: + logger.warning('No interface name provided for UP event') + + logger.info(f'Turning up interface {interface}') + try: + cmd(f'sudo ip route flush proto 42 dev {interface}') + cmd(f'sudo ip neigh flush dev {interface}') + except Exception as err: + logger.error( + f'Unable to flush route on interface "{interface}": {err}') + + +def peer_up(dmvpn_type: str, conn: str) -> None: + """Proceed NHRP peer UP event + + Args: + dmvpn_type (str): a type of peer + conn (str): an IKE profile name + """ + logger.info(f'Peer UP event for {dmvpn_type} using IKE profile {conn}') src_nbma = os.getenv('NHRP_SRCNBMA') - # dest_addr = os.getenv('NHRP_DESTADDR') dest_nbma = os.getenv('NHRP_DESTNBMA') dest_mtu = os.getenv('NHRP_DESTMTU') + if not src_nbma or not dest_nbma: + logger.error( + f'Can not get NHRP NBMA addresses: local {src_nbma}, ' + f'remote {dest_nbma}') + return + + logger.info(f'NBMA addresses: local {src_nbma}, remote {dest_nbma}') if dest_mtu: add_peer_route(src_nbma, dest_nbma, dest_mtu) - if conn and dmvpn_type == 'spoke' and process_named_running('charon'): - vici_terminate(conn, 'dmvpn', src_nbma, dest_nbma) + vici_terminate(conn, src_nbma, dest_nbma) vici_initiate(conn, 'dmvpn', src_nbma, dest_nbma) -def peer_down(dmvpn_type, conn): +def peer_down(dmvpn_type: str, conn: str) -> None: + """Proceed NHRP peer DOWN event + + Args: + dmvpn_type (str): a type of peer + conn (str): an IKE profile name + """ + logger.info(f'Peer DOWN event for {dmvpn_type} using IKE profile {conn}') + src_nbma = os.getenv('NHRP_SRCNBMA') dest_nbma = os.getenv('NHRP_DESTNBMA') + if not src_nbma or not dest_nbma: + logger.error( + f'Can not get NHRP NBMA addresses: local {src_nbma}, ' + f'remote {dest_nbma}') + return + + logger.info(f'NBMA addresses: local {src_nbma}, remote {dest_nbma}') if conn and dmvpn_type == 'spoke' and process_named_running('charon'): - vici_terminate(conn, 'dmvpn', src_nbma, dest_nbma) + vici_terminate(conn, src_nbma, dest_nbma) + try: + cmd(f'sudo ip route del {dest_nbma} src {src_nbma} proto 42') + except Exception as err: + logger.error( + f'Unable to del route from {src_nbma} to {dest_nbma}: {err}') - cmd(f'sudo ip route del {dest_nbma} src {src_nbma} proto 42') +def route_up(interface: str) -> None: + """Proceed NHRP route UP event + + Args: + interface (str): an interface name + """ + logger.info(f'Route UP event for interface {interface}') -def route_up(interface): dest_addr = os.getenv('NHRP_DESTADDR') dest_prefix = os.getenv('NHRP_DESTPREFIX') next_hop = os.getenv('NHRP_NEXTHOP') - cmd(f'sudo ip route replace {dest_addr}/{dest_prefix} proto 42 \ - via {next_hop} dev {interface}') - cmd('sudo ip route flush cache') + if not dest_addr or not dest_prefix or not next_hop: + logger.error( + f'Can not get route details: dest_addr {dest_addr}, ' + f'dest_prefix {dest_prefix}, next_hop {next_hop}') + return + + logger.info( + f'Route details: dest_addr {dest_addr}, dest_prefix {dest_prefix}, ' + f'next_hop {next_hop}') + try: + cmd(f'sudo ip route replace {dest_addr}/{dest_prefix} proto 42 \ + via {next_hop} dev {interface}') + cmd('sudo ip route flush cache') + except Exception as err: + logger.error( + f'Unable replace or flush route to {dest_addr}/{dest_prefix} ' + f'via {next_hop} dev {interface}: {err}') + + +def route_down(interface: str) -> None: + """Proceed NHRP route DOWN event + + Args: + interface (str): an interface name + """ + logger.info(f'Route DOWN event for interface {interface}') -def route_down(interface): dest_addr = os.getenv('NHRP_DESTADDR') dest_prefix = os.getenv('NHRP_DESTPREFIX') - cmd(f'sudo ip route del {dest_addr}/{dest_prefix} proto 42') - cmd('sudo ip route flush cache') + if not dest_addr or not dest_prefix: + logger.error( + f'Can not get route details: dest_addr {dest_addr}, ' + f'dest_prefix {dest_prefix}') + return + + logger.info( + f'Route details: dest_addr {dest_addr}, dest_prefix {dest_prefix}') + try: + cmd(f'sudo ip route del {dest_addr}/{dest_prefix} proto 42') + cmd('sudo ip route flush cache') + except Exception as err: + logger.error( + f'Unable delete or flush route to {dest_addr}/{dest_prefix}: ' + f'{err}') if __name__ == '__main__': + logger = getLogger('opennhrp-script', syslog=True) + logger.debug( + f'Running script with arguments: {sys.argv}, ' + f'environment: {os.environ}') + action = sys.argv[1] interface = os.getenv('NHRP_INTERFACE') - dmvpn_type, profile_name = parse_type_ipsec(interface) - dmvpn_conn = None + if not interface: + logger.error('Can not get NHRP interface name') + sys.exit(1) - if profile_name: - dmvpn_conn = f'dmvpn-{profile_name}-{interface}' + dmvpn_type, profile_name = parse_type_ipsec(interface) + if not dmvpn_type: + logger.info(f'Interface {interface} is not NHRP tunnel') + sys.exit() + dmvpn_conn: str = '' + if profile_name: + dmvpn_conn: str = f'dmvpn-{profile_name}-{interface}' if action == 'interface-up': iface_up(interface) elif action == 'peer-register': @@ -186,3 +375,5 @@ if __name__ == '__main__': route_up(interface) elif action == 'route-down': route_down(interface) + + sys.exit() diff --git a/src/etc/systemd/system/wpa_supplicant-wired@.service.d/override.conf b/src/etc/systemd/system/wpa_supplicant-wired@.service.d/override.conf new file mode 100644 index 000000000..030b89a2b --- /dev/null +++ b/src/etc/systemd/system/wpa_supplicant-wired@.service.d/override.conf @@ -0,0 +1,11 @@ +[Unit] +After= +After=vyos-router.service + +[Service] +WorkingDirectory= +WorkingDirectory=/run/wpa_supplicant +PIDFile=/run/wpa_supplicant/%I.pid +ExecStart= +ExecStart=/sbin/wpa_supplicant -c/run/wpa_supplicant/%I.conf -Dwired -P/run/wpa_supplicant/%I.pid -i%I +ExecReload=/bin/kill -HUP $MAINPID diff --git a/src/migration-scripts/bgp/2-to-3 b/src/migration-scripts/bgp/2-to-3 new file mode 100755 index 000000000..7ced0a3b0 --- /dev/null +++ b/src/migration-scripts/bgp/2-to-3 @@ -0,0 +1,51 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2022 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/>. + +# T4257: Discussion on changing BGP autonomous system number syntax + +from sys import argv +from sys import exit + +from vyos.configtree import ConfigTree + +if (len(argv) < 1): + print("Must specify file name!") + exit(1) + +file_name = argv[1] + +with open(file_name, 'r') as f: + config_file = f.read() + +config = ConfigTree(config_file) + +# Check if BGP is even configured. Then check if local-as exists, then add the system-as, then remove the local-as. This is for global configuration. +if config.exists(['protocols', 'bgp']): + if config.exists(['protocols', 'bgp', 'local-as']): + config.rename(['protocols', 'bgp', 'local-as'], 'system-as') + +# Check if vrf names are configured. Then check if local-as exists inside of a name, then add the system-as, then remove the local-as. This is for vrf configuration. +if config.exists(['vrf', 'name']): + for vrf in config.list_nodes(['vrf', 'name']): + if config.exists(['vrf', f'name {vrf}', 'protocols', 'bgp', 'local-as']): + config.rename(['vrf', f'name {vrf}', 'protocols', 'bgp', 'local-as'], 'system-as') + +try: + with open(file_name, 'w') as f: + f.write(config.to_string()) +except OSError as e: + print(f'Failed to save the modified config: {e}') + exit(1) diff --git a/src/op_mode/bridge.py b/src/op_mode/bridge.py index 411aa06d1..fe8dadd70 100755 --- a/src/op_mode/bridge.py +++ b/src/op_mode/bridge.py @@ -197,6 +197,6 @@ if __name__ == '__main__': res = vyos.opmode.run(sys.modules[__name__]) if res: print(res) - except ValueError as e: + except (ValueError, vyos.opmode.Error) as e: print(e) sys.exit(1) diff --git a/src/op_mode/conntrack.py b/src/op_mode/conntrack.py index 1441d110f..b27aa6060 100755 --- a/src/op_mode/conntrack.py +++ b/src/op_mode/conntrack.py @@ -51,6 +51,21 @@ def _get_raw_data(family): return _xml_to_dict(xml) +def _get_raw_statistics(): + entries = [] + data = cmd('sudo conntrack -S') + data = data.replace(' \t', '').split('\n') + for entry in data: + entries.append(entry.split()) + return entries + + +def get_formatted_statistics(entries): + headers = ["CPU", "Found", "Invalid", "Insert", "Insert fail", "Drop", "Early drop", "Errors", "Search restart"] + output = tabulate(entries, headers, numalign="left") + return output + + def get_formatted_output(dict_data): """ :param xml: @@ -111,11 +126,19 @@ def show(raw: bool, family: str): return get_formatted_output(conntrack_data) +def show_statistics(raw: bool): + conntrack_statistics = _get_raw_statistics() + if raw: + return conntrack_statistics + else: + return get_formatted_statistics(conntrack_statistics) + + if __name__ == '__main__': try: res = vyos.opmode.run(sys.modules[__name__]) if res: print(res) - except ValueError as e: + except (ValueError, vyos.opmode.Error) as e: print(e) sys.exit(1) diff --git a/src/op_mode/container.py b/src/op_mode/container.py index 78d42f800..ce466ffc1 100755 --- a/src/op_mode/container.py +++ b/src/op_mode/container.py @@ -80,6 +80,6 @@ if __name__ == '__main__': res = vyos.opmode.run(sys.modules[__name__]) if res: print(res) - except ValueError as e: + except (ValueError, vyos.opmode.Error) as e: print(e) sys.exit(1) diff --git a/src/op_mode/cpu.py b/src/op_mode/cpu.py index f9c425826..d53663c17 100755 --- a/src/op_mode/cpu.py +++ b/src/op_mode/cpu.py @@ -76,7 +76,7 @@ if __name__ == '__main__': res = vyos.opmode.run(sys.modules[__name__]) if res: print(res) - except ValueError as e: + except (ValueError, vyos.opmode.Error) as e: print(e) sys.exit(1) diff --git a/src/op_mode/dns.py b/src/op_mode/dns.py index 717652b9b..9e5b1040c 100755 --- a/src/op_mode/dns.py +++ b/src/op_mode/dns.py @@ -90,6 +90,6 @@ if __name__ == '__main__': res = vyos.opmode.run(sys.modules[__name__]) if res: print(res) - except ValueError as e: + except (ValueError, vyos.opmode.Error) as e: print(e) sys.exit(1) diff --git a/src/op_mode/ipsec.py b/src/op_mode/ipsec.py index caa1eef51..a4d1b4cb1 100755 --- a/src/op_mode/ipsec.py +++ b/src/op_mode/ipsec.py @@ -182,6 +182,6 @@ if __name__ == '__main__': res = vyos.opmode.run(sys.modules[__name__]) if res: print(res) - except ValueError as e: + except (ValueError, vyos.opmode.Error) as e: print(e) sys.exit(1) diff --git a/src/op_mode/memory.py b/src/op_mode/memory.py index a3870e498..178544be4 100755 --- a/src/op_mode/memory.py +++ b/src/op_mode/memory.py @@ -84,7 +84,7 @@ if __name__ == '__main__': res = vyos.opmode.run(sys.modules[__name__]) if res: print(res) - except ValueError as e: + except (ValueError, vyos.opmode.Error) as e: print(e) sys.exit(1) diff --git a/src/op_mode/nat.py b/src/op_mode/nat.py index a98fc4227..dec04aa48 100755 --- a/src/op_mode/nat.py +++ b/src/op_mode/nat.py @@ -27,7 +27,7 @@ from vyos.util import dict_search import vyos.opmode -def _get_json_data(direction): +def _get_json_data(direction, family): """ Get NAT format JSON """ @@ -35,14 +35,15 @@ def _get_json_data(direction): chain = 'POSTROUTING' if direction == 'destination': chain = 'PREROUTING' - return cmd(f'sudo nft --json list chain ip nat {chain}') + family = 'ip6' if family == 'inet6' else 'ip' + return cmd(f'sudo nft --json list chain {family} nat {chain}') -def _get_raw_data_rules(direction): +def _get_raw_data_rules(direction, family): """Get interested rules :returns dict """ - data = _get_json_data(direction) + data = _get_json_data(direction, family) data_dict = json.loads(data) rules = [] for rule in data_dict['nftables']: @@ -51,10 +52,12 @@ def _get_raw_data_rules(direction): return rules -def _get_formatted_output_rules(data, direction): +def _get_formatted_output_rules(data, direction, family): # Add default values before loop sport, dport, proto = 'any', 'any', 'any' - saddr, daddr = '0.0.0.0/0', '0.0.0.0/0' + saddr = '::/0' if family == 'inet6' else '0.0.0.0/0' + daddr = '::/0' if family == 'inet6' else '0.0.0.0/0' + data_entries = [] for rule in data: if 'comment' in rule['rule']: @@ -69,11 +72,13 @@ def _get_formatted_output_rules(data, direction): if 'prefix' in match['right'] or 'set' in match['right']: # Merge dict src/dst l3_l4 parameters my_dict = {**match['left']['payload'], **match['right']} + my_dict['op'] = match['op'] + op = '!' if my_dict.get('op') == '!=' else '' proto = my_dict.get('protocol').upper() if my_dict['field'] == 'saddr': - saddr = f'{my_dict["prefix"]["addr"]}/{my_dict["prefix"]["len"]}' + saddr = f'{op}{my_dict["prefix"]["addr"]}/{my_dict["prefix"]["len"]}' elif my_dict['field'] == 'daddr': - daddr = f'{my_dict["prefix"]["addr"]}/{my_dict["prefix"]["len"]}' + daddr = f'{op}{my_dict["prefix"]["addr"]}/{my_dict["prefix"]["len"]}' elif my_dict['field'] == 'sport': # Port range or single port if jmespath.search('set[*].range', my_dict): @@ -96,8 +101,8 @@ def _get_formatted_output_rules(data, direction): if jmespath.search('left.payload.field', match) == 'daddr': daddr = match.get('right') else: - saddr = '0.0.0.0/0' - daddr = '0.0.0.0/0' + saddr = '::/0' if family == 'inet6' else '0.0.0.0/0' + daddr = '::/0' if family == 'inet6' else '0.0.0.0/0' sport = 'any' dport = 'any' proto = 'any' @@ -175,16 +180,16 @@ def _get_formatted_output_statistics(data, direction): return output -def show_rules(raw: bool, direction: str): - nat_rules = _get_raw_data_rules(direction) +def show_rules(raw: bool, direction: str, family: str): + nat_rules = _get_raw_data_rules(direction, family) if raw: return nat_rules else: - return _get_formatted_output_rules(nat_rules, direction) + return _get_formatted_output_rules(nat_rules, direction, family) -def show_statistics(raw: bool, direction: str): - nat_statistics = _get_raw_data_rules(direction) +def show_statistics(raw: bool, direction: str, family: str): + nat_statistics = _get_raw_data_rules(direction, family) if raw: return nat_statistics else: @@ -196,6 +201,6 @@ if __name__ == '__main__': res = vyos.opmode.run(sys.modules[__name__]) if res: print(res) - except ValueError as e: + except (ValueError, vyos.opmode.Error) as e: print(e) sys.exit(1) diff --git a/src/op_mode/neighbor.py b/src/op_mode/neighbor.py index d86a372ac..264dbdc72 100755 --- a/src/op_mode/neighbor.py +++ b/src/op_mode/neighbor.py @@ -116,7 +116,7 @@ if __name__ == '__main__': res = vyos.opmode.run(sys.modules[__name__]) if res: print(res) - except ValueError as e: + except (ValueError, vyos.opmode.Error) as e: print(e) sys.exit(1) diff --git a/src/op_mode/openconnect-control.py b/src/op_mode/openconnect-control.py index a128cc011..20c50e779 100755 --- a/src/op_mode/openconnect-control.py +++ b/src/op_mode/openconnect-control.py @@ -19,7 +19,6 @@ import argparse import json from vyos.config import Config -from vyos.util import commit_in_progress from vyos.util import popen from vyos.util import run from vyos.util import DEVNULL @@ -60,10 +59,6 @@ def main(): # Check is Openconnect server configured is_ocserv_configured() - if commit_in_progress(): - print('Cannot restart openconnect while a commit is in progress') - exit(1) - if args.action == "restart": run("sudo systemctl restart ocserv.service") sys.exit(0) diff --git a/src/op_mode/openconnect.py b/src/op_mode/openconnect.py new file mode 100755 index 000000000..00992c66a --- /dev/null +++ b/src/op_mode/openconnect.py @@ -0,0 +1,81 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2022 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +import sys +import json + +from tabulate import tabulate +from vyos.configquery import ConfigTreeQuery +from vyos.util import rc_cmd + +import vyos.opmode + + +occtl = '/usr/bin/occtl' +occtl_socket = '/run/ocserv/occtl.socket' + + +def _get_raw_data_sessions(): + rc, out = rc_cmd(f'sudo {occtl} --json --socket-file {occtl_socket} show users') + if rc != 0: + output = {'openconnect': + { + 'configured': False, + 'return_code': rc, + 'reason': out + } + } + return output + + sessions = json.loads(out) + return sessions + + +def _get_formatted_sessions(data): + headers = ["Interface", "Username", "IP", "Remote IP", "RX", "TX", "State", "Uptime"] + ses_list = [] + for ses in data: + ses_list.append([ + ses["Device"], ses["Username"], ses["IPv4"], ses["Remote IP"], + ses["_RX"], ses["_TX"], ses["State"], ses["_Connected at"] + ]) + if len(ses_list) > 0: + output = tabulate(ses_list, headers) + else: + output = 'No active openconnect sessions' + return output + + +def show_sessions(raw: bool): + config = ConfigTreeQuery() + if not config.exists('vpn openconnect') and not raw: + print('Openconnect is not configured') + exit(0) + + openconnect_data = _get_raw_data_sessions() + if raw: + return openconnect_data + return _get_formatted_sessions(openconnect_data) + + +if __name__ == '__main__': + try: + res = vyos.opmode.run(sys.modules[__name__]) + if res: + print(res) + except (ValueError, vyos.opmode.Error) as e: + print(e) + sys.exit(1) diff --git a/src/op_mode/restart_dhcp_relay.py b/src/op_mode/restart_dhcp_relay.py index db5a48970..9203c009f 100755 --- a/src/op_mode/restart_dhcp_relay.py +++ b/src/op_mode/restart_dhcp_relay.py @@ -43,7 +43,7 @@ if __name__ == '__main__': if commit_in_progress(): print('Cannot restart DHCP relay while a commit is in progress') exit(1) - call('systemctl restart isc-dhcp-server.service') + call('systemctl restart isc-dhcp-relay.service') sys.exit(0) elif args.ipv6: @@ -54,7 +54,7 @@ if __name__ == '__main__': if commit_in_progress(): print('Cannot restart DHCPv6 relay while commit is in progress') exit(1) - call('systemctl restart isc-dhcp-server6.service') + call('systemctl restart isc-dhcp-relay6.service') sys.exit(0) else: diff --git a/src/op_mode/route.py b/src/op_mode/route.py index 3bb06adac..e1eee5bbf 100644 --- a/src/op_mode/route.py +++ b/src/op_mode/route.py @@ -92,7 +92,7 @@ if __name__ == '__main__': res = vyos.opmode.run(sys.modules[__name__]) if res: print(res) - except ValueError as e: + except (ValueError, vyos.opmode.Error) as e: print(e) sys.exit(1) diff --git a/src/op_mode/show_nat66_rules.py b/src/op_mode/show_nat66_rules.py deleted file mode 100755 index 967ec9d37..000000000 --- a/src/op_mode/show_nat66_rules.py +++ /dev/null @@ -1,102 +0,0 @@ -#!/usr/bin/env python3 -# -# Copyright (C) 2021 VyOS maintainers and contributors -# -# This program is free software; you can redistribute it and/or modify -# it under the terms of the GNU General Public License version 2 or later as -# published by the Free Software Foundation. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see <http://www.gnu.org/licenses/>. - -import jmespath -import json - -from argparse import ArgumentParser -from jinja2 import Template -from sys import exit -from vyos.util import cmd -from vyos.util import dict_search - -parser = ArgumentParser() -group = parser.add_mutually_exclusive_group() -group.add_argument("--source", help="Show statistics for configured source NAT rules", action="store_true") -group.add_argument("--destination", help="Show statistics for configured destination NAT rules", action="store_true") -args = parser.parse_args() - -if args.source or args.destination: - tmp = cmd('sudo nft -j list table ip6 nat') - tmp = json.loads(tmp) - - format_nat66_rule = '{0: <10} {1: <50} {2: <50} {3: <10}' - print(format_nat66_rule.format("Rule", "Source" if args.source else "Destination", "Translation", "Outbound Interface" if args.source else "Inbound Interface")) - print(format_nat66_rule.format("----", "------" if args.source else "-----------", "-----------", "------------------" if args.source else "-----------------")) - - data_json = jmespath.search('nftables[?rule].rule[?chain]', tmp) - for idx in range(0, len(data_json)): - data = data_json[idx] - - # The following key values must exist - # When the rule JSON does not have some keys, this is not a rule we can work with - continue_rule = False - for key in ['comment', 'chain', 'expr']: - if key not in data: - continue_rule = True - continue - if continue_rule: - continue - - comment = data['comment'] - - # Check the annotation to see if the annotation format is created by VYOS - continue_rule = True - for comment_prefix in ['SRC-NAT66-', 'DST-NAT66-']: - if comment_prefix in comment: - continue_rule = False - if continue_rule: - continue - - # When log is detected from the second index of expr, then this rule should be ignored - if 'log' in data['expr'][2]: - continue - - rule = comment.replace('SRC-NAT66-','') - rule = rule.replace('DST-NAT66-','') - chain = data['chain'] - if not ((args.source and chain == 'POSTROUTING') or (not args.source and chain == 'PREROUTING')): - continue - interface = dict_search('match.right', data['expr'][0]) - srcdest = dict_search('match.right.prefix.addr', data['expr'][2]) - if srcdest: - addr_tmp = dict_search('match.right.prefix.len', data['expr'][2]) - if addr_tmp: - srcdest = srcdest + '/' + str(addr_tmp) - else: - srcdest = dict_search('match.right', data['expr'][2]) - - tran_addr_json = dict_search('snat.addr' if args.source else 'dnat.addr', data['expr'][3]) - if tran_addr_json: - if isinstance(srcdest_json,str): - tran_addr = tran_addr_json - - if 'prefix' in tran_addr_json: - addr_tmp = dict_search('snat.addr.prefix.addr' if args.source else 'dnat.addr.prefix.addr', data['expr'][3]) - len_tmp = dict_search('snat.addr.prefix.len' if args.source else 'dnat.addr.prefix.len', data['expr'][3]) - if addr_tmp: - tran_addr = addr_tmp + '/' + str(len_tmp) - else: - if 'masquerade' in data['expr'][3]: - tran_addr = 'masquerade' - - print(format_nat66_rule.format(rule, srcdest, tran_addr, interface)) - - exit(0) -else: - parser.print_help() - exit(1) - diff --git a/src/op_mode/version.py b/src/op_mode/version.py index 06208c3e5..ad0293aca 100755 --- a/src/op_mode/version.py +++ b/src/op_mode/version.py @@ -78,7 +78,7 @@ if __name__ == '__main__': res = vyos.opmode.run(sys.modules[__name__]) if res: print(res) - except ValueError as e: + except (ValueError, vyos.opmode.Error) as e: print(e) sys.exit(1) diff --git a/src/op_mode/vrf.py b/src/op_mode/vrf.py index e3d944d90..aeb50fe6e 100755 --- a/src/op_mode/vrf.py +++ b/src/op_mode/vrf.py @@ -90,6 +90,6 @@ if __name__ == "__main__": res = vyos.opmode.run(sys.modules[__name__]) if res: print(res) - except ValueError as e: + except (ValueError, vyos.opmode.Error) as e: print(e) sys.exit(1) diff --git a/src/services/api/graphql/graphql/mutations.py b/src/services/api/graphql/graphql/mutations.py index 3e89fb239..c8ae0f516 100644 --- a/src/services/api/graphql/graphql/mutations.py +++ b/src/services/api/graphql/graphql/mutations.py @@ -21,7 +21,7 @@ from makefun import with_signature from .. import state from .. import key_auth -from api.graphql.recipes.session import Session +from api.graphql.session.session import Session mutation = ObjectType("Mutation") @@ -71,7 +71,7 @@ def make_mutation_resolver(mutation_name, class_name, session_func): # one may override the session functions with a local subclass try: - mod = import_module(f'api.graphql.recipes.{func_base_name}') + mod = import_module(f'api.graphql.session.override.{func_base_name}') klass = getattr(mod, class_name) except ImportError: # otherwise, dynamically generate subclass to invoke subclass diff --git a/src/services/api/graphql/graphql/queries.py b/src/services/api/graphql/graphql/queries.py index f6544709e..921a66274 100644 --- a/src/services/api/graphql/graphql/queries.py +++ b/src/services/api/graphql/graphql/queries.py @@ -21,7 +21,7 @@ from makefun import with_signature from .. import state from .. import key_auth -from api.graphql.recipes.session import Session +from api.graphql.session.session import Session query = ObjectType("Query") @@ -71,7 +71,7 @@ def make_query_resolver(query_name, class_name, session_func): # one may override the session functions with a local subclass try: - mod = import_module(f'api.graphql.recipes.{func_base_name}') + mod = import_module(f'api.graphql.session.override.{func_base_name}') klass = getattr(mod, class_name) except ImportError: # otherwise, dynamically generate subclass to invoke subclass diff --git a/src/services/api/graphql/recipes/__init__.py b/src/services/api/graphql/session/__init__.py index e69de29bb..e69de29bb 100644 --- a/src/services/api/graphql/recipes/__init__.py +++ b/src/services/api/graphql/session/__init__.py diff --git a/src/services/api/graphql/recipes/queries/system_status.py b/src/services/api/graphql/session/composite/system_status.py index 8dadcc9f3..8dadcc9f3 100755 --- a/src/services/api/graphql/recipes/queries/system_status.py +++ b/src/services/api/graphql/session/composite/system_status.py diff --git a/src/services/api/graphql/recipes/remove_firewall_address_group_members.py b/src/services/api/graphql/session/override/remove_firewall_address_group_members.py index b91932e14..b91932e14 100644 --- a/src/services/api/graphql/recipes/remove_firewall_address_group_members.py +++ b/src/services/api/graphql/session/override/remove_firewall_address_group_members.py diff --git a/src/services/api/graphql/recipes/session.py b/src/services/api/graphql/session/session.py index ac185beb7..23bc7154c 100644 --- a/src/services/api/graphql/recipes/session.py +++ b/src/services/api/graphql/session/session.py @@ -149,7 +149,7 @@ class Session: return res def system_status(self): - import api.graphql.recipes.queries.system_status as system_status + import api.graphql.session.composite.system_status as system_status session = self._session data = self._data diff --git a/src/services/api/graphql/recipes/templates/create_dhcp_server.tmpl b/src/services/api/graphql/session/templates/create_dhcp_server.tmpl index 70de43183..70de43183 100644 --- a/src/services/api/graphql/recipes/templates/create_dhcp_server.tmpl +++ b/src/services/api/graphql/session/templates/create_dhcp_server.tmpl diff --git a/src/services/api/graphql/recipes/templates/create_firewall_address_group.tmpl b/src/services/api/graphql/session/templates/create_firewall_address_group.tmpl index a890d0086..a890d0086 100644 --- a/src/services/api/graphql/recipes/templates/create_firewall_address_group.tmpl +++ b/src/services/api/graphql/session/templates/create_firewall_address_group.tmpl diff --git a/src/services/api/graphql/recipes/templates/create_firewall_address_ipv_6_group.tmpl b/src/services/api/graphql/session/templates/create_firewall_address_ipv_6_group.tmpl index e9b660722..e9b660722 100644 --- a/src/services/api/graphql/recipes/templates/create_firewall_address_ipv_6_group.tmpl +++ b/src/services/api/graphql/session/templates/create_firewall_address_ipv_6_group.tmpl diff --git a/src/services/api/graphql/recipes/templates/create_interface_ethernet.tmpl b/src/services/api/graphql/session/templates/create_interface_ethernet.tmpl index d9d7ed691..d9d7ed691 100644 --- a/src/services/api/graphql/recipes/templates/create_interface_ethernet.tmpl +++ b/src/services/api/graphql/session/templates/create_interface_ethernet.tmpl diff --git a/src/services/api/graphql/recipes/templates/remove_firewall_address_group_members.tmpl b/src/services/api/graphql/session/templates/remove_firewall_address_group_members.tmpl index 458f3e5fc..458f3e5fc 100644 --- a/src/services/api/graphql/recipes/templates/remove_firewall_address_group_members.tmpl +++ b/src/services/api/graphql/session/templates/remove_firewall_address_group_members.tmpl diff --git a/src/services/api/graphql/recipes/templates/remove_firewall_address_ipv_6_group_members.tmpl b/src/services/api/graphql/session/templates/remove_firewall_address_ipv_6_group_members.tmpl index 0efa0b226..0efa0b226 100644 --- a/src/services/api/graphql/recipes/templates/remove_firewall_address_ipv_6_group_members.tmpl +++ b/src/services/api/graphql/session/templates/remove_firewall_address_ipv_6_group_members.tmpl diff --git a/src/services/api/graphql/recipes/templates/update_firewall_address_group_members.tmpl b/src/services/api/graphql/session/templates/update_firewall_address_group_members.tmpl index f56c61231..f56c61231 100644 --- a/src/services/api/graphql/recipes/templates/update_firewall_address_group_members.tmpl +++ b/src/services/api/graphql/session/templates/update_firewall_address_group_members.tmpl diff --git a/src/services/api/graphql/recipes/templates/update_firewall_address_ipv_6_group_members.tmpl b/src/services/api/graphql/session/templates/update_firewall_address_ipv_6_group_members.tmpl index f98a5517c..f98a5517c 100644 --- a/src/services/api/graphql/recipes/templates/update_firewall_address_ipv_6_group_members.tmpl +++ b/src/services/api/graphql/session/templates/update_firewall_address_ipv_6_group_members.tmpl diff --git a/src/services/api/graphql/utils/schema_from_op_mode.py b/src/services/api/graphql/utils/schema_from_op_mode.py index d27586747..f990aae52 100755 --- a/src/services/api/graphql/utils/schema_from_op_mode.py +++ b/src/services/api/graphql/utils/schema_from_op_mode.py @@ -25,7 +25,10 @@ from inspect import signature, getmembers, isfunction from jinja2 import Template from vyos.defaults import directories -from . util import load_as_module, is_op_mode_function_name, is_show_function_name +if __package__ is None or __package__ == '': + from util import load_as_module, is_op_mode_function_name, is_show_function_name +else: + from . util import load_as_module, is_op_mode_function_name, is_show_function_name OP_MODE_PATH = directories['op_mode'] SCHEMA_PATH = directories['api_schema'] diff --git a/src/services/vyos-http-api-server b/src/services/vyos-http-api-server index af8837e1e..190f3409d 100755 --- a/src/services/vyos-http-api-server +++ b/src/services/vyos-http-api-server @@ -678,6 +678,7 @@ if __name__ == '__main__': server_config = load_server_config() except Exception as err: logger.critical(f"Failed to load the HTTP API server config: {err}") + sys.exit(1) config_session = ConfigSession(os.getpid()) diff --git a/src/system/keepalived-fifo.py b/src/system/keepalived-fifo.py index a8df232ae..a0fccd1d0 100755 --- a/src/system/keepalived-fifo.py +++ b/src/system/keepalived-fifo.py @@ -30,6 +30,7 @@ from vyos.ifconfig.vrrp import VRRP from vyos.configquery import ConfigTreeQuery from vyos.util import cmd from vyos.util import dict_search +from vyos.util import commit_in_progress # configure logging logger = logging.getLogger(__name__) @@ -63,6 +64,17 @@ class KeepalivedFifo: # load configuration def _config_load(self): + # For VRRP configuration to be read, the commit must be finished + count = 1 + while commit_in_progress(): + if ( count <= 40 ): + logger.debug(f'commit in progress try: {count}') + else: + logger.error(f'commit still in progress after {count} continuing anyway') + break + count += 1 + time.sleep(0.5) + try: base = ['high-availability', 'vrrp'] conf = ConfigTreeQuery() diff --git a/data/templates/monitoring/systemd_vyos_telegraf_service.j2 b/src/systemd/telegraf.service index 234ef5586..553942ac6 100644 --- a/data/templates/monitoring/systemd_vyos_telegraf_service.j2 +++ b/src/systemd/telegraf.service @@ -5,8 +5,7 @@ After=network.target [Service] EnvironmentFile=-/etc/default/telegraf -User=telegraf -ExecStart=/usr/bin/telegraf -config /run/telegraf/vyos-telegraf.conf -config-directory /etc/telegraf/telegraf.d $TELEGRAF_OPTS +ExecStart=/usr/bin/telegraf --config /run/telegraf/vyos-telegraf.conf --config-directory /etc/telegraf/telegraf.d ExecReload=/bin/kill -HUP $MAINPID Restart=on-failure RestartForceExitStatus=SIGPIPE diff --git a/src/validators/ipv6-address-exclude b/src/validators/ipv6-address-exclude new file mode 100755 index 000000000..be1d3db25 --- /dev/null +++ b/src/validators/ipv6-address-exclude @@ -0,0 +1,7 @@ +#!/bin/sh +arg="$1" +if [ "${arg:0:1}" != "!" ]; then + exit 1 +fi +path=$(dirname "$0") +${path}/ipv6-address "${arg:1}" diff --git a/src/validators/ipv6-prefix-exclude b/src/validators/ipv6-prefix-exclude new file mode 100755 index 000000000..6fa4f1d8d --- /dev/null +++ b/src/validators/ipv6-prefix-exclude @@ -0,0 +1,7 @@ +#!/bin/sh +arg="$1" +if [ "${arg:0:1}" != "!" ]; then + exit 1 +fi +path=$(dirname "$0") +${path}/ipv6-prefix "${arg:1}" |