diff options
67 files changed, 2335 insertions, 257 deletions
diff --git a/data/templates/dhcp-server/dhcpd.conf.tmpl b/data/templates/dhcp-server/dhcpd.conf.tmpl index da2f28ced..dbd864b5e 100644 --- a/data/templates/dhcp-server/dhcpd.conf.tmpl +++ b/data/templates/dhcp-server/dhcpd.conf.tmpl @@ -42,9 +42,9 @@ failover peer "{{ failover.name }}" { secondary; {% endif %} address {{ failover.source_address }}; - port 520; + port 647; peer address {{ failover.remote }}; - peer port 520; + peer port 647; max-response-delay 30; max-unacked-updates 10; load balance max seconds 3; diff --git a/data/templates/monitoring/override.conf.tmpl b/data/templates/monitoring/override.conf.tmpl new file mode 100644 index 000000000..f8f150791 --- /dev/null +++ b/data/templates/monitoring/override.conf.tmpl @@ -0,0 +1,7 @@ +[Unit] +After=vyos-router.service +ConditionPathExists=/run/telegraf/vyos-telegraf.conf +[Service] +Environment=INFLUX_TOKEN={{ authentication.token }} +CapabilityBoundingSet=CAP_NET_RAW CAP_NET_ADMIN CAP_SYS_ADMIN +AmbientCapabilities=CAP_NET_RAW CAP_NET_ADMIN diff --git a/data/templates/monitoring/syslog_telegraf.tmpl b/data/templates/monitoring/syslog_telegraf.tmpl new file mode 100644 index 000000000..cdcbd92a4 --- /dev/null +++ b/data/templates/monitoring/syslog_telegraf.tmpl @@ -0,0 +1,5 @@ +# Generated by /usr/libexec/vyos/conf_mode/service_monitoring_telegraf.py + +$ModLoad omuxsock +$OMUxSockSocket /run/telegraf/telegraf_syslog.sock +*.notice :omuxsock: diff --git a/data/templates/monitoring/systemd_vyos_telegraf_service.tmpl b/data/templates/monitoring/systemd_vyos_telegraf_service.tmpl new file mode 100644 index 000000000..234ef5586 --- /dev/null +++ b/data/templates/monitoring/systemd_vyos_telegraf_service.tmpl @@ -0,0 +1,16 @@ +[Unit] +Description=The plugin-driven server agent for reporting metrics into InfluxDB +Documentation=https://github.com/influxdata/telegraf +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 +ExecReload=/bin/kill -HUP $MAINPID +Restart=on-failure +RestartForceExitStatus=SIGPIPE +KillMode=control-group + +[Install] +WantedBy=multi-user.target diff --git a/data/templates/monitoring/telegraf.tmpl b/data/templates/monitoring/telegraf.tmpl new file mode 100644 index 000000000..d3145a500 --- /dev/null +++ b/data/templates/monitoring/telegraf.tmpl @@ -0,0 +1,60 @@ +# Generated by /usr/libexec/vyos/conf_mode/service_monitoring_telegraf.py + +[agent] + interval = "10s" + round_interval = true + metric_batch_size = 1000 + metric_buffer_limit = 10000 + collection_jitter = "0s" + flush_interval = "10s" + flush_jitter = "0s" + precision = "" + debug = false + quiet = false + logfile = "" + hostname = "" + omit_hostname = false +[[outputs.influxdb_v2]] + urls = ["{{ url }}:{{ port }}"] + insecure_skip_verify = true + token = "$INFLUX_TOKEN" + organization = "{{ authentication.organization }}" + bucket = "{{ bucket }}" +[[inputs.cpu]] + percpu = true + totalcpu = true + collect_cpu_time = false + report_active = false +[[inputs.disk]] + ignore_fs = ["devtmpfs", "devfs"] +[[inputs.diskio]] +[[inputs.mem]] +[[inputs.net]] +[[inputs.system]] +[[inputs.netstat]] +[[inputs.processes]] +[[inputs.kernel]] +[[inputs.interrupts]] +[[inputs.linux_sysctl_fs]] +[[inputs.systemd_units]] +[[inputs.conntrack]] + files = ["ip_conntrack_count","ip_conntrack_max","nf_conntrack_count","nf_conntrack_max"] + dirs = ["/proc/sys/net/ipv4/netfilter","/proc/sys/net/netfilter"] +[[inputs.ethtool]] + interface_include = {{ interfaces_ethernet }} +[[inputs.ntpq]] + dns_lookup = true +[[inputs.internal]] +[[inputs.nstat]] +[[inputs.syslog]] + server = "unixgram:///run/telegraf/telegraf_syslog.sock" + best_effort = true + syslog_standard = "RFC3164" +[[inputs.exec]] + commands = [ + "{{ custom_scripts_dir }}/show_firewall_input_filter.py", + "{{ custom_scripts_dir }}/show_interfaces_input_filter.py", + "{{ custom_scripts_dir }}/vyos_services_input_filter.py" + ] + timeout = "10s" + data_format = "influx" diff --git a/data/templates/openvpn/server.conf.tmpl b/data/templates/openvpn/server.conf.tmpl index c2b0c2ef9..75aae2981 100644 --- a/data/templates/openvpn/server.conf.tmpl +++ b/data/templates/openvpn/server.conf.tmpl @@ -135,11 +135,13 @@ ping {{ keep_alive.interval }} ping-restart {{ keep_alive.failure_count }} {% if device_type == 'tap' %} -{% for laddr, laddr_conf in local_address.items() if laddr | is_ipv4 %} -{% if laddr_conf is defined and laddr_conf.subnet_mask is defined and laddr_conf.subnet_mask is not none %} +{% if local_address is defined and local_address is not none %} +{% for laddr, laddr_conf in local_address.items() if laddr | is_ipv4 %} +{% if laddr_conf is defined and laddr_conf.subnet_mask is defined and laddr_conf.subnet_mask is not none %} ifconfig {{ laddr }} {{ laddr_conf.subnet_mask }} -{% endif %} -{% endfor %} +{% endif %} +{% endfor %} +{% endif %} {% else %} {% for laddr in local_address if laddr | is_ipv4 %} {% for raddr in remote_address if raddr | is_ipv4 %} diff --git a/data/templates/vrrp/keepalived.conf.tmpl b/data/templates/vrrp/keepalived.conf.tmpl index afbdc978e..10d5c6d24 100644 --- a/data/templates/vrrp/keepalived.conf.tmpl +++ b/data/templates/vrrp/keepalived.conf.tmpl @@ -28,6 +28,9 @@ vrrp_instance {{ name }} { virtual_router_id {{ group_config.vrid }} priority {{ group_config.priority }} advert_int {{ group_config.advertise_interval }} +{% if group_config.track is defined and group_config.track.exclude_vrrp_interface is defined %} + dont_track_primary +{% endif %} {% if group_config.no_preempt is not defined and group_config.preempt_delay is defined and group_config.preempt_delay is not none %} preempt_delay {{ group_config.preempt_delay }} {% elif group_config.no_preempt is defined %} @@ -61,8 +64,8 @@ vrrp_instance {{ name }} { {% endif %} {% if group_config.virtual_address is defined and group_config.virtual_address is not none %} virtual_ipaddress { -{% for addr in group_config.virtual_address %} - {{ addr }} +{% for addr, addr_config in group_config.virtual_address.items() %} + {{ addr }}{{ ' dev ' + addr_config.interface if addr_config.interface is defined }} {% endfor %} } {% endif %} @@ -73,6 +76,13 @@ vrrp_instance {{ name }} { {% endfor %} } {% endif %} +{% if group_config.track is defined and group_config.track.interface is defined and group_config.track.interface is not none %} + track_interface { +{% for interface in group_config.track.interface %} + {{ interface }} +{% endfor %} + } +{% endif %} {% if group_config.health_check is defined and group_config.health_check.script is defined and group_config.health_check.script is not none %} track_script { healthcheck_{{ name }} diff --git a/debian/control b/debian/control index 8cafd8257..7706c6523 100644 --- a/debian/control +++ b/debian/control @@ -130,6 +130,7 @@ Depends: ssl-cert, sudo, systemd, + telegraf (>= 1.20), tcpdump, tcptraceroute, telnet, diff --git a/debian/vyos-1x.install b/debian/vyos-1x.install index 082c5a62f..38a032a63 100644 --- a/debian/vyos-1x.install +++ b/debian/vyos-1x.install @@ -5,6 +5,7 @@ etc/ppp etc/rsyslog.d etc/systemd etc/sysctl.d +etc/telegraf etc/udev etc/update-motd.d etc/vyos diff --git a/debian/vyos-1x.postinst b/debian/vyos-1x.postinst index 92948de12..d54f6c5af 100644 --- a/debian/vyos-1x.postinst +++ b/debian/vyos-1x.postinst @@ -33,3 +33,12 @@ fi # ensure hte proxy user has a proper shell chsh -s /bin/sh proxy + +# Remove unwanted daemon files from /etc +# conntackd +DELETE="/etc/logrotate.d/conntrackd.distrib /etc/init.d/conntrackd /etc/default/conntrackd" +for file in $DELETE; do + if [ -f ${file} ]; then + rm -f ${file} + fi +done diff --git a/debian/vyos-1x.preinst b/debian/vyos-1x.preinst new file mode 100644 index 000000000..a3773d141 --- /dev/null +++ b/debian/vyos-1x.preinst @@ -0,0 +1 @@ +dpkg-divert --package vyos-1x --add --rename /etc/logrotate.d/conntrackd diff --git a/interface-definitions/dhcp-relay.xml.in b/interface-definitions/dhcp-relay.xml.in index 0d485ef80..e9eb4b5c7 100644 --- a/interface-definitions/dhcp-relay.xml.in +++ b/interface-definitions/dhcp-relay.xml.in @@ -49,6 +49,7 @@ </constraint> <constraintErrorMessage>max-size must be a value between 64 and 1400</constraintErrorMessage> </properties> + <defaultValue>576</defaultValue> </leafNode> <leafNode name="relay-agents-packets"> <properties> diff --git a/interface-definitions/include/generic-interface-broadcast.xml.i b/interface-definitions/include/generic-interface-broadcast.xml.i new file mode 100644 index 000000000..6f76dde1a --- /dev/null +++ b/interface-definitions/include/generic-interface-broadcast.xml.i @@ -0,0 +1,17 @@ +<!-- include start from generic-interface-broadcast.xml.i --> +<leafNode name="interface"> + <properties> + <help>Interface Name to use</help> + <completionHelp> + <script>${vyos_completion_dir}/list_interfaces.py --broadcast</script> + </completionHelp> + <valueHelp> + <format>txt</format> + <description>Interface name</description> + </valueHelp> + <constraint> + <validator name="interface-name"/> + </constraint> + </properties> +</leafNode> +<!-- include end --> diff --git a/interface-definitions/include/interface/inbound-interface.xml.i b/interface-definitions/include/interface/inbound-interface.xml.i new file mode 100644 index 000000000..5a8d47280 --- /dev/null +++ b/interface-definitions/include/interface/inbound-interface.xml.i @@ -0,0 +1,10 @@ +<!-- include start from interface/inbound-interface.xml.i --> +<leafNode name="inbound-interface"> + <properties> + <help>Inbound Interface</help> + <completionHelp> + <script>${vyos_completion_dir}/list_interfaces.py</script> + </completionHelp> + </properties> +</leafNode> +<!-- include end --> diff --git a/interface-definitions/include/interface/tunnel-remote-multi.xml.i b/interface-definitions/include/interface/tunnel-remote-multi.xml.i new file mode 100644 index 000000000..f672087a4 --- /dev/null +++ b/interface-definitions/include/interface/tunnel-remote-multi.xml.i @@ -0,0 +1,19 @@ +<!-- include start from interface/tunnel-remote-multi.xml.i --> +<leafNode name="remote"> + <properties> + <help>Tunnel remote address</help> + <valueHelp> + <format>ipv4</format> + <description>Tunnel remote IPv4 address</description> + </valueHelp> + <valueHelp> + <format>ipv6</format> + <description>Tunnel remote IPv6 address</description> + </valueHelp> + <constraint> + <validator name="ip-address"/> + </constraint> + <multi/> + </properties> +</leafNode> +<!-- include end --> diff --git a/interface-definitions/include/ssh-user.xml.i b/interface-definitions/include/ssh-user.xml.i index 677602dd8..17ba05a90 100644 --- a/interface-definitions/include/ssh-user.xml.i +++ b/interface-definitions/include/ssh-user.xml.i @@ -3,9 +3,9 @@ <properties> <help>Allow specific users to login</help> <constraint> - <regex>[a-z_][a-z0-9_-]{1,31}[$]?</regex> + <regex>^[-_a-zA-Z0-9.]{1,100}</regex> </constraint> - <constraintErrorMessage>illegal characters or more than 32 characters</constraintErrorMessage> + <constraintErrorMessage>Illegal characters or more than 100 characters</constraintErrorMessage> <multi/> </properties> </leafNode> diff --git a/interface-definitions/include/tunnel-remote.xml.i b/interface-definitions/include/tunnel-remote.xml.i index 324d100d4..2a8891b85 100644 --- a/interface-definitions/include/tunnel-remote.xml.i +++ b/interface-definitions/include/tunnel-remote.xml.i @@ -1,4 +1,4 @@ -<!-- include start from tunnel-remote.xml.i --> +<!-- include start from interface/tunnel-remote.xml.i --> <leafNode name="remote"> <properties> <help>Tunnel remote address</help> diff --git a/interface-definitions/interfaces-macsec.xml.in b/interface-definitions/interfaces-macsec.xml.in index 4a566ef8b..96dcf3ca0 100644 --- a/interface-definitions/interfaces-macsec.xml.in +++ b/interface-definitions/interfaces-macsec.xml.in @@ -16,7 +16,9 @@ </valueHelp> </properties> <children> - #include <include/interface/address-ipv4-ipv6.xml.i> + #include <include/interface/address-ipv4-ipv6-dhcp.xml.i> + #include <include/interface/dhcp-options.xml.i> + #include <include/interface/dhcpv6-options.xml.i> #include <include/interface/ipv4-options.xml.i> #include <include/interface/ipv6-options.xml.i> <node name="security"> diff --git a/interface-definitions/interfaces-vxlan.xml.in b/interface-definitions/interfaces-vxlan.xml.in index 6dcc3f5a0..efc019e04 100644 --- a/interface-definitions/interfaces-vxlan.xml.in +++ b/interface-definitions/interfaces-vxlan.xml.in @@ -59,7 +59,6 @@ </leafNode> #include <include/source-address-ipv4-ipv6.xml.i> #include <include/source-interface.xml.i> - #include <include/tunnel-remote.xml.i> <leafNode name="vni"> <properties> <help>Virtual Network Identifier</help> @@ -72,6 +71,7 @@ </constraint> </properties> </leafNode> + #include <include/interface/tunnel-remote-multi.xml.i> #include <include/interface/vrf.xml.i> </children> </tagNode> diff --git a/interface-definitions/interfaces-wireless.xml.in b/interface-definitions/interfaces-wireless.xml.in index 048c7b475..cc3fe2a6a 100644 --- a/interface-definitions/interfaces-wireless.xml.in +++ b/interface-definitions/interfaces-wireless.xml.in @@ -527,6 +527,7 @@ <regex>^(disabled|optional|required)$</regex> </constraint> </properties> + <defaultValue>disabled</defaultValue> </leafNode> <leafNode name="mode"> <properties> diff --git a/interface-definitions/policy-local-route.xml.in b/interface-definitions/policy-local-route.xml.in index 3769c3748..573a7963f 100644 --- a/interface-definitions/policy-local-route.xml.in +++ b/interface-definitions/policy-local-route.xml.in @@ -14,7 +14,7 @@ <valueHelp> <!-- table main with prio 32766 --> <format>u32:1-32765</format> - <description>Local-route rule number (1-219)</description> + <description>Local-route rule number (1-32765)</description> </valueHelp> <constraint> <validator name="numeric" argument="--range 1-32765"/> @@ -40,6 +40,18 @@ </leafNode> </children> </node> + <leafNode name="fwmark"> + <properties> + <help>Match fwmark value</help> + <valueHelp> + <format>u32:1-2147483647</format> + <description>Address to match against</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 1-2147483647"/> + </constraint> + </properties> + </leafNode> <leafNode name="source"> <properties> <help>Source address or prefix</help> @@ -58,6 +70,115 @@ <multi/> </properties> </leafNode> + <leafNode name="destination"> + <properties> + <help>Destination address or prefix</help> + <valueHelp> + <format>ipv4</format> + <description>Address to match against</description> + </valueHelp> + <valueHelp> + <format>ipv4net</format> + <description>Prefix to match against</description> + </valueHelp> + <constraint> + <validator name="ipv4-address"/> + <validator name="ip-prefix"/> + </constraint> + <multi/> + </properties> + </leafNode> + #include <include/interface/inbound-interface.xml.i> + </children> + </tagNode> + </children> + </node> + <node name="local-route6" owner="${vyos_conf_scripts_dir}/policy-local-route.py"> + <properties> + <help>IPv6 policy route of local traffic</help> + </properties> + <children> + <tagNode name="rule"> + <properties> + <help>IPv6 policy local-route rule set number</help> + <valueHelp> + <!-- table main with prio 32766 --> + <format>u32:1-32765</format> + <description>Local-route rule number (1-32765)</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 1-32765"/> + </constraint> + </properties> + <children> + <node name="set"> + <properties> + <help>Packet modifications</help> + </properties> + <children> + <leafNode name="table"> + <properties> + <help>Routing table to forward packet with</help> + <valueHelp> + <format>u32:1-200</format> + <description>Table number</description> + </valueHelp> + <completionHelp> + <list>main</list> + </completionHelp> + </properties> + </leafNode> + </children> + </node> + <leafNode name="fwmark"> + <properties> + <help>Match fwmark value</help> + <valueHelp> + <format>u32:1-2147483647</format> + <description>Address to match against</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 1-2147483647"/> + </constraint> + </properties> + </leafNode> + <leafNode name="source"> + <properties> + <help>Source address or prefix</help> + <valueHelp> + <format>ipv4</format> + <description>Address to match against</description> + </valueHelp> + <valueHelp> + <format>ipv4net</format> + <description>Prefix to match against</description> + </valueHelp> + <constraint> + <validator name="ipv6-address"/> + <validator name="ipv6-prefix"/> + </constraint> + <multi/> + </properties> + </leafNode> + <leafNode name="destination"> + <properties> + <help>Destination address or prefix</help> + <valueHelp> + <format>ipv6</format> + <description>Address to match against</description> + </valueHelp> + <valueHelp> + <format>ipv6net</format> + <description>Prefix to match against</description> + </valueHelp> + <constraint> + <validator name="ipv6-address"/> + <validator name="ipv6-prefix"/> + </constraint> + <multi/> + </properties> + </leafNode> + #include <include/interface/inbound-interface.xml.i> </children> </tagNode> </children> diff --git a/interface-definitions/service_monitoring_telegraf.xml.in b/interface-definitions/service_monitoring_telegraf.xml.in new file mode 100644 index 000000000..81ba67430 --- /dev/null +++ b/interface-definitions/service_monitoring_telegraf.xml.in @@ -0,0 +1,111 @@ +<?xml version="1.0"?> +<interfaceDefinition> + <node name="service"> + <children> + <node name="monitoring"> + <properties> + <help>Monitoring services</help> + <priority>1280</priority> + </properties> + <children> + <node name="telegraf" owner="${vyos_conf_scripts_dir}/service_monitoring_telegraf.py"> + <properties> + <help>Telegraf monitoring</help> + </properties> + <children> + <node name="authentication"> + <properties> + <help>Authentication parameters</help> + </properties> + <children> + <leafNode name="organization"> + <properties> + <help>Authentication organization for InfluxDB v2 [REQUIRED]</help> + <constraint> + <regex>^[a-zA-Z][1-9a-zA-Z@_\-.]{2,50}$</regex> + </constraint> + <constraintErrorMessage>Organization name must be alphanumeric and can contain hyphens, underscores and at symbol.</constraintErrorMessage> + </properties> + </leafNode> + <leafNode name="token"> + <properties> + <help>Authentication token for InfluxDB v2 [REQUIRED]</help> + <valueHelp> + <format>txt</format> + <description>Authentication token</description> + </valueHelp> + <constraint> + <regex>^[a-zA-Z0-9-_]{86}==$</regex> + </constraint> + <constraintErrorMessage>Token must be 88 characters long and must contain only [a-zA-Z0-9-_] and '==' characters.</constraintErrorMessage> + </properties> + </leafNode> + </children> + </node> + <leafNode name="bucket"> + <properties> + <help>Remote bucket, by default (main)</help> + </properties> + <defaultValue>main</defaultValue> + </leafNode> + <leafNode name="source"> + <properties> + <help>Source parameters for monitoring (default: all)</help> + <completionHelp> + <list>all hardware-utilization logs network system telegraf</list> + </completionHelp> + <valueHelp> + <format>all</format> + <description>All parameters (default)</description> + </valueHelp> + <valueHelp> + <format>hardware-utilization</format> + <description>Hardware-utilization parameters (CPU, disk, memory)</description> + </valueHelp> + <valueHelp> + <format>logs</format> + <description>Logs parameters</description> + </valueHelp> + <valueHelp> + <format>network</format> + <description>Network parameters (net, netstat, nftables)</description> + </valueHelp> + <valueHelp> + <format>system</format> + <description>System parameters (system, processes, interrupts)</description> + </valueHelp> + <valueHelp> + <format>telegraf</format> + <description>Telegraf internal statistics</description> + </valueHelp> + <constraint> + <regex>^(all|hardware-utilization|logs|network|system|telegraf)$</regex> + </constraint> + <multi/> + </properties> + <defaultValue>all</defaultValue> + </leafNode> + <leafNode name="url"> + <properties> + <help>Remote URL [REQUIRED]</help> + <valueHelp> + <format>url</format> + <description>Remote URL to InfluxDB v2</description> + </valueHelp> + <constraint> + <regex>^(http:\/\/www\.|https:\/\/www\.|http:\/\/|https:\/\/)?[a-z0-9]+([\-\.]{1}[a-z0-9]+)*\.[a-z]{2,5}?(\/.*)?$</regex> + </constraint> + <constraintErrorMessage>Incorrect URL format.</constraintErrorMessage> + </properties> + </leafNode> + #include <include/port-number.xml.i> + <leafNode name="port"> + <defaultValue>8086</defaultValue> + </leafNode> + </children> + </node> + </children> + </node> + </children> + </node> +</interfaceDefinition> diff --git a/interface-definitions/ssh.xml.in b/interface-definitions/ssh.xml.in index e3b9d16e1..867037295 100644 --- a/interface-definitions/ssh.xml.in +++ b/interface-definitions/ssh.xml.in @@ -44,7 +44,7 @@ <list>3des-cbc aes128-cbc aes192-cbc aes256-cbc rijndael-cbc@lysator.liu.se aes128-ctr aes192-ctr aes256-ctr aes128-gcm@openssh.com aes256-gcm@openssh.com chacha20-poly1305@openssh.com</list> </completionHelp> <constraint> - <regex>^(3des-cbc|aes128-cbc|aes192-cbc|aes256-cbc|rijndael-cbc@lysator.liu.se|aes128-ctr|aes192-ctr|aes256-ctr|aes128-gcm@openssh.com|aes256-gcm@openssh.com|chacha20-poly1305@openssh.com)$</regex> + <regex>(3des-cbc|aes128-cbc|aes192-cbc|aes256-cbc|rijndael-cbc@lysator.liu.se|aes128-ctr|aes192-ctr|aes256-ctr|aes128-gcm@openssh.com|aes256-gcm@openssh.com|chacha20-poly1305@openssh.com)</regex> </constraint> <multi/> </properties> @@ -70,7 +70,7 @@ </completionHelp> <multi/> <constraint> - <regex>^(diffie-hellman-group1-sha1|diffie-hellman-group14-sha1|diffie-hellman-group14-sha256|diffie-hellman-group16-sha512|diffie-hellman-group18-sha512|diffie-hellman-group-exchange-sha1|diffie-hellman-group-exchange-sha256|ecdh-sha2-nistp256|ecdh-sha2-nistp384|ecdh-sha2-nistp521|curve25519-sha256|curve25519-sha256@libssh.org)$</regex> + <regex>(diffie-hellman-group1-sha1|diffie-hellman-group14-sha1|diffie-hellman-group14-sha256|diffie-hellman-group16-sha512|diffie-hellman-group18-sha512|diffie-hellman-group-exchange-sha1|diffie-hellman-group-exchange-sha256|ecdh-sha2-nistp256|ecdh-sha2-nistp384|ecdh-sha2-nistp521|curve25519-sha256|curve25519-sha256@libssh.org)</regex> </constraint> </properties> </leafNode> @@ -102,7 +102,7 @@ <description>enable logging of failed login attempts</description> </valueHelp> <constraint> - <regex>^(quiet|fatal|error|info|verbose)$</regex> + <regex>(quiet|fatal|error|info|verbose)</regex> </constraint> </properties> <defaultValue>INFO</defaultValue> @@ -115,7 +115,7 @@ <list>hmac-sha1 hmac-sha1-96 hmac-sha2-256 hmac-sha2-512 hmac-md5 hmac-md5-96 umac-64@openssh.com umac-128@openssh.com hmac-sha1-etm@openssh.com hmac-sha1-96-etm@openssh.com hmac-sha2-256-etm@openssh.com hmac-sha2-512-etm@openssh.com hmac-md5-etm@openssh.com hmac-md5-96-etm@openssh.com umac-64-etm@openssh.com umac-128-etm@openssh.com</list> </completionHelp> <constraint> - <regex>^(hmac-sha1|hmac-sha1-96|hmac-sha2-256|hmac-sha2-512|hmac-md5|hmac-md5-96|umac-64@openssh.com|umac-128@openssh.com|hmac-sha1-etm@openssh.com|hmac-sha1-96-etm@openssh.com|hmac-sha2-256-etm@openssh.com|hmac-sha2-512-etm@openssh.com|hmac-md5-etm@openssh.com|hmac-md5-96-etm@openssh.com|umac-64-etm@openssh.com|umac-128-etm@openssh.com)$</regex> + <regex>(hmac-sha1|hmac-sha1-96|hmac-sha2-256|hmac-sha2-512|hmac-md5|hmac-md5-96|umac-64@openssh.com|umac-128@openssh.com|hmac-sha1-etm@openssh.com|hmac-sha1-96-etm@openssh.com|hmac-sha2-256-etm@openssh.com|hmac-sha2-512-etm@openssh.com|hmac-md5-etm@openssh.com|hmac-md5-96-etm@openssh.com|umac-64-etm@openssh.com|umac-128-etm@openssh.com)</regex> </constraint> <multi/> </properties> diff --git a/interface-definitions/vrrp.xml.in b/interface-definitions/vrrp.xml.in index b58cf735c..337a0bbc9 100644 --- a/interface-definitions/vrrp.xml.in +++ b/interface-definitions/vrrp.xml.in @@ -188,6 +188,35 @@ <help>Use VRRP virtual MAC address as per RFC3768</help> </properties> </leafNode> + <node name="track"> + <properties> + <help>Track settings</help> + </properties> + <children> + <leafNode name="exclude-vrrp-interface"> + <properties> + <valueless/> + <help>Disable track state of main interface</help> + </properties> + </leafNode> + <leafNode name="interface"> + <properties> + <help>Interface name state check</help> + <completionHelp> + <script>${vyos_completion_dir}/list_interfaces.py --broadcast</script> + </completionHelp> + <valueHelp> + <format>txt</format> + <description>Interface name</description> + </valueHelp> + <constraint> + <validator name="interface-name"/> + </constraint> + <multi/> + </properties> + </leafNode> + </children> + </node> <node name="transition-script"> <properties> <help>VRRP transition scripts</help> @@ -233,7 +262,7 @@ </leafNode> </children> </node> - <leafNode name="virtual-address"> + <tagNode name="virtual-address"> <properties> <help>Virtual address (IPv4 or IPv6, but they must not be mixed in one group)</help> <valueHelp> @@ -249,9 +278,11 @@ <validator name="ipv6-host"/> </constraint> <constraintErrorMessage>Virtual address must be a valid IPv4 or IPv6 address with prefix length (e.g. 192.0.2.3/24 or 2001:db8:ff::10/64)</constraintErrorMessage> - <multi/> </properties> - </leafNode> + <children> + #include <include/generic-interface-broadcast.xml.i> + </children> + </tagNode> <leafNode name="virtual-address-excluded"> <properties> <help>Virtual address (If you need additional IPv4 and IPv6 in same group)</help> diff --git a/op-mode-definitions/generate-public-key-command.xml.in b/op-mode-definitions/generate-public-key-command.xml.in new file mode 100644 index 000000000..024cfe594 --- /dev/null +++ b/op-mode-definitions/generate-public-key-command.xml.in @@ -0,0 +1,33 @@ +<?xml version="1.0"?> +<interfaceDefinition> + <node name="generate"> + <children> + <node name="public-key-command"> + <properties> + <help>Generate configuration mode command to add OpenSSH public key from file</help> + </properties> + <children> + <tagNode name="user"> + <properties> + <help>Username of public key owner</help> + <completionHelp> + <list><username></list> + </completionHelp> + </properties> + <children> + <tagNode name="path"> + <properties> + <help>Local path or remote URL of OpenSSH public key</help> + <completionHelp> + <list>URL PATH</list> + </completionHelp> + </properties> + <command>${vyos_op_scripts_dir}/generate_public_key_command.py "$4" "$6"</command> + </tagNode> + </children> + </tagNode> + </children> + </node> + </children> + </node> +</interfaceDefinition> diff --git a/python/vyos/configdict.py b/python/vyos/configdict.py index 486260152..1f245f3d2 100644 --- a/python/vyos/configdict.py +++ b/python/vyos/configdict.py @@ -117,6 +117,12 @@ def leaf_node_changed(conf, path): D.set_level(conf.get_level()) (new, old) = D.get_value_diff(path) if new != old: + if isinstance(old, dict): + # valueLess nodes return {} if node is deleted + return True + if old is None and isinstance(new, dict): + # valueLess nodes return {} if node was added + return True if old is None: return [] if isinstance(old, str): @@ -387,6 +393,15 @@ def get_interface_dict(config, base, ifname=''): bond = is_member(config, ifname, 'bonding') if bond: dict.update({'is_bond_member' : bond}) + # Check if any DHCP options changed which require a client restat + for leaf_node in ['client-id', 'default-route-distance', 'host-name', + 'no-default-route', 'vendor-class-id']: + dhcp = leaf_node_changed(config, ['dhcp-options', leaf_node]) + if dhcp: + dict.update({'dhcp_options_old' : dhcp}) + # one option is suffiecient to set 'dhcp_options_old' key + break + # Some interfaces come with a source_interface which must also not be part # of any other bond or bridge interface as it is exclusivly assigned as the # Kernels "lower" interface to this new "virtual/upper" interface. @@ -431,6 +446,15 @@ def get_interface_dict(config, base, ifname=''): bridge = is_member(config, f'{ifname}.{vif}', 'bridge') if bridge: dict['vif'][vif].update({'is_bridge_member' : bridge}) + # Check if any DHCP options changed which require a client restat + for leaf_node in ['client-id', 'default-route-distance', 'host-name', + 'no-default-route', 'vendor-class-id']: + dhcp = leaf_node_changed(config, ['vif', vif, 'dhcp-options', leaf_node]) + if dhcp: + dict['vif'][vif].update({'dhcp_options_old' : dhcp}) + # one option is suffiecient to set 'dhcp_options_old' key + break + for vif_s, vif_s_config in dict.get('vif_s', {}).items(): default_vif_s_values = defaults(base + ['vif-s']) # XXX: T2665: we only wan't the vif-s defaults - do not care about vif-c @@ -454,6 +478,15 @@ def get_interface_dict(config, base, ifname=''): bridge = is_member(config, f'{ifname}.{vif_s}', 'bridge') if bridge: dict['vif_s'][vif_s].update({'is_bridge_member' : bridge}) + # Check if any DHCP options changed which require a client restat + for leaf_node in ['client-id', 'default-route-distance', 'host-name', + 'no-default-route', 'vendor-class-id']: + dhcp = leaf_node_changed(config, ['vif-s', vif_s, 'dhcp-options', leaf_node]) + if dhcp: + dict['vif_s'][vif_s].update({'dhcp_options_old' : dhcp}) + # one option is suffiecient to set 'dhcp_options_old' key + break + for vif_c, vif_c_config in vif_s_config.get('vif_c', {}).items(): default_vif_c_values = defaults(base + ['vif-s', 'vif-c']) @@ -477,6 +510,16 @@ def get_interface_dict(config, base, ifname=''): if bridge: dict['vif_s'][vif_s]['vif_c'][vif_c].update( {'is_bridge_member' : bridge}) + # Check if any DHCP options changed which require a client restat + for leaf_node in ['client-id', 'default-route-distance', 'host-name', + 'no-default-route', 'vendor-class-id']: + dhcp = leaf_node_changed(config, ['vif-s', vif_s, 'vif-c', vif_c, + 'dhcp-options', leaf_node]) + if dhcp: + dict['vif_s'][vif_s]['vif_c'][vif_c].update({'dhcp_options_old' : dhcp}) + # one option is suffiecient to set 'dhcp_options_old' key + break + # Check vif, vif-s/vif-c VLAN interfaces for removal dict = get_removed_vlans(config, dict) return dict @@ -501,7 +544,6 @@ def get_vlan_ids(interface): return vlan_ids - def get_accel_dict(config, base, chap_secrets): """ Common utility function to retrieve and mangle the Accel-PPP configuration diff --git a/python/vyos/configverify.py b/python/vyos/configverify.py index 3aece499e..6566e8863 100644 --- a/python/vyos/configverify.py +++ b/python/vyos/configverify.py @@ -196,9 +196,10 @@ def verify_bridge_delete(config): when interface also is part of a bridge. """ if 'is_bridge_member' in config: - raise ConfigError( - 'Interface "{ifname}" cannot be deleted as it is a ' - 'member of bridge "{is_bridge_member}"!'.format(**config)) + interface = config['ifname'] + for bridge in config['is_bridge_member']: + raise ConfigError(f'Interface "{interface}" cannot be deleted as it ' + f'is a member of bridge "{bridge}"!') def verify_interface_exists(ifname): """ diff --git a/python/vyos/ethtool.py b/python/vyos/ethtool.py index e45b0f041..672ee2cc9 100644 --- a/python/vyos/ethtool.py +++ b/python/vyos/ethtool.py @@ -18,6 +18,9 @@ import re from vyos.util import popen +# These drivers do not support using ethtool to change the speed, duplex, or flow control settings +_drivers_without_speed_duplex_flow = ['vmxnet3', 'virtio_net', 'xen_netfront', 'iavf', 'ice', 'i40e'] + class Ethtool: """ Class is used to retrive and cache information about an ethernet adapter @@ -41,7 +44,7 @@ class Ethtool: # '100' : {'full': '', 'half': ''}, # '1000': {'full': ''} # } - _speed_duplex = { } + _speed_duplex = {'auto': {'auto': ''}} _ring_buffers = { } _ring_buffers_max = { } _driver_name = None @@ -188,7 +191,7 @@ class Ethtool: if duplex not in ['auto', 'full', 'half']: raise ValueError(f'Value "{duplex}" for duplex is invalid!') - if self.get_driver_name() in ['vmxnet3', 'virtio_net', 'xen_netfront']: + if self.get_driver_name() in _drivers_without_speed_duplex_flow: return False if speed in self._speed_duplex: @@ -198,7 +201,7 @@ class Ethtool: def check_flow_control(self): """ Check if the NIC supports flow-control """ - if self.get_driver_name() in ['vmxnet3', 'virtio_net', 'xen_netfront']: + if self.get_driver_name() in _drivers_without_speed_duplex_flow: return False return self._flow_control diff --git a/python/vyos/ifconfig/bridge.py b/python/vyos/ifconfig/bridge.py index 27073b266..ffd9c590f 100644 --- a/python/vyos/ifconfig/bridge.py +++ b/python/vyos/ifconfig/bridge.py @@ -298,7 +298,6 @@ class BridgeIf(Interface): tmp = dict_search('member.interface', config) if tmp: - for interface, interface_config in tmp.items(): # if interface does yet not exist bail out early and # add it later @@ -316,10 +315,13 @@ class BridgeIf(Interface): # enslave interface port to bridge self.add_port(interface) - # always set private-vlan/port isolation - tmp = dict_search('isolated', interface_config) - value = 'on' if (tmp != None) else 'off' - lower.set_port_isolation(value) + if not interface.startswith('wlan'): + # always set private-vlan/port isolation - this can not be + # done when lower link is a wifi link, as it will trigger: + # RTNETLINK answers: Operation not supported + tmp = dict_search('isolated', interface_config) + value = 'on' if (tmp != None) else 'off' + lower.set_port_isolation(value) # set bridge port path cost if 'cost' in interface_config: diff --git a/python/vyos/ifconfig/interface.py b/python/vyos/ifconfig/interface.py index 036ca1413..430940c57 100644 --- a/python/vyos/ifconfig/interface.py +++ b/python/vyos/ifconfig/interface.py @@ -1135,12 +1135,11 @@ class Interface(Control): options_file = f'{config_base}_{ifname}.options' pid_file = f'{config_base}_{ifname}.pid' lease_file = f'{config_base}_{ifname}.leases' - - # Stop client with old config files to get the right IF_METRIC. systemd_service = f'dhclient@{ifname}.service' - if is_systemd_service_active(systemd_service): - self._cmd(f'systemctl stop {systemd_service}') + # 'up' check is mandatory b/c even if the interface is A/D, as soon as + # the DHCP client is started the interface will be placed in u/u state. + # This is not what we intended to do when disabling an interface. if enable and 'disable' not in self._config: if dict_search('dhcp_options.host_name', self._config) == None: # read configured system hostname. @@ -1151,16 +1150,19 @@ class Interface(Control): tmp = {'dhcp_options' : { 'host_name' : hostname}} self._config = dict_merge(tmp, self._config) - render(options_file, 'dhcp-client/daemon-options.tmpl', - self._config) - render(config_file, 'dhcp-client/ipv4.tmpl', - self._config) + render(options_file, 'dhcp-client/daemon-options.tmpl', self._config) + render(config_file, 'dhcp-client/ipv4.tmpl', self._config) - # 'up' check is mandatory b/c even if the interface is A/D, as soon as - # the DHCP client is started the interface will be placed in u/u state. - # This is not what we intended to do when disabling an interface. - return self._cmd(f'systemctl restart {systemd_service}') + # When the DHCP client is restarted a brief outage will occur, as + # the old lease is released a new one is acquired (T4203). We will + # only restart DHCP client if it's option changed, or if it's not + # running, but it should be running (e.g. on system startup) + if 'dhcp_options_old' in self._config or not is_systemd_service_active(systemd_service): + return self._cmd(f'systemctl restart {systemd_service}') + return None else: + if is_systemd_service_active(systemd_service): + self._cmd(f'systemctl stop {systemd_service}') # cleanup old config files for file in [config_file, options_file, pid_file, lease_file]: if os.path.isfile(file): diff --git a/python/vyos/ifconfig/vxlan.py b/python/vyos/ifconfig/vxlan.py index d73fb47b8..ec5fd167d 100644 --- a/python/vyos/ifconfig/vxlan.py +++ b/python/vyos/ifconfig/vxlan.py @@ -1,4 +1,4 @@ -# Copyright 2019-2021 VyOS maintainers and contributors <maintainers@vyos.io> +# Copyright 2019-2022 VyOS maintainers and contributors <maintainers@vyos.io> # # This library is free software; you can redistribute it and/or # modify it under the terms of the GNU Lesser General Public @@ -65,6 +65,16 @@ class VXLANIf(Interface): 'parameters.nolearning' : 'nolearning', } + # IPv6 flowlabels can only be used on IPv6 tunnels, thus we need to + # ensure that at least the first remote IP address is passed to the + # tunnel creation command. Subsequent tunnel remote addresses can later + # be added to the FDB + remote_list = None + if 'remote' in self.config: + # skip first element as this is already configured as remote + remote_list = self.config['remote'][1:] + self.config['remote'] = self.config['remote'][0] + cmd = 'ip link add {ifname} type {type} id {vni} dstport {port}' for vyos_key, iproute2_key in mapping.items(): # dict_search will return an empty dict "{}" for valueless nodes like @@ -79,3 +89,10 @@ class VXLANIf(Interface): self._cmd(cmd.format(**self.config)) # interface is always A/D down. It needs to be enabled explicitly self.set_admin_state('down') + + # VXLAN tunnel is always recreated on any change - see interfaces-vxlan.py + if remote_list: + for remote in remote_list: + cmd = f'bridge fdb append to 00:00:00:00:00:00 dst {remote} ' \ + 'port {port} dev {ifname}' + self._cmd(cmd.format(**self.config)) diff --git a/python/vyos/ifconfig/wireless.py b/python/vyos/ifconfig/wireless.py index d897715db..11120a3d3 100644 --- a/python/vyos/ifconfig/wireless.py +++ b/python/vyos/ifconfig/wireless.py @@ -54,10 +54,10 @@ class WiFiIf(Interface): on any interface. """ # We can not call add_to_bridge() until wpa_supplicant is running, thus - # we will remove the key from the config dict and react to this specal - # case in thie derived class. + # we will remove the key from the config dict and react to this special + # case in this derived class. # re-add ourselves to any bridge we might have fallen out of - bridge_member = '' + bridge_member = None if 'is_bridge_member' in config: bridge_member = config['is_bridge_member'] del config['is_bridge_member'] diff --git a/python/vyos/util.py b/python/vyos/util.py index f2f302559..76ba0fb60 100644 --- a/python/vyos/util.py +++ b/python/vyos/util.py @@ -694,6 +694,14 @@ def dict_search(path, dict_object): c = c.get(p, {}) return c.get(parts[-1], None) +def get_bridge_fdb(interface): + """ Returns the forwarding database entries for a given interface """ + if not os.path.exists(f'/sys/class/net/{interface}'): + return None + from json import loads + tmp = loads(cmd(f'bridge -j fdb show dev {interface}')) + return tmp + def get_interface_config(interface): """ Returns the used encapsulation protocol for given interface. If interface does not exist, None is returned. @@ -805,3 +813,12 @@ def boot_configuration_complete() -> bool: if os.path.isfile(config_status): return True return False + +def sysctl(name, value): + """ Change value via sysctl() - return True if changed, False otherwise """ + tmp = cmd(f'sysctl {name}') + # last list index contains the actual value - only write if value differs + if tmp.split()[-1] != str(value): + call(f'sysctl -wq {name}={value}') + return True + return False diff --git a/smoketest/configs/basic-vyos b/smoketest/configs/basic-vyos new file mode 100644 index 000000000..493feed5b --- /dev/null +++ b/smoketest/configs/basic-vyos @@ -0,0 +1,88 @@ +interfaces { + ethernet eth0 { + address 192.168.0.1/24 + duplex auto + smp-affinity auto + speed auto + } + ethernet eth1 { + address 100.64.0.0/31 + duplex auto + smp-affinity auto + speed auto + } + loopback lo { + } +} +protocols { + static { + route 0.0.0.0/0 { + next-hop 100.64.0.1 { + } + } + } +} +service { + dhcp-server { + shared-network-name LAN { + authoritative + subnet 192.168.0.0/24 { + default-router 192.168.0.1 + dns-server 192.168.0.1 + domain-name vyos.net + domain-search vyos.net + range LANDynamic { + start 192.168.0.20 + stop 192.168.0.240 + } + } + } + } + dns { + forwarding { + allow-from 192.168.0.0/16 + cache-size 10000 + dnssec off + listen-address 192.168.0.1 + } + } + ssh { + ciphers aes128-ctr,aes192-ctr,aes256-ctr + ciphers chacha20-poly1305@openssh.com,rijndael-cbc@lysator.liu.se + listen-address 192.168.0.1 + key-exchange curve25519-sha256@libssh.org + key-exchange diffie-hellman-group1-sha1,diffie-hellman-group-exchange-sha1,diffie-hellman-group-exchange-sha256 + port 22 + } +} +system { + config-management { + commit-revisions 100 + } + console { + device ttyS0 { + speed 115200 + } + } + host-name vyos + login { + user vyos { + authentication { + encrypted-password $6$O5gJRlDYQpj$MtrCV9lxMnZPMbcxlU7.FI793MImNHznxGoMFgm3Q6QP3vfKJyOSRCt3Ka/GzFQyW1yZS4NS616NLHaIPPFHc0 + plaintext-password "" + } + } + } + name-server 192.168.0.1 + syslog { + global { + facility all { + level info + } + } + } + time-zone Europe/Berlin +} +/* Warning: Do not remove the following line. */ +/* === vyatta-config-version: "broadcast-relay@1:cluster@1:config-management@1:conntrack-sync@1:conntrack@1:dhcp-relay@2:dhcp-server@5:dns-forwarding@1:firewall@5:ipsec@5:l2tp@1:mdns@1:nat@4:ntp@1:pptp@1:qos@1:quagga@6:snmp@1:ssh@1:system@9:vrrp@2:wanloadbalance@3:webgui@1:webproxy@1:webproxy@2:zone-policy@1" === */ +/* Release version: 1.2.6 */ diff --git a/smoketest/configs/dialup-router-complex b/smoketest/configs/dialup-router-complex index fef79ea56..1b62deb5c 100644 --- a/smoketest/configs/dialup-router-complex +++ b/smoketest/configs/dialup-router-complex @@ -267,6 +267,22 @@ firewall { } protocol udp } + rule 800 { + action drop + description "SSH anti brute force" + destination { + port ssh + } + log enable + protocol tcp + recent { + count 4 + time 60 + } + state { + new enable + } + } } name DMZ-WAN { default-action accept diff --git a/smoketest/scripts/cli/base_interfaces_test.py b/smoketest/scripts/cli/base_interfaces_test.py index 4092b0f49..b3492b074 100644 --- a/smoketest/scripts/cli/base_interfaces_test.py +++ b/smoketest/scripts/cli/base_interfaces_test.py @@ -1,4 +1,4 @@ -# 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 @@ -56,6 +56,7 @@ def is_mirrored_to(interface, mirror_if, qdisc): class BasicInterfaceTest: class TestCase(VyOSUnitTestSHIM.TestCase): + _test_dhcp = False _test_ip = False _test_mtu = False _test_vlan = False @@ -96,6 +97,35 @@ class BasicInterfaceTest: for intf in self._interfaces: self.assertNotIn(intf, interfaces()) + # No daemon that was started during a test should remain running + for daemon in ['dhcp6c', 'dhclient']: + self.assertFalse(process_named_running(daemon)) + + def test_dhcp_disable_interface(self): + if not self._test_dhcp: + self.skipTest('not supported') + + # When interface is configured as admin down, it must be admin down + # even when dhcpc starts on the given interface + for interface in self._interfaces: + self.cli_set(self._base_path + [interface, 'disable']) + for option in self._options.get(interface, []): + self.cli_set(self._base_path + [interface] + option.split()) + + self.cli_set(self._base_path + [interface, 'disable']) + + # Also enable DHCP (ISC DHCP always places interface in admin up + # state so we check that we do not start DHCP client. + # https://phabricator.vyos.net/T2767 + self.cli_set(self._base_path + [interface, 'address', 'dhcp']) + + self.cli_commit() + + # Validate interface state + for interface in self._interfaces: + flags = read_file(f'/sys/class/net/{interface}/flags') + self.assertEqual(int(flags, 16) & 1, 0) + def test_span_mirror(self): if not self._mirror_interfaces: self.skipTest('not supported') diff --git a/smoketest/scripts/cli/test_ha_vrrp.py b/smoketest/scripts/cli/test_ha_vrrp.py index 27ea39870..8abc70ccd 100755 --- a/smoketest/scripts/cli/test_ha_vrrp.py +++ b/smoketest/scripts/cli/test_ha_vrrp.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2021 VyOS maintainers and contributors +# Copyright (C) 2021-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 @@ -166,5 +166,37 @@ class TestVRRP(VyOSUnitTestSHIM.TestCase): for group in groups: self.assertIn(f'{group}', config) + + def test_04_exclude_vrrp_interface(self): + group = 'VyOS-WAN' + none_vrrp_interface = 'eth2' + vlan_id = '24' + vip = '100.64.24.1/24' + vip_dev = '192.0.2.2/24' + vrid = '150' + group_base = base_path + ['group', group] + + self.cli_set(['interfaces', 'ethernet', vrrp_interface, 'vif', vlan_id, 'address', '100.64.24.11/24']) + self.cli_set(group_base + ['interface', f'{vrrp_interface}.{vlan_id}']) + self.cli_set(group_base + ['virtual-address', vip]) + self.cli_set(group_base + ['virtual-address', vip_dev, 'interface', none_vrrp_interface]) + self.cli_set(group_base + ['track', 'exclude-vrrp-interface']) + self.cli_set(group_base + ['track', 'interface', none_vrrp_interface]) + self.cli_set(group_base + ['vrid', vrid]) + + # commit changes + self.cli_commit() + + config = getConfig(f'vrrp_instance {group}') + + self.assertIn(f'interface {vrrp_interface}.{vlan_id}', config) + self.assertIn(f'virtual_router_id {vrid}', config) + self.assertIn(f'dont_track_primary', config) + self.assertIn(f' {vip}', config) + self.assertIn(f' {vip_dev} dev {none_vrrp_interface}', config) + self.assertIn(f'track_interface', config) + self.assertIn(f' {none_vrrp_interface}', config) + + if __name__ == '__main__': unittest.main(verbosity=2) diff --git a/smoketest/scripts/cli/test_interfaces_bonding.py b/smoketest/scripts/cli/test_interfaces_bonding.py index 1d9a887bd..4f2fe979a 100755 --- a/smoketest/scripts/cli/test_interfaces_bonding.py +++ b/smoketest/scripts/cli/test_interfaces_bonding.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 @@ -28,6 +28,7 @@ from vyos.util import read_file class BondingInterfaceTest(BasicInterfaceTest.TestCase): @classmethod def setUpClass(cls): + cls._test_dhcp = True cls._test_ip = True cls._test_ipv6 = True cls._test_ipv6_pd = True diff --git a/smoketest/scripts/cli/test_interfaces_bridge.py b/smoketest/scripts/cli/test_interfaces_bridge.py index 2152dba72..09441326a 100755 --- a/smoketest/scripts/cli/test_interfaces_bridge.py +++ b/smoketest/scripts/cli/test_interfaces_bridge.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2020-2021 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 @@ -31,6 +31,7 @@ from vyos.validate import is_intf_addr_assigned class BridgeInterfaceTest(BasicInterfaceTest.TestCase): @classmethod def setUpClass(cls): + cls._test_dhcp = True cls._test_ip = True cls._test_ipv6 = True cls._test_ipv6_pd = True diff --git a/smoketest/scripts/cli/test_interfaces_ethernet.py b/smoketest/scripts/cli/test_interfaces_ethernet.py index a31d75423..b45b29917 100755 --- a/smoketest/scripts/cli/test_interfaces_ethernet.py +++ b/smoketest/scripts/cli/test_interfaces_ethernet.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2020-2021 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 @@ -37,6 +37,7 @@ def get_wpa_supplicant_value(interface, key): class EthernetInterfaceTest(BasicInterfaceTest.TestCase): @classmethod def setUpClass(cls): + cls._test_dhcp = True cls._test_ip = True cls._test_ipv6 = True cls._test_ipv6_pd = True @@ -82,24 +83,6 @@ class EthernetInterfaceTest(BasicInterfaceTest.TestCase): self.cli_commit() - def test_dhcp_disable_interface(self): - # When interface is configured as admin down, it must be admin down - # even when dhcpc starts on the given interface - for interface in self._interfaces: - self.cli_set(self._base_path + [interface, 'disable']) - - # Also enable DHCP (ISC DHCP always places interface in admin up - # state so we check that we do not start DHCP client. - # https://phabricator.vyos.net/T2767 - self.cli_set(self._base_path + [interface, 'address', 'dhcp']) - - self.cli_commit() - - # Validate interface state - for interface in self._interfaces: - flags = read_file(f'/sys/class/net/{interface}/flags') - self.assertEqual(int(flags, 16) & 1, 0) - def test_offloading_rps(self): # enable RPS on all available CPUs, RPS works woth a CPU bitmask, # where each bit represents a CPU (core/thread). The formula below diff --git a/smoketest/scripts/cli/test_interfaces_macsec.py b/smoketest/scripts/cli/test_interfaces_macsec.py index e4280a5b7..5b10bfa44 100755 --- a/smoketest/scripts/cli/test_interfaces_macsec.py +++ b/smoketest/scripts/cli/test_interfaces_macsec.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2020-2021 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 @@ -40,6 +40,7 @@ def get_cipher(interface): class MACsecInterfaceTest(BasicInterfaceTest.TestCase): @classmethod def setUpClass(cls): + cls._test_dhcp = True cls._test_ip = True cls._test_ipv6 = True cls._base_path = ['interfaces', 'macsec'] diff --git a/smoketest/scripts/cli/test_interfaces_pseudo_ethernet.py b/smoketest/scripts/cli/test_interfaces_pseudo_ethernet.py index dacbca99c..a30d9a799 100755 --- a/smoketest/scripts/cli/test_interfaces_pseudo_ethernet.py +++ b/smoketest/scripts/cli/test_interfaces_pseudo_ethernet.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2020-2021 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 @@ -22,6 +22,7 @@ from base_interfaces_test import BasicInterfaceTest class PEthInterfaceTest(BasicInterfaceTest.TestCase): @classmethod def setUpClass(cls): + cls._test_dhcp = True cls._test_ip = True cls._test_ipv6 = True cls._test_ipv6_pd = True diff --git a/smoketest/scripts/cli/test_interfaces_tunnel.py b/smoketest/scripts/cli/test_interfaces_tunnel.py index ff8778828..38af42e8b 100755 --- a/smoketest/scripts/cli/test_interfaces_tunnel.py +++ b/smoketest/scripts/cli/test_interfaces_tunnel.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2020-2021 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 @@ -44,14 +44,14 @@ class TunnelInterfaceTest(BasicInterfaceTest.TestCase): # call base-classes classmethod super(cls, cls).setUpClass() - def setUp(self): - super().setUp() - self.cli_set(['interfaces', 'dummy', source_if, 'address', self.local_v4 + '/32']) - self.cli_set(['interfaces', 'dummy', source_if, 'address', self.local_v6 + '/128']) + # create some test interfaces + cls.cli_set(cls, ['interfaces', 'dummy', source_if, 'address', cls.local_v4 + '/32']) + cls.cli_set(cls, ['interfaces', 'dummy', source_if, 'address', cls.local_v6 + '/128']) - def tearDown(self): - self.cli_delete(['interfaces', 'dummy', source_if]) - super().tearDown() + @classmethod + def tearDownClass(cls): + cls.cli_delete(cls, ['interfaces', 'dummy', source_if]) + super().tearDownClass() def test_ipv4_encapsulations(self): # When running tests ensure that for certain encapsulation types the @@ -202,7 +202,7 @@ class TunnelInterfaceTest(BasicInterfaceTest.TestCase): self.assertEqual(encapsulation, conf['linkinfo']['info_kind']) self.assertEqual(self.local_v4, conf['linkinfo']['info_data']['local']) self.assertEqual(remote_ip4, conf['linkinfo']['info_data']['remote']) - self.assertEqual(64, conf['linkinfo']['info_data']['ttl']) + self.assertEqual(64, conf['linkinfo']['info_data']['ttl']) # Change remote ip address (inc host by 2 new_remote = inc_ip(remote_ip4, 2) @@ -213,5 +213,89 @@ class TunnelInterfaceTest(BasicInterfaceTest.TestCase): conf = get_interface_config(interface) self.assertEqual(new_remote, conf['linkinfo']['info_data']['remote']) + def test_tunnel_src_any_gre_key(self): + interface = f'tun1280' + encapsulation = 'gre' + src_addr = '0.0.0.0' + key = '127' + + self.cli_set(self._base_path + [interface, 'encapsulation', encapsulation]) + self.cli_set(self._base_path + [interface, 'source-address', src_addr]) + # GRE key must be supplied with a 0.0.0.0 source address + with self.assertRaises(ConfigSessionError): + self.cli_commit() + self.cli_set(self._base_path + [interface, 'parameters', 'ip', 'key', key]) + + self.cli_commit() + + def test_multiple_gre_tunnel_same_remote(self): + tunnels = { + 'tun10' : { + 'encapsulation' : 'gre', + 'source_interface' : source_if, + 'remote' : '1.2.3.4', + }, + 'tun20' : { + 'encapsulation' : 'gre', + 'source_interface' : source_if, + 'remote' : '1.2.3.4', + }, + } + + for tunnel, tunnel_config in tunnels.items(): + self.cli_set(self._base_path + [tunnel, 'encapsulation', tunnel_config['encapsulation']]) + if 'source_interface' in tunnel_config: + self.cli_set(self._base_path + [tunnel, 'source-interface', tunnel_config['source_interface']]) + if 'remote' in tunnel_config: + self.cli_set(self._base_path + [tunnel, 'remote', tunnel_config['remote']]) + + # GRE key must be supplied when two or more tunnels are formed to the same desitnation + with self.assertRaises(ConfigSessionError): + self.cli_commit() + for tunnel, tunnel_config in tunnels.items(): + self.cli_set(self._base_path + [tunnel, 'parameters', 'ip', 'key', tunnel.lstrip('tun')]) + + self.cli_commit() + + for tunnel, tunnel_config in tunnels.items(): + conf = get_interface_config(tunnel) + ip_key = tunnel.lstrip('tun') + + self.assertEqual(tunnel_config['source_interface'], conf['link']) + self.assertEqual(tunnel_config['encapsulation'], conf['linkinfo']['info_kind']) + self.assertEqual(tunnel_config['remote'], conf['linkinfo']['info_data']['remote']) + self.assertEqual(f'0.0.0.{ip_key}', conf['linkinfo']['info_data']['ikey']) + self.assertEqual(f'0.0.0.{ip_key}', conf['linkinfo']['info_data']['okey']) + + def test_multiple_gre_tunnel_different_remote(self): + tunnels = { + 'tun10' : { + 'encapsulation' : 'gre', + 'source_interface' : source_if, + 'remote' : '1.2.3.4', + }, + 'tun20' : { + 'encapsulation' : 'gre', + 'source_interface' : source_if, + 'remote' : '1.2.3.5', + }, + } + + for tunnel, tunnel_config in tunnels.items(): + self.cli_set(self._base_path + [tunnel, 'encapsulation', tunnel_config['encapsulation']]) + if 'source_interface' in tunnel_config: + self.cli_set(self._base_path + [tunnel, 'source-interface', tunnel_config['source_interface']]) + if 'remote' in tunnel_config: + self.cli_set(self._base_path + [tunnel, 'remote', tunnel_config['remote']]) + + self.cli_commit() + + for tunnel, tunnel_config in tunnels.items(): + conf = get_interface_config(tunnel) + + self.assertEqual(tunnel_config['source_interface'], conf['link']) + self.assertEqual(tunnel_config['encapsulation'], conf['linkinfo']['info_kind']) + self.assertEqual(tunnel_config['remote'], conf['linkinfo']['info_data']['remote']) + if __name__ == '__main__': unittest.main(verbosity=2) diff --git a/smoketest/scripts/cli/test_interfaces_vxlan.py b/smoketest/scripts/cli/test_interfaces_vxlan.py index 184b411d7..ccd6c29d6 100755 --- a/smoketest/scripts/cli/test_interfaces_vxlan.py +++ b/smoketest/scripts/cli/test_interfaces_vxlan.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2020-2021 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 @@ -17,8 +17,9 @@ import unittest from vyos.ifconfig import Interface +from vyos.util import get_bridge_fdb from vyos.util import get_interface_config - +from vyos.template import is_ipv6 from base_interfaces_test import BasicInterfaceTest class VXLANInterfaceTest(BasicInterfaceTest.TestCase): @@ -32,10 +33,60 @@ class VXLANInterfaceTest(BasicInterfaceTest.TestCase): 'vxlan10': ['vni 10', 'remote 127.0.0.2'], 'vxlan20': ['vni 20', 'group 239.1.1.1', 'source-interface eth0'], 'vxlan30': ['vni 30', 'remote 2001:db8:2000::1', 'source-address 2001:db8:1000::1'], + 'vxlan40': ['vni 40', 'remote 127.0.0.2', 'remote 127.0.0.3'], + 'vxlan50': ['vni 50', 'remote 2001:db8:2000::1', 'remote 2001:db8:2000::2'], } cls._interfaces = list(cls._options) # call base-classes classmethod super(cls, cls).setUpClass() + def test_01vxlan_parameters(self): + for intf in self._interfaces: + for option in self._options.get(intf, []): + self.cli_set(self._base_path + [intf] + option.split()) + + self.cli_commit() + + for interface in self._interfaces: + options = get_interface_config(interface) + bridge = get_bridge_fdb(interface) + + vni = options['linkinfo']['info_data']['id'] + self.assertIn(f'vni {vni}', self._options[interface]) + + if any('source-interface' in s for s in self._options[interface]): + link = options['linkinfo']['info_data']['link'] + self.assertIn(f'source-interface {link}', self._options[interface]) + + # Verify source-address setting was properly configured on the Kernel + if any('source-address' in s for s in self._options[interface]): + for s in self._options[interface]: + if 'source-address' in s: + address = s.split()[-1] + if is_ipv6(address): + tmp = options['linkinfo']['info_data']['local6'] + else: + tmp = options['linkinfo']['info_data']['local'] + self.assertIn(f'source-address {tmp}', self._options[interface]) + + # Verify remote setting was properly configured on the Kernel + if any('remote' in s for s in self._options[interface]): + for s in self._options[interface]: + if 'remote' in s: + for fdb in bridge: + if 'mac' in fdb and fdb['mac'] == '00:00:00:00:00:00': + remote = fdb['dst'] + self.assertIn(f'remote {remote}', self._options[interface]) + + if any('group' in s for s in self._options[interface]): + group = options['linkinfo']['info_data']['group'] + self.assertIn(f'group {group}', self._options[interface]) + + if any('external' in s for s in self._options[interface]): + self.assertTrue(options['linkinfo']['info_data']['external']) + + self.assertEqual('vxlan', options['linkinfo']['info_kind']) + self.assertEqual(Interface(interface).get_admin_state(), 'up') + if __name__ == '__main__': unittest.main(verbosity=2) diff --git a/smoketest/scripts/cli/test_policy.py b/smoketest/scripts/cli/test_policy.py index f1d195381..50f2d7b43 100755 --- a/smoketest/scripts/cli/test_policy.py +++ b/smoketest/scripts/cli/test_policy.py @@ -678,14 +678,413 @@ class TestPolicy(VyOSUnitTestSHIM.TestCase): self.cli_commit() - # Check generated configuration - - # Expected values original = """ 50: from 203.0.113.1 lookup 23 50: from 203.0.113.2 lookup 23 """ tmp = cmd('ip rule show prio 50') + + self.assertEqual(sort_ip(tmp), sort_ip(original)) + + # Test set table for fwmark + def test_fwmark_table_id(self): + path = base_path + ['local-route'] + + fwmk = '24' + rule = '101' + table = '154' + + self.cli_set(path + ['rule', rule, 'set', 'table', table]) + self.cli_set(path + ['rule', rule, 'fwmark', fwmk]) + + self.cli_commit() + + original = """ + 101: from all fwmark 0x18 lookup 154 + """ + tmp = cmd('ip rule show prio 101') + + self.assertEqual(sort_ip(tmp), sort_ip(original)) + + # Test set table for destination + def test_destination_table_id(self): + path = base_path + ['local-route'] + + dst = '203.0.113.1' + rule = '102' + table = '154' + + self.cli_set(path + ['rule', rule, 'set', 'table', table]) + self.cli_set(path + ['rule', rule, 'destination', dst]) + + self.cli_commit() + + original = """ + 102: from all to 203.0.113.1 lookup 154 + """ + tmp = cmd('ip rule show prio 102') + + self.assertEqual(sort_ip(tmp), sort_ip(original)) + + # Test set table for sources with fwmark + def test_fwmark_sources_table_id(self): + path = base_path + ['local-route'] + + sources = ['203.0.113.11', '203.0.113.12'] + fwmk = '23' + rule = '100' + table = '150' + for src in sources: + self.cli_set(path + ['rule', rule, 'set', 'table', table]) + self.cli_set(path + ['rule', rule, 'source', src]) + self.cli_set(path + ['rule', rule, 'fwmark', fwmk]) + + self.cli_commit() + + original = """ + 100: from 203.0.113.11 fwmark 0x17 lookup 150 + 100: from 203.0.113.12 fwmark 0x17 lookup 150 + """ + tmp = cmd('ip rule show prio 100') + + self.assertEqual(sort_ip(tmp), sort_ip(original)) + + # Test set table for sources with iif + def test_iif_sources_table_id(self): + path = base_path + ['local-route'] + + sources = ['203.0.113.11', '203.0.113.12'] + iif = 'lo' + rule = '100' + table = '150' + + self.cli_set(path + ['rule', rule, 'set', 'table', table]) + self.cli_set(path + ['rule', rule, 'inbound-interface', iif]) + for src in sources: + self.cli_set(path + ['rule', rule, 'source', src]) + + self.cli_commit() + + # Check generated configuration + # Expected values + original = """ + 100: from 203.0.113.11 iif lo lookup 150 + 100: from 203.0.113.12 iif lo lookup 150 + """ + tmp = cmd('ip rule show prio 100') + + self.assertEqual(sort_ip(tmp), sort_ip(original)) + + # Test set table for sources and destinations with fwmark + def test_fwmark_sources_destination_table_id(self): + path = base_path + ['local-route'] + + sources = ['203.0.113.11', '203.0.113.12'] + destinations = ['203.0.113.13', '203.0.113.15'] + fwmk = '23' + rule = '103' + table = '150' + for src in sources: + for dst in destinations: + self.cli_set(path + ['rule', rule, 'set', 'table', table]) + self.cli_set(path + ['rule', rule, 'source', src]) + self.cli_set(path + ['rule', rule, 'destination', dst]) + self.cli_set(path + ['rule', rule, 'fwmark', fwmk]) + + self.cli_commit() + + original = """ + 103: from 203.0.113.11 to 203.0.113.13 fwmark 0x17 lookup 150 + 103: from 203.0.113.11 to 203.0.113.15 fwmark 0x17 lookup 150 + 103: from 203.0.113.12 to 203.0.113.13 fwmark 0x17 lookup 150 + 103: from 203.0.113.12 to 203.0.113.15 fwmark 0x17 lookup 150 + """ + tmp = cmd('ip rule show prio 103') + + self.assertEqual(sort_ip(tmp), sort_ip(original)) + + # Test set table ipv6 for some sources ipv6 + def test_ipv6_table_id(self): + path = base_path + ['local-route6'] + + sources = ['2001:db8:123::/48', '2001:db8:126::/48'] + rule = '50' + table = '23' + for src in sources: + self.cli_set(path + ['rule', rule, 'set', 'table', table]) + self.cli_set(path + ['rule', rule, 'source', src]) + + self.cli_commit() + + original = """ + 50: from 2001:db8:123::/48 lookup 23 + 50: from 2001:db8:126::/48 lookup 23 + """ + tmp = cmd('ip -6 rule show prio 50') + + self.assertEqual(sort_ip(tmp), sort_ip(original)) + + # Test set table for fwmark ipv6 + def test_fwmark_ipv6_table_id(self): + path = base_path + ['local-route6'] + + fwmk = '24' + rule = '100' + table = '154' + + self.cli_set(path + ['rule', rule, 'set', 'table', table]) + self.cli_set(path + ['rule', rule, 'fwmark', fwmk]) + + self.cli_commit() + + original = """ + 100: from all fwmark 0x18 lookup 154 + """ + tmp = cmd('ip -6 rule show prio 100') + + self.assertEqual(sort_ip(tmp), sort_ip(original)) + + # Test set table for destination ipv6 + def test_destination_ipv6_table_id(self): + path = base_path + ['local-route6'] + + dst = '2001:db8:1337::/126' + rule = '101' + table = '154' + + self.cli_set(path + ['rule', rule, 'set', 'table', table]) + self.cli_set(path + ['rule', rule, 'destination', dst]) + + self.cli_commit() + + original = """ + 101: from all to 2001:db8:1337::/126 lookup 154 + """ + tmp = cmd('ip -6 rule show prio 101') + + self.assertEqual(sort_ip(tmp), sort_ip(original)) + + # Test set table for sources with fwmark ipv6 + def test_fwmark_sources_ipv6_table_id(self): + path = base_path + ['local-route6'] + + sources = ['2001:db8:1338::/126', '2001:db8:1339::/126'] + fwmk = '23' + rule = '102' + table = '150' + for src in sources: + self.cli_set(path + ['rule', rule, 'set', 'table', table]) + self.cli_set(path + ['rule', rule, 'source', src]) + self.cli_set(path + ['rule', rule, 'fwmark', fwmk]) + + self.cli_commit() + + original = """ + 102: from 2001:db8:1338::/126 fwmark 0x17 lookup 150 + 102: from 2001:db8:1339::/126 fwmark 0x17 lookup 150 + """ + tmp = cmd('ip -6 rule show prio 102') + + self.assertEqual(sort_ip(tmp), sort_ip(original)) + + # Test set table for sources with iif ipv6 + def test_iif_sources_ipv6_table_id(self): + path = base_path + ['local-route6'] + + sources = ['2001:db8:1338::/126', '2001:db8:1339::/126'] + iif = 'lo' + rule = '102' + table = '150' + for src in sources: + self.cli_set(path + ['rule', rule, 'set', 'table', table]) + self.cli_set(path + ['rule', rule, 'source', src]) + self.cli_set(path + ['rule', rule, 'inbound-interface', iif]) + + self.cli_commit() + + # Check generated configuration + # Expected values + original = """ + 102: from 2001:db8:1338::/126 iif lo lookup 150 + 102: from 2001:db8:1339::/126 iif lo lookup 150 + """ + tmp = cmd('ip -6 rule show prio 102') + + self.assertEqual(sort_ip(tmp), sort_ip(original)) + + # Test set table for sources and destinations with fwmark ipv6 + def test_fwmark_sources_destination_ipv6_table_id(self): + path = base_path + ['local-route6'] + + sources = ['2001:db8:1338::/126', '2001:db8:1339::/56'] + destinations = ['2001:db8:13::/48', '2001:db8:16::/48'] + fwmk = '23' + rule = '103' + table = '150' + for src in sources: + for dst in destinations: + self.cli_set(path + ['rule', rule, 'set', 'table', table]) + self.cli_set(path + ['rule', rule, 'source', src]) + self.cli_set(path + ['rule', rule, 'destination', dst]) + self.cli_set(path + ['rule', rule, 'fwmark', fwmk]) + + self.cli_commit() + + original = """ + 103: from 2001:db8:1338::/126 to 2001:db8:13::/48 fwmark 0x17 lookup 150 + 103: from 2001:db8:1338::/126 to 2001:db8:16::/48 fwmark 0x17 lookup 150 + 103: from 2001:db8:1339::/56 to 2001:db8:13::/48 fwmark 0x17 lookup 150 + 103: from 2001:db8:1339::/56 to 2001:db8:16::/48 fwmark 0x17 lookup 150 + """ + tmp = cmd('ip -6 rule show prio 103') + + self.assertEqual(sort_ip(tmp), sort_ip(original)) + + # Test delete table for sources and destination with fwmark ipv4/ipv6 + def test_delete_ipv4_ipv6_table_id(self): + path = base_path + ['local-route'] + path_v6 = base_path + ['local-route6'] + + sources = ['203.0.113.0/24', '203.0.114.5'] + destinations = ['203.0.112.0/24', '203.0.116.5'] + sources_v6 = ['2001:db8:1338::/126', '2001:db8:1339::/56'] + destinations_v6 = ['2001:db8:13::/48', '2001:db8:16::/48'] + fwmk = '23' + rule = '103' + table = '150' + for src in sources: + for dst in destinations: + self.cli_set(path + ['rule', rule, 'set', 'table', table]) + self.cli_set(path + ['rule', rule, 'source', src]) + self.cli_set(path + ['rule', rule, 'destination', dst]) + self.cli_set(path + ['rule', rule, 'fwmark', fwmk]) + + for src in sources_v6: + for dst in destinations_v6: + self.cli_set(path_v6 + ['rule', rule, 'set', 'table', table]) + self.cli_set(path_v6 + ['rule', rule, 'source', src]) + self.cli_set(path_v6 + ['rule', rule, 'destination', dst]) + self.cli_set(path_v6 + ['rule', rule, 'fwmark', fwmk]) + + self.cli_commit() + + original = """ + 103: from 203.0.113.0/24 to 203.0.116.5 fwmark 0x17 lookup 150 + 103: from 203.0.114.5 to 203.0.112.0/24 fwmark 0x17 lookup 150 + 103: from 203.0.114.5 to 203.0.116.5 fwmark 0x17 lookup 150 + 103: from 203.0.113.0/24 to 203.0.112.0/24 fwmark 0x17 lookup 150 + """ + original_v6 = """ + 103: from 2001:db8:1338::/126 to 2001:db8:16::/48 fwmark 0x17 lookup 150 + 103: from 2001:db8:1339::/56 to 2001:db8:13::/48 fwmark 0x17 lookup 150 + 103: from 2001:db8:1339::/56 to 2001:db8:16::/48 fwmark 0x17 lookup 150 + 103: from 2001:db8:1338::/126 to 2001:db8:13::/48 fwmark 0x17 lookup 150 + """ + tmp = cmd('ip rule show prio 103') + tmp_v6 = cmd('ip -6 rule show prio 103') + + self.assertEqual(sort_ip(tmp), sort_ip(original)) + self.assertEqual(sort_ip(tmp_v6), sort_ip(original_v6)) + + self.cli_delete(path) + self.cli_delete(path_v6) + self.cli_commit() + + tmp = cmd('ip rule show prio 103') + tmp_v6 = cmd('ip -6 rule show prio 103') + + self.assertEqual(sort_ip(tmp), []) + self.assertEqual(sort_ip(tmp_v6), []) + + # Test multiple commits ipv4 + def test_multiple_commit_ipv4_table_id(self): + path = base_path + ['local-route'] + + sources = ['192.0.2.1', '192.0.2.2'] + destination = '203.0.113.25' + rule = '105' + table = '151' + self.cli_set(path + ['rule', rule, 'set', 'table', table]) + for src in sources: + self.cli_set(path + ['rule', rule, 'source', src]) + + self.cli_commit() + + original_first = """ + 105: from 192.0.2.1 lookup 151 + 105: from 192.0.2.2 lookup 151 + """ + tmp = cmd('ip rule show prio 105') + + self.assertEqual(sort_ip(tmp), sort_ip(original_first)) + + # Create second commit with added destination + self.cli_set(path + ['rule', rule, 'destination', destination]) + self.cli_commit() + + original_second = """ + 105: from 192.0.2.1 to 203.0.113.25 lookup 151 + 105: from 192.0.2.2 to 203.0.113.25 lookup 151 + """ + tmp = cmd('ip rule show prio 105') + + self.assertEqual(sort_ip(tmp), sort_ip(original_second)) + + +def sort_ip(output): + o = '\n'.join([' '.join(line.strip().split()) for line in output.strip().splitlines()]) + o = o.splitlines() + o.sort() + return o + + # Test set table for fwmark + def test_fwmark_table_id(self): + path = base_path + ['local-route'] + + fwmk = '24' + rule = '101' + table = '154' + + self.cli_set(path + ['rule', rule, 'set', 'table', table]) + self.cli_set(path + ['rule', rule, 'fwmark', fwmk]) + + self.cli_commit() + + # Check generated configuration + + # Expected values + original = """ + 101: from all fwmark 0x18 lookup 154 + """ + tmp = cmd('ip rule show prio 101') + original = original.split() + tmp = tmp.split() + + self.assertEqual(tmp, original) + + # Test set table for sources with fwmark + def test_fwmark_sources_table_id(self): + path = base_path + ['local-route'] + + sources = ['203.0.113.11', '203.0.113.12'] + fwmk = '23' + rule = '100' + table = '150' + for src in sources: + self.cli_set(path + ['rule', rule, 'set', 'table', table]) + self.cli_set(path + ['rule', rule, 'source', src]) + self.cli_set(path + ['rule', rule, 'fwmark', fwmk]) + + self.cli_commit() + + # Check generated configuration + + # Expected values + original = """ + 100: from 203.0.113.11 fwmark 0x17 lookup 150 + 100: from 203.0.113.12 fwmark 0x17 lookup 150 + """ + tmp = cmd('ip rule show prio 100') original = original.split() tmp = tmp.split() diff --git a/smoketest/scripts/cli/test_service_dhcp-server.py b/smoketest/scripts/cli/test_service_dhcp-server.py index 14666db15..9adb9c042 100755 --- a/smoketest/scripts/cli/test_service_dhcp-server.py +++ b/smoketest/scripts/cli/test_service_dhcp-server.py @@ -461,12 +461,11 @@ class TestServiceDHCPServer(VyOSUnitTestSHIM.TestCase): self.assertIn(f'mclt 1800;', config) self.assertIn(f'mclt 1800;', config) self.assertIn(f'split 128;', config) - self.assertIn(f'port 520;', config) - self.assertIn(f'peer port 520;', config) + self.assertIn(f'port 647;', config) + self.assertIn(f'peer port 647;', config) self.assertIn(f'max-response-delay 30;', config) self.assertIn(f'max-unacked-updates 10;', config) self.assertIn(f'load balance max seconds 3;', config) - self.assertIn(f'peer port 520;', config) self.assertIn(f'address {failover_local};', config) self.assertIn(f'peer address {failover_remote};', config) diff --git a/smoketest/scripts/cli/test_service_lldp.py b/smoketest/scripts/cli/test_service_lldp.py new file mode 100755 index 000000000..64fdd9d1b --- /dev/null +++ b/smoketest/scripts/cli/test_service_lldp.py @@ -0,0 +1,127 @@ +#!/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 re +import os +import unittest + +from base_vyostest_shim import VyOSUnitTestSHIM + +from vyos.configsession import ConfigSessionError +from vyos.ifconfig import Section +from vyos.util import cmd +from vyos.util import process_named_running +from vyos.util import read_file +from vyos.version import get_version_data + +PROCESS_NAME = 'lldpd' +LLDPD_CONF = '/etc/lldpd.d/01-vyos.conf' +base_path = ['service', 'lldp'] +mgmt_if = 'dum83513' +mgmt_addr = ['1.2.3.4', '1.2.3.5'] + +class TestServiceLLDP(VyOSUnitTestSHIM.TestCase): + @classmethod + def setUpClass(cls): + # call base-classes classmethod + super(cls, cls).setUpClass() + + # create a test interfaces + for addr in mgmt_addr: + cls.cli_set(cls, ['interfaces', 'dummy', mgmt_if, 'address', addr + '/32']) + + # ensure we can also run this test on a live system - so lets clean + # out the current configuration :) + cls.cli_delete(cls, base_path) + + @classmethod + def tearDownClass(cls): + cls.cli_delete(cls, ['interfaces', 'dummy', mgmt_if]) + super().tearDownClass() + + def tearDown(self): + # service must be running after it was configured + self.assertTrue(process_named_running(PROCESS_NAME)) + + # delete/stop LLDP service + self.cli_delete(base_path) + self.cli_commit() + + # service is no longer allowed to run after it was removed + self.assertFalse(process_named_running(PROCESS_NAME)) + + def test_01_lldp_basic(self): + self.cli_set(base_path) + self.cli_commit() + + config = read_file(LLDPD_CONF) + version_data = get_version_data() + version = version_data['version'] + self.assertIn(f'configure system platform VyOS', config) + self.assertIn(f'configure system description "VyOS {version}"', config) + + def test_02_lldp_mgmt_address(self): + for addr in mgmt_addr: + self.cli_set(base_path + ['management-address', addr]) + self.cli_commit() + + config = read_file(LLDPD_CONF) + self.assertIn(f'configure system ip management pattern {",".join(mgmt_addr)}', config) + + def test_03_lldp_interfaces(self): + for interface in Section.interfaces('ethernet'): + if not '.' in interface: + self.cli_set(base_path + ['interface', interface]) + + # commit changes + self.cli_commit() + + # verify configuration + config = read_file(LLDPD_CONF) + + interface_list = [] + for interface in Section.interfaces('ethernet'): + if not '.' in interface: + interface_list.append(interface) + tmp = ','.join(interface_list) + self.assertIn(f'configure system interface pattern "{tmp}"', config) + + def test_04_lldp_all_interfaces(self): + self.cli_set(base_path + ['interface', 'all']) + # commit changes + self.cli_commit() + + # verify configuration + config = read_file(LLDPD_CONF) + self.assertIn(f'configure system interface pattern "*"', config) + + def test_05_lldp_location(self): + interface = 'eth0' + elin = '1234567890' + self.cli_set(base_path + ['interface', interface, 'location', 'elin', elin]) + + # commit changes + self.cli_commit() + + # verify configuration + config = read_file(LLDPD_CONF) + + self.assertIn(f'configure ports {interface} med location elin "{elin}"', config) + self.assertIn(f'configure system interface pattern "{interface}"', config) + + +if __name__ == '__main__': + unittest.main(verbosity=2) diff --git a/smoketest/scripts/cli/test_service_monitoring_telegraf.py b/smoketest/scripts/cli/test_service_monitoring_telegraf.py new file mode 100755 index 000000000..09937513e --- /dev/null +++ b/smoketest/scripts/cli/test_service_monitoring_telegraf.py @@ -0,0 +1,65 @@ +#!/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 unittest + +from base_vyostest_shim import VyOSUnitTestSHIM + +from vyos.configsession import ConfigSession +from vyos.configsession import ConfigSessionError +from vyos.util import process_named_running +from vyos.util import read_file + +PROCESS_NAME = 'telegraf' +TELEGRAF_CONF = '/run/telegraf/vyos-telegraf.conf' +base_path = ['service', 'monitoring', 'telegraf'] +org = 'log@in.local' +token = 'GuRJc12tIzfjnYdKRAIYbxdWd2aTpOT9PVYNddzDnFV4HkAcD7u7-kndTFXjGuXzJN6TTxmrvPODB4mnFcseDV==' +port = '8888' +url = 'https://foo.local' +bucket = 'main' +inputs = ['cpu', 'disk', 'mem', 'net', 'system', 'kernel', 'interrupts', 'syslog'] + +class TestMonitoringTelegraf(VyOSUnitTestSHIM.TestCase): + def tearDown(self): + self.cli_delete(base_path) + self.cli_commit() + + def test_01_basic_config(self): + self.cli_set(base_path + ['authentication', 'organization', org]) + self.cli_set(base_path + ['authentication', 'token', token]) + self.cli_set(base_path + ['port', port]) + self.cli_set(base_path + ['url', url]) + + # commit changes + self.cli_commit() + + # Check for running process + self.assertTrue(process_named_running(PROCESS_NAME)) + + config = read_file(TELEGRAF_CONF) + + # Check telegraf config + self.assertIn(f'organization = "{org}"', config) + self.assertIn(f' token = "$INFLUX_TOKEN"', config) + self.assertIn(f'urls = ["{url}:{port}"]', config) + self.assertIn(f'bucket = "{bucket}"', config) + + for input in inputs: + self.assertIn(input, config) + +if __name__ == '__main__': + unittest.main(verbosity=2) diff --git a/smoketest/scripts/cli/test_service_webproxy.py b/smoketest/scripts/cli/test_service_webproxy.py index d12cc7d58..dfccced0a 100755 --- a/smoketest/scripts/cli/test_service_webproxy.py +++ b/smoketest/scripts/cli/test_service_webproxy.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 @@ -30,11 +30,19 @@ listen_if = 'dum3632' listen_ip = '192.0.2.1' class TestServiceWebProxy(VyOSUnitTestSHIM.TestCase): - def setUp(self): - self.cli_set(['interfaces', 'dummy', listen_if, 'address', listen_ip + '/32']) + @classmethod + def setUpClass(cls): + # call base-classes classmethod + super(cls, cls).setUpClass() + # create a test interfaces + cls.cli_set(cls, ['interfaces', 'dummy', listen_if, 'address', listen_ip + '/32']) + + @classmethod + def tearDownClass(cls): + cls.cli_delete(cls, ['interfaces', 'dummy', listen_if]) + super().tearDownClass() def tearDown(self): - self.cli_delete(['interfaces', 'dummy', listen_if]) self.cli_delete(base_path) self.cli_commit() diff --git a/src/conf_mode/conntrack_sync.py b/src/conf_mode/conntrack_sync.py index 7f22fa2dd..d4bf14d52 100755 --- a/src/conf_mode/conntrack_sync.py +++ b/src/conf_mode/conntrack_sync.py @@ -82,9 +82,9 @@ def verify(conntrack): raise ConfigError('Cannot configure all with other protocol') if 'listen_address' in conntrack: - address = conntrack['listen_address'] - if not is_addr_assigned(address): - raise ConfigError(f'Specified listen-address {address} not assigned to any interface!') + for address in conntrack['listen_address']: + if not is_addr_assigned(address): + raise ConfigError(f'Specified listen-address {address} not assigned to any interface!') vrrp_group = dict_search('failover_mechanism.vrrp.sync_group', conntrack) if vrrp_group == None: diff --git a/src/conf_mode/interfaces-bonding.py b/src/conf_mode/interfaces-bonding.py index 431d65f1f..d5be21949 100755 --- a/src/conf_mode/interfaces-bonding.py +++ b/src/conf_mode/interfaces-bonding.py @@ -132,10 +132,10 @@ def verify(bond): return None if 'arp_monitor' in bond: - if 'target' in bond['arp_monitor'] and len(int(bond['arp_monitor']['target'])) > 16: + if 'target' in bond['arp_monitor'] and len(bond['arp_monitor']['target']) > 16: raise ConfigError('The maximum number of arp-monitor targets is 16') - if 'interval' in bond['arp_monitor'] and len(int(bond['arp_monitor']['interval'])) > 0: + if 'interval' in bond['arp_monitor'] and int(bond['arp_monitor']['interval']) > 0: if bond['mode'] in ['802.3ad', 'balance-tlb', 'balance-alb']: raise ConfigError('ARP link monitoring does not work for mode 802.3ad, ' \ 'transmit-load-balance or adaptive-load-balance') diff --git a/src/conf_mode/interfaces-bridge.py b/src/conf_mode/interfaces-bridge.py index 4d3ebc587..f4dba9d4a 100755 --- a/src/conf_mode/interfaces-bridge.py +++ b/src/conf_mode/interfaces-bridge.py @@ -22,7 +22,6 @@ from netifaces import interfaces from vyos.config import Config from vyos.configdict import get_interface_dict from vyos.configdict import node_changed -from vyos.configdict import leaf_node_changed from vyos.configdict import is_member from vyos.configdict import is_source_interface from vyos.configdict import has_vlan_subinterface_configured diff --git a/src/conf_mode/interfaces-openvpn.py b/src/conf_mode/interfaces-openvpn.py index ffb022847..38ed127ff 100755 --- a/src/conf_mode/interfaces-openvpn.py +++ b/src/conf_mode/interfaces-openvpn.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,7 @@ from shutil import rmtree from vyos.config import Config from vyos.configdict import get_interface_dict +from vyos.configdict import leaf_node_changed from vyos.configverify import verify_vrf from vyos.configverify import verify_bridge_delete from vyos.configverify import verify_diffie_hellman_length @@ -81,6 +82,10 @@ def get_config(config=None): base = ['interfaces', 'openvpn'] openvpn = get_interface_dict(conf, base) + if 'deleted' not in openvpn: + tmp = leaf_node_changed(conf, ['openvpn-option']) + if tmp: openvpn['restart_required'] = '' + openvpn['auth_user_pass_file'] = '/run/openvpn/{ifname}.pw'.format(**openvpn) return openvpn @@ -118,11 +123,12 @@ def verify(openvpn): if 'local_address' not in openvpn and 'is_bridge_member' not in openvpn: raise ConfigError('Must specify "local-address" or add interface to bridge') - if len([addr for addr in openvpn['local_address'] if is_ipv4(addr)]) > 1: - raise ConfigError('Only one IPv4 local-address can be specified') + if 'local_address' in openvpn: + if len([addr for addr in openvpn['local_address'] if is_ipv4(addr)]) > 1: + raise ConfigError('Only one IPv4 local-address can be specified') - if len([addr for addr in openvpn['local_address'] if is_ipv6(addr)]) > 1: - raise ConfigError('Only one IPv6 local-address can be specified') + if len([addr for addr in openvpn['local_address'] if is_ipv6(addr)]) > 1: + raise ConfigError('Only one IPv6 local-address can be specified') if openvpn['device_type'] == 'tun': if 'remote_address' not in openvpn: @@ -161,7 +167,7 @@ def verify(openvpn): if dict_search('remote_host', openvpn) in dict_search('remote_address', openvpn): raise ConfigError('"remote-address" and "remote-host" can not be the same') - if openvpn['device_type'] == 'tap': + if openvpn['device_type'] == 'tap' and 'local_address' in openvpn: # we can only have one local_address, this is ensured above v4addr = None for laddr in openvpn['local_address']: @@ -516,7 +522,10 @@ def apply(openvpn): # No matching OpenVPN process running - maybe it got killed or none # existed - nevertheless, spawn new OpenVPN process - call(f'systemctl reload-or-restart openvpn@{interface}.service') + action = 'reload-or-restart' + if 'restart_required' in openvpn: + action = 'restart' + call(f'systemctl {action} openvpn@{interface}.service') conf = VTunIf.get_config() conf['device_type'] = openvpn['device_type'] diff --git a/src/conf_mode/interfaces-tunnel.py b/src/conf_mode/interfaces-tunnel.py index 2798d321f..c4023586a 100755 --- a/src/conf_mode/interfaces-tunnel.py +++ b/src/conf_mode/interfaces-tunnel.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2018-2021 VyOS maintainers and contributors +# Copyright (C) 2018-2022 yOS 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 @@ -18,12 +18,9 @@ import os from sys import exit from netifaces import interfaces -from ipaddress import IPv4Address from vyos.config import Config -from vyos.configdict import dict_merge from vyos.configdict import get_interface_dict -from vyos.configdict import node_changed from vyos.configdict import leaf_node_changed from vyos.configverify import verify_address from vyos.configverify import verify_bridge_delete @@ -34,8 +31,6 @@ from vyos.configverify import verify_tunnel from vyos.ifconfig import Interface from vyos.ifconfig import Section from vyos.ifconfig import TunnelIf -from vyos.template import is_ipv4 -from vyos.template import is_ipv6 from vyos.util import get_interface_config from vyos.util import dict_search from vyos import ConfigError @@ -54,8 +49,24 @@ def get_config(config=None): base = ['interfaces', 'tunnel'] tunnel = get_interface_dict(conf, base) - tmp = leaf_node_changed(conf, ['encapsulation']) - if tmp: tunnel.update({'encapsulation_changed': {}}) + if 'deleted' not in tunnel: + tmp = leaf_node_changed(conf, ['encapsulation']) + if tmp: tunnel.update({'encapsulation_changed': {}}) + + # We also need to inspect other configured tunnels as there are Kernel + # restrictions where we need to comply. E.g. GRE tunnel key can't be used + # twice, or with multiple GRE tunnels to the same location we must specify + # a GRE key + conf.set_level(base) + tunnel['other_tunnels'] = conf.get_config_dict([], key_mangling=('-', '_'), + get_first_key=True, + no_tag_node_value_mangle=True) + # delete our own instance from this dict + ifname = tunnel['ifname'] + del tunnel['other_tunnels'][ifname] + # if only one tunnel is present on the system, no need to keep this key + if len(tunnel['other_tunnels']) == 0: + del tunnel['other_tunnels'] # We must check if our interface is configured to be a DMVPN member nhrp_base = ['protocols', 'nhrp', 'tunnel'] @@ -76,35 +87,47 @@ def verify(tunnel): verify_tunnel(tunnel) - # If tunnel source address any and key not set + # If tunnel source is any and gre key is not set + interface = tunnel['ifname'] if tunnel['encapsulation'] in ['gre'] and \ dict_search('source_address', tunnel) == '0.0.0.0' and \ dict_search('parameters.ip.key', tunnel) == None: - raise ConfigError('Tunnel parameters ip key must be set!') - - if tunnel['encapsulation'] in ['gre', 'gretap']: - if dict_search('parameters.ip.key', tunnel) != None: - # Check pairs tunnel source-address/encapsulation/key with exists tunnels. - # Prevent the same key for 2 tunnels with same source-address/encap. T2920 - for tunnel_if in Section.interfaces('tunnel'): - # It makes no sense to run the test for re-used GRE keys on our - # own interface we are currently working on - if tunnel['ifname'] == tunnel_if: - continue - tunnel_cfg = get_interface_config(tunnel_if) - # no match on encapsulation - bail out - if dict_search('linkinfo.info_kind', tunnel_cfg) != tunnel['encapsulation']: - continue - new_source_address = dict_search('source_address', tunnel) - # Convert tunnel key to ip key, format "ip -j link show" - # 1 => 0.0.0.1, 999 => 0.0.3.231 - orig_new_key = dict_search('parameters.ip.key', tunnel) - new_key = IPv4Address(int(orig_new_key)) - new_key = str(new_key) - if dict_search('address', tunnel_cfg) == new_source_address and \ - dict_search('linkinfo.info_data.ikey', tunnel_cfg) == new_key: - raise ConfigError(f'Key "{orig_new_key}" for source-address "{new_source_address}" ' \ + raise ConfigError(f'"parameters ip key" must be set for {interface} when '\ + 'encapsulation is GRE!') + + gre_encapsulations = ['gre', 'gretap'] + if tunnel['encapsulation'] in gre_encapsulations and 'other_tunnels' in tunnel: + # Check pairs tunnel source-address/encapsulation/key with exists tunnels. + # Prevent the same key for 2 tunnels with same source-address/encap. T2920 + for o_tunnel, o_tunnel_conf in tunnel['other_tunnels'].items(): + # no match on encapsulation - bail out + our_encapsulation = tunnel['encapsulation'] + their_encapsulation = o_tunnel_conf['encapsulation'] + if our_encapsulation in gre_encapsulations and their_encapsulation \ + not in gre_encapsulations: + continue + + our_address = dict_search('source_address', tunnel) + our_key = dict_search('parameters.ip.key', tunnel) + their_address = dict_search('source_address', o_tunnel_conf) + their_key = dict_search('parameters.ip.key', o_tunnel_conf) + if our_key != None: + if their_address == our_address and their_key == our_key: + raise ConfigError(f'Key "{our_key}" for source-address "{our_address}" ' \ f'is already used for tunnel "{tunnel_if}"!') + else: + our_source_if = dict_search('source_interface', tunnel) + their_source_if = dict_search('source_interface', o_tunnel_conf) + our_remote = dict_search('remote', tunnel) + their_remote = dict_search('remote', o_tunnel_conf) + # If no IP GRE key is defined we can not have more then one GRE tunnel + # bound to any one interface/IP address and the same remote. This will + # result in a OS PermissionError: add tunnel "gre0" failed: File exists + if (their_address == our_address or our_source_if == their_source_if) and \ + our_remote == their_remote: + raise ConfigError(f'Missing required "ip key" parameter when '\ + 'running more then one GRE based tunnel on the '\ + 'same source-interface/source-address') # Keys are not allowed with ipip and sit tunnels if tunnel['encapsulation'] in ['ipip', 'sit']: diff --git a/src/conf_mode/interfaces-vxlan.py b/src/conf_mode/interfaces-vxlan.py index c035557f0..5ee4603af 100755 --- a/src/conf_mode/interfaces-vxlan.py +++ b/src/conf_mode/interfaces-vxlan.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 @@ -21,6 +21,7 @@ from netifaces import interfaces from vyos.config import Config from vyos.configdict import get_interface_dict +from vyos.configdict import leaf_node_changed from vyos.configverify import verify_address from vyos.configverify import verify_bridge_delete from vyos.configverify import verify_mtu_ipv6 @@ -34,8 +35,8 @@ airbag.enable() def get_config(config=None): """ - Retrive CLI config as dictionary. Dictionary can never be empty, as at least the - interface name will be added or a deleted flag + Retrive CLI config as dictionary. Dictionary can never be empty, as at least + the interface name will be added or a deleted flag """ if config: conf = config @@ -44,6 +45,16 @@ def get_config(config=None): base = ['interfaces', 'vxlan'] vxlan = get_interface_dict(conf, base) + # VXLAN interfaces are picky and require recreation if certain parameters + # change. But a VXLAN interface should - of course - not be re-created if + # it's description or IP address is adjusted. Feels somehow logic doesn't it? + for cli_option in ['external', 'gpe', 'group', 'port', 'remote', + 'source-address', 'source-interface', 'vni', + 'parameters ip dont-fragment', 'parameters ip tos', + 'parameters ip ttl']: + if leaf_node_changed(conf, cli_option.split()): + vxlan.update({'rebuild_required': {}}) + return vxlan def verify(vxlan): @@ -56,8 +67,7 @@ def verify(vxlan): if 'group' in vxlan: if 'source_interface' not in vxlan: - raise ConfigError('Multicast VXLAN requires an underlaying interface ') - + raise ConfigError('Multicast VXLAN requires an underlaying interface') verify_source_interface(vxlan) if not any(tmp in ['group', 'remote', 'source_address'] for tmp in vxlan): @@ -81,22 +91,41 @@ def verify(vxlan): raise ConfigError(f'Underlaying device MTU is to small ({lower_mtu} '\ f'bytes) for VXLAN overhead ({vxlan_overhead} bytes!)') + # Check for mixed IPv4 and IPv6 addresses + protocol = None + if 'source_address' in vxlan: + if is_ipv6(vxlan['source_address']): + protocol = 'ipv6' + else: + protocol = 'ipv4' + + if 'remote' in vxlan: + error_msg = 'Can not mix both IPv4 and IPv6 for VXLAN underlay' + for remote in vxlan['remote']: + if is_ipv6(remote): + if protocol == 'ipv4': + raise ConfigError(error_msg) + protocol = 'ipv6' + else: + if protocol == 'ipv6': + raise ConfigError(error_msg) + protocol = 'ipv4' + verify_mtu_ipv6(vxlan) verify_address(vxlan) return None - def generate(vxlan): return None - def apply(vxlan): # Check if the VXLAN interface already exists - if vxlan['ifname'] in interfaces(): - v = VXLANIf(vxlan['ifname']) - # VXLAN is super picky and the tunnel always needs to be recreated, - # thus we can simply always delete it first. - v.remove() + if 'rebuild_required' in vxlan or 'delete' in vxlan: + if vxlan['ifname'] in interfaces(): + v = VXLANIf(vxlan['ifname']) + # VXLAN is super picky and the tunnel always needs to be recreated, + # thus we can simply always delete it first. + v.remove() if 'deleted' not in vxlan: # This is a special type of interface which needs additional parameters @@ -116,7 +145,6 @@ def apply(vxlan): return None - if __name__ == '__main__': try: c = get_config() diff --git a/src/conf_mode/policy-local-route.py b/src/conf_mode/policy-local-route.py index 013f22665..0a4597869 100755 --- a/src/conf_mode/policy-local-route.py +++ b/src/conf_mode/policy-local-route.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2020 VyOS maintainers and contributors +# Copyright (C) 2020-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 @@ -18,6 +18,7 @@ import os from sys import exit +from netifaces import interfaces from vyos.config import Config from vyos.configdict import dict_merge from vyos.configdict import node_changed @@ -35,27 +36,96 @@ def get_config(config=None): conf = config else: conf = Config() - base = ['policy', 'local-route'] + base = ['policy'] + pbr = conf.get_config_dict(base, key_mangling=('-', '_'), get_first_key=True) - # delete policy local-route - dict = {} - tmp = node_changed(conf, ['policy', 'local-route', 'rule'], key_mangling=('-', '_')) - if tmp: - for rule in (tmp or []): - src = leaf_node_changed(conf, ['policy', 'local-route', 'rule', rule, 'source']) - if src: - dict = dict_merge({'rule_remove' : {rule : {'source' : src}}}, dict) + for route in ['local_route', 'local_route6']: + dict_id = 'rule_remove' if route == 'local_route' else 'rule6_remove' + route_key = 'local-route' if route == 'local_route' else 'local-route6' + base_rule = base + [route_key, 'rule'] + + # delete policy local-route + dict = {} + tmp = node_changed(conf, base_rule, key_mangling=('-', '_')) + if tmp: + for rule in (tmp or []): + src = leaf_node_changed(conf, base_rule + [rule, 'source']) + fwmk = leaf_node_changed(conf, base_rule + [rule, 'fwmark']) + iif = leaf_node_changed(conf, base_rule + [rule, 'inbound-interface']) + dst = leaf_node_changed(conf, base_rule + [rule, 'destination']) + rule_def = {} + if src: + rule_def = dict_merge({'source' : src}, rule_def) + if fwmk: + rule_def = dict_merge({'fwmark' : fwmk}, rule_def) + if iif: + rule_def = dict_merge({'inbound_interface' : iif}, rule_def) + if dst: + rule_def = dict_merge({'destination' : dst}, rule_def) + dict = dict_merge({dict_id : {rule : rule_def}}, dict) pbr.update(dict) - - # delete policy local-route rule x source x.x.x.x - if 'rule' in pbr: - for rule in pbr['rule']: - src = leaf_node_changed(conf, ['policy', 'local-route', 'rule', rule, 'source']) - if src: - dict = dict_merge({'rule_remove' : {rule : {'source' : src}}}, dict) + if fwmk: + dict = dict_merge({'rule_remove' : {rule : {'fwmark' : fwmk}}}, dict) pbr.update(dict) + if not route in pbr: + continue + + # delete policy local-route rule x source x.x.x.x + # delete policy local-route rule x fwmark x + # delete policy local-route rule x destination x.x.x.x + if 'rule' in pbr[route]: + for rule, rule_config in pbr[route]['rule'].items(): + src = leaf_node_changed(conf, base_rule + [rule, 'source']) + fwmk = leaf_node_changed(conf, base_rule + [rule, 'fwmark']) + iif = leaf_node_changed(conf, base_rule + [rule, 'inbound-interface']) + dst = leaf_node_changed(conf, base_rule + [rule, 'destination']) + # keep track of changes in configuration + # otherwise we might remove an existing node although nothing else has changed + changed = False + + rule_def = {} + # src is None if there are no changes to src + if src is None: + # if src hasn't changed, include it in the removal selector + # if a new selector is added, we have to remove all previous rules without this selector + # to make sure we remove all previous rules with this source(s), it will be included + if 'source' in rule_config: + rule_def = dict_merge({'source': rule_config['source']}, rule_def) + else: + # if src is not None, it's previous content will be returned + # this can be an empty array if it's just being set, or the previous value + # either way, something has to be changed and we only want to remove previous values + changed = True + # set the old value for removal if it's not empty + if len(src) > 0: + rule_def = dict_merge({'source' : src}, rule_def) + if fwmk is None: + if 'fwmark' in rule_config: + rule_def = dict_merge({'fwmark': rule_config['fwmark']}, rule_def) + else: + changed = True + if len(fwmk) > 0: + rule_def = dict_merge({'fwmark' : fwmk}, rule_def) + if iif is None: + if 'inbound_interface' in rule_config: + rule_def = dict_merge({'inbound_interface': rule_config['inbound_interface']}, rule_def) + else: + changed = True + if len(iif) > 0: + rule_def = dict_merge({'inbound_interface' : iif}, rule_def) + if dst is None: + if 'destination' in rule_config: + rule_def = dict_merge({'destination': rule_config['destination']}, rule_def) + else: + changed = True + if len(dst) > 0: + rule_def = dict_merge({'destination' : dst}, rule_def) + if changed: + dict = dict_merge({dict_id : {rule : rule_def}}, dict) + pbr.update(dict) + return pbr def verify(pbr): @@ -63,13 +133,25 @@ def verify(pbr): if not pbr: return None - if 'rule' in pbr: - for rule in pbr['rule']: - if 'source' not in pbr['rule'][rule]: - raise ConfigError('Source address required!') - else: - if 'set' not in pbr['rule'][rule] or 'table' not in pbr['rule'][rule]['set']: - raise ConfigError('Table set is required!') + for route in ['local_route', 'local_route6']: + if not route in pbr: + continue + + pbr_route = pbr[route] + if 'rule' in pbr_route: + for rule in pbr_route['rule']: + if 'source' not in pbr_route['rule'][rule] \ + and 'destination' not in pbr_route['rule'][rule] \ + and 'fwmark' not in pbr_route['rule'][rule] \ + and 'inbound_interface' not in pbr_route['rule'][rule]: + raise ConfigError('Source or destination address or fwmark or inbound-interface is required!') + else: + if 'set' not in pbr_route['rule'][rule] or 'table' not in pbr_route['rule'][rule]['set']: + raise ConfigError('Table set is required!') + if 'inbound_interface' in pbr_route['rule'][rule]: + interface = pbr_route['rule'][rule]['inbound_interface'] + if interface not in interfaces(): + raise ConfigError(f'Interface "{interface}" does not exist') return None @@ -84,18 +166,51 @@ def apply(pbr): return None # Delete old rule if needed - if 'rule_remove' in pbr: - for rule in pbr['rule_remove']: - for src in pbr['rule_remove'][rule]['source']: - call(f'ip rule del prio {rule} from {src}') + for rule_rm in ['rule_remove', 'rule6_remove']: + if rule_rm in pbr: + v6 = " -6" if rule_rm == 'rule6_remove' else "" + for rule, rule_config in pbr[rule_rm].items(): + rule_config['source'] = rule_config['source'] if 'source' in rule_config else [''] + for src in rule_config['source']: + f_src = '' if src == '' else f' from {src} ' + rule_config['destination'] = rule_config['destination'] if 'destination' in rule_config else [''] + for dst in rule_config['destination']: + f_dst = '' if dst == '' else f' to {dst} ' + rule_config['fwmark'] = rule_config['fwmark'] if 'fwmark' in rule_config else [''] + for fwmk in rule_config['fwmark']: + f_fwmk = '' if fwmk == '' else f' fwmark {fwmk} ' + rule_config['inbound_interface'] = rule_config['inbound_interface'] if 'inbound_interface' in rule_config else [''] + for iif in rule_config['inbound_interface']: + f_iif = '' if iif == '' else f' iif {iif} ' + call(f'ip{v6} rule del prio {rule} {f_src}{f_dst}{f_fwmk}{f_iif}') # Generate new config - if 'rule' in pbr: - for rule in pbr['rule']: - table = pbr['rule'][rule]['set']['table'] - if pbr['rule'][rule]['source']: - for src in pbr['rule'][rule]['source']: - call(f'ip rule add prio {rule} from {src} lookup {table}') + for route in ['local_route', 'local_route6']: + if not route in pbr: + continue + + v6 = " -6" if route == 'local_route6' else "" + + pbr_route = pbr[route] + if 'rule' in pbr_route: + for rule, rule_config in pbr_route['rule'].items(): + table = rule_config['set']['table'] + + rule_config['source'] = rule_config['source'] if 'source' in rule_config else ['all'] + for src in rule_config['source'] or ['all']: + f_src = '' if src == '' else f' from {src} ' + rule_config['destination'] = rule_config['destination'] if 'destination' in rule_config else ['all'] + for dst in rule_config['destination']: + f_dst = '' if dst == '' else f' to {dst} ' + f_fwmk = '' + if 'fwmark' in rule_config: + fwmk = rule_config['fwmark'] + f_fwmk = f' fwmark {fwmk} ' + f_iif = '' + if 'inbound_interface' in rule_config: + iif = rule_config['inbound_interface'] + f_iif = f' iif {iif} ' + call(f'ip{v6} rule add prio {rule} {f_src}{f_dst}{f_fwmk}{f_iif} lookup {table}') return None diff --git a/src/conf_mode/service_monitoring_telegraf.py b/src/conf_mode/service_monitoring_telegraf.py new file mode 100755 index 000000000..8a972b9fe --- /dev/null +++ b/src/conf_mode/service_monitoring_telegraf.py @@ -0,0 +1,175 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2021-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 os +import json + +from sys import exit +from shutil import rmtree + +from vyos.config import Config +from vyos.configdict import dict_merge +from vyos.ifconfig import Section +from vyos.template import render +from vyos.util import call +from vyos.util import chown +from vyos.util import cmd +from vyos.xml import defaults +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' +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' + + +def get_interfaces(type='', vlan=True): + """ + Get interfaces + get_interfaces() + ['dum0', 'eth0', 'eth1', 'eth1.5', 'lo', 'tun0'] + + get_interfaces("dummy") + ['dum0'] + """ + interfaces = [] + ifaces = Section.interfaces(type) + for iface in ifaces: + if vlan == False and '.' in iface: + continue + interfaces.append(iface) + + return interfaces + +def get_nft_filter_chains(): + """ + Get nft chains for table filter + """ + nft = cmd('nft --json list table ip filter') + nft = json.loads(nft) + chain_list = [] + + for output in nft['nftables']: + if 'chain' in output: + chain = output['chain']['name'] + chain_list.append(chain) + + return chain_list + + +def get_config(config=None): + + if config: + conf = config + else: + conf = Config() + base = ['service', 'monitoring', 'telegraf'] + 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) + + # 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) + monitoring = dict_merge(default_values, monitoring) + + monitoring['custom_scripts_dir'] = custom_scripts_dir + monitoring['interfaces_ethernet'] = get_interfaces('ethernet', vlan=False) + monitoring['nft_chains'] = get_nft_filter_chains() + + return monitoring + +def verify(monitoring): + # bail out early - looks like removal from running config + if not monitoring: + return None + + if 'authentication' not in monitoring or \ + 'organization' not in monitoring['authentication'] or \ + 'token' not in monitoring['authentication']: + raise ConfigError(f'Authentication "organization and token" are mandatory!') + + if 'url' not in monitoring: + raise ConfigError(f'Monitoring "url" is mandatory!') + + return None + +def generate(monitoring): + if not monitoring: + # Delete config and systemd files + config_files = [config_telegraf, systemd_telegraf_service, systemd_override, syslog_telegraf] + for file in config_files: + if os.path.isfile(file): + os.unlink(file) + + # Delete old directories + if os.path.isdir(cache_dir): + rmtree(cache_dir, ignore_errors=True) + + return None + + # Create telegraf cache dir + if not os.path.exists(cache_dir): + os.makedirs(cache_dir) + + 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.tmpl', monitoring) + render(systemd_telegraf_service, 'monitoring/systemd_vyos_telegraf_service.tmpl', monitoring) + render(systemd_override, 'monitoring/override.conf.tmpl', monitoring, permission=0o640) + render(syslog_telegraf, 'monitoring/syslog_telegraf.tmpl', monitoring) + + chown(base_dir, 'telegraf', 'telegraf') + + return None + +def apply(monitoring): + # Reload systemd manager configuration + call('systemctl daemon-reload') + if monitoring: + call('systemctl restart vyos-telegraf.service') + else: + call('systemctl stop vyos-telegraf.service') + # Telegraf include custom rsyslog config changes + call('systemctl restart rsyslog') + +if __name__ == '__main__': + try: + c = get_config() + verify(c) + generate(c) + apply(c) + except ConfigError as e: + print(e) + exit(1) diff --git a/src/conf_mode/vrf.py b/src/conf_mode/vrf.py index 9d36819be..e64f35ab5 100755 --- a/src/conf_mode/vrf.py +++ b/src/conf_mode/vrf.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2020-2021 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 @@ -26,16 +26,23 @@ from vyos.template import render from vyos.util import call from vyos.util import cmd from vyos.util import dict_search +from vyos.util import sysctl from vyos import ConfigError from vyos import airbag airbag.enable() config_file = r'/etc/iproute2/rt_tables.d/vyos-vrf.conf' -def list_rules(): - command = 'ip -j -4 rule show' - answer = loads(cmd(command)) - return [_ for _ in answer if _] +def has_rule(af : str, priority : int, table : str): + """ Check if a given ip rule exists """ + if af not in ['-4', '-6']: + raise ValueError() + command = f'ip -j {af} rule show' + for tmp in loads(cmd(command)): + if {'priority', 'table'} <= set(tmp): + if tmp['priority'] == priority and tmp['table'] == table: + return True + return False def vrf_interfaces(c, match): matched = [] @@ -64,7 +71,6 @@ def vrf_routing(c, match): c.set_level(old_level) return matched - def get_config(config=None): if config: conf = config @@ -138,36 +144,63 @@ def apply(vrf): bind_all = '0' if 'bind-to-all' in vrf: bind_all = '1' - call(f'sysctl -wq net.ipv4.tcp_l3mdev_accept={bind_all}') - call(f'sysctl -wq net.ipv4.udp_l3mdev_accept={bind_all}') + sysctl('net.ipv4.tcp_l3mdev_accept', bind_all) + sysctl('net.ipv4.udp_l3mdev_accept', bind_all) for tmp in (dict_search('vrf_remove', vrf) or []): if os.path.isdir(f'/sys/class/net/{tmp}'): - call(f'ip -4 route del vrf {tmp} unreachable default metric 4278198272') - call(f'ip -6 route del vrf {tmp} unreachable default metric 4278198272') call(f'ip link delete dev {tmp}') if 'name' in vrf: + # Linux routing uses rules to find tables - routing targets are then + # looked up in those tables. If the lookup got a matching route, the + # process ends. + # + # TL;DR; first table with a matching entry wins! + # + # You can see your routing table lookup rules using "ip rule", sadly the + # local lookup is hit before any VRF lookup. Pinging an addresses from the + # VRF will usually find a hit in the local table, and never reach the VRF + # routing table - this is usually not what you want. Thus we will + # re-arrange the tables and move the local lookup further down once VRFs + # are enabled. + # + # Thanks to https://stbuehler.de/blog/article/2020/02/29/using_vrf__virtual_routing_and_forwarding__on_linux.html + + for afi in ['-4', '-6']: + # move lookup local to pref 32765 (from 0) + if not has_rule(afi, 32765, 'local'): + call(f'ip {afi} rule add pref 32765 table local') + if has_rule(afi, 0, 'local'): + call(f'ip {afi} rule del pref 0') + # make sure that in VRFs after failed lookup in the VRF specific table + # nothing else is reached + if not has_rule(afi, 1000, 'l3mdev'): + # this should be added by the kernel when a VRF is created + # add it here for completeness + call(f'ip {afi} rule add pref 1000 l3mdev protocol kernel') + + # add another rule with an unreachable target which only triggers in VRF context + # if a route could not be reached + if not has_rule(afi, 2000, 'l3mdev'): + call(f'ip {afi} rule add pref 2000 l3mdev unreachable') + for name, config in vrf['name'].items(): table = config['table'] - if not os.path.isdir(f'/sys/class/net/{name}'): # For each VRF apart from your default context create a VRF # interface with a separate routing table call(f'ip link add {name} type vrf table {table}') - # The kernel Documentation/networking/vrf.txt also recommends - # adding unreachable routes to the VRF routing tables so that routes - # afterwards are taken. - call(f'ip -4 route add vrf {name} unreachable default metric 4278198272') - call(f'ip -6 route add vrf {name} unreachable default metric 4278198272') - # We also should add proper loopback IP addresses to the newly - # created VRFs for services bound to the loopback address (SNMP, NTP) - call(f'ip -4 addr add 127.0.0.1/8 dev {name}') - call(f'ip -6 addr add ::1/128 dev {name}') # set VRF description for e.g. SNMP monitoring vrf_if = Interface(name) + # We also should add proper loopback IP addresses to the newly + # created VRFs for services bound to the loopback address (SNMP, NTP) + vrf_if.add_addr('127.0.0.1/8') + vrf_if.add_addr('::1/128') + # add VRF description if available vrf_if.set_alias(config.get('description', '')) + # Enable/Disable of an interface must always be done at the end of the # derived class to make use of the ref-counting set_admin_state() # function. We will only enable the interface if 'up' was called as @@ -178,38 +211,6 @@ def apply(vrf): state = 'down' if 'disable' in config else 'up' vrf_if.set_admin_state(state) - # Linux routing uses rules to find tables - routing targets are then - # looked up in those tables. If the lookup got a matching route, the - # process ends. - # - # TL;DR; first table with a matching entry wins! - # - # You can see your routing table lookup rules using "ip rule", sadly the - # local lookup is hit before any VRF lookup. Pinging an addresses from the - # VRF will usually find a hit in the local table, and never reach the VRF - # routing table - this is usually not what you want. Thus we will - # re-arrange the tables and move the local lookup furhter down once VRFs - # are enabled. - - # get current preference on local table - local_pref = [r.get('priority') for r in list_rules() if r.get('table') == 'local'][0] - - # change preference when VRFs are enabled and local lookup table is default - if not local_pref and 'name' in vrf: - for af in ['-4', '-6']: - call(f'ip {af} rule add pref 32765 table local') - call(f'ip {af} rule del pref 0') - - # return to default lookup preference when no VRF is configured - if 'name' not in vrf: - for af in ['-4', '-6']: - call(f'ip {af} rule add pref 0 table local') - call(f'ip {af} rule del pref 32765') - - # clean out l3mdev-table rule if present - if 1000 in [r.get('priority') for r in list_rules() if r.get('priority') == 1000]: - call(f'ip {af} rule del pref 1000') - return None if __name__ == '__main__': diff --git a/src/etc/logrotate.d/conntrackd b/src/etc/logrotate.d/conntrackd new file mode 100644 index 000000000..b0b09dec1 --- /dev/null +++ b/src/etc/logrotate.d/conntrackd @@ -0,0 +1,9 @@ +/var/log/conntrackd-stats.log { + weekly + rotate 2 + missingok + + postrotate + systemctl restart conntrackd.service > /dev/null + endscript +} diff --git a/src/etc/telegraf/custom_scripts/show_firewall_input_filter.py b/src/etc/telegraf/custom_scripts/show_firewall_input_filter.py new file mode 100755 index 000000000..bf4bfd05d --- /dev/null +++ b/src/etc/telegraf/custom_scripts/show_firewall_input_filter.py @@ -0,0 +1,73 @@ +#!/usr/bin/env python3 + +import json +import re +import time + +from vyos.util import cmd + + +def get_nft_filter_chains(): + """ + Get list of nft chains for table filter + """ + nft = cmd('/usr/sbin/nft --json list table ip filter') + nft = json.loads(nft) + chain_list = [] + + for output in nft['nftables']: + if 'chain' in output: + chain = output['chain']['name'] + chain_list.append(chain) + + return chain_list + + +def get_nftables_details(name): + """ + Get dict, counters packets and bytes for chain + """ + command = f'/usr/sbin/nft list chain ip filter {name}' + try: + results = cmd(command) + except: + return {} + + # Trick to remove 'NAME_' from chain name in the comment + # It was added to any chain T4218 + # counter packets 0 bytes 0 return comment "FOO default-action accept" + comment_name = name.replace("NAME_", "") + out = {} + for line in results.split('\n'): + comment_search = re.search(rf'{comment_name}[\- ](\d+|default-action)', line) + if not comment_search: + continue + + rule = {} + rule_id = comment_search[1] + counter_search = re.search(r'counter packets (\d+) bytes (\d+)', line) + if counter_search: + rule['packets'] = counter_search[1] + rule['bytes'] = counter_search[2] + + rule['conditions'] = re.sub(r'(\b(counter packets \d+ bytes \d+|drop|reject|return|log)\b|comment "[\w\-]+")', '', line).strip() + out[rule_id] = rule + return out + + +def get_nft_telegraf(name): + """ + Get data for telegraf in influxDB format + """ + for rule, rule_config in get_nftables_details(name).items(): + print(f'nftables,table=filter,chain={name},' + f'ruleid={rule} ' + f'pkts={rule_config["packets"]}i,' + f'bytes={rule_config["bytes"]}i ' + f'{str(int(time.time()))}000000000') + + +chains = get_nft_filter_chains() + +for chain in chains: + get_nft_telegraf(chain) diff --git a/src/etc/telegraf/custom_scripts/show_interfaces_input_filter.py b/src/etc/telegraf/custom_scripts/show_interfaces_input_filter.py new file mode 100755 index 000000000..0c7474156 --- /dev/null +++ b/src/etc/telegraf/custom_scripts/show_interfaces_input_filter.py @@ -0,0 +1,88 @@ +#!/usr/bin/env python3 + +from vyos.ifconfig import Section +from vyos.ifconfig import Interface + +import time + +def get_interfaces(type='', vlan=True): + """ + Get interfaces: + ['dum0', 'eth0', 'eth1', 'eth1.5', 'lo', 'tun0'] + """ + interfaces = [] + ifaces = Section.interfaces(type) + for iface in ifaces: + if vlan == False and '.' in iface: + continue + interfaces.append(iface) + + return interfaces + +def get_interface_addresses(iface, link_local_v6=False): + """ + Get IP and IPv6 addresses from interface in one string + By default don't get IPv6 link-local addresses + If interface doesn't have address, return "-" + """ + addresses = [] + addrs = Interface(iface).get_addr() + + for addr in addrs: + if link_local_v6 == False: + if addr.startswith('fe80::'): + continue + addresses.append(addr) + + if not addresses: + return "-" + + return (" ".join(addresses)) + +def get_interface_description(iface): + """ + Get interface description + If none return "empty" + """ + description = Interface(iface).get_alias() + + if not description: + return "empty" + + return description + +def get_interface_admin_state(iface): + """ + Interface administrative state + up => 0, down => 2 + """ + state = Interface(iface).get_admin_state() + if state == 'up': + admin_state = 0 + if state == 'down': + admin_state = 2 + + return admin_state + +def get_interface_oper_state(iface): + """ + Interface operational state + up => 0, down => 1 + """ + state = Interface(iface).operational.get_state() + if state == 'down': + oper_state = 1 + else: + oper_state = 0 + + return oper_state + +interfaces = get_interfaces() + +for iface in interfaces: + print(f'show_interfaces,interface={iface} ' + f'ip_addresses="{get_interface_addresses(iface)}",' + f'state={get_interface_admin_state(iface)}i,' + f'link={get_interface_oper_state(iface)}i,' + f'description="{get_interface_description(iface)}" ' + f'{str(int(time.time()))}000000000') diff --git a/src/etc/telegraf/custom_scripts/vyos_services_input_filter.py b/src/etc/telegraf/custom_scripts/vyos_services_input_filter.py new file mode 100755 index 000000000..df4eed131 --- /dev/null +++ b/src/etc/telegraf/custom_scripts/vyos_services_input_filter.py @@ -0,0 +1,61 @@ +#!/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 time +from vyos.configquery import ConfigTreeQuery +from vyos.util import is_systemd_service_running, process_named_running + +# Availible services and prouceses +# 1 - service +# 2 - process +services = { + "protocols bgp" : "bgpd", + "protocols ospf" : "ospfd", + "protocols ospfv3" : "ospf6d", + "protocols rip" : "ripd", + "protocols ripng" : "ripngd", + "protocols isis" : "isisd", + "service pppoe" : "accel-ppp@pppoe.service", + "vpn l2tp remote-access" : "accel-ppp@l2tp.service", + "vpn pptp remote-access" : "accel-ppp@pptp.service", + "vpn sstp" : "accel-ppp@sstp.service", + "vpn ipsec" : "charon" +} + +# Configured services +conf_services = { + 'zebra' : 0, + 'staticd' : 0, +} +# Get configured service and create list to check if process running +config = ConfigTreeQuery() +for service in services: + if config.exists(service): + conf_services[services[service]] = 0 + +for conf_service in conf_services: + status = 0 + if ".service" in conf_service: + # Check systemd service + if is_systemd_service_running(conf_service): + status = 1 + else: + # Check process + if process_named_running(conf_service): + status = 1 + print(f'vyos_services,service="{conf_service}" ' + f'status={str(status)}i {str(int(time.time()))}000000000') diff --git a/src/helpers/vyos-vrrp-conntracksync.sh b/src/helpers/vyos-vrrp-conntracksync.sh index 4501aa63e..0cc718938 100755 --- a/src/helpers/vyos-vrrp-conntracksync.sh +++ b/src/helpers/vyos-vrrp-conntracksync.sh @@ -14,12 +14,10 @@ # Modified by : Mohit Mehta <mohit@vyatta.com> # Slight modifications were made to this script for running with Vyatta # The original script came from 0.9.14 debian conntrack-tools package -# -# CONNTRACKD_BIN=/usr/sbin/conntrackd CONNTRACKD_LOCK=/var/lock/conntrack.lock -CONNTRACKD_CONFIG=/etc/conntrackd/conntrackd.conf +CONNTRACKD_CONFIG=/run/conntrackd/conntrackd.conf FACILITY=daemon LEVEL=notice TAG=conntrack-tools @@ -29,6 +27,10 @@ FAILOVER_STATE="/var/run/vyatta-conntrackd-failover-state" $LOGCMD "vyatta-vrrp-conntracksync invoked at `date`" +if ! systemctl is-active --quiet conntrackd.service; then + echo "conntrackd service not running" + exit 1 +fi if [ ! -e $FAILOVER_STATE ]; then mkdir -p /var/run diff --git a/src/migration-scripts/ssh/1-to-2 b/src/migration-scripts/ssh/1-to-2 index bc8815753..31c40df16 100755 --- a/src/migration-scripts/ssh/1-to-2 +++ b/src/migration-scripts/ssh/1-to-2 @@ -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 @@ -30,26 +30,52 @@ file_name = argv[1] with open(file_name, 'r') as f: config_file = f.read() -base = ['service', 'ssh', 'loglevel'] +base = ['service', 'ssh'] config = ConfigTree(config_file) if not config.exists(base): # Nothing to do exit(0) -else: - # red in configured loglevel and convert it to lower case - tmp = config.return_value(base).lower() +path_loglevel = base + ['loglevel'] +if config.exists(path_loglevel): + # red in configured loglevel and convert it to lower case + tmp = config.return_value(path_loglevel).lower() # VyOS 1.2 had no proper value validation on the CLI thus the # user could use any arbitrary values - sanitize them if tmp not in ['quiet', 'fatal', 'error', 'info', 'verbose']: tmp = 'info' + config.set(path_loglevel, value=tmp) + +# T4273: migrate ssh cipher list to multi node +path_ciphers = base + ['ciphers'] +if config.exists(path_ciphers): + tmp = [] + # get curtrent cipher list - comma delimited + for cipher in config.return_values(path_ciphers): + tmp.extend(cipher.split(',')) + # delete old cipher suite representation + config.delete(path_ciphers) - config.set(base, value=tmp) + for cipher in tmp: + config.set(path_ciphers, value=cipher, replace=False) - try: - with open(file_name, 'w') as f: - f.write(config.to_string()) - except OSError as e: - print("Failed to save the modified config: {}".format(e)) - exit(1) +# T4273: migrate ssh key-exchange list to multi node +path_kex = base + ['key-exchange'] +if config.exists(path_kex): + tmp = [] + # get curtrent cipher list - comma delimited + for kex in config.return_values(path_kex): + tmp.extend(kex.split(',')) + # delete old cipher suite representation + config.delete(path_kex) + + for kex in tmp: + config.set(path_kex, value=kex, replace=False) + +try: + with open(file_name, 'w') as f: + f.write(config.to_string()) +except OSError as e: + print("Failed to save the modified config: {}".format(e)) + exit(1) diff --git a/src/op_mode/generate_public_key_command.py b/src/op_mode/generate_public_key_command.py new file mode 100755 index 000000000..7a7b6c923 --- /dev/null +++ b/src/op_mode/generate_public_key_command.py @@ -0,0 +1,41 @@ +#!/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 os +import sys +import urllib.parse + +import vyos.remote + +def get_key(path): + url = urllib.parse.urlparse(path) + if url.scheme == 'file' or url.scheme == '': + with open(os.path.expanduser(path), 'r') as f: + key_string = f.read() + else: + key_string = vyos.remote.get_remote_config(path) + return key_string.split() + +username = sys.argv[1] +algorithm, key, identifier = get_key(sys.argv[2]) + +print('# To add this key as an embedded key, run the following commands:') +print('configure') +print(f'set system login user {username} authentication public-keys {identifier} key {key}') +print(f'set system login user {username} authentication public-keys {identifier} type {algorithm}') +print('commit') +print('save') +print('exit') diff --git a/src/op_mode/lldp_op.py b/src/op_mode/lldp_op.py index b9ebc991a..17f6bf552 100755 --- a/src/op_mode/lldp_op.py +++ b/src/op_mode/lldp_op.py @@ -54,6 +54,7 @@ def parse_data(data, interface): for local_if, values in neighbor.items(): if interface is not None and local_if != interface: continue + cap = '' for chassis, c_value in values.get('chassis', {}).items(): # bail out early if no capabilities found if 'capability' not in c_value: @@ -62,7 +63,6 @@ def parse_data(data, interface): if isinstance(capabilities, dict): capabilities = [capabilities] - cap = '' for capability in capabilities: if capability['enabled']: if capability['type'] == 'Router': diff --git a/src/op_mode/show_dhcpv6.py b/src/op_mode/show_dhcpv6.py index ac211fb0a..f70f04298 100755 --- a/src/op_mode/show_dhcpv6.py +++ b/src/op_mode/show_dhcpv6.py @@ -139,7 +139,7 @@ def get_leases(config, leases, state, pool=None, sort='ip'): # apply output/display sort if sort == 'ip': - leases = sorted(leases, key = lambda k: int(ip_address(k['ip']))) + leases = sorted(leases, key = lambda k: int(ip_address(k['ip'].split('/')[0]))) else: leases = sorted(leases, key = lambda k: k[sort]) |