summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--Makefile1
-rw-r--r--data/templates/accel-ppp/pppoe.config.tmpl18
-rw-r--r--data/templates/accel-ppp/sstp.config.tmpl29
-rw-r--r--data/templates/dhcp-client/daemon-options.tmpl1
-rw-r--r--data/templates/dhcp-client/ipv4.tmpl2
-rw-r--r--data/templates/dhcp-client/ipv6.tmpl45
-rw-r--r--data/templates/dhcpv6-server/dhcpdv6.conf.tmpl7
-rw-r--r--data/templates/firewall/nftables-nat.tmpl139
-rw-r--r--data/templates/frr/bfd.frr.tmpl (renamed from data/templates/frr-bfd/bfd.frr.tmpl)0
-rw-r--r--data/templates/frr/igmp.frr.tmpl (renamed from data/templates/igmp/igmp.frr.tmpl)0
-rw-r--r--data/templates/frr/ldpd.frr.tmpl (renamed from data/templates/mpls/ldpd.frr.tmpl)0
-rw-r--r--data/templates/frr/pimd.frr.tmpl (renamed from data/templates/pim/pimd.frr.tmpl)0
-rw-r--r--data/templates/frr/static_mcast.frr.tmpl (renamed from data/templates/frr-mcast/static_mcast.frr.tmpl)0
-rw-r--r--data/templates/openvpn/server.conf.tmpl2
-rw-r--r--data/templates/pppoe/ip-down.script.tmpl5
-rw-r--r--data/templates/pppoe/ipv6-up.script.tmpl5
-rw-r--r--data/templates/salt-minion/minion.tmpl10
-rw-r--r--data/templates/system-login/pam_radius_auth.conf.tmpl4
-rw-r--r--debian/changelog241
-rw-r--r--debian/control11
-rwxr-xr-xdebian/rules3
-rw-r--r--debian/vyos-1x.install1
-rw-r--r--debian/vyos-1x.postinst21
-rw-r--r--interface-definitions/dhcpv6-server.xml.in38
-rw-r--r--interface-definitions/include/dhcp-options.xml.i (renamed from interface-definitions/include/dhcp-dhcpv6-options.xml.i)19
-rw-r--r--interface-definitions/include/dhcpv6-options.xml.i65
-rw-r--r--interface-definitions/include/ipv6-address.xml.i11
-rw-r--r--interface-definitions/include/nat-address.xml.i37
-rw-r--r--interface-definitions/include/nat-outbound-interface.xml.i8
-rw-r--r--interface-definitions/include/nat-port.xml.i17
-rw-r--r--interface-definitions/include/nat-rule.xml.i303
-rw-r--r--interface-definitions/include/nat-translation-port.xml.i13
-rw-r--r--interface-definitions/include/vif-s.xml.i6
-rw-r--r--interface-definitions/include/vif.xml.i3
-rw-r--r--interface-definitions/interfaces-bonding.xml.in3
-rw-r--r--interface-definitions/interfaces-bridge.xml.in3
-rw-r--r--interface-definitions/interfaces-ethernet.xml.in3
-rw-r--r--interface-definitions/interfaces-pppoe.xml.in1
-rw-r--r--interface-definitions/interfaces-pseudo-ethernet.xml.in3
-rw-r--r--interface-definitions/interfaces-wireguard.xml.in8
-rw-r--r--interface-definitions/interfaces-wireless.xml.in7
-rw-r--r--interface-definitions/nat.xml.in180
-rw-r--r--interface-definitions/salt-minion.xml.in78
-rw-r--r--interface-definitions/service_pppoe-server.xml.in28
-rw-r--r--interface-definitions/system-login.xml.in1
-rw-r--r--interface-definitions/vpn_sstp.xml.in15
-rw-r--r--op-mode-definitions/nat.xml98
-rw-r--r--op-mode-definitions/show-interfaces-pppoe.xml2
-rw-r--r--op-mode-definitions/traceroute.xml70
-rw-r--r--python/vyos/airbag.py17
-rw-r--r--python/vyos/component_versions.py2
-rw-r--r--python/vyos/config.py66
-rw-r--r--python/vyos/configdict.py425
-rw-r--r--python/vyos/debug.py37
-rw-r--r--python/vyos/dicts.py3
-rw-r--r--python/vyos/ifconfig/dhcp.py129
-rw-r--r--python/vyos/ifconfig/interface.py161
-rw-r--r--python/vyos/ifconfig/section.py21
-rw-r--r--python/vyos/ifconfig/vlan.py40
-rw-r--r--python/vyos/ifconfig/wireguard.py2
-rw-r--r--python/vyos/ifconfig_vlan.py159
-rw-r--r--python/vyos/template.py10
-rw-r--r--python/vyos/util.py167
-rw-r--r--python/vyos/validate.py68
-rw-r--r--schema/op-mode-definition.rnc4
-rw-r--r--schema/op-mode-definition.rng5
-rwxr-xr-xscripts/build-command-templates2
-rwxr-xr-xsrc/conf_mode/dhcp_relay.py5
-rwxr-xr-xsrc/conf_mode/dhcp_server.py5
-rwxr-xr-xsrc/conf_mode/dhcpv6_relay.py5
-rwxr-xr-xsrc/conf_mode/dhcpv6_server.py145
-rwxr-xr-xsrc/conf_mode/dns_forwarding.py6
-rwxr-xr-xsrc/conf_mode/dynamic_dns.py4
-rwxr-xr-xsrc/conf_mode/flow_accounting_conf.py2
-rwxr-xr-xsrc/conf_mode/interfaces-bonding.py331
-rwxr-xr-xsrc/conf_mode/interfaces-bridge.py156
-rwxr-xr-xsrc/conf_mode/interfaces-dummy.py36
-rwxr-xr-xsrc/conf_mode/interfaces-ethernet.py262
-rwxr-xr-xsrc/conf_mode/interfaces-geneve.py22
-rwxr-xr-xsrc/conf_mode/interfaces-l2tpv3.py56
-rwxr-xr-xsrc/conf_mode/interfaces-openvpn.py82
-rwxr-xr-xsrc/conf_mode/interfaces-pppoe.py56
-rwxr-xr-xsrc/conf_mode/interfaces-pseudo-ethernet.py248
-rwxr-xr-xsrc/conf_mode/interfaces-tunnel.py55
-rwxr-xr-xsrc/conf_mode/interfaces-vxlan.py53
-rwxr-xr-xsrc/conf_mode/interfaces-wireguard.py69
-rwxr-xr-xsrc/conf_mode/interfaces-wireless.py212
-rwxr-xr-xsrc/conf_mode/interfaces-wirelessmodem.py60
-rwxr-xr-xsrc/conf_mode/ipsec-settings.py2
-rwxr-xr-xsrc/conf_mode/nat.py268
-rwxr-xr-xsrc/conf_mode/protocols_bfd.py2
-rwxr-xr-xsrc/conf_mode/protocols_igmp.py2
-rwxr-xr-xsrc/conf_mode/protocols_mpls.py2
-rwxr-xr-xsrc/conf_mode/protocols_pim.py2
-rwxr-xr-xsrc/conf_mode/protocols_static_multicast.py2
-rwxr-xr-xsrc/conf_mode/salt-minion.py103
-rwxr-xr-xsrc/conf_mode/service_ipoe-server.py20
-rwxr-xr-xsrc/conf_mode/service_pppoe-server.py22
-rwxr-xr-xsrc/conf_mode/system-login.py11
-rwxr-xr-xsrc/conf_mode/vpn_l2tp.py4
-rwxr-xr-xsrc/conf_mode/vpn_pptp.py4
-rwxr-xr-xsrc/conf_mode/vpn_sstp.py60
-rw-r--r--src/etc/dhcp/dhclient-enter-hooks.d/03-vyos-ipwrapper8
-rw-r--r--src/etc/dhcp/dhclient-exit-hooks.d/01-vyos-cleanup64
-rw-r--r--src/etc/dhcp/dhclient-exit-hooks.d/02-vyos-dhcp-renew-rfc3442148
-rw-r--r--src/etc/sysctl.d/31-vyos-addr_gen_mode.conf14
-rw-r--r--src/etc/systemd/system/pdns-recursor.service.d/override.conf2
-rwxr-xr-xsrc/helpers/validate-value.py45
-rwxr-xr-xsrc/migration-scripts/dhcpv6-server/0-to-161
-rwxr-xr-xsrc/migration-scripts/nat/4-to-558
-rwxr-xr-xsrc/migration-scripts/salt/0-to-158
-rwxr-xr-xsrc/op_mode/flow_accounting_op.py2
-rwxr-xr-xsrc/op_mode/powerctrl.py291
-rwxr-xr-xsrc/op_mode/show_interfaces.py43
-rwxr-xr-xsrc/op_mode/show_nat_statistics.py63
-rwxr-xr-xsrc/op_mode/to_be_migrated/vyatta-nat-translations.pl267
-rwxr-xr-xsrc/services/vyos-hostsd13
-rwxr-xr-xsrc/services/vyos-http-api-server92
-rwxr-xr-xsrc/system/on-dhcp-event.sh4
-rw-r--r--src/systemd/dhclient@.service18
-rw-r--r--src/systemd/dhcp6c@.service16
-rw-r--r--src/systemd/isc-dhcp-relay.service8
-rw-r--r--src/systemd/isc-dhcp-relay6.service8
-rw-r--r--src/systemd/isc-dhcp-server.service19
-rw-r--r--src/systemd/isc-dhcp-server6.service20
-rwxr-xr-xsrc/validators/file-exists71
-rwxr-xr-xsrc/validators/fqdn21
-rwxr-xr-xsrc/validators/ip-protocol41
-rwxr-xr-xsrc/validators/ipv4-address-exclude7
-rwxr-xr-xsrc/validators/ipv4-prefix-exclude7
-rwxr-xr-xsrc/validators/ipv4-range33
-rwxr-xr-xsrc/validators/ipv4-range-exclude7
-rwxr-xr-xsrc/validators/mac-address17
-rwxr-xr-xsrc/validators/numeric78
-rwxr-xr-xsrc/validators/script31
-rwxr-xr-xsrc/validators/timezone22
-rwxr-xr-xsrc/validators/vrf-name31
137 files changed, 4561 insertions, 2384 deletions
diff --git a/Makefile b/Makefile
index 3a47572b7..90a350e3b 100644
--- a/Makefile
+++ b/Makefile
@@ -120,6 +120,7 @@ clean:
.PHONY: test
test:
+ set -e; python3 -m compileall -q .
PYTHONPATH=python/ python3 -m "nose" --with-xunit src --with-coverage --cover-erase --cover-xml --cover-package src/conf_mode,src/op_mode,src/completion,src/helpers,src/validators,src/tests --verbose
.PHONY: sonar
diff --git a/data/templates/accel-ppp/pppoe.config.tmpl b/data/templates/accel-ppp/pppoe.config.tmpl
index b6a239fad..aa9254e61 100644
--- a/data/templates/accel-ppp/pppoe.config.tmpl
+++ b/data/templates/accel-ppp/pppoe.config.tmpl
@@ -12,10 +12,9 @@ ipv6pool
ipv6_nd
ipv6_dhcp
{% endif %}
-auth_pap
-auth_chap_md5
-auth_mschap_v1
-auth_mschap_v2
+{% for proto in auth_proto: %}
+{{proto}}
+{% endfor%}
shaper
{% if snmp %}
net-snmp
@@ -54,6 +53,9 @@ gw-ip-address={{ ppp_gw }}
{% endif %}
{% if client_ipv6_pool %}
+[ipv6-nd]
+AdvAutonomousFlag=1
+
[ipv6-pool]
{% for p in client_ipv6_pool %}
{{ p.prefix }},{{ p.mask }}
@@ -115,12 +117,14 @@ dae-server={{ radius_dynamic_author.server }}:{{ radius_dynamic_author.port }},{
{% endif -%}
{% endif %}
+{% if sesscrtl != 'disable' %}
+[common]
+single-session={{ sesscrtl }}
+{% endif %}
+
[ppp]
verbose=1
check-ip=1
-{% if not sesscrtl == 'disable' %}
-single-session={{sesscrtl}}
-{% endif -%}
{% if ppp_ccp %}
ccp=1
{% endif %}
diff --git a/data/templates/accel-ppp/sstp.config.tmpl b/data/templates/accel-ppp/sstp.config.tmpl
index c3dc83429..411fca489 100644
--- a/data/templates/accel-ppp/sstp.config.tmpl
+++ b/data/templates/accel-ppp/sstp.config.tmpl
@@ -9,6 +9,9 @@ chap-secrets
radius
{% endif -%}
ippool
+ipv6pool
+ipv6_nd
+ipv6_dhcp
{% for proto in auth_proto %}
{{proto}}
@@ -51,6 +54,14 @@ dns{{ loop.index }}={{ dns }}
{% endfor -%}
{% endif %}
+{% if dnsv6 %}
+[ipv6-dns]
+{% for dns in dnsv6 -%}
+{{ dns }}
+{% endfor -%}
+{% endif %}
+
+
{% if auth_mode == 'local' %}
[chap-secrets]
chap-secrets={{ chap_secrets_file }}
@@ -87,6 +98,9 @@ check-ip=1
{% if mtu %}
mtu={{ mtu }}
{% endif -%}
+{% if client_ipv6_pool %}
+ipv6=allow
+{% endif %}
{% if ppp_mppe %}
mppe={{ ppp_mppe }}
@@ -101,6 +115,21 @@ lcp-echo-failure={{ ppp_echo_failure }}
lcp-echo-timeout={{ ppp_echo_timeout }}
{% endif %}
+{% if client_ipv6_pool %}
+[ipv6-pool]
+{% for p in client_ipv6_pool %}
+{{ p.prefix }},{{ p.mask }}
+{% endfor %}
+{% for p in client_ipv6_delegate_prefix %}
+delegate={{ p.prefix }},{{ p.mask }}
+{% endfor %}
+{% endif %}
+
+{% if client_ipv6_delegate_prefix %}
+[ipv6-dhcp]
+verbose=1
+{% endif %}
+
{% if radius_shaper_attr %}
[shaper]
verbose=1
diff --git a/data/templates/dhcp-client/daemon-options.tmpl b/data/templates/dhcp-client/daemon-options.tmpl
new file mode 100644
index 000000000..b5a10c3b8
--- /dev/null
+++ b/data/templates/dhcp-client/daemon-options.tmpl
@@ -0,0 +1 @@
+DHCLIENT_OPTS="-nw -cf {{ conf_file }} -pf {{ pid_file }} -lf {{ lease_file }} {{ '-S' if dhcpv6_prm_only }} {{ '-T' if dhcpv6_temporary }} {{ ifname }}"
diff --git a/data/templates/dhcp-client/ipv4.tmpl b/data/templates/dhcp-client/ipv4.tmpl
index 43f273077..ab772b5f6 100644
--- a/data/templates/dhcp-client/ipv4.tmpl
+++ b/data/templates/dhcp-client/ipv4.tmpl
@@ -1,4 +1,4 @@
-# generated by ifconfig.py
+# generated by dhcp.py
option rfc3442-classless-static-routes code 121 = array of unsigned integer 8;
timeout 60;
retry 300;
diff --git a/data/templates/dhcp-client/ipv6.tmpl b/data/templates/dhcp-client/ipv6.tmpl
index 83db40c5f..6cfe24d3e 100644
--- a/data/templates/dhcp-client/ipv6.tmpl
+++ b/data/templates/dhcp-client/ipv6.tmpl
@@ -1,4 +1,41 @@
-# generated by ifconfig.py
-interface "{{ ifname }}" {
- request routers, domain-name-servers, domain-name;
-}
+# generated by dhcp.py
+# man https://www.unix.com/man-page/debian/5/dhcp6c.conf/
+
+interface {{ ifname }} {
+ request domain-name-servers;
+ request domain-name;
+{% if dhcpv6_prm_only %}
+ information-only;
+{% endif %}
+{% if not dhcpv6_temporary %}
+ send ia-na 1; # non-temporary address
+{% endif %}
+{% if dhcpv6_pd %}
+ send ia-pd 2; # prefix delegation
+{% endif %}
+};
+
+{% if not dhcpv6_temporary %}
+id-assoc na 1 {
+ # Identity association NA
+};
+{% endif %}
+
+{% if dhcpv6_pd %}
+id-assoc pd 2 {
+{% for intf in dhcpv6_pd %}
+ prefix-interface {{ intf.ifname }} {
+{% if intf.sla_id %}
+ sla-id {{ intf.sla_id }};
+{% endif %}
+{% if intf.sla_len %}
+ sla-len {{ intf.sla_len }};
+{% endif %}
+{% if intf.if_id %}
+ ifid {{ intf.if_id }};
+{% endif %}
+ };
+{% endfor %}
+};
+{% endif %}
+
diff --git a/data/templates/dhcpv6-server/dhcpdv6.conf.tmpl b/data/templates/dhcpv6-server/dhcpdv6.conf.tmpl
index 80d620fcf..ff7822b0d 100644
--- a/data/templates/dhcpv6-server/dhcpdv6.conf.tmpl
+++ b/data/templates/dhcpv6-server/dhcpdv6.conf.tmpl
@@ -21,7 +21,7 @@ shared-network {{ network.name }} {
range6 {{ range.start }} {{ range.stop }};
{%- endfor %}
{%- if subnet.domain_search %}
- option dhcp6.domain-search {{ subnet.domain_search | join(', ') }};
+ option dhcp6.domain-search "{{ subnet.domain_search | join('", "') }}";
{%- endif %}
{%- if subnet.lease_def %}
default-lease-time {{ subnet.lease_def }};
@@ -51,11 +51,14 @@ shared-network {{ network.name }} {
option dhcp6.sip-servers-addresses {{ subnet.sip_address | join(', ') }};
{%- endif %}
{%- if subnet.sip_hostname %}
- option dhcp6.sip-servers-names {{ subnet.sip_hostname | join(', ') }};
+ option dhcp6.sip-servers-names "{{ subnet.sip_hostname | join('", "') }}";
{%- endif %}
{%- if subnet.sntp_server %}
option dhcp6.sntp-servers {{ subnet.sntp_server | join(', ') }};
{%- endif %}
+ {%- for prefix in subnet.prefix_delegation %}
+ prefix6 {{ prefix.start }} {{ prefix.stop }} /{{ prefix.length }};
+ {%- endfor %}
{%- for host in subnet.static_mapping %}
{% if not host.disabled -%}
host {{ network.name }}_{{ host.name }} {
diff --git a/data/templates/firewall/nftables-nat.tmpl b/data/templates/firewall/nftables-nat.tmpl
new file mode 100644
index 000000000..abb32ddc6
--- /dev/null
+++ b/data/templates/firewall/nftables-nat.tmpl
@@ -0,0 +1,139 @@
+#!/usr/sbin/nft -f
+
+# Start with clean NAT table
+flush table nat
+
+{% if helper_functions == 'remove' %}
+{# NAT if going to be disabled - remove rules and targets from nftables #}
+
+{% set base_command = "delete rule ip raw" %}
+{{ base_command }} PREROUTING handle {{ pre_ct_ignore }}
+{{ base_command }} OUTPUT handle {{ out_ct_ignore }}
+{{ base_command }} PREROUTING handle {{ pre_ct_conntrack }}
+{{ base_command }} OUTPUT handle {{ out_ct_conntrack }}
+
+delete chain ip raw NAT_CONNTRACK
+
+{% elif helper_functions == 'add' %}
+{# NAT if enabled - add targets to nftables #}
+add chain ip raw NAT_CONNTRACK
+add rule ip raw NAT_CONNTRACK counter accept
+
+{% set base_command = "add rule ip raw" %}
+
+{{ base_command }} PREROUTING position {{ pre_ct_ignore }} counter jump VYATTA_CT_HELPER
+{{ base_command }} OUTPUT position {{ out_ct_ignore }} counter jump VYATTA_CT_HELPER
+{{ base_command }} PREROUTING position {{ pre_ct_conntrack }} counter jump NAT_CONNTRACK
+{{ base_command }} OUTPUT position {{ out_ct_conntrack }} counter jump NAT_CONNTRACK
+{% endif %}
+
+{% macro nat_rule(rule, chain) %}
+{% set src_addr = "ip saddr " + rule.source_address if rule.source_address %}
+{% set src_port = "sport { " + rule.source_port +" }" if rule.source_port %}
+{% set dst_addr = "ip daddr " + rule.dest_address if rule.dest_address %}
+{% set dst_port = "dport { " + rule.dest_port +" }" if rule.dest_port %}
+{% set comment = "DST-NAT-" + rule.number %}
+
+{% if chain == "PREROUTING" %}
+{% set interface = " iifname \"" + rule.interface_in + "\"" %}
+{% set trns_addr = "dnat to " + rule.translation_address %}
+{% elif chain == "POSTROUTING" %}
+{% set interface = " oifname \"" + rule.interface_out + "\"" %}
+{% set trns_addr = rule.translation_address %}
+{% if rule.translation_address != 'masquerade' %}
+{% set trns_addr = "snat to " + trns_addr %}
+{% endif %}
+{% endif %}
+{% set trns_port = ":" + rule.translation_port if rule.translation_port %}
+
+{% if rule.protocol == "tcp_udp" %}
+{% set protocol = "tcp" %}
+{% set comment = comment + " tcp_udp" %}
+{% else %}
+{% set protocol = rule.protocol %}
+{% endif %}
+
+{% if rule.log %}
+{% set base_log = "[NAT-DST-" + rule.number %}
+{% if rule.exclude %}
+{% set log = base_log + "-EXCL]" %}
+{% elif rule.translation_address == 'masquerade' %}
+{% set log = base_log + "-MASQ]" %}
+{% else %}
+{% set log = base_log + "]" %}
+{% endif %}
+{% endif %}
+
+{% if rule.exclude %}
+{# rule has been marked as "exclude" thus we simply return here #}
+{% set trns_addr = "return" %}
+{% set trns_port = "" %}
+{% endif %}
+
+{% set output = "add rule ip nat " + chain + interface %}
+
+{% if protocol != "all" %}
+{% set output = output + " ip protocol " + protocol %}
+{% endif %}
+
+{% if src_addr %}
+{% set output = output + " " + src_addr %}
+{% endif %}
+{% if src_port %}
+{% set output = output + " " + protocol + " " + src_port %}
+{% endif %}
+
+{% if dst_addr %}
+{% set output = output + " " + dst_addr %}
+{% endif %}
+{% if dst_port %}
+{% set output = output + " " + protocol + " " + dst_port %}
+{% endif %}
+
+{# Count packets #}
+{% set output = output + " counter" %}
+
+{# Special handling of log option, we must repeat the entire rule before the #}
+{# NAT translation options are added, this is essential #}
+{% if log %}
+{% set log_output = output + " log prefix \"" + log + "\" comment \"" + comment + "\"" %}
+{% endif %}
+
+{% if trns_addr %}
+{% set output = output + " " + trns_addr %}
+{% endif %}
+
+{% if trns_port %}
+{# Do not add a whitespace here, translation port must be directly added after IP address #}
+{# e.g. 192.0.2.10:3389 #}
+{% set output = output + trns_port %}
+{% endif %}
+
+{% if comment %}
+{% set output = output + " comment \"" + comment + "\"" %}
+{% endif %}
+
+{{ log_output if log_output }}
+{{ output }}
+
+{# Special handling if protocol is tcp_udp, we must repeat the entire rule with udp as protocol #}
+{% if rule.protocol == "tcp_udp" %}
+{# Beware of trailing whitespace, without it the comment tcp_udp will be changed to udp_udp #}
+{{ log_output | replace("tcp ", "udp ") if log_output }}
+{{ output | replace("tcp ", "udp ") }}
+{% endif %}
+{% endmacro %}
+
+#
+# Destination NAT rules build up here
+#
+{% for rule in destination if not rule.disabled -%}
+{{ nat_rule(rule, 'PREROUTING') }}
+{% endfor %}
+
+#
+# Source NAT rules build up here
+#
+{% for rule in source if not rule.disabled -%}
+{{ nat_rule(rule, 'POSTROUTING') }}
+{% endfor %}
diff --git a/data/templates/frr-bfd/bfd.frr.tmpl b/data/templates/frr/bfd.frr.tmpl
index 7df4bfd01..7df4bfd01 100644
--- a/data/templates/frr-bfd/bfd.frr.tmpl
+++ b/data/templates/frr/bfd.frr.tmpl
diff --git a/data/templates/igmp/igmp.frr.tmpl b/data/templates/frr/igmp.frr.tmpl
index de4696c1f..de4696c1f 100644
--- a/data/templates/igmp/igmp.frr.tmpl
+++ b/data/templates/frr/igmp.frr.tmpl
diff --git a/data/templates/mpls/ldpd.frr.tmpl b/data/templates/frr/ldpd.frr.tmpl
index bbff88ae5..bbff88ae5 100644
--- a/data/templates/mpls/ldpd.frr.tmpl
+++ b/data/templates/frr/ldpd.frr.tmpl
diff --git a/data/templates/pim/pimd.frr.tmpl b/data/templates/frr/pimd.frr.tmpl
index 1d1532c60..1d1532c60 100644
--- a/data/templates/pim/pimd.frr.tmpl
+++ b/data/templates/frr/pimd.frr.tmpl
diff --git a/data/templates/frr-mcast/static_mcast.frr.tmpl b/data/templates/frr/static_mcast.frr.tmpl
index 86d619ab0..86d619ab0 100644
--- a/data/templates/frr-mcast/static_mcast.frr.tmpl
+++ b/data/templates/frr/static_mcast.frr.tmpl
diff --git a/data/templates/openvpn/server.conf.tmpl b/data/templates/openvpn/server.conf.tmpl
index 75ab602f8..401f8e04b 100644
--- a/data/templates/openvpn/server.conf.tmpl
+++ b/data/templates/openvpn/server.conf.tmpl
@@ -74,7 +74,7 @@ nobind
topology {% if server_topology == 'point-to-point' %}p2p{% else %}{{ server_topology }}{% endif %}
{%- endif %}
-{%- if bridge_member %}
+{%- if is_bridge_member %}
mode server
tls-server
{%- else %}
diff --git a/data/templates/pppoe/ip-down.script.tmpl b/data/templates/pppoe/ip-down.script.tmpl
index a68fc099c..fe8fd7584 100644
--- a/data/templates/pppoe/ip-down.script.tmpl
+++ b/data/templates/pppoe/ip-down.script.tmpl
@@ -26,3 +26,8 @@ fi
# Always delete default route when interface goes down
vtysh -c "conf t" ${VRF_NAME} -c "no ip route 0.0.0.0/0 {{ intf }} ${VRF_NAME}"
{% endif %}
+
+{% if dhcpv6_pd %}
+# Start wide dhcpv6 client
+systemctl stop dhcp6c@{{ intf }}.service
+{% endif %}
diff --git a/data/templates/pppoe/ipv6-up.script.tmpl b/data/templates/pppoe/ipv6-up.script.tmpl
index a4b08ddaf..90873229a 100644
--- a/data/templates/pppoe/ipv6-up.script.tmpl
+++ b/data/templates/pppoe/ipv6-up.script.tmpl
@@ -39,3 +39,8 @@ echo 2 > /proc/sys/net/ipv6/conf/{{ intf }}/accept_ra
# Autoconfigure addresses using Prefix Information in Router Advertisements.
echo 1 > /proc/sys/net/ipv6/conf/{{ intf }}/autoconfigure
{% endif %}
+
+{% if dhcpv6_pd %}
+# Start wide dhcpv6 client
+systemctl start dhcp6c@{{ intf }}.service
+{% endif %}
diff --git a/data/templates/salt-minion/minion.tmpl b/data/templates/salt-minion/minion.tmpl
index 5e50d588c..9369573a4 100644
--- a/data/templates/salt-minion/minion.tmpl
+++ b/data/templates/salt-minion/minion.tmpl
@@ -12,7 +12,7 @@
#
# Prior to changing this value, the master should be stopped and all Salt
# caches should be cleared.
-hash_type: {{ hash_type }}
+hash_type: {{ hash }}
##### Logging settings #####
##########################################
@@ -21,11 +21,7 @@ hash_type: {{ hash_type }}
# location. Remote logging works best when configured to use rsyslogd(8) (e.g.:
# ``file:///dev/log``), with rsyslogd(8) configured for network logging. The URI
# format is: <file|udp|tcp>://<host|socketpath>:<port-if-required>/<log-facility>
-#log_file: /var/log/salt/minion
-#log_file: file:///dev/log
-#log_file: udp://loghost:10514
-#
-log_file: {{ log_file }}
+log_file: file:///dev/log
# The level of messages to send to the console.
# One of 'garbage', 'trace', 'debug', info', 'warning', 'error', 'critical'.
@@ -58,6 +54,6 @@ id: {{ salt_id }}
# The number of minutes between mine updates.
-mine_interval: {{ mine_interval }}
+mine_interval: {{ interval }}
verify_master_pubkey_sign: {{ verify_master_pubkey_sign }}
diff --git a/data/templates/system-login/pam_radius_auth.conf.tmpl b/data/templates/system-login/pam_radius_auth.conf.tmpl
index 6cff67867..ad196fa3d 100644
--- a/data/templates/system-login/pam_radius_auth.conf.tmpl
+++ b/data/templates/system-login/pam_radius_auth.conf.tmpl
@@ -10,4 +10,8 @@
priv-lvl 15
mapped_priv_user radius_priv_user
+
+{% if radius_vrf %}
+vrf-name {{ radius_vrf }}
+{% endif %}
{% endif %}
diff --git a/debian/changelog b/debian/changelog
index 80d8bf425..fba9d77d0 100644
--- a/debian/changelog
+++ b/debian/changelog
@@ -1,235 +1,10 @@
-vyos-1x (1.3.0-17) unstable; urgency=medium
+vyos-1x (1.0.0) unstable; urgency=medium
- * vyos-1x-vmware: add dependency on vyos-1x
-
- -- Christian Poessinger <christian@poessinger.com> Sat, 21 Mar 2020 20:47:22 +0100
-
-vyos-1x (1.3.0-16) unstable; urgency=low
-
- * [arp] moving 'show arp' to 'show protocols static arp'
-
- -- hagbard <vyosdev@derith.de> Thu, 14 Mar 2019 11:15:42 -0700
-
-vyos-1x (1.3.0-15) unstable; urgency=low
-
- * bugfix for show arp interface ethX
-
- -- hagbard <vyosdev@derith.de> Mon, 11 Mar 2019 15:28:22 -0700
-
-vyos-1x (1.3.0-14) unstable; urgency=low
-
- * T1294 - Trying to delete 'system syslog' throws an exception/traceback
-
- -- hagbard <vyosdev@derith.de> Mon, 11 Mar 2019 15:06:39 -0700
-
-vyos-1x (1.3.0-13) unstable; urgency=low
-
- * T1288 - python implementation of 'set protocols static arp '
-
- -- hagbard <vyosdev@derith.de> Mon, 11 Mar 2019 13:46:19 -0700
-
-vyos-1x (1.3.0-12) unstable; urgency=low
-
- * T1280 - reverted all the changes due to unexpected behaviour
-
- -- hagbard <vyosdev@derith.de> Fri, 08 Mar 2019 15:18:08 -0800
-
-vyos-1x (1.3.0-11) unstable; urgency=low
-
- * T1282 - make $PreserveFQDN on configurable via cli
-
- -- hagbard <vyosdev@derith.de> Thu, 07 Mar 2019 13:11:31 -0800
-
-vyos-1x (1.3.0-10) unstable; urgency=low
-
- * T1280 - allow-clients on NTP configuration
-
- -- hagbard <vyosdev@derith.de> Thu, 07 Mar 2019 12:08:50 -0800
-
-vyos-1x (1.3.0-9) unstable; urgency=low
-
- * mppe cfg options added
-
- -- hagbard <vyosdev@derith.de> Wed, 06 Mar 2019 14:52:19 -0800
-
-vyos-1x (1.3.0-8) unstable; urgency=low
-
- * enable option for setting mppe in pptp, default is 128 bit is required.
-
- -- hagbard <vyosdev@derith.de> Wed, 06 Mar 2019 14:06:43 -0800
-
-vyos-1x (1.3.0-7) unstable; urgency=low
-
- * op-mode typo fixed
-
- -- hagbard <vyosdev@derith.de> Wed, 06 Mar 2019 12:33:16 -0800
-
-vyos-1x (1.3.0-6) unstable; urgency=low
-
- * version bump for bugfix radius config
-
- -- hagbard <vyosdev@derith.de> Wed, 06 Mar 2019 12:23:25 -0800
-
-vyos-1x (1.3.0-5) unstable; urgency=low
-
- * bumping version for T833: accel-ppp: pptp implementation
-
- -- hagbard <vyosdev@derith.de> Wed, 06 Mar 2019 12:06:45 -0800
-
-vyos-1x (1.3.0-4) unstable; urgency=low
-
- * adding noquery notrust to the default restict option as per T1280
-
- -- hagbard <vyosdev@derith.de> Mon, 04 Mar 2019 15:07:17 -0800
-
-vyos-1x (1.3.0-3) unstable; urgency=low
-
- fixes T1262 - dhcp requested WAN ip address doesn't get search parameter in /etc/resolv.conf
-
- -- hagbard <vyosdev@derith.de> Fri, 22 Feb 2019 10:50:24 -0800
-
-vyos-1x (1.3.0-2) unstable; urgency=low
-
- fixes T1257 - implement 'set system static-host-mapping' in host_name.py and remove old function calls
-
- -- hagbard <vyosdev@derith.de> Thu, 21 Feb 2019 15:53:30 -0800
-
-vyos-1x (1.3.0-1) unstable; urgency=medium
-
- * Changing version for the new branch.
-
- -- Daniil Baturin <daniil@baturin.org> Thu, 21 Feb 2019 22:52:26 +0100
-
-vyos-1x (1.2.0-14) unstable; urgency=low
-
- * fixes T1254 - generate wireguard keypair fails when executed on the iso
- - keypair can now be generated and used from a running iso
-
- -- hagbard <vyosdev@derith.de> Tue, 19 Feb 2019 12:31:50 -0800
-
-vyos-1x (1.2.0-13) unstable; urgency=low
-
- * fixes T1238 - Wireguard allows invalid IP's
-
- -- hagbard <vyosdev@derith.de> Sat, 09 Feb 2019 14:42:13 -0800
-
-vyos-1x (1.2.0-12) unstable; urgency=low
-
- * fixes T1225: wireguard implement 'set int wireguard wg0 peer name disable' to disable single peers
-
- -- hagbard <vyosdev@derith.de> Mon, 04 Feb 2019 10:26:50 -0800
-
-vyos-1x (1.2.0-11) unstable; urgency=low
-
- * Fix: T1217 - cant delete wireguard wg0 interface
-
- -- hagbard <vyosdev@derith.de> Wed, 30 Jan 2019 14:54:45 -0800
-
-vyos-1x (1.2.0-10) unstable; urgency=low
-
- * T1178: Scheduled script breaks ability to modify configuration
-
- -- hagbard <vyosdev@derith.de> Tue, 22 Jan 2019 13:35:09 -0800
-
-vyos-1x (1.2.0-9) unstable; urgency=low
-
- * T1168: Upgrade: 1,1,7 -> 1.2.0-epa2
- - keyword change in config
-
- -- hagbard <vyosdev@derith.de> Mon, 07 Jan 2019 11:42:49 -0800
-
-vyos-1x (1.2.0-8) unstable; urgency=low
-
- * T1162: WireGuard: Unable to modify tunnels - KeyError: 'state'
-
- -- hagbard <vyosdev@derith.de> Sun, 06 Jan 2019 15:58:40 -0800
-
-vyos-1x (1.2.0-7) unstable; urgency=low
-
- * T1061: Wireguard: Missing option to administrativly shutdown interface
-
- -- hagbard <vyosdev@derith.de> Fri, 30 Nov 2018 10:22:41 -0800
-
-vyos-1x (1.2.0-6) unstable; urgency=medium
-
- * adding vyos-accel-ppp-ipoe-kmod for T989
-
- -- hagbard <vyosdev@derith.de> Thu, 22 Nov 2018 10:56:15 -0800
-
-vyos-1x (1.2.0-5) unstable; urgency=medium
-
- * T835: accel-ppp: pppoe implementation
-
- -- hagbard <vyosdev@derith.de> Fri, 09 Nov 2018 10:49:48 -0800
-
-vyos-1x (1.2.0-4) unstable; urgency=medium
-
- * T240 adds feature system integrity check
-
- -- hagbard <vyosdev@derith.de> Mon, 29 Oct 2018 11:10:18 -0700
-
-vyos-1x (1.2.0-3) unstable; urgency=medium
-
- * T933: adding vmac_xmit_base if use_vmac has been chosen
- to avoid split-brain
-
- -- hagbard <vyosdev@derith.de> Thu, 25 Oct 2018 11:14:44 -0700
-
-vyos-1x (1.2.0-2) unstable; urgency=medium
-
- * T773: adding wireguard support
-
- -- hagbard <vyosdev@derith.de> Sat, 11 Aug 2018 15:51:34 -0700
-
-vyos-1x (1.2.0-1) unstable; urgency=medium
-
- * T666, T616: new implementation of the VRRP CLI.
-
- -- Daniil Baturin <daniil@baturin.org> Fri, 27 Jul 2018 10:25:52 +0200
-
-vyos-1x (1.0.6) unstable; urgency=medium
-
- * T736: Rewrite remote logging (syslog) to XML/Python
-
- -- hagbard <vyosdev@derith.de> Tue, 24 Jul 2018 10:59:25 -0700
-
-vyos-1x (1.0.5) unstable; urgency=medium
-
- * T606: Error in DNS Forwarder listen-on
- * T608: Cannot configure broadcast-relay service
-
- -- Christian Poessinger <christian@poessinger.com> Thu, 19 Apr 2018 21:16:28 +0200
-
-vyos-1x (1.0.4) unstable; urgency=medium
-
- * T560: dns-forwarding: replace dnsmasq with pdns-recursor
- * T588: Rewrite 'service dns forwarding' in new XML style format
-
- -- Christian Poessinger <christian@poessinger.com> Sun, 15 Apr 2018 16:13:32 +0200
-
-vyos-1x (1.0.3) unstable; urgency=medium
-
- * T379: Add UDP broadcast relay support
- * mdns repeater scripts - remove python subprocess
- * Support setting optional 'type' node in command templates
-
- -- Christian Poessinger <christian@poessinger.com> Sat, 06 Jan 2018 13:18:30 +0100
-
-vyos-1x (1.0.2) unstable; urgency=low
-
- * Added mdns-repeater configuration nodes
-
- -- Christian Poessinger <christian@poessinger.com> Sat, 09 Dec 2017 10:39:35 +0100
-
-vyos-1x (1.0.1) unstable; urgency=low
-
- * Added the Python library for reading VyOS configs
-
- -- Daniil Baturin <daniil@baturin.org> Thu, 17 Aug 2017 22:22:17 -0400
-
-vyos-1x (1.0.0) unstable; urgency=low
-
- * Created the package
-
- -- Daniil Baturin <daniil@baturin.org> Thu, 17 Aug 2017 20:17:04 -0400
+ * Dummy changelog entry for vyos-1x repository
+ This is a internal VyOS package and the VyOS package process does not use
+ the debian package changelog for its changes, please refer to the
+ GitHub commitlog and the vyos release-notes for more details.
+ The correct verion number of this package is auto-generated by GIT
+ on build-time
+ -- Runar Borge <runar@borge.nu> Sat, 9 May 2020 22:00:00 +0100
diff --git a/debian/control b/debian/control
index 7b95b2c75..eec2c087e 100644
--- a/debian/control
+++ b/debian/control
@@ -28,15 +28,19 @@ Depends: python3,
python3-isc-dhcp-leases,
python3-hurry.filesize,
python3-vici (>= 5.7.2),
- python3-bottle,
+ python3-flask,
+ python3-waitress,
python3-netaddr,
python3-zmq,
+ python3-jmespath,
cron,
+ systemd,
easy-rsa,
ipaddrcheck,
tcpdump,
tshark,
isc-dhcp-client,
+ wide-dhcpv6-client,
bmon,
hvinfo,
file,
@@ -89,6 +93,11 @@ Depends: python3,
pmacct (>= 1.6.0),
python3-certbot-nginx,
pppoe,
+ libxml-simple-perl,
+ salt-minion,
+ vyos-utils,
+ nftables (>= 0.9.3),
+ conntrack,
${shlibs:Depends},
${misc:Depends}
Description: VyOS configuration scripts and data
diff --git a/debian/rules b/debian/rules
index 144132389..3e408b538 100755
--- a/debian/rules
+++ b/debian/rules
@@ -15,6 +15,9 @@ SERVICES_DIR := usr/libexec/vyos/services
%:
dh $@ --with python3, --with quilt
+override_dh_gencontrol:
+ dh_gencontrol -- -v$(shell (git describe --tags --long --match 'vyos/*' --dirty 2>/dev/null || echo 0.0-no.git.tag) | sed -E 's%vyos/%%' | sed -E 's%-dirty%+dirty%')
+
override_dh_auto_build:
make all
diff --git a/debian/vyos-1x.install b/debian/vyos-1x.install
index dd8eebc0b..599f3f3f5 100644
--- a/debian/vyos-1x.install
+++ b/debian/vyos-1x.install
@@ -2,6 +2,7 @@ etc/dhcp
etc/ppp
etc/rsyslog.d
etc/systemd
+etc/sysctl.d
etc/udev
etc/vyos
lib/
diff --git a/debian/vyos-1x.postinst b/debian/vyos-1x.postinst
new file mode 100644
index 000000000..a308401ee
--- /dev/null
+++ b/debian/vyos-1x.postinst
@@ -0,0 +1,21 @@
+#!/bin/sh -e
+if ! deb-systemd-helper --quiet was-enabled salt-minion.service; then
+ # Enables the unit on first installation, creates new
+ # symlinks on upgrades if the unit file has changed.
+ deb-systemd-helper disable salt-minion.service >/dev/null || true
+fi
+
+if [ -x "/etc/init.d/salt-minion" ]; then
+ update-rc.d -f salt-minion remove >/dev/null
+fi
+
+# Add minion user for salt-minion
+if ! grep -q '^minion' /etc/passwd; then
+ adduser --quiet --firstuid 100 --system --disabled-login --ingroup vyattacfg --gecos "salt minion user" --shell /bin/vbash minion
+ adduser --quiet minion frrvty
+ adduser --quiet minion sudo
+ adduser --quiet minion adm
+ adduser --quiet minion dip
+ adduser --quiet minion disk
+ adduser --quiet minion users
+fi
diff --git a/interface-definitions/dhcpv6-server.xml.in b/interface-definitions/dhcpv6-server.xml.in
index 7d4c0de23..4073b46b2 100644
--- a/interface-definitions/dhcpv6-server.xml.in
+++ b/interface-definitions/dhcpv6-server.xml.in
@@ -126,16 +126,37 @@
<leafNode name="default">
<properties>
<help>Default time (in seconds) that will be assigned to a lease</help>
+ <valueHelp>
+ <format>1-4294967295</format>
+ <description>DHCPv6 valid lifetime</description>
+ </valueHelp>
+ <constraint>
+ <validator name="numeric" argument="--range 1-4294967295"/>
+ </constraint>
</properties>
</leafNode>
<leafNode name="maximum">
<properties>
<help>Maximum time (in seconds) that will be assigned to a lease</help>
+ <valueHelp>
+ <format>1-4294967295</format>
+ <description>Maximum lease time in seconds</description>
+ </valueHelp>
+ <constraint>
+ <validator name="numeric" argument="--range 1-4294967295"/>
+ </constraint>
</properties>
</leafNode>
<leafNode name="minimum">
<properties>
<help>Minimum time (in seconds) that will be assigned to a lease</help>
+ <valueHelp>
+ <format>1-4294967295</format>
+ <description>Minimum lease time in seconds</description>
+ </valueHelp>
+ <constraint>
+ <validator name="numeric" argument="--range 1-4294967295"/>
+ </constraint>
</properties>
</leafNode>
</children>
@@ -243,29 +264,24 @@
</tagNode>
</children>
</node>
- <leafNode name="sip-server-address">
+ <leafNode name="sip-server">
<properties>
<help>IPv6 address of SIP server</help>
<valueHelp>
<format>ipv6</format>
<description>IPv6 address of SIP server</description>
</valueHelp>
+ <valueHelp>
+ <format>hostname</format>
+ <description>FQDN of SIP server</description>
+ </valueHelp>
<constraint>
<validator name="ipv6-address"/>
+ <validator name="fqdn"/>
</constraint>
<multi/>
</properties>
</leafNode>
- <leafNode name="sip-server-name">
- <properties>
- <help>SIP server name</help>
- <constraint>
- <regex>[-_a-zA-Z0-9.]+</regex>
- </constraint>
- <constraintErrorMessage>Invalid SIP server name. May only contain letters, numbers and .-_</constraintErrorMessage>
- <multi/>
- </properties>
- </leafNode>
<leafNode name="sntp-server">
<properties>
<help>IPv6 address of an SNTP server for client to use</help>
diff --git a/interface-definitions/include/dhcp-dhcpv6-options.xml.i b/interface-definitions/include/dhcp-options.xml.i
index e4387863b..0f71d9321 100644
--- a/interface-definitions/include/dhcp-dhcpv6-options.xml.i
+++ b/interface-definitions/include/dhcp-options.xml.i
@@ -20,22 +20,3 @@
</leafNode>
</children>
</node>
-<node name="dhcpv6-options">
- <properties>
- <help>DHCPv6 options</help>
- </properties>
- <children>
- <leafNode name="parameters-only">
- <properties>
- <help>Acquire only config parameters, no address</help>
- <valueless/>
- </properties>
- </leafNode>
- <leafNode name="temporary">
- <properties>
- <help>IPv6 "temporary" address</help>
- <valueless/>
- </properties>
- </leafNode>
- </children>
-</node>
diff --git a/interface-definitions/include/dhcpv6-options.xml.i b/interface-definitions/include/dhcpv6-options.xml.i
new file mode 100644
index 000000000..2c5058d2c
--- /dev/null
+++ b/interface-definitions/include/dhcpv6-options.xml.i
@@ -0,0 +1,65 @@
+<node name="dhcpv6-options">
+ <properties>
+ <help>DHCPv6 options</help>
+ </properties>
+ <children>
+ <tagNode name="delegate">
+ <properties>
+ <help>Delegate IPv6 prefix from provider to this interface</help>
+ <completionHelp>
+ <script>${vyos_completion_dir}/list_interfaces.py --broadcast</script>
+ </completionHelp>
+ </properties>
+ <children>
+ <leafNode name="interface-id">
+ <properties>
+ <help>Interface address identifier</help>
+ <valueHelp>
+ <format>0-</format>
+ <description>Used to form IPv6 interface address (default: EUI-64)</description>
+ </valueHelp>
+ <constraint>
+ <validator name="numeric" argument="--non-negative"/>
+ </constraint>
+ </properties>
+ </leafNode>
+ <leafNode name="sla-id">
+ <properties>
+ <help>Interface site-Level aggregator (SLA)</help>
+ <valueHelp>
+ <format>0-128</format>
+ <description>Decimal integer which fits in the length of SLA IDs</description>
+ </valueHelp>
+ <constraint>
+ <validator name="numeric" argument="--range 0-128"/>
+ </constraint>
+ </properties>
+ </leafNode>
+ <leafNode name="sla-len">
+ <properties>
+ <help>Site-Level aggregator (SLA) length</help>
+ <valueHelp>
+ <format>0-128</format>
+ <description>Length of delegated prefix</description>
+ </valueHelp>
+ <constraint>
+ <validator name="numeric" argument="--range 0-128"/>
+ </constraint>
+ </properties>
+ </leafNode>
+ </children>
+ </tagNode>
+ <leafNode name="parameters-only">
+ <properties>
+ <help>Acquire only config parameters, no address</help>
+ <valueless/>
+ </properties>
+ </leafNode>
+ <leafNode name="temporary">
+ <properties>
+ <help>IPv6 "temporary" address</help>
+ <valueless/>
+ </properties>
+ </leafNode>
+ </children>
+</node>
diff --git a/interface-definitions/include/ipv6-address.xml.i b/interface-definitions/include/ipv6-address.xml.i
index 507d5dcc1..34f54e4c1 100644
--- a/interface-definitions/include/ipv6-address.xml.i
+++ b/interface-definitions/include/ipv6-address.xml.i
@@ -8,14 +8,21 @@
</leafNode>
<leafNode name="eui64">
<properties>
- <help>ssign IPv6 address using EUI-64 based on MAC address</help>
+ <help>Prefix for IPv6 address with MAC-based EUI-64</help>
<valueHelp>
<format>ipv6net</format>
- <description>IPv6 address and prefix length</description>
+ <description>IPv6 network and prefix length</description>
</valueHelp>
<constraint>
<validator name="ipv6-prefix"/>
</constraint>
+ <multi/>
+ </properties>
+ </leafNode>
+ <leafNode name="no-default-link-local">
+ <properties>
+ <help>Remove the default link-local address from the interface</help>
+ <valueless/>
</properties>
</leafNode>
</children>
diff --git a/interface-definitions/include/nat-address.xml.i b/interface-definitions/include/nat-address.xml.i
new file mode 100644
index 000000000..933dae07b
--- /dev/null
+++ b/interface-definitions/include/nat-address.xml.i
@@ -0,0 +1,37 @@
+<leafNode name="address">
+ <properties>
+ <help>IP address, subnet, or range</help>
+ <valueHelp>
+ <format>ipv4</format>
+ <description>IPv4 address to match</description>
+ </valueHelp>
+ <valueHelp>
+ <format>ipv4net</format>
+ <description>IPv4 prefix to match</description>
+ </valueHelp>
+ <valueHelp>
+ <format>ipv4range</format>
+ <description>IPv4 address range to match</description>
+ </valueHelp>
+ <valueHelp>
+ <format>!ipv4</format>
+ <description>Match everything except the specified address</description>
+ </valueHelp>
+ <valueHelp>
+ <format>!ipv4net</format>
+ <description>Match everything except the specified prefix</description>
+ </valueHelp>
+ <valueHelp>
+ <format>!ipv4range</format>
+ <description>Match everything except the specified range</description>
+ </valueHelp>
+ <constraint>
+ <validator name="ipv4-address"/>
+ <validator name="ipv4-prefix"/>
+ <validator name="ipv4-range"/>
+ <validator name="ipv4-address-exclude"/>
+ <validator name="ipv4-prefix-exclude"/>
+ <validator name="ipv4-range-exclude"/>
+ </constraint>
+ </properties>
+</leafNode>
diff --git a/interface-definitions/include/nat-outbound-interface.xml.i b/interface-definitions/include/nat-outbound-interface.xml.i
new file mode 100644
index 000000000..d562f7f03
--- /dev/null
+++ b/interface-definitions/include/nat-outbound-interface.xml.i
@@ -0,0 +1,8 @@
+<leafNode name="outbound-interface">
+ <properties>
+ <help>Outbound interface of NAT traffic</help>
+ <completionHelp>
+ <script>${vyos_completion_dir}/list_interfaces.py</script>
+ </completionHelp>
+ </properties>
+</leafNode>
diff --git a/interface-definitions/include/nat-port.xml.i b/interface-definitions/include/nat-port.xml.i
new file mode 100644
index 000000000..24803ae05
--- /dev/null
+++ b/interface-definitions/include/nat-port.xml.i
@@ -0,0 +1,17 @@
+<leafNode name="port">
+ <properties>
+ <help>Port number</help>
+ <valueHelp>
+ <format>1-65535</format>
+ <description>Numeric IP port</description>
+ </valueHelp>
+ <valueHelp>
+ <format>start-end</format>
+ <description>Numbered port range (e.g., 1001-1005)</description>
+ </valueHelp>
+ <valueHelp>
+ <format> </format>
+ <description>\n\nMultiple destination ports can be specified as a comma-separated list.\nThe whole list can also be negated using '!'.\nFor example: '!22,telnet,http,123,1001-1005'</description>
+ </valueHelp>
+ </properties>
+</leafNode>
diff --git a/interface-definitions/include/nat-rule.xml.i b/interface-definitions/include/nat-rule.xml.i
new file mode 100644
index 000000000..f62a08987
--- /dev/null
+++ b/interface-definitions/include/nat-rule.xml.i
@@ -0,0 +1,303 @@
+<tagNode name="rule">
+ <properties>
+ <help>Rule number for NAT</help>
+ <valueHelp>
+ <format>1-9999</format>
+ <description>Number for this NAT rule</description>
+ </valueHelp>
+ <constraint>
+ <validator name="numeric" argument="--range 1-9999"/>
+ </constraint>
+ <constraintErrorMessage>NAT rule number must be between 1 and 9999</constraintErrorMessage>
+ </properties>
+ <children>
+ <leafNode name="description">
+ <properties>
+ <help>Rule description</help>
+ </properties>
+ </leafNode>
+ <node name="destination">
+ <properties>
+ <help>NAT destination parameters</help>
+ </properties>
+ <children>
+ #include <include/nat-address.xml.i>
+ #include <include/nat-port.xml.i>
+ </children>
+ </node>
+ <leafNode name="disable">
+ <properties>
+ <help>Disable NAT rule</help>
+ <valueless/>
+ </properties>
+ </leafNode>
+ <leafNode name="exclude">
+ <properties>
+ <help>Exclude packets matching this rule from NAT</help>
+ <valueless/>
+ </properties>
+ </leafNode>
+ <leafNode name="log">
+ <properties>
+ <help>NAT rule logging</help>
+ <valueless/>
+ </properties>
+ </leafNode>
+ <leafNode name="protocol">
+ <properties>
+ <help>Protocol to NAT</help>
+ <completionHelp>
+ <list>all ip hopopt icmp igmp ggp ipencap st tcp egp igp pup udp tcp_udp hmp xns-idp rdp iso-tp4 dccp xtp ddp idpr-cmtp ipv6 ipv6-route ipv6-frag idrp rsvp gre esp ah skip ipv6-icmp ipv6-nonxt ipv6-opts rspf vmtp eigrp ospf ax.25 ipip etherip encap 99 pim ipcomp vrrp l2tp isis sctp fc mobility-header udplite mpls-in-ip manet hip shim6 wesp rohc</list>
+ </completionHelp>
+ <valueHelp>
+ <format>all</format>
+ <description>All IP protocols</description>
+ </valueHelp>
+ <valueHelp>
+ <format>ip</format>
+ <description>Internet Protocol, pseudo protocol number</description>
+ </valueHelp>
+ <valueHelp>
+ <format>hopopt</format>
+ <description>IPv6 Hop-by-Hop Option [RFC1883]</description>
+ </valueHelp>
+ <valueHelp>
+ <format>icmp</format>
+ <description>internet control message protocol</description>
+ </valueHelp>
+ <valueHelp>
+ <format>igmp</format>
+ <description>Internet Group Management</description>
+ </valueHelp>
+ <valueHelp>
+ <format>ggp</format>
+ <description>gateway-gateway protocol</description>
+ </valueHelp>
+ <valueHelp>
+ <format>ipencap</format>
+ <description>IP encapsulated in IP (officially IP)</description>
+ </valueHelp>
+ <valueHelp>
+ <format>st</format>
+ <description>ST datagram mode</description>
+ </valueHelp>
+ <valueHelp>
+ <format>tcp</format>
+ <description>transmission control protocol</description>
+ </valueHelp>
+ <valueHelp>
+ <format>egp</format>
+ <description>exterior gateway protocol</description>
+ </valueHelp>
+ <valueHelp>
+ <format>igp</format>
+ <description>any private interior gateway (Cisco)</description>
+ </valueHelp>
+ <valueHelp>
+ <format>pup</format>
+ <description>PARC universal packet protocol</description>
+ </valueHelp>
+ <valueHelp>
+ <format>udp</format>
+ <description>user datagram protocol</description>
+ </valueHelp>
+ <valueHelp>
+ <format>tcp_udp</format>
+ <description>Both TCP and UDP</description>
+ </valueHelp>
+ <valueHelp>
+ <format>hmp</format>
+ <description>host monitoring protocol</description>
+ </valueHelp>
+ <valueHelp>
+ <format>xns-idp</format>
+ <description>Xerox NS IDP</description>
+ </valueHelp>
+ <valueHelp>
+ <format>rdp</format>
+ <description>"reliable datagram" protocol</description>
+ </valueHelp>
+ <valueHelp>
+ <format>iso-tp4</format>
+ <description>ISO Transport Protocol class 4 [RFC905]</description>
+ </valueHelp>
+ <valueHelp>
+ <format>dccp</format>
+ <description>Datagram Congestion Control Prot. [RFC4340]</description>
+ </valueHelp>
+ <valueHelp>
+ <format>xtp</format>
+ <description>Xpress Transfer Protocol</description>
+ </valueHelp>
+ <valueHelp>
+ <format>ddp</format>
+ <description>Datagram Delivery Protocol</description>
+ </valueHelp>
+ <valueHelp>
+ <format>idpr-cmtp</format>
+ <description>IDPR Control Message Transport</description>
+ </valueHelp>
+ <valueHelp>
+ <format>Ipv6</format>
+ <description>Internet Protocol, version 6</description>
+ </valueHelp>
+ <valueHelp>
+ <format>ipv6-route</format>
+ <description>Routing Header for IPv6</description>
+ </valueHelp>
+ <valueHelp>
+ <format>ipv6-frag</format>
+ <description>Fragment Header for IPv6</description>
+ </valueHelp>
+ <valueHelp>
+ <format>idrp</format>
+ <description>Inter-Domain Routing Protocol</description>
+ </valueHelp>
+ <valueHelp>
+ <format>rsvp</format>
+ <description>Reservation Protocol</description>
+ </valueHelp>
+ <valueHelp>
+ <format>gre</format>
+ <description>General Routing Encapsulation</description>
+ </valueHelp>
+ <valueHelp>
+ <format>esp</format>
+ <description>Encap Security Payload [RFC2406]</description>
+ </valueHelp>
+ <valueHelp>
+ <format>ah</format>
+ <description>Authentication Header [RFC2402]</description>
+ </valueHelp>
+ <valueHelp>
+ <format>skip</format>
+ <description>SKIP</description>
+ </valueHelp>
+ <valueHelp>
+ <format>ipv6-icmp</format>
+ <description>ICMP for IPv6</description>
+ </valueHelp>
+ <valueHelp>
+ <format>ipv6-nonxt</format>
+ <description>No Next Header for IPv6</description>
+ </valueHelp>
+ <valueHelp>
+ <format>ipv6-opts</format>
+ <description>Destination Options for IPv6</description>
+ </valueHelp>
+ <valueHelp>
+ <format>rspf</format>
+ <description>Radio Shortest Path First (officially CPHB)</description>
+ </valueHelp>
+ <valueHelp>
+ <format>vmtp</format>
+ <description>Versatile Message Transport</description>
+ </valueHelp>
+ <valueHelp>
+ <format>eigrp</format>
+ <description>Enhanced Interior Routing Protocol (Cisco)</description>
+ </valueHelp>
+ <valueHelp>
+ <format>ospf</format>
+ <description>Open Shortest Path First IGP</description>
+ </valueHelp>
+ <valueHelp>
+ <format>ax.25</format>
+ <description>AX.25 frames</description>
+ </valueHelp>
+ <valueHelp>
+ <format>ipip</format>
+ <description>IP-within-IP Encapsulation Protocol</description>
+ </valueHelp>
+ <valueHelp>
+ <format>etherip</format>
+ <description>Ethernet-within-IP Encapsulation [RFC3378]</description>
+ </valueHelp>
+ <valueHelp>
+ <format>encap</format>
+ <description>Yet Another IP encapsulation [RFC1241]</description>
+ </valueHelp>
+ <valueHelp>
+ <format>99</format>
+ <description>Any private encryption scheme</description>
+ </valueHelp>
+ <valueHelp>
+ <format>pim</format>
+ <description>Protocol Independent Multicast</description>
+ </valueHelp>
+ <valueHelp>
+ <format>ipcomp</format>
+ <description>IP Payload Compression Protocol</description>
+ </valueHelp>
+ <valueHelp>
+ <format>vrrp</format>
+ <description>Virtual Router Redundancy Protocol [RFC5798]</description>
+ </valueHelp>
+ <valueHelp>
+ <format>l2tp</format>
+ <description>Layer Two Tunneling Protocol [RFC2661]</description>
+ </valueHelp>
+ <valueHelp>
+ <format>isis</format>
+ <description>IS-IS over IPv4</description>
+ </valueHelp>
+ <valueHelp>
+ <format>sctp</format>
+ <description>Stream Control Transmission Protocol</description>
+ </valueHelp>
+ <valueHelp>
+ <format>fc</format>
+ <description>Fibre Channel</description>
+ </valueHelp>
+ <valueHelp>
+ <format>mobility-header</format>
+ <description>Mobility Support for IPv6 [RFC3775]</description>
+ </valueHelp>
+ <valueHelp>
+ <format>udplite</format>
+ <description>UDP-Lite [RFC3828]</description>
+ </valueHelp>
+ <valueHelp>
+ <format>mpls-in-ip</format>
+ <description>MPLS-in-IP [RFC4023]</description>
+ </valueHelp>
+ <valueHelp>
+ <format>manet</format>
+ <description>MANET Protocols [RFC5498]</description>
+ </valueHelp>
+ <valueHelp>
+ <format>hip</format>
+ <description>Host Identity Protocol</description>
+ </valueHelp>
+ <valueHelp>
+ <format>shim6</format>
+ <description>Shim6 Protocol</description>
+ </valueHelp>
+ <valueHelp>
+ <format>wesp</format>
+ <description>Wrapped Encapsulating Security Payload</description>
+ </valueHelp>
+ <valueHelp>
+ <format>rohc</format>
+ <description>Robust Header Compression</description>
+ </valueHelp>
+ <valueHelp>
+ <format>0-255</format>
+ <description>IP protocol number</description>
+ </valueHelp>
+ <constraint>
+ <validator name="ip-protocol"/>
+ </constraint>
+ </properties>
+ </leafNode>
+ <node name="source">
+ <properties>
+ <help>NAT source parameters</help>
+ </properties>
+ <children>
+ #include <include/nat-address.xml.i>
+ #include <include/nat-port.xml.i>
+ </children>
+ </node>
+ </children>
+</tagNode>
diff --git a/interface-definitions/include/nat-translation-port.xml.i b/interface-definitions/include/nat-translation-port.xml.i
new file mode 100644
index 000000000..93de471e3
--- /dev/null
+++ b/interface-definitions/include/nat-translation-port.xml.i
@@ -0,0 +1,13 @@
+<leafNode name="port">
+ <properties>
+ <help>Port number</help>
+ <valueHelp>
+ <format>1-65535</format>
+ <description>Numeric IP port</description>
+ </valueHelp>
+ <valueHelp>
+ <format>&lt;start&gt;-&lt;end&gt;</format>
+ <description>Numbered port range (e.g., 1001-1005)</description>
+ </valueHelp>
+ </properties>
+</leafNode>
diff --git a/interface-definitions/include/vif-s.xml.i b/interface-definitions/include/vif-s.xml.i
index ab2dcd955..a6d7c81ce 100644
--- a/interface-definitions/include/vif-s.xml.i
+++ b/interface-definitions/include/vif-s.xml.i
@@ -9,7 +9,8 @@
<children>
#include <include/address-ipv4-ipv6-dhcp.xml.i>
#include <include/interface-description.xml.i>
- #include <include/dhcp-dhcpv6-options.xml.i>
+ #include <include/dhcp-options.xml.i>
+ #include <include/dhcpv6-options.xml.i>
#include <include/interface-disable-link-detect.xml.i>
#include <include/interface-disable.xml.i>
<leafNode name="ethertype">
@@ -53,7 +54,8 @@
<children>
#include <include/address-ipv4-ipv6-dhcp.xml.i>
#include <include/interface-description.xml.i>
- #include <include/dhcp-dhcpv6-options.xml.i>
+ #include <include/dhcp-options.xml.i>
+ #include <include/dhcpv6-options.xml.i>
#include <include/interface-disable-link-detect.xml.i>
#include <include/interface-disable.xml.i>
#include <include/interface-mac.xml.i>
diff --git a/interface-definitions/include/vif.xml.i b/interface-definitions/include/vif.xml.i
index 819534dc1..5a4e52122 100644
--- a/interface-definitions/include/vif.xml.i
+++ b/interface-definitions/include/vif.xml.i
@@ -13,7 +13,8 @@
<children>
#include <include/address-ipv4-ipv6-dhcp.xml.i>
#include <include/interface-description.xml.i>
- #include <include/dhcp-dhcpv6-options.xml.i>
+ #include <include/dhcp-options.xml.i>
+ #include <include/dhcpv6-options.xml.i>
#include <include/interface-disable-link-detect.xml.i>
#include <include/interface-disable.xml.i>
#include <include/interface-vrf.xml.i>
diff --git a/interface-definitions/interfaces-bonding.xml.in b/interface-definitions/interfaces-bonding.xml.in
index 07a6abd30..ddd52979b 100644
--- a/interface-definitions/interfaces-bonding.xml.in
+++ b/interface-definitions/interfaces-bonding.xml.in
@@ -50,7 +50,8 @@
</children>
</node>
#include <include/interface-description.xml.i>
- #include <include/dhcp-dhcpv6-options.xml.i>
+ #include <include/dhcp-options.xml.i>
+ #include <include/dhcpv6-options.xml.i>
#include <include/interface-disable-link-detect.xml.i>
#include <include/interface-disable.xml.i>
#include <include/interface-vrf.xml.i>
diff --git a/interface-definitions/interfaces-bridge.xml.in b/interface-definitions/interfaces-bridge.xml.in
index 818bc9c0e..6b610e623 100644
--- a/interface-definitions/interfaces-bridge.xml.in
+++ b/interface-definitions/interfaces-bridge.xml.in
@@ -34,7 +34,8 @@
</properties>
</leafNode>
#include <include/interface-description.xml.i>
- #include <include/dhcp-dhcpv6-options.xml.i>
+ #include <include/dhcp-options.xml.i>
+ #include <include/dhcpv6-options.xml.i>
#include <include/interface-disable-link-detect.xml.i>
#include <include/interface-disable.xml.i>
#include <include/interface-vrf.xml.i>
diff --git a/interface-definitions/interfaces-ethernet.xml.in b/interface-definitions/interfaces-ethernet.xml.in
index 89669f966..1e32a15f8 100644
--- a/interface-definitions/interfaces-ethernet.xml.in
+++ b/interface-definitions/interfaces-ethernet.xml.in
@@ -22,7 +22,8 @@
<children>
#include <include/address-ipv4-ipv6-dhcp.xml.i>
#include <include/interface-description.xml.i>
- #include <include/dhcp-dhcpv6-options.xml.i>
+ #include <include/dhcp-options.xml.i>
+ #include <include/dhcpv6-options.xml.i>
<leafNode name="disable-flow-control">
<properties>
<help>Disable Ethernet flow control (pause frames)</help>
diff --git a/interface-definitions/interfaces-pppoe.xml.in b/interface-definitions/interfaces-pppoe.xml.in
index d69e0b42c..0092f9ce5 100644
--- a/interface-definitions/interfaces-pppoe.xml.in
+++ b/interface-definitions/interfaces-pppoe.xml.in
@@ -72,6 +72,7 @@
</valueHelp>
</properties>
</leafNode>
+ #include <include/dhcpv6-options.xml.i>
#include <include/interface-description.xml.i>
#include <include/interface-disable.xml.i>
#include <include/interface-vrf.xml.i>
diff --git a/interface-definitions/interfaces-pseudo-ethernet.xml.in b/interface-definitions/interfaces-pseudo-ethernet.xml.in
index ea267cf81..61fd6c9fd 100644
--- a/interface-definitions/interfaces-pseudo-ethernet.xml.in
+++ b/interface-definitions/interfaces-pseudo-ethernet.xml.in
@@ -18,7 +18,8 @@
<children>
#include <include/address-ipv4-ipv6-dhcp.xml.i>
#include <include/interface-description.xml.i>
- #include <include/dhcp-dhcpv6-options.xml.i>
+ #include <include/dhcp-options.xml.i>
+ #include <include/dhcpv6-options.xml.i>
#include <include/interface-disable-link-detect.xml.i>
#include <include/interface-disable.xml.i>
#include <include/interface-vrf.xml.i>
diff --git a/interface-definitions/interfaces-wireguard.xml.in b/interface-definitions/interfaces-wireguard.xml.in
index 9db608afb..5894f159d 100644
--- a/interface-definitions/interfaces-wireguard.xml.in
+++ b/interface-definitions/interfaces-wireguard.xml.in
@@ -89,10 +89,14 @@
<help>IP address of tunnel remote end</help>
<valueHelp>
<format>ipv4</format>
- <description>IP address to listen for incoming connections</description>
+ <description>IPv4 address to listen for incoming connections</description>
+ </valueHelp>
+ <valueHelp>
+ <format>ipv6</format>
+ <description>IPv6 address to listen for incoming connections</description>
</valueHelp>
<constraint>
- <validator name="ipv4-address"/>
+ <validator name="ip-address"/>
</constraint>
</properties>
</leafNode>
diff --git a/interface-definitions/interfaces-wireless.xml.in b/interface-definitions/interfaces-wireless.xml.in
index 3edcbb8ff..d21074b6a 100644
--- a/interface-definitions/interfaces-wireless.xml.in
+++ b/interface-definitions/interfaces-wireless.xml.in
@@ -446,7 +446,8 @@
</properties>
</leafNode>
#include <include/interface-description.xml.i>
- #include <include/dhcp-dhcpv6-options.xml.i>
+ #include <include/dhcp-options.xml.i>
+ #include <include/dhcpv6-options.xml.i>
<leafNode name="disable-broadcast-ssid">
<properties>
<help>Disable broadcast of SSID from access-point</help>
@@ -463,10 +464,13 @@
</leafNode>
<node name="ip">
<children>
+ #include <include/interface-arp-cache-timeout.xml.i>
#include <include/interface-disable-arp-filter.xml.i>
#include <include/interface-enable-arp-accept.xml.i>
#include <include/interface-enable-arp-announce.xml.i>
#include <include/interface-enable-arp-ignore.xml.i>
+ #include <include/interface-enable-proxy-arp.xml.i>
+ #include <include/interface-proxy-arp-pvlan.xml.i>
</children>
</node>
<node name="ipv6">
@@ -761,6 +765,7 @@
</properties>
</leafNode>
#include <include/vif.xml.i>
+ #include <include/vif-s.xml.i>
</children>
</tagNode>
</children>
diff --git a/interface-definitions/nat.xml.in b/interface-definitions/nat.xml.in
new file mode 100644
index 000000000..a0b3b815b
--- /dev/null
+++ b/interface-definitions/nat.xml.in
@@ -0,0 +1,180 @@
+<?xml version="1.0"?>
+<interfaceDefinition>
+ <node name="nat" owner="sudo ${vyos_conf_scripts_dir}/nat.py">
+ <properties>
+ <help>Network Address Translation (NAT) parameters</help>
+ <priority>220</priority>
+ </properties>
+ <children>
+ <node name="destination">
+ <properties>
+ <help>Destination NAT settings</help>
+ </properties>
+ <children>
+ #include <include/nat-rule.xml.i>
+ <tagNode name="rule">
+ <children>
+ <leafNode name="inbound-interface">
+ <properties>
+ <help>Inbound interface of NAT traffic</help>
+ <completionHelp>
+ <script>${vyos_completion_dir}/list_interfaces.py</script>
+ </completionHelp>
+ </properties>
+ </leafNode>
+ <node name="translation">
+ <properties>
+ <help>Inside NAT IP (destination NAT only)</help>
+ </properties>
+ <children>
+ <leafNode name="address">
+ <properties>
+ <help>IP address, subnet, or range</help>
+ <valueHelp>
+ <format>ipv4</format>
+ <description>IPv4 address to match</description>
+ </valueHelp>
+ <valueHelp>
+ <format>ipv4net</format>
+ <description>IPv4 prefix to match</description>
+ </valueHelp>
+ <valueHelp>
+ <format>ipv4range</format>
+ <description>IPv4 address range to match</description>
+ </valueHelp>
+ <!-- TODO: add general iptables constraint script -->
+ </properties>
+ </leafNode>
+ #include <include/nat-translation-port.xml.i>
+ </children>
+ </node>
+ </children>
+ </tagNode>
+ </children>
+ </node>
+ <node name="nptv6">
+ <properties>
+ <help>IPv6-to-IPv6 Network Prefix Translation Settings</help>
+ </properties>
+ <children>
+ <tagNode name="rule">
+ <properties>
+ <help>NPTv6 rule number</help>
+ <valueHelp>
+ <format>1-9999</format>
+ <description>Number for this rule</description>
+ </valueHelp>
+ <constraint>
+ <validator name="numeric" argument="--range 1-9999"/>
+ </constraint>
+ <constraintErrorMessage>NAT rule number must be between 1 and 9999</constraintErrorMessage>
+ </properties>
+ <children>
+ <leafNode name="description">
+ <properties>
+ <help>Rule description</help>
+ </properties>
+ </leafNode>
+ <leafNode name="disable">
+ <properties>
+ <help>Disable NAT rule</help>
+ <valueless/>
+ </properties>
+ </leafNode>
+ #include <include/nat-outbound-interface.xml.i>
+ <node name="source">
+ <properties>
+ <help>IPv6 source prefix options</help>
+ </properties>
+ <children>
+ <leafNode name="prefix">
+ <properties>
+ <help>IPv6 prefix to be translated</help>
+ <valueHelp>
+ <format>ipv6net</format>
+ <description>IPv6 prefix</description>
+ </valueHelp>
+ <constraint>
+ <validator name="ipv6-prefix"/>
+ </constraint>
+ </properties>
+ </leafNode>
+ </children>
+ </node>
+ <node name="translation">
+ <properties>
+ <help>Translated IPv6 prefix options</help>
+ </properties>
+ <children>
+ <leafNode name="prefix">
+ <properties>
+ <help>IPv6 prefix to translate to</help>
+ <valueHelp>
+ <format>ipv6net</format>
+ <description>IPv6 prefix</description>
+ </valueHelp>
+ <constraint>
+ <validator name="ipv6-prefix"/>
+ </constraint>
+ </properties>
+ </leafNode>
+ </children>
+ </node>
+ </children>
+ </tagNode>
+ </children>
+ </node>
+ <node name="source">
+ <properties>
+ <help>Source NAT settings</help>
+ </properties>
+ <children>
+ #include <include/nat-rule.xml.i>
+ <tagNode name="rule">
+ <children>
+ #include <include/nat-outbound-interface.xml.i>
+ <node name="translation">
+ <properties>
+ <help>Outside NAT IP (source NAT only)</help>
+ </properties>
+ <children>
+ <leafNode name="address">
+ <properties>
+ <help>IP address, subnet, or range</help>
+ <completionHelp>
+ <list>masquerade</list>
+ </completionHelp>
+ <valueHelp>
+ <format>ipv4</format>
+ <description>IPv4 address to match</description>
+ </valueHelp>
+ <valueHelp>
+ <format>ipv4net</format>
+ <description>IPv4 prefix to match</description>
+ </valueHelp>
+ <valueHelp>
+ <format>ipv4range</format>
+ <description>IPv4 address range to match</description>
+ </valueHelp>
+ <valueHelp>
+ <format>masquerade</format>
+ <description>NAT to the primary address of outbound-interface</description>
+ </valueHelp>
+ <constraint>
+ <validator name="ipv4-prefix"/>
+ <validator name="ipv4-address"/>
+ <validator name="ipv4-range"/>
+ <regex>(masquerade)</regex>
+ </constraint>
+ </properties>
+ </leafNode>
+ #include <include/nat-translation-port.xml.i>
+ </children>
+ </node>
+ </children>
+ </tagNode>
+ </children>
+ </node>
+ </children>
+ </node>
+</interfaceDefinition>
diff --git a/interface-definitions/salt-minion.xml.in b/interface-definitions/salt-minion.xml.in
index 9aa60249a..97f882a6a 100644
--- a/interface-definitions/salt-minion.xml.in
+++ b/interface-definitions/salt-minion.xml.in
@@ -1,5 +1,4 @@
<?xml version="1.0"?>
-<!--Salt-minion configuration -->
<interfaceDefinition>
<node name="service">
<children>
@@ -9,73 +8,56 @@
<priority>500</priority>
</properties>
<children>
- <leafNode name="hash_type">
+ <leafNode name="hash">
<properties>
- <help>The hash_type is the hash to use when discovering the hash of a file on the master server.</help>
+ <help>Hash used when discovering file on master server (default: sha256)</help>
+ <completionHelp>
+ <list>md5 sha1 sha224 sha256 sha384 sha512</list>
+ </completionHelp>
+ <constraint>
+ <regex>(md5|sha1|sha224|sha256|sha384|sha512)</regex>
+ </constraint>
</properties>
</leafNode>
- <leafNode name="log_file">
- <properties>
- <help>The location of the minion log file.</help>
- </properties>
- </leafNode>
- <leafNode name="log_level">
+ <leafNode name="master">
<properties>
- <help>Log level</help>
- <valueHelp>
- <format>garbage</format>
- <description>log garbage info</description>
- </valueHelp>
- <valueHelp>
- <format>trace</format>
- <description>log trace info</description>
- </valueHelp>
- <valueHelp>
- <format>debug</format>
- <description>log debug info</description>
- </valueHelp>
- <valueHelp>
- <format>info</format>
- <description>log info</description>
- </valueHelp>
- <valueHelp>
- <format>warning</format>
- <description>log warning info</description>
- </valueHelp>
+ <help>The hostname or IP address of the master.</help>
<valueHelp>
- <format>error</format>
- <description>log error info</description>
+ <format>ipv4</format>
+ <description>Remote syslog server IPv4 address</description>
</valueHelp>
<valueHelp>
- <format>critical</format>
- <description>log critical info</description>
+ <format>hostname</format>
+ <description>Remote syslog server FQDN</description>
</valueHelp>
- </properties>
- </leafNode>
- <leafNode name="master">
- <properties>
- <help>The hostname or IP address of the master.</help>
+ <constraint>
+ <validator name="ip-address"/>
+ <validator name="fqdn"/>
+ </constraint>
+ <constraintErrorMessage>Invalid FQDN or IP address</constraintErrorMessage>
<multi/>
</properties>
</leafNode>
<leafNode name="id">
<properties>
- <help>Explicitly declare the id for this minion to use.</help>
- </properties>
- </leafNode>
- <leafNode name="user">
- <properties>
- <help>The user to run the Salt processes.</help>
+ <help>Explicitly declare ID for this minion to use (default: hostname)</help>
</properties>
</leafNode>
- <leafNode name="mine_interval">
+ <leafNode name="interval">
<properties>
- <help>The number of minutes between mine updates.</help>
+ <help>Interval in minutes between updates (default: 60)</help>
+ <valueHelp>
+ <format>&lt;1-1440&gt;</format>
+ <description>Update interval in minutes</description>
+ </valueHelp>
+ <constraint>
+ <validator name="numeric" argument="--range 1-1440"/>
+ </constraint>
</properties>
</leafNode>
<leafNode name="master-key">
<properties>
- <help>Enables verification of the master-public-signature returned by the master in auth-replies.</help>
+ <help>URL with signature of master for auth reply verification</help>
</properties>
</leafNode>
</children>
diff --git a/interface-definitions/service_pppoe-server.xml.in b/interface-definitions/service_pppoe-server.xml.in
index 6b09b3db4..c7ba2617a 100644
--- a/interface-definitions/service_pppoe-server.xml.in
+++ b/interface-definitions/service_pppoe-server.xml.in
@@ -137,6 +137,34 @@
</node>
</children>
</node>
+ <leafNode name="protocols">
+ <properties>
+ <help>Authentication protocol</help>
+ <valueHelp>
+ <format>pap</format>
+ <description>Allow PAP authentication [Password Authentication Protocol]</description>
+ </valueHelp>
+ <valueHelp>
+ <format>chap</format>
+ <description>Allow CHAP authentication [Challenge Handshake Authentication Protocol]</description>
+ </valueHelp>
+ <valueHelp>
+ <format>mschap</format>
+ <description>Allow MS-CHAP authentication [Microsoft Challenge Handshake Authentication Protocol, Version 1]</description>
+ </valueHelp>
+ <valueHelp>
+ <format>mschap-v2</format>
+ <description>Allow MS-CHAPv2 authentication [Microsoft Challenge Handshake Authentication Protocol, Version 2]</description>
+ </valueHelp>
+ <constraint>
+ <regex>(pap|chap|mschap|mschap-v2)</regex>
+ </constraint>
+ <completionHelp>
+ <list>pap chap mschap mschap-v2</list>
+ </completionHelp>
+ <multi />
+ </properties>
+ </leafNode>
</children>
</node>
<node name="client-ip-pool">
diff --git a/interface-definitions/system-login.xml.in b/interface-definitions/system-login.xml.in
index 2499a192c..053b6babd 100644
--- a/interface-definitions/system-login.xml.in
+++ b/interface-definitions/system-login.xml.in
@@ -130,6 +130,7 @@
</leafNode>
</children>
</tagNode>
+ #include <include/interface-vrf.xml.i>
</children>
</node>
</children>
diff --git a/interface-definitions/vpn_sstp.xml.in b/interface-definitions/vpn_sstp.xml.in
index 7e4471015..f0c93b882 100644
--- a/interface-definitions/vpn_sstp.xml.in
+++ b/interface-definitions/vpn_sstp.xml.in
@@ -207,19 +207,8 @@
</leafNode>
</children>
</node>
- <leafNode name="name-server">
- <properties>
- <help>DNS servers propagated to clients</help>
- <valueHelp>
- <format>ipv4</format>
- <description>IPv4 address</description>
- </valueHelp>
- <constraint>
- <validator name="ipv4-address"/>
- </constraint>
- <multi/>
- </properties>
- </leafNode>
+ #include <include/accel-client-ipv6-pool.xml.in>
+ #include <include/accel-name-server.xml.in>
#include <include/interface-mtu-68-1500.xml.i>
</children>
</node>
diff --git a/op-mode-definitions/nat.xml b/op-mode-definitions/nat.xml
new file mode 100644
index 000000000..ffaa2cba3
--- /dev/null
+++ b/op-mode-definitions/nat.xml
@@ -0,0 +1,98 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interfaceDefinition>
+ <node name="show">
+ <children>
+ <node name="nat">
+ <properties>
+ <help>Show Network Address Translation (NAT) information</help>
+ </properties>
+ <children>
+ <node name="source">
+ <properties>
+ <help>Show source Network Address Translation (NAT) information</help>
+ </properties>
+ <children>
+ <node name="rules">
+ <properties>
+ <help>Show configured source NAT rules</help>
+ </properties>
+ <command>echo To be migrated to Python - https://phabricator.vyos.net/T2459</command>
+ </node>
+ <node name="statistics">
+ <properties>
+ <help>Show statistics for configured source NAT rules</help>
+ </properties>
+ <command>${vyos_op_scripts_dir}/show_nat_statistics.py --source</command>
+ </node>
+ <node name="translations">
+ <properties>
+ <help>Show active source NAT translations</help>
+ </properties>
+ <children>
+ <tagNode name="address">
+ <properties>
+ <help>Show active source NAT translations for an IP address</help>
+ <completionHelp>
+ <list>&lt;x.x.x.x&gt;</list>
+ </completionHelp>
+ </properties>
+ <command>${vyos_op_scripts_dir}/to_be_migrated/vyatta-nat-translations.pl --type=source --verbose --ipaddr="$6"</command>
+ </tagNode>
+ <node name="detail">
+ <properties>
+ <help>Show active source NAT translations detail</help>
+ </properties>
+ <command>${vyos_op_scripts_dir}/to_be_migrated/vyatta-nat-translations.pl --type=source --verbose</command>
+ </node>
+ </children>
+ <command>${vyos_op_scripts_dir}/to_be_migrated/vyatta-nat-translations.pl --type=source</command>
+ </node>
+ </children>
+ </node>
+ <node name="destination">
+ <properties>
+ <help>Show destination Network Address Translation (NAT) information</help>
+ </properties>
+ <children>
+ <node name="rules">
+ <properties>
+ <help>Show configured destination NAT rules</help>
+ </properties>
+ <command>echo To be migrated to Python - https://phabricator.vyos.net/T2459</command>
+ </node>
+ <node name="statistics">
+ <properties>
+ <help>Show statistics for configured destination NAT rules</help>
+ </properties>
+ <command>${vyos_op_scripts_dir}/show_nat_statistics.py --destination</command>
+ </node>
+ <node name="translations">
+ <properties>
+ <help>Show active destination NAT translations</help>
+ </properties>
+ <children>
+ <tagNode name="address">
+ <properties>
+ <help>Show active NAT destination translations for an IP address</help>
+ <completionHelp>
+ <list>&lt;x.x.x.x&gt;</list>
+ </completionHelp>
+ </properties>
+ <command>${vyos_op_scripts_dir}/to_be_migrated/vyatta-nat-translations.pl --type=destination --verbose --ipaddr="$6"</command>
+ </tagNode>
+ <node name="detail">
+ <properties>
+ <help>Show active destination NAT translations detail</help>
+ </properties>
+ <command>${vyos_op_scripts_dir}/to_be_migrated/vyatta-nat-translations.pl --type=destination --verbose</command>
+ </node>
+ </children>
+ <command>${vyos_op_scripts_dir}/to_be_migrated/vyatta-nat-translations.pl --type=destination</command>
+ </node>
+ </children>
+ </node>
+ </children>
+ </node>
+ </children>
+ </node>
+</interfaceDefinition>
diff --git a/op-mode-definitions/show-interfaces-pppoe.xml b/op-mode-definitions/show-interfaces-pppoe.xml
index e68d05da9..211ad9808 100644
--- a/op-mode-definitions/show-interfaces-pppoe.xml
+++ b/op-mode-definitions/show-interfaces-pppoe.xml
@@ -11,7 +11,7 @@
<script>${vyos_completion_dir}/list_pppoe_peers.sh</script>
</completionHelp>
</properties>
- <command>${vyos_op_scripts_dir}/show_interfaces.py pppoe --intf="$4"</command>
+ <command>${vyos_op_scripts_dir}/show_interfaces.py --intf="$4"</command>
<children>
<node name="log">
<properties>
diff --git a/op-mode-definitions/traceroute.xml b/op-mode-definitions/traceroute.xml
index d623fe103..1aea8eef6 100644
--- a/op-mode-definitions/traceroute.xml
+++ b/op-mode-definitions/traceroute.xml
@@ -1,71 +1,70 @@
<?xml version="1.0"?>
<interfaceDefinition>
- <node name="traceroute">
+ <tagNode name="traceroute">
<properties>
<help>Track network path to node</help>
+ <completionHelp>
+ <list>&lt;hostname&gt; &lt;x.x.x.x&gt; &lt;h:h:h:h:h:h:h:h&gt;</list>
+ </completionHelp>
</properties>
+ <command>/usr/bin/traceroute "$2"</command>
+ </tagNode>
+ <node name="traceroute">
<children>
- <tagNode name="">
- <properties>
- <help>Track network path to specified node</help>
- <completionHelp>
- <list>&lt;hostname&gt; &lt;x.x.x.x&gt; &lt;h:h:h:h:h:h:h:h&gt;</list>
- </completionHelp>
- </properties>
- <command>/usr/bin/traceroute $2</command>
- </tagNode>
<tagNode name="ipv4">
<properties>
- <help>Track network path to &lt;hostname|IPv4 address&gt;</help>
+ <help>Explicitly use IPv4 when tracing the path</help>
<completionHelp>
<list>&lt;hostname&gt; &lt;x.x.x.x&gt;</list>
</completionHelp>
</properties>
- <command>/usr/bin/traceroute -4 $3</command>
+ <command>/usr/bin/traceroute -4 "$3"</command>
</tagNode>
<tagNode name="ipv6">
<properties>
- <help>Track network path to &lt;hostname|IPv6 address&gt;</help>
+ <help>Explicitly use IPv6 when tracing the path</help>
<completionHelp>
<list>&lt;hostname&gt; &lt;h:h:h:h:h:h:h:h&gt;</list>
</completionHelp>
</properties>
- <command>/usr/bin/traceroute -6 $3</command>
+ <command>/usr/bin/traceroute -6 "$3"</command>
</tagNode>
<tagNode name="vrf">
<properties>
- <help>Track network path to specified node via given VRF instance</help>
+ <help>Track network path to specified node via given VRF</help>
<completionHelp>
<path>vrf name</path>
</completionHelp>
</properties>
<children>
+ <!-- we need an empty tagNode to pass in a plain fqdn/ip address and
+ let traceroute decide how to handle this parameter -->
<tagNode name="">
<properties>
- <help>Track network path to specified node</help>
+ <help>Track network path to specified node via given VRF</help>
<completionHelp>
<list>&lt;hostname&gt; &lt;x.x.x.x&gt; &lt;h:h:h:h:h:h:h:h&gt;</list>
</completionHelp>
</properties>
- <command>sudo ip vrf exec "$3" traceroute "$4"</command>
+ <command>sudo /usr/sbin/ip vrf exec "$3" /usr/bin/traceroute "$4"</command>
</tagNode>
<tagNode name="ipv4">
<properties>
- <help>Track network path to &lt;hostname|IPv4 address&gt;</help>
+ <help>Explicitly use IPv4 when tracing the path via given VRF</help>
<completionHelp>
<list>&lt;hostname&gt; &lt;x.x.x.x&gt;</list>
</completionHelp>
</properties>
- <command>sudo ip vrf exec "$3" traceroute -4 "$5"</command>
+ <command>sudo /usr/sbin/ip vrf exec "$3" /usr/bin/traceroute -4 "$5"</command>
</tagNode>
<tagNode name="ipv6">
<properties>
- <help>Track network path to &lt;hostname|IPv6 address&gt;</help>
+ <help>Explicitly use IPv6 when tracing the path via given VRF</help>
<completionHelp>
<list>&lt;hostname&gt; &lt;h:h:h:h:h:h:h:h&gt;</list>
</completionHelp>
</properties>
- <command>sudo ip vrf exec "$3" traceroute -6 "$5"</command>
+ <command>sudo /usr/sbin/ip vrf exec "$3" /usr/bin/traceroute -6 "$5"</command>
</tagNode>
</children>
</tagNode>
@@ -75,13 +74,38 @@
<children>
<tagNode name="traceroute">
<properties>
- <help>Monitor the path to a destination in realtime</help>
+ <help>Monitor path to destination in realtime</help>
<completionHelp>
<list>&lt;hostname&gt; &lt;x.x.x.x&gt; &lt;h:h:h:h:h:h:h:h&gt;</list>
</completionHelp>
</properties>
- <command>/usr/bin/mtr $3</command>
+ <command>/usr/bin/mtr "$3"</command>
</tagNode>
+ <node name="traceroute">
+ <children>
+ <tagNode name="vrf">
+ <properties>
+ <help>Monitor path to destination in realtime via given VRF</help>
+ <completionHelp>
+ <path>vrf name</path>
+ </completionHelp>
+ </properties>
+ <children>
+ <!-- we need an empty tagNode to pass in a plain fqdn/ip address and
+ let traceroute decide how to handle this parameter -->
+ <tagNode name="">
+ <properties>
+ <help>Track network path to specified node via given VRF</help>
+ <completionHelp>
+ <list>&lt;hostname&gt; &lt;x.x.x.x&gt; &lt;h:h:h:h:h:h:h:h&gt;</list>
+ </completionHelp>
+ </properties>
+ <command>sudo /usr/sbin/ip vrf exec "$4" /usr/bin/mtr "$5"</command>
+ </tagNode>
+ </children>
+ </tagNode>
+ </children>
+ </node>
</children>
</node>
</interfaceDefinition>
diff --git a/python/vyos/airbag.py b/python/vyos/airbag.py
index 6698aa404..b7838d8a2 100644
--- a/python/vyos/airbag.py
+++ b/python/vyos/airbag.py
@@ -26,6 +26,17 @@ from vyos.version import get_full_version_data
DISABLE = False
+_noteworthy = []
+
+def noteworthy(msg):
+ """
+ noteworthy can be use to take note things which we may not want to
+ report to the user may but be worth including in bug report
+ if something goes wrong later on
+ """
+ _noteworthy.append(msg)
+
+
# emulate a file object
class _IO(object):
def __init__(self, std, log):
@@ -58,11 +69,16 @@ def bug_report(dtype, value, trace):
information = get_full_version_data()
trace = '\n'.join(format_exception(dtype, value, trace)).replace('\n\n','\n')
+ note = ''
+ if _noteworthy:
+ note = 'noteworthy:\n'
+ note += '\n'.join(_noteworthy)
information.update({
'date': datetime.now().strftime('%Y-%m-%d %H:%M:%S'),
'trace': trace,
'instructions': COMMUNITY if 'rolling' in get_version() else SUPPORTED,
+ 'note': note,
})
sys.stdout.write(INTRO.format(**information))
@@ -145,6 +161,7 @@ Hardware S/N: {hardware_serial}
Hardware UUID: {hardware_uuid}
{trace}
+{note}
"""
INTRO = """\
diff --git a/python/vyos/component_versions.py b/python/vyos/component_versions.py
index ec54a1576..90b458aae 100644
--- a/python/vyos/component_versions.py
+++ b/python/vyos/component_versions.py
@@ -51,7 +51,7 @@ def get_component_versions_from_file(config_file_name='/opt/vyatta/etc/config/co
"""
f = open(config_file_name, 'r')
for line_in_config in f:
- component_version = return_version(line_in_config)
+ component_version = get_component_version(line_in_config)
if component_version:
return component_version
raise ValueError("no config string in file:", config_file_name)
diff --git a/python/vyos/config.py b/python/vyos/config.py
index 75055a603..54cb518c3 100644
--- a/python/vyos/config.py
+++ b/python/vyos/config.py
@@ -97,12 +97,12 @@ class Config(object):
self.__session_env = None
# Running config can be obtained either from op or conf mode, it always succeeds
- # (if config system is initialized at all).
+ # once the config system is initialized during boot;
+ # before initialization, set to empty string
if os.path.isfile('/tmp/vyos-config-status'):
running_config_text = self._run([self._cli_shell_api, '--show-active-only', '--show-show-defaults', '--show-ignore-edit', 'showConfig'])
else:
- with open('/opt/vyatta/etc/config/config.boot') as f:
- running_config_text = f.read()
+ running_config_text = ''
# Session config ("active") only exists in conf mode.
# In op mode, we'll just use the same running config for both active and session configs.
@@ -112,7 +112,10 @@ class Config(object):
session_config_text = running_config_text
self._session_config = vyos.configtree.ConfigTree(session_config_text)
- self._running_config = vyos.configtree.ConfigTree(running_config_text)
+ if running_config_text:
+ self._running_config = vyos.configtree.ConfigTree(running_config_text)
+ else:
+ self._running_config = None
def _make_command(self, op, path):
args = path.split()
@@ -155,7 +158,7 @@ class Config(object):
``exists("system name-server"`` without ``set_level``.
Args:
- path (str): relative config path
+ path (str|list): relative config path
"""
# Make sure there's always a space between default path (level)
# and path supplied as method argument
@@ -166,7 +169,7 @@ class Config(object):
else:
self._level = []
elif isinstance(path, list):
- self._level = path
+ self._level = path.copy()
else:
raise TypeError("Level path must be either a whitespace-separated string or a list")
@@ -177,7 +180,7 @@ class Config(object):
Returns:
str: current edit level
"""
- return(self._level)
+ return(self._level.copy())
def exists(self, path):
"""
@@ -278,8 +281,12 @@ class Config(object):
Returns: a dict representation of the config
"""
res = self.show_config(self._make_path(path), effective=effective)
- config_tree = vyos.configtree.ConfigTree(res)
- config_dict = json.loads(config_tree.to_json())
+ if res:
+ config_tree = vyos.configtree.ConfigTree(res)
+ config_dict = json.loads(config_tree.to_json())
+ else:
+ config_dict = {}
+
return config_dict
def is_multi(self, path):
@@ -386,7 +393,7 @@ class Config(object):
values = []
if not values:
- return(default)
+ return(default.copy())
else:
return(values)
@@ -407,7 +414,7 @@ class Config(object):
nodes = []
if not nodes:
- return(default)
+ return(default.copy())
else:
return(nodes)
@@ -425,7 +432,10 @@ class Config(object):
This function is safe to use in operational mode. In configuration mode,
it ignores uncommited changes.
"""
- return(self._running_config.exists(self._make_path(path)))
+ if self._running_config:
+ return(self._running_config.exists(self._make_path(path)))
+
+ return False
def return_effective_value(self, path, default=None):
"""
@@ -438,9 +448,12 @@ class Config(object):
Returns:
str: Node value
"""
- try:
- value = self._running_config.return_value(self._make_path(path))
- except vyos.configtree.ConfigTreeError:
+ if self._running_config:
+ try:
+ value = self._running_config.return_value(self._make_path(path))
+ except vyos.configtree.ConfigTreeError:
+ value = None
+ else:
value = None
if not value:
@@ -448,7 +461,6 @@ class Config(object):
else:
return(value)
-
def return_effective_values(self, path, default=[]):
"""
Retrieve all values of a multi-value node in a running (effective) config
@@ -459,13 +471,16 @@ class Config(object):
Returns:
str list: A list of values
"""
- try:
- values = self._running_config.return_values(self._make_path(path))
- except vyos.configtree.ConfigTreeError:
+ if self._running_config:
+ try:
+ values = self._running_config.return_values(self._make_path(path))
+ except vyos.configtree.ConfigTreeError:
+ values = []
+ else:
values = []
if not values:
- return(default)
+ return(default.copy())
else:
return(values)
@@ -482,12 +497,15 @@ class Config(object):
Raises:
VyOSError: if the node is not a tag node
"""
- try:
- nodes = self._running_config.list_nodes(self._make_path(path))
- except vyos.configtree.ConfigTreeError:
+ if self._running_config:
+ try:
+ nodes = self._running_config.list_nodes(self._make_path(path))
+ except vyos.configtree.ConfigTreeError:
+ nodes = []
+ else:
nodes = []
if not nodes:
- return(default)
+ return(default.copy())
else:
return(nodes)
diff --git a/python/vyos/configdict.py b/python/vyos/configdict.py
index 24fe174d2..eec64e964 100644
--- a/python/vyos/configdict.py
+++ b/python/vyos/configdict.py
@@ -18,7 +18,13 @@ A library for retrieving value dicts from VyOS configs in a declarative fashion.
"""
+from enum import Enum
+from copy import deepcopy
+
from vyos import ConfigError
+from vyos.ifconfig import Interface
+from vyos.validate import is_member
+from vyos.util import ifname_from_config
def retrieve_config(path_hash, base_path, config):
"""
@@ -97,172 +103,359 @@ def get_ethertype(ethertype_val):
else:
raise ConfigError('invalid ethertype "{}"'.format(ethertype_val))
+interface_default_data = {
+ 'address': [],
+ 'address_remove': [],
+ 'description': '',
+ 'dhcp_client_id': '',
+ 'dhcp_hostname': '',
+ 'dhcp_vendor_class_id': '',
+ 'dhcpv6_prm_only': False,
+ 'dhcpv6_temporary': False,
+ 'dhcpv6_pd': [],
+ 'disable': False,
+ 'disable_link_detect': 1,
+ 'ip_disable_arp_filter': 1,
+ 'ip_enable_arp_accept': 0,
+ 'ip_enable_arp_announce': 0,
+ 'ip_enable_arp_ignore': 0,
+ 'ip_proxy_arp': 0,
+ 'ipv6_accept_ra': 1,
+ 'ipv6_autoconf': 0,
+ 'ipv6_eui64_prefix': [],
+ 'ipv6_eui64_prefix_remove': [],
+ 'ipv6_forwarding': 1,
+ 'ipv6_dup_addr_detect': 1,
+ 'is_bridge_member': False,
+ 'mac': '',
+ 'mtu': 1500,
+ 'vrf': ''
+}
+
+vlan_default = {
+ **interface_default_data,
+ 'egress_qos': '',
+ 'egress_qos_changed': False,
+ 'ingress_qos': '',
+ 'ingress_qos_changed': False,
+ 'vif_c': {},
+ 'vif_c_remove': []
+}
+
+# see: https://docs.python.org/3/library/enum.html#functional-api
+disable = Enum('disable','none was now both')
+
+def disable_state(conf, check=[3,5,7]):
+ """
+ return if and how a particual section of the configuration is has disable'd
+ using "disable" including if it was disabled by one of its parent.
+
+ check: a list of the level we should check, here 7,5 and 3
+ interfaces ethernet eth1 vif-s 1 vif-c 2 disable
+ interfaces ethernet eth1 vif 1 disable
+ interfaces ethernet eth1 disable
+
+ it returns an enum (none, was, now, both)
+ """
+
+ # save where we are in the config
+ current_level = conf.get_level()
+
+ # logic to figure out if the interface (or one of it parent is disabled)
+ eff_disable = False
+ act_disable = False
+
+ levels = check[:]
+ working_level = current_level[:]
+
+ while levels:
+ position = len(working_level)
+ if not position:
+ break
+ if position not in levels:
+ working_level = working_level[:-1]
+ continue
+
+ levels.remove(position)
+ conf.set_level(working_level)
+ working_level = working_level[:-1]
+
+ eff_disable = eff_disable or conf.exists_effective('disable')
+ act_disable = act_disable or conf.exists('disable')
-def vlan_to_dict(conf):
+ conf.set_level(current_level)
+
+ # how the disabling changed
+ if eff_disable and act_disable:
+ return disable.both
+ if eff_disable and not eff_disable:
+ return disable.was
+ if not eff_disable and act_disable:
+ return disable.now
+ return disable.none
+
+
+def intf_to_dict(conf, default):
"""
Common used function which will extract VLAN related information from config
and represent the result as Python dictionary.
Function call's itself recursively if a vif-s/vif-c pair is detected.
"""
- vlan = {
- 'id': conf.get_level()[-1], # get the '100' in 'interfaces bonding bond0 vif-s 100'
- 'address': [],
- 'address_remove': [],
- 'description': '',
- 'dhcp_client_id': '',
- 'dhcp_hostname': '',
- 'dhcp_vendor_class_id': '',
- 'dhcpv6_prm_only': False,
- 'dhcpv6_temporary': False,
- 'disable': False,
- 'disable_link_detect': 1,
- 'egress_qos': '',
- 'egress_qos_changed': False,
- 'ip_disable_arp_filter': 1,
- 'ip_enable_arp_accept': 0,
- 'ip_enable_arp_announce': 0,
- 'ip_enable_arp_ignore': 0,
- 'ip_proxy_arp': 0,
- 'ipv6_autoconf': 0,
- 'ipv6_forwarding': 1,
- 'ipv6_dup_addr_detect': 1,
- 'ingress_qos': '',
- 'ingress_qos_changed': False,
- 'mac': '',
- 'mtu': 1500,
- 'vrf': ''
- }
- # retrieve configured interface addresses
- if conf.exists('address'):
- vlan['address'] = conf.return_values('address')
-
- # Determine interface addresses (currently effective) - to determine which
- # address is no longer valid and needs to be removed from the bond
- eff_addr = conf.return_effective_values('address')
- act_addr = conf.return_values('address')
- vlan['address_remove'] = list_diff(eff_addr, act_addr)
+
+ intf = deepcopy(default)
+ intf['intf'] = ifname_from_config(conf)
# retrieve interface description
- if conf.exists('description'):
- vlan['description'] = conf.return_value('description')
+ if conf.exists(['description']):
+ intf['description'] = conf.return_value(['description'])
# get DHCP client identifier
- if conf.exists('dhcp-options client-id'):
- vlan['dhcp_client_id'] = conf.return_value('dhcp-options client-id')
+ if conf.exists(['dhcp-options', 'client-id']):
+ intf['dhcp_client_id'] = conf.return_value(['dhcp-options', 'client-id'])
# DHCP client host name (overrides the system host name)
- if conf.exists('dhcp-options host-name'):
- vlan['dhcp_hostname'] = conf.return_value('dhcp-options host-name')
+ if conf.exists(['dhcp-options', 'host-name']):
+ intf['dhcp_hostname'] = conf.return_value(['dhcp-options', 'host-name'])
# DHCP client vendor identifier
- if conf.exists('dhcp-options vendor-class-id'):
- vlan['dhcp_vendor_class_id'] = conf.return_value('dhcp-options vendor-class-id')
+ if conf.exists(['dhcp-options', 'vendor-class-id']):
+ intf['dhcp_vendor_class_id'] = conf.return_value(
+ ['dhcp-options', 'vendor-class-id'])
# DHCPv6 only acquire config parameters, no address
- if conf.exists('dhcpv6-options parameters-only'):
- vlan['dhcpv6_prm_only'] = True
+ if conf.exists(['dhcpv6-options', 'parameters-only']):
+ intf['dhcpv6_prm_only'] = True
+
+ # DHCPv6 prefix delegation (RFC3633)
+ current_level = conf.get_level()
+ if conf.exists(['dhcpv6-options', 'delegate']):
+ for interface in conf.list_nodes(['dhcpv6-options', 'delegate']):
+ conf.set_level(current_level + ['dhcpv6-options', 'delegate', interface])
+ pd = {
+ 'ifname': interface,
+ 'sla_id': '',
+ 'sla_len': '',
+ 'if_id': ''
+ }
+
+ if conf.exists(['sla-id']):
+ pd['sla_id'] = conf.return_value(['sla-id'])
+
+ if conf.exists(['sla-len']):
+ pd['sla_len'] = conf.return_value(['sla-len'])
+
+ if conf.exists(['interface-id']):
+ pd['if_id'] = conf.return_value(['interface-id'])
+
+ intf['dhcpv6_pd'].append(pd)
+
+ # re-set config level
+ conf.set_level(current_level)
# DHCPv6 temporary IPv6 address
- if conf.exists('dhcpv6-options temporary'):
- vlan['dhcpv6_temporary'] = True
+ if conf.exists(['dhcpv6-options', 'temporary']):
+ intf['dhcpv6_temporary'] = True
# ignore link state changes
- if conf.exists('disable-link-detect'):
- vlan['disable_link_detect'] = 2
-
- # disable VLAN interface
- if conf.exists('disable'):
- vlan['disable'] = True
+ if conf.exists(['disable-link-detect']):
+ intf['disable_link_detect'] = 2
# ARP filter configuration
- if conf.exists('ip disable-arp-filter'):
- vlan['ip_disable_arp_filter'] = 0
+ if conf.exists(['ip', 'disable-arp-filter']):
+ intf['ip_disable_arp_filter'] = 0
# ARP enable accept
- if conf.exists('ip enable-arp-accept'):
- vlan['ip_enable_arp_accept'] = 1
+ if conf.exists(['ip', 'enable-arp-accept']):
+ intf['ip_enable_arp_accept'] = 1
# ARP enable announce
- if conf.exists('ip enable-arp-announce'):
- vlan['ip_enable_arp_announce'] = 1
+ if conf.exists(['ip', 'enable-arp-announce']):
+ intf['ip_enable_arp_announce'] = 1
# ARP enable ignore
- if conf.exists('ip enable-arp-ignore'):
- vlan['ip_enable_arp_ignore'] = 1
+ if conf.exists(['ip', 'enable-arp-ignore']):
+ intf['ip_enable_arp_ignore'] = 1
# Enable Proxy ARP
- if conf.exists('ip enable-proxy-arp'):
- vlan['ip_proxy_arp'] = 1
+ if conf.exists(['ip', 'enable-proxy-arp']):
+ intf['ip_proxy_arp'] = 1
# Enable acquisition of IPv6 address using stateless autoconfig (SLAAC)
- if conf.exists('ipv6 address autoconf'):
- vlan['ipv6_autoconf'] = 1
+ if conf.exists(['ipv6', 'address', 'autoconf']):
+ intf['ipv6_autoconf'] = 1
# Disable IPv6 forwarding on this interface
- if conf.exists('ipv6 disable-forwarding'):
- vlan['ipv6_forwarding'] = 0
+ if conf.exists(['ipv6', 'disable-forwarding']):
+ intf['ipv6_forwarding'] = 0
+
+ # check if interface is member of a bridge
+ intf['is_bridge_member'] = is_member(conf, intf['intf'], 'bridge')
# IPv6 Duplicate Address Detection (DAD) tries
- if conf.exists('ipv6 dup-addr-detect-transmits'):
- vlan['ipv6_dup_addr_detect'] = int(conf.return_value('ipv6 dup-addr-detect-transmits'))
+ if conf.exists(['ipv6', 'dup-addr-detect-transmits']):
+ intf['ipv6_dup_addr_detect'] = int(
+ conf.return_value(['ipv6', 'dup-addr-detect-transmits']))
# Media Access Control (MAC) address
- if conf.exists('mac'):
- vlan['mac'] = conf.return_value('mac')
+ if conf.exists(['mac']):
+ intf['mac'] = conf.return_value(['mac'])
# Maximum Transmission Unit (MTU)
- if conf.exists('mtu'):
- vlan['mtu'] = int(conf.return_value('mtu'))
+ if conf.exists(['mtu']):
+ intf['mtu'] = int(conf.return_value(['mtu']))
# retrieve VRF instance
- if conf.exists('vrf'):
- vlan['vrf'] = conf.return_value('vrf')
+ if conf.exists(['vrf']):
+ intf['vrf'] = conf.return_value(['vrf'])
- # VLAN egress QoS
- if conf.exists('egress-qos'):
- vlan['egress_qos'] = conf.return_value('egress-qos')
+ # egress QoS
+ if conf.exists(['egress-qos']):
+ intf['egress_qos'] = conf.return_value(['egress-qos'])
# egress changes QoS require VLAN interface recreation
- if conf.return_effective_value('egress-qos'):
- if vlan['egress_qos'] != conf.return_effective_value('egress-qos'):
- vlan['egress_qos_changed'] = True
+ if conf.return_effective_value(['egress-qos']):
+ if intf['egress_qos'] != conf.return_effective_value(['egress-qos']):
+ intf['egress_qos_changed'] = True
- # VLAN ingress QoS
- if conf.exists('ingress-qos'):
- vlan['ingress_qos'] = conf.return_value('ingress-qos')
+ # ingress QoS
+ if conf.exists(['ingress-qos']):
+ intf['ingress_qos'] = conf.return_value(['ingress-qos'])
# ingress changes QoS require VLAN interface recreation
- if conf.return_effective_value('ingress-qos'):
- if vlan['ingress_qos'] != conf.return_effective_value('ingress-qos'):
- vlan['ingress_qos_changed'] = True
+ if conf.return_effective_value(['ingress-qos']):
+ if intf['ingress_qos'] != conf.return_effective_value(['ingress-qos']):
+ intf['ingress_qos_changed'] = True
+
+ # Get the interface addresses
+ intf['address'] = conf.return_values(['address'])
+
+ # addresses to remove - difference between effective and working config
+ intf['address_remove'] = list_diff(
+ conf.return_effective_values(['address']), intf['address'])
+
+ # Get prefixes for IPv6 addressing based on MAC address (EUI-64)
+ intf['ipv6_eui64_prefix'] = conf.return_values(['ipv6', 'address', 'eui64'])
+
+ # EUI64 to remove - difference between effective and working config
+ intf['ipv6_eui64_prefix_remove'] = list_diff(
+ conf.return_effective_values(['ipv6', 'address', 'eui64']),
+ intf['ipv6_eui64_prefix'])
+
+ # Determine if the interface should be disabled
+ disabled = disable_state(conf)
+ if disabled == disable.both:
+ # was and is still disabled
+ intf['disable'] = True
+ elif disabled == disable.now:
+ # it is now disable but was not before
+ intf['disable'] = True
+ elif disabled == disable.was:
+ # it was disable but not anymore
+ intf['disable'] = False
+ else:
+ # normal change
+ intf['disable'] = False
+
+ # Remove the default link-local address if no-default-link-local is set,
+ # if member of a bridge or if disabled (it may not have a MAC if it's down)
+ if ( conf.exists(['ipv6', 'address', 'no-default-link-local'])
+ or intf.get('is_bridge_member') or intf['disable'] ):
+ intf['ipv6_eui64_prefix_remove'].append('fe80::/64')
+ else:
+ # add the link-local by default to make IPv6 work
+ intf['ipv6_eui64_prefix'].append('fe80::/64')
- # ethertype is mandatory on vif-s nodes and only exists here!
- # check if this is a vif-s node at all:
- if conf.get_level()[-2] == 'vif-s':
- vlan['vif_c'] = []
- vlan['vif_c_remove'] = []
-
- # ethertype uses a default of 0x88A8
- tmp = '0x88A8'
- if conf.exists('ethertype'):
- tmp = conf.return_value('ethertype')
- vlan['ethertype'] = get_ethertype(tmp)
-
- # get vif-c interfaces (currently effective) - to determine which vif-c
+ # If MAC has changed, remove and re-add all IPv6 EUI64 addresses
+ try:
+ interface = Interface(intf['intf'], create=False)
+ if intf['mac'] and intf['mac'] != interface.get_mac():
+ intf['ipv6_eui64_prefix_remove'] += intf['ipv6_eui64_prefix']
+ except Exception:
+ # If the interface does not exist, it could not have changed
+ pass
+
+ # to make IPv6 SLAAC and DHCPv6 work with forwarding=1,
+ # accept_ra must be 2
+ if intf['ipv6_autoconf'] or 'dhcpv6' in intf['address']:
+ intf['ipv6_accept_ra'] = 2
+
+ return intf, disable
+
+
+
+def add_to_dict(conf, disabled, ifdict, section, key):
+ """
+ parse a section of vif/vif-s/vif-c and add them to the dict
+ follow the convention to:
+ * use the "key" for what to add
+ * use the "key" what what to remove
+
+ conf: is the Config() already at the level we need to parse
+ disabled: is a disable enum so we know how to handle to data
+ intf: if the interface dictionary
+ section: is the section name to parse (vif/vif-s/vif-c)
+ key: is the dict key to use (vif/vifs/vifc)
+ """
+
+ if not conf.exists(section):
+ return ifdict
+
+ effect = conf.list_effective_nodes(section)
+ active = conf.list_nodes(section)
+
+ # the section to parse for vlan
+ sections = []
+
+ # determine which interfaces to add or remove based on disable state
+ if disabled == disable.both:
+ # was and is still disabled
+ ifdict[f'{key}_remove'] = []
+ elif disabled == disable.now:
+ # it is now disable but was not before
+ ifdict[f'{key}_remove'] = effect
+ elif disabled == disable.was:
+ # it was disable but not anymore
+ ifdict[f'{key}_remove'] = []
+ sections = active
+ else:
+ # normal change
+ # get interfaces (currently effective) - to determine which
# interface is no longer present and needs to be removed
- eff_intf = conf.list_effective_nodes('vif-c')
- act_intf = conf.list_nodes('vif-c')
- vlan['vif_c_remove'] = list_diff(eff_intf, act_intf)
-
- # check if there is a Q-in-Q vlan customer interface
- # and call this function recursively
- if conf.exists('vif-c'):
- cfg_level = conf.get_level()
- # add new key (vif-c) to dictionary
- for vif in conf.list_nodes('vif-c'):
- # set config level to vif interface
- conf.set_level(cfg_level + ['vif-c', vif])
- vlan['vif_c'].append(vlan_to_dict(conf))
+ ifdict[f'{key}_remove'] = list_diff(effect, active)
+ sections = active
+
+ current_level = conf.get_level()
+
+ # add each section, the key must already exists
+ for s in sections:
+ # set config level to vif interface
+ conf.set_level(current_level + [section, s])
+ # add the vlan config as a key (vlan id) - value (config) pair
+ ifdict[key][s] = vlan_to_dict(conf)
+
+ # re-set configuration level to leave things as found
+ conf.set_level(current_level)
+
+ return ifdict
+
+
+def vlan_to_dict(conf, default=vlan_default):
+ vlan, disabled = intf_to_dict(conf, default)
+
+ # if this is a not within vif-s node, we are done
+ if conf.get_level()[-2] != 'vif-s':
+ return vlan
+
+ # ethertype is mandatory on vif-s nodes and only exists here!
+ # ethertype uses a default of 0x88A8
+ tmp = '0x88A8'
+ if conf.exists('ethertype'):
+ tmp = conf.return_value('ethertype')
+ vlan['ethertype'] = get_ethertype(tmp)
+
+ # check if there is a Q-in-Q vlan customer interface
+ # and call this function recursively
+ add_to_dict(conf, disable, vlan, 'vif-c', 'vif_c')
return vlan
diff --git a/python/vyos/debug.py b/python/vyos/debug.py
index 20090fb85..6ce42b173 100644
--- a/python/vyos/debug.py
+++ b/python/vyos/debug.py
@@ -15,7 +15,7 @@
import os
import sys
-
+from datetime import datetime
def message(message, flag='', destination=sys.stdout):
"""
@@ -41,11 +41,12 @@ def message(message, flag='', destination=sys.stdout):
try:
# at boot the file is created as root:vyattacfg
# at runtime the file is created as user:vyattacfg
- # the default permission are 644
- mask = os.umask(0o113)
+ # but the helper scripts are not run as this so it
+ # need the default permission to be 666 (an not 660)
+ mask = os.umask(0o111)
with open(logfile, 'a') as f:
- f.write(_format('log', message))
+ f.write(_timed(_format('log', message)))
finally:
os.umask(mask)
@@ -80,10 +81,22 @@ def enabled(flag):
return _fromenv(flag) or _fromfile(flag)
+def _timed(message):
+ now = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
+ return f'{now} {message}'
+
+
+def _remove_invisible(string):
+ for char in ('\0', '\a', '\b', '\f', '\v'):
+ string = string.replace(char, '')
+ return string
+
+
def _format(flag, message):
"""
format a log message
"""
+ message = _remove_invisible(message)
return f'DEBUG/{flag.upper():<7} {message}\n'
@@ -133,7 +146,7 @@ def _contentenv(flag):
return os.environ.get(f'VYOS_{flag.upper()}_DEBUG', '').strip()
-def _contentfile(flag):
+def _contentfile(flag, default=''):
"""
Check if debug exist for a given debug flag name
@@ -153,7 +166,8 @@ def _contentfile(flag):
if not os.path.isfile(flagfile):
continue
with open(flagfile) as f:
- return f.readline().strip()
+ content = f.readline().strip()
+ return content or default
return ''
@@ -166,7 +180,7 @@ def _logfile(flag, default):
"""
# For log we return the location of the log file
- log_location = _contentenv(flag) or _contentfile(flag)
+ log_location = _contentenv(flag) or _contentfile(flag, default)
# it was not set
if not log_location:
@@ -177,6 +191,15 @@ def _logfile(flag, default):
not log_location.startswith('/config/') and \
not log_location.startswith('/var/log/'):
return default
+ # Do not allow to escape the folders
if '..' in log_location:
return default
+
+ if not os.path.exists(log_location):
+ return log_location
+
+ # this permission is unique the the config and var folder
+ stat = os.stat(log_location).st_mode
+ if stat != 0o100666:
+ return default
return log_location
diff --git a/python/vyos/dicts.py b/python/vyos/dicts.py
index 79cab4a08..b12cda40f 100644
--- a/python/vyos/dicts.py
+++ b/python/vyos/dicts.py
@@ -15,6 +15,9 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
+from vyos import ConfigError
+
+
class FixedDict(dict):
"""
FixedDict: A dictionnary not allowing new keys to be created after initialisation.
diff --git a/python/vyos/ifconfig/dhcp.py b/python/vyos/ifconfig/dhcp.py
index d4ff9c2cd..f8fdeb6a9 100644
--- a/python/vyos/ifconfig/dhcp.py
+++ b/python/vyos/ifconfig/dhcp.py
@@ -19,28 +19,19 @@ from vyos.dicts import FixedDict
from vyos.ifconfig.control import Control
from vyos.template import render
-
-class _DHCP (Control):
- client_base = r'/var/lib/dhcp/dhclient_'
-
- def __init__(self, ifname, version, **kargs):
- super().__init__(**kargs)
- self.version = version
- self.file = {
- 'ifname': ifname,
- 'conf': self.client_base + ifname + '.' + version + 'conf',
- 'pid': self.client_base + ifname + '.' + version + 'pid',
- 'lease': self.client_base + ifname + '.' + version + 'leases',
- }
-
-class _DHCPv4 (_DHCP):
+class _DHCPv4 (Control):
def __init__(self, ifname):
- super().__init__(ifname, '')
+ super().__init__()
+ config_base = r'/var/lib/dhcp/dhclient_'
self.options = FixedDict(**{
'ifname': ifname,
'hostname': '',
'client_id': '',
- 'vendor_class_id': ''
+ 'vendor_class_id': '',
+ 'conf_file': config_base + f'{ifname}.conf',
+ 'options_file': config_base + f'{ifname}.options',
+ 'pid_file': config_base + f'{ifname}.pid',
+ 'lease_file': config_base + f'{ifname}.leases',
})
# replace dhcpv4/v6 with systemd.networkd?
@@ -55,25 +46,16 @@ class _DHCPv4 (_DHCP):
>>> j = Interface('eth0')
>>> j.dhcp.v4.set()
"""
-
if not self.options['hostname']:
# read configured system hostname.
# maybe change to vyos hostd client ???
with open('/etc/hostname', 'r') as f:
self.options['hostname'] = f.read().rstrip('\n')
- render(self.file['conf'], 'dhcp-client/ipv4.tmpl' ,self.options)
+ render(self.options['options_file'], 'dhcp-client/daemon-options.tmpl', self.options)
+ render(self.options['conf_file'], 'dhcp-client/ipv4.tmpl', self.options)
- cmd = 'start-stop-daemon'
- cmd += ' --start'
- cmd += ' --oknodo'
- cmd += ' --quiet'
- cmd += ' --pidfile {pid}'
- cmd += ' --exec /sbin/dhclient'
- cmd += ' --'
- # now pass arguments to dhclient binary
- cmd += ' -4 -nw -cf {conf} -pf {pid} -lf {lease} {ifname}'
- return self._cmd(cmd.format(**self.file))
+ return self._cmd('systemctl restart dhclient@{ifname}.service'.format(**self.options))
def delete(self):
"""
@@ -86,44 +68,27 @@ class _DHCPv4 (_DHCP):
>>> j = Interface('eth0')
>>> j.dhcp.v4.delete()
"""
- if not os.path.isfile(self.file['pid']):
+ if not os.path.isfile(self.options['pid_file']):
self._debug_msg('No DHCP client PID found')
return None
- # with open(self.file['pid'], 'r') as f:
- # pid = int(f.read())
-
- # stop dhclient, we need to call dhclient and tell it should release the
- # aquired IP address. tcpdump tells me:
- # 172.16.35.103.68 > 172.16.35.254.67: [bad udp cksum 0xa0cb -> 0xb943!] BOOTP/DHCP, Request from 00:50:56:9d:11:df, length 300, xid 0x620e6946, Flags [none] (0x0000)
- # Client-IP 172.16.35.103
- # Client-Ethernet-Address 00:50:56:9d:11:df
- # Vendor-rfc1048 Extensions
- # Magic Cookie 0x63825363
- # DHCP-Message Option 53, length 1: Release
- # Server-ID Option 54, length 4: 172.16.35.254
- # Hostname Option 12, length 10: "vyos"
- #
- cmd = '/sbin/dhclient -cf {conf} -pf {pid} -lf {lease} -r {ifname}'
- self._cmd(cmd.format(**self.file))
+ self._cmd('systemctl stop dhclient@{ifname}.service'.format(**self.options))
# cleanup old config files
- for name in ('conf', 'pid', 'lease'):
- if os.path.isfile(self.file[name]):
- os.remove(self.file[name])
-
+ for name in ('conf_file', 'options_file', 'pid_file', 'lease_file'):
+ if os.path.isfile(self.options[name]):
+ os.remove(self.options[name])
-class _DHCPv6 (_DHCP):
+class _DHCPv6 (Control):
def __init__(self, ifname):
- super().__init__(ifname, 'v6')
+ super().__init__()
self.options = FixedDict(**{
'ifname': ifname,
'dhcpv6_prm_only': False,
'dhcpv6_temporary': False,
+ 'dhcpv6_pd': [],
})
- self.file.update({
- 'accept_ra': f'/proc/sys/net/ipv6/conf/{ifname}/accept_ra',
- })
+ self._conf_file = f'/run/dhcp6c/dhcp6c.{ifname}.conf'
def set(self):
"""
@@ -134,7 +99,7 @@ class _DHCPv6 (_DHCP):
>>> from vyos.ifconfig import Interface
>>> j = Interface('eth0')
- >>> j.set_dhcpv6()
+ >>> j.dhcp.v6.set()
"""
# better save then sorry .. should be checked in interface script
@@ -143,29 +108,8 @@ class _DHCPv6 (_DHCP):
raise Exception(
'DHCPv6 temporary and parameters-only options are mutually exclusive!')
- render(self.file['conf'], 'dhcp-client/ipv6.tmpl', self.options)
-
- # no longer accept router announcements on this interface
- self._write_sysfs(self.file['accept_ra'], 0)
-
- # assemble command-line to start DHCPv6 client (dhclient)
- cmd = 'start-stop-daemon'
- cmd += ' --start'
- cmd += ' --oknodo'
- cmd += ' --quiet'
- cmd += ' --pidfile {pid}'
- cmd += ' --exec /sbin/dhclient'
- cmd += ' --'
- # now pass arguments to dhclient binary
- cmd += ' -6 -nw -cf {conf} -pf {pid} -lf {lease}'
- # add optional arguments
- if self.options['dhcpv6_prm_only']:
- cmd += ' -S'
- if self.options['dhcpv6_temporary']:
- cmd += ' -T'
- cmd += ' {ifname}'
-
- return self._cmd(cmd.format(**self.file))
+ render(self._conf_file, 'dhcp-client/ipv6.tmpl', self.options, trim_blocks=True)
+ return self._cmd('systemctl restart dhcp6c@{ifname}.service'.format(**self.options))
def delete(self):
"""
@@ -176,33 +120,16 @@ class _DHCPv6 (_DHCP):
>>> from vyos.ifconfig import Interface
>>> j = Interface('eth0')
- >>> j.del_dhcpv6()
+ >>> j.dhcp.v6.delete()
"""
- if not os.path.isfile(self.file['pid']):
- self._debug_msg('No DHCPv6 client PID found')
- return None
-
- # with open(self.file['pid'], 'r') as f:
- # pid = int(f.read())
-
- # stop dhclient
- cmd = 'start-stop-daemon'
- cmd += ' --start'
- cmd += ' --oknodo'
- cmd += ' --quiet'
- cmd += ' --pidfile {pid}'
- self._cmd(cmd.format(**self.file))
-
- # accept router announcements on this interface
- self._write_sysfs(self.options['accept_ra'], 1)
+ self._cmd('systemctl stop dhcp6c@{ifname}.service'.format(**self.options))
# cleanup old config files
- for name in ('conf', 'pid', 'lease'):
- if os.path.isfile(self.file[name]):
- os.remove(self.file[name])
+ if os.path.isfile(self._conf_file):
+ os.remove(self._conf_file)
-class DHCP (object):
+class DHCP(object):
def __init__(self, ifname):
self.v4 = _DHCPv4(ifname)
self.v6 = _DHCPv6(ifname)
diff --git a/python/vyos/ifconfig/interface.py b/python/vyos/ifconfig/interface.py
index 62c30dbf7..61f2c6482 100644
--- a/python/vyos/ifconfig/interface.py
+++ b/python/vyos/ifconfig/interface.py
@@ -1,4 +1,4 @@
-# Copyright 2019 VyOS maintainers and contributors <maintainers@vyos.io>
+# Copyright 2019-2020 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
@@ -42,6 +42,7 @@ from vyos.ifconfig.control import Control
from vyos.ifconfig.dhcp import DHCP
from vyos.ifconfig.vrrp import VRRP
from vyos.ifconfig.operational import Operational
+from vyos.ifconfig import Section
class Interface(Control):
@@ -133,8 +134,12 @@ class Interface(Control):
'validate': assert_boolean,
'location': '/proc/sys/net/ipv4/conf/{ifname}/arp_ignore',
},
+ 'ipv6_accept_ra': {
+ 'validate': lambda ara: assert_range(ara,0,3),
+ 'location': '/proc/sys/net/ipv6/conf/{ifname}/accept_ra',
+ },
'ipv6_autoconf': {
- 'validate': lambda fwd: assert_range(fwd,0,2),
+ 'validate': lambda aco: assert_range(aco,0,2),
'location': '/proc/sys/net/ipv6/conf/{ifname}/autoconf',
},
'ipv6_forwarding': {
@@ -219,7 +224,7 @@ class Interface(Control):
else:
raise Exception('interface "{}" not found'.format(self.config['ifname']))
- # list of assigned IP addresses
+ # temporary list of assigned IP addresses
self._addr = []
self.operational = self.OperationalClass(ifname)
@@ -240,15 +245,11 @@ class Interface(Control):
>>> i = Interface('eth0')
>>> i.remove()
"""
- # stop DHCP(v6) if running
- self.dhcp.v4.delete()
- self.dhcp.v6.delete()
# remove all assigned IP addresses from interface - this is a bit redundant
# as the kernel will remove all addresses on interface deletion, but we
# can not delete ALL interfaces, see below
- for addr in self.get_addr():
- self.del_addr(addr)
+ self.flush_addrs()
# ---------------------------------------------------------------------
# Any class can define an eternal regex in its definition
@@ -412,6 +413,21 @@ class Interface(Control):
"""
return self.set_interface('arp_ignore', arp_ignore)
+ def set_ipv6_accept_ra(self, accept_ra):
+ """
+ Accept Router Advertisements; autoconfigure using them.
+
+ It also determines whether or not to transmit Router Solicitations.
+ If and only if the functional setting is to accept Router
+ Advertisements, Router Solicitations will be transmitted.
+
+ 0 - Do not accept Router Advertisements.
+ 1 - (default) Accept Router Advertisements if forwarding is disabled.
+ 2 - Overrule forwarding behaviour. Accept Router Advertisements even if
+ forwarding is enabled.
+ """
+ return self.set_interface('ipv6_accept_ra', accept_ra)
+
def set_ipv6_autoconf(self, autoconf):
"""
Autoconfigure addresses using Prefix Information in Router
@@ -419,39 +435,28 @@ class Interface(Control):
"""
return self.set_interface('ipv6_autoconf', autoconf)
- def set_ipv6_eui64_address(self, prefix):
+ def add_ipv6_eui64_address(self, prefix):
"""
Extended Unique Identifier (EUI), as per RFC2373, allows a host to
- assign iteslf a unique IPv6 address based on a given IPv6 prefix.
+ assign itself a unique IPv6 address based on a given IPv6 prefix.
- If prefix is passed address is assigned, if prefix is '' address is
- removed from interface.
+ Calculate the EUI64 from the interface's MAC, then assign it
+ with the given prefix to the interface.
"""
- # if prefix is an empty string convert it to None so mac2eui64 works
- # as expected
- if not prefix:
- prefix = None
eui64 = mac2eui64(self.get_mac(), prefix)
+ prefixlen = prefix.split('/')[1]
+ self.add_addr(f'{eui64}/{prefixlen}')
- if not prefix:
- # if prefix is empty - thus removed - we need to walk through all
- # interface IPv6 addresses and find the one with the calculated
- # EUI-64 identifier. The address is then removed
- for addr in self.get_addr():
- addr_wo_prefix = addr.split('/')[0]
- if is_ipv6(addr_wo_prefix):
- if eui64 in IPv6Address(addr_wo_prefix).exploded:
- self.del_addr(addr)
-
- return None
+ def del_ipv6_eui64_address(self, prefix):
+ """
+ Delete the address based on the interface's MAC-based EUI64
+ combined with the prefix address.
+ """
+ eui64 = mac2eui64(self.get_mac(), prefix)
+ prefixlen = prefix.split('/')[1]
+ self.del_addr(f'{eui64}/{prefixlen}')
- # calculate and add EUI-64 IPv6 address
- if IPv6Network(prefix):
- # we also need to take the subnet length into account
- prefix = prefix.split('/')[1]
- eui64 = f'{eui64}/{prefix}'
- self.add_addr(eui64 )
def set_ipv6_forwarding(self, forwarding):
"""
@@ -632,7 +637,8 @@ class Interface(Control):
def add_addr(self, addr):
"""
Add IP(v6) address to interface. Address is only added if it is not
- already assigned to that interface.
+ already assigned to that interface. Address format must be validated
+ and compressed/normalized before calling this function.
addr: can be an IPv4 address, IPv6 address, dhcp or dhcpv6!
IPv4: add IPv4 address to interface
@@ -640,6 +646,7 @@ class Interface(Control):
dhcp: start dhclient (IPv4) on interface
dhcpv6: start dhclient (IPv6) on interface
+ Returns False if address is already assigned and wasn't re-added.
Example:
>>> from vyos.ifconfig import Interface
>>> j = Interface('eth0')
@@ -648,32 +655,41 @@ class Interface(Control):
>>> j.get_addr()
['192.0.2.1/24', '2001:db8::ffff/64']
"""
+ # XXX: normalize/compress with ipaddress if calling functions don't?
+ # is subnet mask always passed, and in the same way?
- # cache new IP address which is assigned to interface
- self._addr.append(addr)
+ # do not add same address twice
+ if addr in self._addr:
+ return False
- # we can not have both DHCP and static IPv4 addresses assigned to an interface
- if 'dhcp' in self._addr:
- for addr in self._addr:
- # do not change below 'if' ordering esle you will get an exception as:
- # ValueError: 'dhcp' does not appear to be an IPv4 or IPv6 address
- if addr != 'dhcp' and is_ipv4(addr):
- raise ConfigError(
- "Can't configure both static IPv4 and DHCP address on the same interface")
+ # we can't have both DHCP and static IPv4 addresses assigned
+ for a in self._addr:
+ if ( ( addr == 'dhcp' and a != 'dhcpv6' and is_ipv4(a) ) or
+ ( a == 'dhcp' and addr != 'dhcpv6' and is_ipv4(addr) ) ):
+ raise ConfigError((
+ "Can't configure both static IPv4 and DHCP address "
+ "on the same interface"))
+ # add to interface
if addr == 'dhcp':
self.dhcp.v4.set()
elif addr == 'dhcpv6':
self.dhcp.v6.set()
+ elif not is_intf_addr_assigned(self.ifname, addr):
+ self._cmd(f'ip addr add "{addr}" dev "{self.ifname}"')
else:
- if not is_intf_addr_assigned(self.config['ifname'], addr):
- cmd = 'ip addr add "{}" dev "{}"'.format(addr, self.config['ifname'])
- return self._cmd(cmd)
+ return False
+
+ # add to cache
+ self._addr.append(addr)
+
+ return True
def del_addr(self, addr):
"""
- Delete IP(v6) address to interface. Address is only added if it is
- assigned to that interface.
+ Delete IP(v6) address from interface. Address is only deleted if it is
+ assigned to that interface. Address format must be exactly the same as
+ was used when adding the address.
addr: can be an IPv4 address, IPv6 address, dhcp or dhcpv6!
IPv4: delete IPv4 address from interface
@@ -681,6 +697,7 @@ class Interface(Control):
dhcp: stop dhclient (IPv4) on interface
dhcpv6: stop dhclient (IPv6) on interface
+ Returns False if address isn't already assigned and wasn't deleted.
Example:
>>> from vyos.ifconfig import Interface
>>> j = Interface('eth0')
@@ -692,11 +709,51 @@ class Interface(Control):
>>> j.get_addr()
['2001:db8::ffff/64']
"""
+
+ # remove from interface
if addr == 'dhcp':
self.dhcp.v4.delete()
elif addr == 'dhcpv6':
self.dhcp.v6.delete()
+ elif is_intf_addr_assigned(self.ifname, addr):
+ self._cmd(f'ip addr del "{addr}" dev "{self.ifname}"')
else:
- if is_intf_addr_assigned(self.config['ifname'], addr):
- cmd = 'ip addr del "{}" dev "{}"'.format(addr, self.config['ifname'])
- return self._cmd(cmd)
+ return False
+
+ # remove from cache
+ if addr in self._addr:
+ self._addr.remove(addr)
+
+ return True
+
+ def flush_addrs(self):
+ """
+ Flush all addresses from an interface, including DHCP.
+
+ Will raise an exception on error.
+ """
+ # stop DHCP(v6) if running
+ self.dhcp.v4.delete()
+ self.dhcp.v6.delete()
+
+ # flush all addresses
+ self._cmd(f'ip addr flush dev "{self.ifname}"')
+
+ def add_to_bridge(self, br):
+ """
+ Adds the interface to the bridge with the passed port config.
+
+ Returns False if bridge doesn't exist.
+ """
+
+ # check if the bridge exists (on boot it doesn't)
+ if br not in Section.interfaces('bridge'):
+ return False
+
+ self.flush_addrs()
+ # add interface to bridge - use Section.klass to get BridgeIf class
+ Section.klass(br)(br, create=False).add_port(self.ifname)
+
+ # TODO: port config (STP)
+
+ return True
diff --git a/python/vyos/ifconfig/section.py b/python/vyos/ifconfig/section.py
index 092236fef..926c22e8a 100644
--- a/python/vyos/ifconfig/section.py
+++ b/python/vyos/ifconfig/section.py
@@ -54,7 +54,7 @@ class Section:
name = name.rstrip('0123456789')
name = name.rstrip('.')
if vlan:
- name = name.rstrip('0123456789')
+ name = name.rstrip('0123456789.')
return name
@classmethod
@@ -137,3 +137,22 @@ class Section:
eth, lo, vxlan, dum, ...
"""
return list(cls._prefixes.keys())
+
+ @classmethod
+ def get_config_path(cls, name):
+ """
+ get config path to interface with .vif or .vif-s.vif-c
+ example: eth0.1.2 -> 'ethernet eth0 vif-s 1 vif-c 2'
+ Returns False if interface name is invalid (not found in sections)
+ """
+ sect = cls.section(name)
+ if sect:
+ splinterface = name.split('.')
+ intfpath = f'{sect} {splinterface[0]}'
+ if len(splinterface) == 2:
+ intfpath += f' vif {splinterface[1]}'
+ elif len(splinterface) == 3:
+ intfpath += f' vif-s {splinterface[1]} vif-c {splinterface[2]}'
+ return intfpath
+ else:
+ return False
diff --git a/python/vyos/ifconfig/vlan.py b/python/vyos/ifconfig/vlan.py
index 7b1e00d87..d68e8f6cd 100644
--- a/python/vyos/ifconfig/vlan.py
+++ b/python/vyos/ifconfig/vlan.py
@@ -101,26 +101,26 @@ class VLAN:
>>> i.add_vlan(10)
"""
vlan_ifname = self.config['ifname'] + '.' + str(vlan_id)
- if not os.path.exists(f'/sys/class/net/{vlan_ifname}'):
- self._vlan_id = int(vlan_id)
-
- if ethertype:
- self._ethertype = ethertype
- ethertype = 'proto {}'.format(ethertype)
-
- # Optional ingress QOS mapping
- opt_i = ''
- if ingress_qos:
- opt_i = 'ingress-qos-map ' + ingress_qos
- # Optional egress QOS mapping
- opt_e = ''
- if egress_qos:
- opt_e = 'egress-qos-map ' + egress_qos
-
- # create interface in the system
- cmd = 'ip link add link {ifname} name {ifname}.{vlan} type vlan {proto} id {vlan} {opt_e} {opt_i}' \
- .format(ifname=self.config['ifname'], vlan=self._vlan_id, proto=ethertype, opt_e=opt_e, opt_i=opt_i)
- self._cmd(cmd)
+ if os.path.exists(f'/sys/class/net/{vlan_ifname}'):
+ return self.__class__(vlan_ifname)
+
+ if ethertype:
+ self._ethertype = ethertype
+ ethertype = 'proto {}'.format(ethertype)
+
+ # Optional ingress QOS mapping
+ opt_i = ''
+ if ingress_qos:
+ opt_i = 'ingress-qos-map ' + ingress_qos
+ # Optional egress QOS mapping
+ opt_e = ''
+ if egress_qos:
+ opt_e = 'egress-qos-map ' + egress_qos
+
+ # create interface in the system
+ cmd = 'ip link add link {ifname} name {ifname}.{vlan} type vlan {proto} id {vlan} {opt_e} {opt_i}' \
+ .format(ifname=self.ifname, vlan=vlan_id, proto=ethertype, opt_e=opt_e, opt_i=opt_i)
+ self._cmd(cmd)
# return new object mapping to the newly created interface
# we can now work on this object for e.g. IP address setting
diff --git a/python/vyos/ifconfig/wireguard.py b/python/vyos/ifconfig/wireguard.py
index fdf5d9347..027b5ea8c 100644
--- a/python/vyos/ifconfig/wireguard.py
+++ b/python/vyos/ifconfig/wireguard.py
@@ -208,7 +208,7 @@ class WireGuardIf(Interface):
else:
cmd += aip
if self.config['endpoint']:
- cmd += " endpoint {}".format(self.config['endpoint'])
+ cmd += " endpoint '{}'".format(self.config['endpoint'])
cmd += " persistent-keepalive {}".format(self.config['keepalive'])
self._cmd(cmd)
diff --git a/python/vyos/ifconfig_vlan.py b/python/vyos/ifconfig_vlan.py
index 899fd17da..a53136ebf 100644
--- a/python/vyos/ifconfig_vlan.py
+++ b/python/vyos/ifconfig_vlan.py
@@ -16,6 +16,53 @@
from netifaces import interfaces
from vyos import ConfigError
+def apply_all_vlans(intf, intfconfig):
+ """
+ Function applies all VLANs to the passed interface.
+
+ intf: object of Interface class
+ intfconfig: dict with interface configuration
+ """
+ # remove no longer required service VLAN interfaces (vif-s)
+ for vif_s in intfconfig['vif_s_remove']:
+ intf.del_vlan(vif_s)
+
+ # create service VLAN interfaces (vif-s)
+ for vif_s_id, vif_s in intfconfig['vif_s'].items():
+ s_vlan = intf.add_vlan(vif_s_id, ethertype=vif_s['ethertype'])
+ apply_vlan_config(s_vlan, vif_s)
+
+ # remove no longer required client VLAN interfaces (vif-c)
+ # on lower service VLAN interface
+ for vif_c in intfconfig['vif_c_remove']:
+ s_vlan.del_vlan(vif_c)
+
+ # create client VLAN interfaces (vif-c)
+ # on lower service VLAN interface
+ for vif_c_id, vif_c in vif_s['vif_c'].items():
+ c_vlan = s_vlan.add_vlan(vif_c_id)
+ apply_vlan_config(c_vlan, vif_c)
+
+ # remove no longer required VLAN interfaces (vif)
+ for vif in intfconfig['vif_remove']:
+ intf.del_vlan(vif)
+
+ # create VLAN interfaces (vif)
+ for vif_id, vif in intfconfig['vif'].items():
+ # QoS priority mapping can only be set during interface creation
+ # so we delete the interface first if required.
+ if vif['egress_qos_changed'] or vif['ingress_qos_changed']:
+ try:
+ # on system bootup the above condition is true but the interface
+ # does not exists, which throws an exception, but that's legal
+ intf.del_vlan(vif_id)
+ except:
+ pass
+
+ vlan = intf.add_vlan(vif_id, ingress_qos=vif['ingress_qos'], egress_qos=vif['egress_qos'])
+ apply_vlan_config(vlan, vif)
+
+
def apply_vlan_config(vlan, config):
"""
Generic function to apply a VLAN configuration from a dictionary
@@ -40,6 +87,9 @@ def apply_vlan_config(vlan, config):
if config['dhcpv6_temporary']:
vlan.dhcp.v6.options['dhcpv6_temporary'] = True
+ if config['dhcpv6_pd']:
+ vlan.dhcp.v6.options['dhcpv6_pd'] = config['dhcpv6_pd']
+
# update interface description used e.g. within SNMP
vlan.set_alias(config['description'])
# ignore link state changes
@@ -54,6 +104,8 @@ def apply_vlan_config(vlan, config):
vlan.set_arp_ignore(config['ip_enable_arp_ignore'])
# configure Proxy ARP
vlan.set_proxy_arp(config['ip_proxy_arp'])
+ # IPv6 accept RA
+ vlan.set_ipv6_accept_ra(config['ipv6_accept_ra'])
# IPv6 address autoconfiguration
vlan.set_ipv6_autoconf(config['ipv6_autoconf'])
# IPv6 forwarding
@@ -63,13 +115,23 @@ def apply_vlan_config(vlan, config):
# Maximum Transmission Unit (MTU)
vlan.set_mtu(config['mtu'])
- # assign/remove VRF
- vlan.set_vrf(config['vrf'])
+ # assign/remove VRF (ONLY when not a member of a bridge,
+ # otherwise 'nomaster' removes it from it)
+ if not config['is_bridge_member']:
+ vlan.set_vrf(config['vrf'])
+
+ # Delete old IPv6 EUI64 addresses before changing MAC
+ for addr in config['ipv6_eui64_prefix_remove']:
+ vlan.del_ipv6_eui64_address(addr)
# Change VLAN interface MAC address
if config['mac']:
vlan.set_mac(config['mac'])
+ # Add IPv6 EUI-based addresses
+ for addr in config['ipv6_eui64_prefix']:
+ vlan.add_ipv6_eui64_address(addr)
+
# enable/disable VLAN interface
if config['disable']:
vlan.set_admin_state('down')
@@ -84,46 +146,97 @@ def apply_vlan_config(vlan, config):
for addr in config['address']:
vlan.add_addr(addr)
+ # re-add ourselves to any bridge we might have fallen out of
+ if config['is_bridge_member']:
+ vlan.add_to_bridge(config['is_bridge_member'])
+
def verify_vlan_config(config):
"""
Generic function to verify VLAN config consistency. Instead of re-
implementing this function in multiple places use single source \o/
"""
- for vif in config['vif']:
+ # config['vif'] is a dict with ids as keys and config dicts as values
+ for vif in config['vif'].values():
# DHCPv6 parameters-only and temporary address are mutually exclusive
if vif['dhcpv6_prm_only'] and vif['dhcpv6_temporary']:
raise ConfigError('DHCPv6 temporary and parameters-only options are mutually exclusive!')
- vrf_name = vif['vrf']
- if vrf_name and vrf_name not in interfaces():
- raise ConfigError(f'VRF "{vrf_name}" does not exist')
+ if ( vif['is_bridge_member']
+ and ( vif['address']
+ or vif['ipv6_eui64_prefix']
+ or vif['ipv6_autoconf'] ) ):
+ raise ConfigError((
+ f'Cannot assign address to vif interface {vif["intf"]} '
+ f'which is a member of bridge {vif["is_bridge_member"]}'))
+
+ if vif['vrf']:
+ if vif['vrf'] not in interfaces():
+ raise ConfigError(f'VRF "{vif["vrf"]}" does not exist')
+
+ if vif['is_bridge_member']:
+ raise ConfigError((
+ f'vif {vif["intf"]} cannot be member of VRF {vif["vrf"]} '
+ f'and bridge {vif["is_bridge_member"]} at the same time!'))
# e.g. wireless interface has no vif_s support
# thus we bail out eraly.
if 'vif_s' not in config.keys():
return
- for vif_s in config['vif_s']:
- for vif in config['vif']:
- if vif['id'] == vif_s['id']:
- raise ConfigError('Can not use identical ID on vif and vif-s interface')
+ # config['vif_s'] is a dict with ids as keys and config dicts as values
+ for vif_s_id, vif_s in config['vif_s'].items():
+ for vif_id, vif in config['vif'].items():
+ if vif_id == vif_s_id:
+ raise ConfigError((
+ f'Cannot use identical ID on vif "{vif["intf"]}" '
+ f'and vif-s "{vif_s["intf"]}"'))
# DHCPv6 parameters-only and temporary address are mutually exclusive
if vif_s['dhcpv6_prm_only'] and vif_s['dhcpv6_temporary']:
- raise ConfigError('DHCPv6 temporary and parameters-only options are mutually exclusive!')
-
- vrf_name = vif_s['vrf']
- if vrf_name and vrf_name not in interfaces():
- raise ConfigError(f'VRF "{vrf_name}" does not exist')
-
- for vif_c in vif_s['vif_c']:
+ raise ConfigError((
+ 'DHCPv6 temporary and parameters-only options are mutually '
+ 'exclusive!'))
+
+ if ( vif_s['is_bridge_member']
+ and ( vif_s['address']
+ or vif_s['ipv6_eui64_prefix']
+ or vif_s['ipv6_autoconf'] ) ):
+ raise ConfigError((
+ f'Cannot assign address to vif-s interface {vif_s["intf"]} '
+ f'which is a member of bridge {vif_s["is_bridge_member"]}'))
+
+ if vif_s['vrf']:
+ if vif_s['vrf'] not in interfaces():
+ raise ConfigError(f'VRF "{vif_s["vrf"]}" does not exist')
+
+ if vif_s['is_bridge_member']:
+ raise ConfigError((
+ f'vif-s {vif_s["intf"]} cannot be member of VRF {vif_s["vrf"]} '
+ f'and bridge {vif_s["is_bridge_member"]} at the same time!'))
+
+ # vif_c is a dict with ids as keys and config dicts as values
+ for vif_c in vif_s['vif_c'].values():
# DHCPv6 parameters-only and temporary address are mutually exclusive
if vif_c['dhcpv6_prm_only'] and vif_c['dhcpv6_temporary']:
- raise ConfigError('DHCPv6 temporary and parameters-only options are mutually exclusive!')
-
- vrf_name = vif_c['vrf']
- if vrf_name and vrf_name not in interfaces():
- raise ConfigError(f'VRF "{vrf_name}" does not exist')
-
+ raise ConfigError((
+ 'DHCPv6 temporary and parameters-only options are '
+ 'mutually exclusive!'))
+
+ if ( vif_c['is_bridge_member']
+ and ( vif_c['address']
+ or vif_c['ipv6_eui64_prefix']
+ or vif_c['ipv6_autoconf'] ) ):
+ raise ConfigError((
+ f'Cannot assign address to vif-c interface {vif_c["intf"]} '
+ f'which is a member of bridge {vif_c["is_bridge_member"]}'))
+
+ if vif_c['vrf']:
+ if vif_c['vrf'] not in interfaces():
+ raise ConfigError(f'VRF "{vif_c["vrf"]}" does not exist')
+
+ if vif_c['is_bridge_member']:
+ raise ConfigError((
+ f'vif-c {vif_c["intf"]} cannot be member of VRF {vif_c["vrf"]} '
+ f'and bridge {vif_c["is_bridge_member"]} at the same time!'))
diff --git a/python/vyos/template.py b/python/vyos/template.py
index 6c73ce753..e4b253ed3 100644
--- a/python/vyos/template.py
+++ b/python/vyos/template.py
@@ -19,6 +19,7 @@ from jinja2 import Environment
from jinja2 import FileSystemLoader
from vyos.defaults import directories
+from vyos.util import chmod, chown, makedir
# reuse the same Environment to improve performance
@@ -32,7 +33,7 @@ _templates_mem = {
}
-def render(destination, template, content, trim_blocks=False, formater=None):
+def render(destination, template, content, trim_blocks=False, formater=None, permission=None, user=None, group=None):
"""
render a template from the template directory, it will raise on any errors
destination: the file where the rendered template must be saved
@@ -46,6 +47,10 @@ def render(destination, template, content, trim_blocks=False, formater=None):
(recovering the load time and overhead caused by having the file out of the code)
"""
+ # Create the directory if it does not exists
+ folder = os.path.dirname(destination)
+ makedir(folder, user, group)
+
# Setup a renderer for the given template
# This is cached and re-used for performance
if template not in _templates_mem[trim_blocks]:
@@ -63,3 +68,6 @@ def render(destination, template, content, trim_blocks=False, formater=None):
# Write client config file
with open(destination, 'w') as f:
f.write(content)
+
+ chmod(destination, permission)
+ chown(destination, user, group)
diff --git a/python/vyos/util.py b/python/vyos/util.py
index 4340332d3..381cd0358 100644
--- a/python/vyos/util.py
+++ b/python/vyos/util.py
@@ -14,6 +14,7 @@
# License along with this library. If not, see <http://www.gnu.org/licenses/>.
import os
+import sys
#
# NOTE: Do not import full classes here, move your import to the function
@@ -25,7 +26,7 @@ import os
# which all have slighty different behaviour
from subprocess import Popen, PIPE, STDOUT, DEVNULL
def popen(command, flag='', shell=None, input=None, timeout=None, env=None,
- stdout=PIPE, stderr=None, decode=None):
+ stdout=PIPE, stderr=PIPE, decode='utf-8'):
"""
popen is a wrapper helper aound subprocess.Popen
with it default setting it will return a tuple (out, err)
@@ -48,12 +49,14 @@ def popen(command, flag='', shell=None, input=None, timeout=None, env=None,
- STDOUT, send the data to be merged with stdout
- DEVNULL, discard the output
decode: specify the expected text encoding (utf-8, ascii, ...)
+ the default is explicitely utf-8 which is python's own default
usage:
to get both stdout, and stderr: popen('command', stdout=PIPE, stderr=STDOUT)
to discard stdout and get stderr: popen('command', stdout=DEVNUL, stderr=PIPE)
"""
from vyos import debug
+ from vyos import airbag
# log if the flag is set, otherwise log if command is set
if not debug.enabled(flag):
flag = 'command'
@@ -77,27 +80,39 @@ def popen(command, flag='', shell=None, input=None, timeout=None, env=None,
stdin=stdin, stdout=stdout, stderr=stderr,
env=env, shell=use_shell,
)
- tmp = p.communicate(input, timeout)
- out1 = b''
- out2 = b''
+
+ pipe = p.communicate(input, timeout)
+
+ pipe_out = b''
if stdout == PIPE:
- out1 = tmp[0]
+ pipe_out = pipe[0]
+
+ pipe_err = b''
if stderr == PIPE:
- out2 += tmp[1]
- decoded1 = out1.decode(decode) if decode else out1.decode()
- decoded2 = out2.decode(decode) if decode else out2.decode()
- decoded1 = decoded1.replace('\r\n', '\n').strip()
- decoded2 = decoded2.replace('\r\n', '\n').strip()
- nl = '\n' if decoded1 and decoded2 else ''
- decoded = decoded1 + nl + decoded2
- if decoded:
- ret_msg = f"returned:\n{decoded}"
- debug.message(ret_msg, flag)
- return decoded, p.returncode
+ pipe_err = pipe[1]
+
+ str_out = pipe_out.decode(decode).replace('\r\n', '\n').strip()
+ str_err = pipe_err.decode(decode).replace('\r\n', '\n').strip()
+
+ out_msg = f"returned (out):\n{str_out}"
+ if str_out:
+ debug.message(out_msg, flag)
+
+ if str_err:
+ err_msg = f"returned (err):\n{str_err}"
+ # this message will also be send to syslog via airbag
+ debug.message(err_msg, flag, destination=sys.stderr)
+
+ # should something go wrong, report this too via airbag
+ airbag.noteworthy(cmd_msg)
+ airbag.noteworthy(out_msg)
+ airbag.noteworthy(err_msg)
+
+ return str_out, p.returncode
def run(command, flag='', shell=None, input=None, timeout=None, env=None,
- stdout=DEVNULL, stderr=None, decode=None):
+ stdout=DEVNULL, stderr=PIPE, decode='utf-8'):
"""
A wrapper around vyos.util.popen, which discard the stdout and
will return the error code of a command
@@ -113,14 +128,15 @@ def run(command, flag='', shell=None, input=None, timeout=None, env=None,
def cmd(command, flag='', shell=None, input=None, timeout=None, env=None,
- stdout=PIPE, stderr=None, decode=None,
- raising=None, message=''):
+ stdout=PIPE, stderr=PIPE, decode='utf-8',
+ raising=None, message='', expect=[0]):
"""
A wrapper around vyos.util.popen, which returns the stdout and
will raise the error code of a command
raising: specify which call should be used when raising (default is OSError)
the class should only require a string as parameter
+ expect: a list of error codes to consider as normal
"""
decoded, code = popen(
command, flag,
@@ -129,7 +145,7 @@ def cmd(command, flag='', shell=None, input=None, timeout=None, env=None,
env=env, shell=shell,
decode=decode,
)
- if code != 0:
+ if code not in expect:
feedback = message + '\n' if message else ''
feedback += f'failed to run command: {command}\n'
feedback += f'returned: {decoded}\n'
@@ -143,7 +159,7 @@ def cmd(command, flag='', shell=None, input=None, timeout=None, env=None,
def call(command, flag='', shell=None, input=None, timeout=None, env=None,
- stdout=PIPE, stderr=None, decode=None):
+ stdout=PIPE, stderr=PIPE, decode='utf-8'):
"""
A wrapper around vyos.util.popen, which print the stdout and
will return the error code of a command
@@ -197,10 +213,24 @@ def chown(path, user, group):
from pwd import getpwnam
from grp import getgrnam
- if os.path.exists(path):
- uid = getpwnam(user).pw_uid
- gid = getgrnam(group).gr_gid
- os.chown(path, uid, gid)
+ if user is None or group is None:
+ return False
+
+ if not os.path.exists(path):
+ return False
+
+ uid = getpwnam(user).pw_uid
+ gid = getgrnam(group).gr_gid
+ os.chown(path, uid, gid)
+ return True
+
+
+def chmod(path, bitmask):
+ if not os.path.exists(path):
+ return
+ if bitmask is None:
+ return
+ os.chmod(path, bitmask)
def chmod_600(path):
@@ -231,6 +261,13 @@ def chmod_755(path):
os.chmod(path, bitmask)
+def makedir(path, user=None, group=None):
+ if os.path.exists(path):
+ return
+ os.mkdir(path)
+ chown(path, user, group)
+
+
def colon_separated_to_dict(data_string, uniquekeys=False):
""" Converts a string containing newline-separated entries
of colon-separated key-value pairs into a dict.
@@ -352,12 +389,9 @@ def get_cfg_group_id():
def file_is_persistent(path):
import re
- if not re.match(r'^(/config|/opt/vyatta/etc/config)', os.path.dirname(path)):
- warning = "Warning: file {0} is outside the /config directory\n".format(path)
- warning += "It will not be automatically migrated to a new image on system update"
- return (False, warning)
- else:
- return (True, None)
+ location = r'^(/config|/opt/vyatta/etc/config)'
+ absolute = os.path.abspath(os.path.dirname(path))
+ return re.match(location,absolute)
def commit_in_progress():
@@ -461,3 +495,74 @@ def get_half_cpus():
if cpu > 1:
cpu /= 2
return int(cpu)
+
+def ifname_from_config(conf):
+ """
+ Gets interface name with VLANs from current config level.
+ Level must be at the interface whose name we want.
+
+ Example:
+ >>> from vyos.util import ifname_from_config
+ >>> from vyos.config import Config
+ >>> conf = Config()
+ >>> conf.set_level('interfaces ethernet eth0 vif-s 1 vif-c 2')
+ >>> ifname_from_config(conf)
+ 'eth0.1.2'
+ """
+ level = conf.get_level()
+
+ # vlans
+ if level[-2] == 'vif' or level[-2] == 'vif-s':
+ return level[-3] + '.' + level[-1]
+ if level[-2] == 'vif-c':
+ return level[-5] + '.' + level[-3] + '.' + level[-1]
+
+ # no vlans
+ return level[-1]
+
+def get_bridge_member_config(conf, br, intf):
+ """
+ Gets bridge port (member) configuration
+
+ Arguments:
+ conf: Config
+ br: bridge name
+ intf: interface name
+
+ Returns:
+ dict with the configuration
+ False if bridge or bridge port doesn't exist
+ """
+ old_level = conf.get_level()
+ conf.set_level([])
+
+ bridge = f'interfaces bridge {br}'
+ member = f'{bridge} member interface {intf}'
+ if not ( conf.exists(bridge) and conf.exists(member) ):
+ return False
+
+ # default bridge port configuration
+ # cost and priority initialized with linux defaults
+ # by reading /sys/devices/virtual/net/br0/brif/eth2/{path_cost,priority}
+ # after adding interface to bridge after reboot
+ memberconf = {
+ 'cost': 100,
+ 'priority': 32,
+ 'arp_cache_tmo': 30,
+ 'disable_link_detect': 1,
+ }
+
+ if conf.exists(f'{member} cost'):
+ memberconf['cost'] = int(conf.return_value(f'{member} cost'))
+
+ if conf.exists(f'{member} priority'):
+ memberconf['priority'] = int(conf.return_value(f'{member} priority'))
+
+ if conf.exists(f'{bridge} ip arp-cache-timeout'):
+ memberconf['arp_cache_tmo'] = int(conf.return_value(f'{bridge} ip arp-cache-timeout'))
+
+ if conf.exists(f'{bridge} disable-link-detect'):
+ memberconf['disable_link_detect'] = 2
+
+ conf.set_level(old_level)
+ return memberconf
diff --git a/python/vyos/validate.py b/python/vyos/validate.py
index 446f6e4ca..6304fa8de 100644
--- a/python/vyos/validate.py
+++ b/python/vyos/validate.py
@@ -205,7 +205,7 @@ def assert_number(n):
def assert_positive(n, smaller=0):
assert_number(n)
if int(n) < smaller:
- raise ValueError(f'{n} is smaller than {limit}')
+ raise ValueError(f'{n} is smaller than {smaller}')
def assert_mtu(mtu, min=68, max=9000):
@@ -241,26 +241,66 @@ def assert_mac(m):
if octets[:5] == (0, 0, 94, 0, 1):
raise ValueError(f'{m} is a VRRP MAC address')
-def is_bridge_member(conf, interface):
+def is_member(conf, interface, intftype=None):
"""
- Checks if passed interfaces is part of a bridge device or not.
-
- Returns a tuple:
- None -> Interface not a bridge member
- Bridge -> Interface is a member of this bridge
+ Checks if passed interface is member of other interface of specified type.
+ intftype is optional, if not passed it will search all known types
+ (currently bridge and bonding)
+
+ Returns:
+ None -> Interface is not a member
+ interface name -> Interface is a member of this interface
+ False -> interface type cannot have members
"""
ret_val = None
- old_level = conf.get_level()
+
+ if intftype not in ['bonding', 'bridge', None]:
+ raise ValueError((
+ f'unknown interface type "{intftype}" or it cannot '
+ f'have member interfaces'))
+
+ intftype = ['bonding', 'bridge'] if intftype == None else [intftype]
# set config level to root
+ old_level = conf.get_level()
conf.set_level([])
- base = ['interfaces', 'bridge']
- for bridge in conf.list_nodes(base):
- members = conf.list_nodes(base + [bridge, 'member', 'interface'])
- if interface in members:
- ret_val = bridge
- break
+
+ for it in intftype:
+ base = 'interfaces ' + it
+ for intf in conf.list_nodes(base):
+ memberintf = f'{base} {intf} member interface'
+ if conf.is_tag(memberintf):
+ if interface in conf.list_nodes(memberintf):
+ ret_val = intf
+ break
+ elif conf.is_leaf(memberintf):
+ if ( conf.exists(memberintf) and
+ interface in conf.return_values(memberintf) ):
+ ret_val = intf
+ break
old_level = conf.set_level(old_level)
return ret_val
+def has_address_configured(conf, intf):
+ """
+ Checks if interface has an address configured.
+ Checks the following config nodes:
+ 'address', 'ipv6 address eui64', 'ipv6 address autoconf'
+
+ Returns True if interface has address configured, False if it doesn't.
+ """
+ from vyos.ifconfig import Section
+ ret = False
+
+ old_level = conf.get_level()
+ conf.set_level([])
+
+ intfpath = 'interfaces ' + Section.get_config_path(intf)
+ if ( conf.exists(f'{intfpath} address') or
+ conf.exists(f'{intfpath} ipv6 address autoconf') or
+ conf.exists(f'{intfpath} ipv6 address eui64') ):
+ ret = True
+
+ conf.set_level(old_level)
+ return ret
diff --git a/schema/op-mode-definition.rnc b/schema/op-mode-definition.rnc
index 9c84de0e4..cbe51e6dc 100644
--- a/schema/op-mode-definition.rnc
+++ b/schema/op-mode-definition.rnc
@@ -24,7 +24,7 @@
# Interface definition starts with interfaceDefinition tag that may contain node tags
start = element interfaceDefinition
{
- node*
+ (node | tagNode)*
}
# node tag may contain node, leafNode, or tagNode tags
@@ -43,7 +43,7 @@ node = element node
tagNode = element tagNode
{
nodeNameAttr,
- (properties? & children & command?)
+ (properties? & children? & command?)
}
# Leaf nodes are terminal configuration nodes that can't have children,
diff --git a/schema/op-mode-definition.rng b/schema/op-mode-definition.rng
index e9e7887cf..900f41e27 100644
--- a/schema/op-mode-definition.rng
+++ b/schema/op-mode-definition.rng
@@ -29,7 +29,10 @@
<start>
<element name="interfaceDefinition">
<zeroOrMore>
- <ref name="node"/>
+ <choice>
+ <ref name="node"/>
+ <ref name="tagNode"/>
+ </choice>
</zeroOrMore>
</element>
</start>
diff --git a/scripts/build-command-templates b/scripts/build-command-templates
index c6534a6d8..767517b29 100755
--- a/scripts/build-command-templates
+++ b/scripts/build-command-templates
@@ -149,7 +149,7 @@ def get_properties(p):
regex_args = " ".join(map(lambda s: "--regex \\\'{0}\\\'".format(s), regexes))
validator_args = " ".join(map(lambda s: "--exec \\\"{0}\\\"".format(s), validators))
- validator_script = '${vyos_libexec_dir}/validate-value.py'
+ validator_script = '${vyos_libexec_dir}/validate-value'
validator_string = "exec \"{0} {1} {2} --value \\\'$VAR(@)\\\'\"; \"{3}\"".format(validator_script, regex_args, validator_args, error_msg)
props["constraint"] = validator_string
diff --git a/src/conf_mode/dhcp_relay.py b/src/conf_mode/dhcp_relay.py
index ce0e01308..d24a46220 100755
--- a/src/conf_mode/dhcp_relay.py
+++ b/src/conf_mode/dhcp_relay.py
@@ -98,11 +98,6 @@ def generate(relay):
if not relay:
return None
- # Create configuration directory on demand
- dirname = os.path.dirname(config_file)
- if not os.path.isdir(dirname):
- os.mkdir(dirname)
-
render(config_file, 'dhcp-relay/config.tmpl', relay)
return None
diff --git a/src/conf_mode/dhcp_server.py b/src/conf_mode/dhcp_server.py
index da01f16eb..1849ece0a 100755
--- a/src/conf_mode/dhcp_server.py
+++ b/src/conf_mode/dhcp_server.py
@@ -594,11 +594,6 @@ def generate(dhcp):
if not dhcp or dhcp['disabled']:
return None
- # Create configuration directory on demand
- dirname = os.path.dirname(config_file)
- if not os.path.isdir(dirname):
- os.mkdir(dirname)
-
# Please see: https://phabricator.vyos.net/T1129 for quoting of the raw parameters
# we can pass to ISC DHCPd
render(config_file, 'dhcp-server/dhcpd.conf.tmpl', dhcp,
diff --git a/src/conf_mode/dhcpv6_relay.py b/src/conf_mode/dhcpv6_relay.py
index cb5a4bbfb..ecc739063 100755
--- a/src/conf_mode/dhcpv6_relay.py
+++ b/src/conf_mode/dhcpv6_relay.py
@@ -84,11 +84,6 @@ def generate(relay):
if relay is None:
return None
- # Create configuration directory on demand
- dirname = os.path.dirname(config_file)
- if not os.path.isdir(dirname):
- os.mkdir(dirname)
-
render(config_file, 'dhcpv6-relay/config.tmpl', relay)
return None
diff --git a/src/conf_mode/dhcpv6_server.py b/src/conf_mode/dhcpv6_server.py
index ce98e39c3..9e24ee591 100755
--- a/src/conf_mode/dhcpv6_server.py
+++ b/src/conf_mode/dhcpv6_server.py
@@ -23,7 +23,7 @@ from copy import deepcopy
from vyos.config import Config
from vyos.template import render
from vyos.util import call
-from vyos.validate import is_subnet_connected
+from vyos.validate import is_subnet_connected, is_ipv6
from vyos import ConfigError
config_file = r'/run/dhcp-server/dhcpdv6.conf'
@@ -37,24 +37,25 @@ default_config_data = {
def get_config():
dhcpv6 = deepcopy(default_config_data)
conf = Config()
- if not conf.exists('service dhcpv6-server'):
+ base = ['service', 'dhcpv6-server']
+ if not conf.exists(base):
return None
else:
- conf.set_level('service dhcpv6-server')
+ conf.set_level(base)
# Check for global disable of DHCPv6 service
- if conf.exists('disable'):
+ if conf.exists(['disable']):
dhcpv6['disabled'] = True
return dhcpv6
# Preference of this DHCPv6 server compared with others
- if conf.exists('preference'):
- dhcpv6['preference'] = conf.return_value('preference')
+ if conf.exists(['preference']):
+ dhcpv6['preference'] = conf.return_value(['preference'])
# check for multiple, shared networks served with DHCPv6 addresses
- if conf.exists('shared-network-name'):
- for network in conf.list_nodes('shared-network-name'):
- conf.set_level('service dhcpv6-server shared-network-name {0}'.format(network))
+ if conf.exists(['shared-network-name']):
+ for network in conf.list_nodes(['shared-network-name']):
+ conf.set_level(base + ['shared-network-name', network])
config = {
'name': network,
'disabled': False,
@@ -62,13 +63,13 @@ def get_config():
}
# If disabled, the shared-network configuration becomes inactive
- if conf.exists('disable'):
+ if conf.exists(['disable']):
config['disabled'] = True
# check for multiple subnet configurations in a shared network
- if conf.exists('subnet'):
- for net in conf.list_nodes('subnet'):
- conf.set_level('service dhcpv6-server shared-network-name {0} subnet {1}'.format(network, net))
+ if conf.exists(['subnet']):
+ for net in conf.list_nodes(['subnet']):
+ conf.set_level(base + ['shared-network-name', network, 'subnet', net])
subnet = {
'network': net,
'range6_prefix': [],
@@ -84,6 +85,7 @@ def get_config():
'nis_server': [],
'nisp_domain': '',
'nisp_server': [],
+ 'prefix_delegation': [],
'sip_address': [],
'sip_hostname': [],
'sntp_server': [],
@@ -94,25 +96,25 @@ def get_config():
# least one address range statement. The range statement gives the lowest and highest
# IP addresses in a range. All IP addresses in the range should be in the subnet in
# which the range statement is declared.
- if conf.exists('address-range prefix'):
- for prefix in conf.list_nodes('address-range prefix'):
+ if conf.exists(['address-range', 'prefix']):
+ for prefix in conf.list_nodes(['address-range', 'prefix']):
range = {
'prefix': prefix,
'temporary': False
}
# Address range will be used for temporary addresses
- if conf.exists('address-range prefix {0} temporary'.format(range['prefix'])):
+ if conf.exists(['address-range' 'prefix', prefix, 'temporary']):
range['temporary'] = True
# Append to subnet temporary range6 list
subnet['range6_prefix'].append(range)
- if conf.exists('address-range start'):
- for range in conf.list_nodes('address-range start'):
+ if conf.exists(['address-range', 'start']):
+ for range in conf.list_nodes(['address-range', 'start']):
range = {
'start': range,
- 'stop': conf.return_value('address-range start {0} stop'.format(range))
+ 'stop': conf.return_value(['address-range', 'start', range, 'stop'])
}
# Append to subnet range6 list
@@ -120,70 +122,83 @@ def get_config():
# The domain-search option specifies a 'search list' of Domain Names to be used
# by the client to locate not-fully-qualified domain names.
- if conf.exists('domain-search'):
- for domain in conf.return_values('domain-search'):
- subnet['domain_search'].append('"' + domain + '"')
+ if conf.exists(['domain-search']):
+ subnet['domain_search'] = conf.return_values(['domain-search'])
# IPv6 address valid lifetime
# (at the end the address is no longer usable by the client)
# (set to 30 days, the usual IPv6 default)
- if conf.exists('lease-time default'):
- subnet['lease_def'] = conf.return_value('lease-time default')
+ if conf.exists(['lease-time', 'default']):
+ subnet['lease_def'] = conf.return_value(['lease-time', 'default'])
# Time should be the maximum length in seconds that will be assigned to a lease.
# The only exception to this is that Dynamic BOOTP lease lengths, which are not
# specified by the client, are not limited by this maximum.
- if conf.exists('lease-time maximum'):
- subnet['lease_max'] = conf.return_value('lease-time maximum')
+ if conf.exists(['lease-time', 'maximum']):
+ subnet['lease_max'] = conf.return_value(['lease-time', 'maximum'])
# Time should be the minimum length in seconds that will be assigned to a lease
- if conf.exists('lease-time minimum'):
- subnet['lease_min'] = conf.return_value('lease-time minimum')
+ if conf.exists(['lease-time', 'minimum']):
+ subnet['lease_min'] = conf.return_value(['lease-time', 'minimum'])
# Specifies a list of Domain Name System name servers available to the client.
# Servers should be listed in order of preference.
- if conf.exists('name-server'):
- subnet['dns_server'] = conf.return_values('name-server')
+ if conf.exists(['name-server']):
+ subnet['dns_server'] = conf.return_values(['name-server'])
# Ancient NIS (Network Information Service) domain name
- if conf.exists('nis-domain'):
- subnet['nis_domain'] = conf.return_value('nis-domain')
+ if conf.exists(['nis-domain']):
+ subnet['nis_domain'] = conf.return_value(['nis-domain'])
# Ancient NIS (Network Information Service) servers
- if conf.exists('nis-server'):
- subnet['nis_server'] = conf.return_values('nis-server')
+ if conf.exists(['nis-server']):
+ subnet['nis_server'] = conf.return_values(['nis-server'])
# Ancient NIS+ (Network Information Service) domain name
- if conf.exists('nisplus-domain'):
- subnet['nisp_domain'] = conf.return_value('nisplus-domain')
+ if conf.exists(['nisplus-domain']):
+ subnet['nisp_domain'] = conf.return_value(['nisplus-domain'])
# Ancient NIS+ (Network Information Service) servers
- if conf.exists('nisplus-server'):
- subnet['nisp_server'] = conf.return_values('nisplus-server')
+ if conf.exists(['nisplus-server']):
+ subnet['nisp_server'] = conf.return_values(['nisplus-server'])
+
+ # Local SIP server that is to be used for all outbound SIP requests - IPv6 address
+ if conf.exists(['sip-server']):
+ for value in conf.return_values(['sip-server']):
+ if is_ipv6(value):
+ subnet['sip_address'].append(value)
+ else:
+ subnet['sip_hostname'].append(value)
+
+ # List of local SNTP servers available for the client to synchronize their clocks
+ if conf.exists(['sntp-server']):
+ subnet['sntp_server'] = conf.return_values(['sntp-server'])
# Prefix Delegation (RFC 3633)
- if conf.exists('prefix-delegation'):
- print('TODO: This option is actually not implemented right now!')
+ if conf.exists(['prefix-delegation', 'start']):
+ for address in conf.list_nodes(['prefix-delegation', 'start']):
+ conf.set_level(base + ['shared-network-name', network, 'subnet', net, 'prefix-delegation', 'start', address])
+ prefix = {
+ 'start' : address,
+ 'stop' : '',
+ 'length' : ''
+ }
- # Local SIP server that is to be used for all outbound SIP requests - IPv6 address
- if conf.exists('sip-server-address'):
- subnet['sip_address'] = conf.return_values('sip-server-address')
+ if conf.exists(['prefix-length']):
+ prefix['length'] = conf.return_value(['prefix-length'])
- # Local SIP server that is to be used for all outbound SIP requests - hostname
- if conf.exists('sip-server-name'):
- for hostname in conf.return_values('sip-server-name'):
- subnet['sip_hostname'].append('"' + hostname + '"')
+ if conf.exists(['stop']):
+ prefix['stop'] = conf.return_value(['stop'])
- # List of local SNTP servers available for the client to synchronize their clocks
- if conf.exists('sntp-server'):
- subnet['sntp_server'] = conf.return_values('sntp-server')
+ subnet['prefix_delegation'].append(prefix)
#
# Static DHCP v6 leases
#
- if conf.exists('static-mapping'):
- for mapping in conf.list_nodes('static-mapping'):
- conf.set_level('service dhcpv6-server shared-network-name {0} subnet {1} static-mapping {2}'.format(network, net, mapping))
+ conf.set_level(base + ['shared-network-name', network, 'subnet', net])
+ if conf.exists(['static-mapping']):
+ for mapping in conf.list_nodes(['static-mapping']):
+ conf.set_level(base + ['shared-network-name', network, 'subnet', net, 'static-mapping', mapping])
mapping = {
'name': mapping,
'disabled': False,
@@ -192,16 +207,16 @@ def get_config():
}
# This static lease is disabled
- if conf.exists('disable'):
+ if conf.exists(['disable']):
mapping['disabled'] = True
# IPv6 address used for this DHCP client
- if conf.exists('ipv6-address'):
- mapping['ipv6_address'] = conf.return_value('ipv6-address')
+ if conf.exists(['ipv6-address']):
+ mapping['ipv6_address'] = conf.return_value(['ipv6-address'])
# This option specifies the client’s DUID identifier. DUIDs are similar but different from DHCPv4 client identifiers
- if conf.exists('identifier'):
- mapping['client_identifier'] = conf.return_value('identifier')
+ if conf.exists(['identifier']):
+ mapping['client_identifier'] = conf.return_value(['identifier'])
# append static mapping configuration tu subnet list
subnet['static_mapping'].append(mapping)
@@ -209,7 +224,6 @@ def get_config():
# append subnet configuration to shared network subnet list
config['subnet'].append(subnet)
-
# append shared network configuration to config dictionary
dhcpv6['shared_network'].append(config)
@@ -282,6 +296,14 @@ def verify(dhcpv6):
else:
range6_stop.append(stop)
+ # Prefix delegation sanity checks
+ for prefix in subnet['prefix_delegation']:
+ if not prefix['stop']:
+ raise ConfigError('Stop address of delegated IPv6 prefix range must be configured')
+
+ if not prefix['length']:
+ raise ConfigError('Length of delegated IPv6 prefix must be configured')
+
# We also have prefixes that require checking
for prefix in subnet['range6_prefix']:
# If configured prefix does not match our subnet, we have to check that it's inside
@@ -335,11 +357,6 @@ def generate(dhcpv6):
if not dhcpv6 or dhcpv6['disabled']:
return None
- # Create configuration directory on demand
- dirname = os.path.dirname(config_file)
- if not os.path.isdir(dirname):
- os.mkdir(dirname)
-
render(config_file, 'dhcpv6-server/dhcpdv6.conf.tmpl', dhcpv6)
return None
diff --git a/src/conf_mode/dns_forwarding.py b/src/conf_mode/dns_forwarding.py
index 567dfa4b3..f87c198f7 100755
--- a/src/conf_mode/dns_forwarding.py
+++ b/src/conf_mode/dns_forwarding.py
@@ -152,11 +152,7 @@ def generate(dns):
if dns is None:
return None
- dirname = os.path.dirname(config_file)
- if not os.path.exists(dirname):
- os.mkdir(dirname)
-
- render(config_file, 'dns-forwarding/recursor.conf.tmpl', dns, trim_blocks=True)
+ render(config_file, 'dns-forwarding/recursor.conf.tmpl', dns, trim_blocks=True, user='pdns', group='pdns')
return None
def apply(dns):
diff --git a/src/conf_mode/dynamic_dns.py b/src/conf_mode/dynamic_dns.py
index 038f77cf9..3386324ae 100755
--- a/src/conf_mode/dynamic_dns.py
+++ b/src/conf_mode/dynamic_dns.py
@@ -217,10 +217,6 @@ def generate(dyndns):
if dyndns['deleted']:
return None
- dirname = os.path.dirname(config_file)
- if not os.path.exists(dirname):
- os.mkdir(dirname)
-
render(config_file, 'dynamic-dns/ddclient.conf.tmpl', dyndns)
# Config file must be accessible only by its owner
diff --git a/src/conf_mode/flow_accounting_conf.py b/src/conf_mode/flow_accounting_conf.py
index 11df81b1d..d691e6abd 100755
--- a/src/conf_mode/flow_accounting_conf.py
+++ b/src/conf_mode/flow_accounting_conf.py
@@ -281,7 +281,7 @@ def verify(config):
# check if configured netflow source-ip exist in the system
if config['netflow']['source-ip']:
source_ip_presented = None
- for iface in Interface.listing():
+ for iface in Section.interfaces():
for address in Interface(iface).get_addr():
# check an IP
regex_filter = re.compile('^(?!(127)|(::1)|(fe80))(?P<ipaddr>[a-f\d\.:]+)/\d+$')
diff --git a/src/conf_mode/interfaces-bonding.py b/src/conf_mode/interfaces-bonding.py
index 380457772..bdca9d170 100755
--- a/src/conf_mode/interfaces-bonding.py
+++ b/src/conf_mode/interfaces-bonding.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2019 VyOS maintainers and contributors
+# Copyright (C) 2019-2020 VyOS maintainers and contributors
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License version 2 or later as
@@ -21,52 +21,30 @@ from sys import exit
from netifaces import interfaces
from vyos.ifconfig import BondIf
-from vyos.ifconfig_vlan import apply_vlan_config, verify_vlan_config
-from vyos.configdict import list_diff, vlan_to_dict
+from vyos.ifconfig_vlan import apply_all_vlans, verify_vlan_config
+from vyos.configdict import list_diff, intf_to_dict, add_to_dict, interface_default_data
from vyos.config import Config
-from vyos.util import call
-from vyos.validate import is_bridge_member
+from vyos.util import call, cmd
+from vyos.validate import is_member, has_address_configured
from vyos import ConfigError
default_config_data = {
- 'address': [],
- 'address_remove': [],
+ **interface_default_data,
'arp_mon_intvl': 0,
'arp_mon_tgt': [],
- 'description': '',
'deleted': False,
- 'dhcp_client_id': '',
- 'dhcp_hostname': '',
- 'dhcp_vendor_class_id': '',
- 'dhcpv6_prm_only': False,
- 'dhcpv6_temporary': False,
- 'disable': False,
- 'disable_link_detect': 1,
'hash_policy': 'layer2',
'intf': '',
'ip_arp_cache_tmo': 30,
- 'ip_disable_arp_filter': 1,
- 'ip_enable_arp_accept': 0,
- 'ip_enable_arp_announce': 0,
- 'ip_enable_arp_ignore': 0,
- 'ip_proxy_arp': 0,
'ip_proxy_arp_pvlan': 0,
- 'ipv6_autoconf': 0,
- 'ipv6_eui64_prefix': '',
- 'ipv6_forwarding': 1,
- 'ipv6_dup_addr_detect': 1,
- 'is_bridge_member': False,
- 'mac': '',
'mode': '802.3ad',
'member': [],
'shutdown_required': False,
- 'mtu': 1500,
'primary': '',
- 'vif_s': [],
+ 'vif_s': {},
'vif_s_remove': [],
- 'vif': [],
+ 'vif': {},
'vif_remove': [],
- 'vrf': ''
}
@@ -89,6 +67,13 @@ def get_bond_mode(mode):
raise ConfigError('invalid bond mode "{}"'.format(mode))
def get_config():
+ # determine tagNode instance
+ if 'VYOS_TAGNODE_VALUE' not in os.environ:
+ raise ConfigError('Interface (VYOS_TAGNODE_VALUE) not specified')
+
+ ifname = os.environ['VYOS_TAGNODE_VALUE']
+ conf = Config()
+
# initialize kernel module if not loaded
if not os.path.isfile('/sys/class/net/bonding_masters'):
import syslog
@@ -97,34 +82,18 @@ def get_config():
syslog.syslog(syslog.LOG_NOTICE, "failed loading bonding kernel module")
raise ConfigError("failed loading bonding kernel module")
- bond = deepcopy(default_config_data)
- conf = Config()
-
- # determine tagNode instance
- if 'VYOS_TAGNODE_VALUE' not in os.environ:
- raise ConfigError('Interface (VYOS_TAGNODE_VALUE) not specified')
-
- bond['intf'] = os.environ['VYOS_TAGNODE_VALUE']
-
# check if bond has been removed
- cfg_base = 'interfaces bonding ' + bond['intf']
+ cfg_base = 'interfaces bonding ' + ifname
if not conf.exists(cfg_base):
+ bond = deepcopy(default_config_data)
+ bond['intf'] = ifname
bond['deleted'] = True
- # check if interface is member if a bridge
- bond['is_bridge_member'] = is_bridge_member(conf, bond['intf'])
return bond
# set new configuration level
conf.set_level(cfg_base)
- # retrieve configured interface addresses
- if conf.exists('address'):
- bond['address'] = conf.return_values('address')
-
- # get interface addresses (currently effective) - to determine which
- # address is no longer valid and needs to be removed
- eff_addr = conf.return_effective_values('address')
- bond['address_remove'] = list_diff(eff_addr, bond['address'])
+ bond, disabled = intf_to_dict(conf, default_config_data)
# ARP link monitoring frequency in milliseconds
if conf.exists('arp-monitor interval'):
@@ -134,38 +103,6 @@ def get_config():
if conf.exists('arp-monitor target'):
bond['arp_mon_tgt'] = conf.return_values('arp-monitor target')
- # retrieve interface description
- if conf.exists('description'):
- bond['description'] = conf.return_value('description')
-
- # get DHCP client identifier
- if conf.exists('dhcp-options client-id'):
- bond['dhcp_client_id'] = conf.return_value('dhcp-options client-id')
-
- # DHCP client host name (overrides the system host name)
- if conf.exists('dhcp-options host-name'):
- bond['dhcp_hostname'] = conf.return_value('dhcp-options host-name')
-
- # DHCP client vendor identifier
- if conf.exists('dhcp-options vendor-class-id'):
- bond['dhcp_vendor_class_id'] = conf.return_value('dhcp-options vendor-class-id')
-
- # DHCPv6 only acquire config parameters, no address
- if conf.exists('dhcpv6-options parameters-only'):
- bond['dhcpv6_prm_only'] = True
-
- # DHCPv6 temporary IPv6 address
- if conf.exists('dhcpv6-options temporary'):
- bond['dhcpv6_temporary'] = True
-
- # ignore link state changes
- if conf.exists('disable-link-detect'):
- bond['disable_link_detect'] = 2
-
- # disable bond interface
- if conf.exists('disable'):
- bond['disable'] = True
-
# Bonding transmit hash policy
if conf.exists('hash-policy'):
bond['hash_policy'] = conf.return_value('hash-policy')
@@ -174,50 +111,10 @@ def get_config():
if conf.exists('ip arp-cache-timeout'):
bond['ip_arp_cache_tmo'] = int(conf.return_value('ip arp-cache-timeout'))
- # ARP filter configuration
- if conf.exists('ip disable-arp-filter'):
- bond['ip_disable_arp_filter'] = 0
-
- # ARP enable accept
- if conf.exists('ip enable-arp-accept'):
- bond['ip_enable_arp_accept'] = 1
-
- # ARP enable announce
- if conf.exists('ip enable-arp-announce'):
- bond['ip_enable_arp_announce'] = 1
-
- # ARP enable ignore
- if conf.exists('ip enable-arp-ignore'):
- bond['ip_enable_arp_ignore'] = 1
-
- # Enable proxy-arp on this interface
- if conf.exists('ip enable-proxy-arp'):
- bond['ip_proxy_arp'] = 1
-
# Enable private VLAN proxy ARP on this interface
if conf.exists('ip proxy-arp-pvlan'):
bond['ip_proxy_arp_pvlan'] = 1
- # Enable acquisition of IPv6 address using stateless autoconfig (SLAAC)
- if conf.exists('ipv6 address autoconf'):
- bond['ipv6_autoconf'] = 1
-
- # Get prefix for IPv6 addressing based on MAC address (EUI-64)
- if conf.exists('ipv6 address eui64'):
- bond['ipv6_eui64_prefix'] = conf.return_value('ipv6 address eui64')
-
- # Disable IPv6 forwarding on this interface
- if conf.exists('ipv6 disable-forwarding'):
- bond['ipv6_forwarding'] = 0
-
- # IPv6 Duplicate Address Detection (DAD) tries
- if conf.exists('ipv6 dup-addr-detect-transmits'):
- bond['ipv6_dup_addr_detect'] = int(conf.return_value('ipv6 dup-addr-detect-transmits'))
-
- # Media Access Control (MAC) address
- if conf.exists('mac'):
- bond['mac'] = conf.return_value('mac')
-
# Bonding mode
if conf.exists('mode'):
act_mode = conf.return_value('mode')
@@ -227,10 +124,6 @@ def get_config():
bond['mode'] = get_bond_mode(act_mode)
- # Maximum Transmission Unit (MTU)
- if conf.exists('mtu'):
- bond['mtu'] = int(conf.return_value('mtu'))
-
# determine bond member interfaces (currently configured)
if conf.exists('member interface'):
bond['member'] = conf.return_values('member interface')
@@ -247,35 +140,8 @@ def get_config():
if conf.exists('primary'):
bond['primary'] = conf.return_value('primary')
- # retrieve VRF instance
- if conf.exists('vrf'):
- bond['vrf'] = conf.return_value('vrf')
-
- # get vif-s interfaces (currently effective) - to determine which vif-s
- # interface is no longer present and needs to be removed
- eff_intf = conf.list_effective_nodes('vif-s')
- act_intf = conf.list_nodes('vif-s')
- bond['vif_s_remove'] = list_diff(eff_intf, act_intf)
-
- if conf.exists('vif-s'):
- for vif_s in conf.list_nodes('vif-s'):
- # set config level to vif-s interface
- conf.set_level(cfg_base + ' vif-s ' + vif_s)
- bond['vif_s'].append(vlan_to_dict(conf))
-
- # re-set configuration level to parse new nodes
- conf.set_level(cfg_base)
- # Determine vif interfaces (currently effective) - to determine which
- # vif interface is no longer present and needs to be removed
- eff_intf = conf.list_effective_nodes('vif')
- act_intf = conf.list_nodes('vif')
- bond['vif_remove'] = list_diff(eff_intf, act_intf)
-
- if conf.exists('vif'):
- for vif in conf.list_nodes('vif'):
- # set config level to vif interface
- conf.set_level(cfg_base + ' vif ' + vif)
- bond['vif'].append(vlan_to_dict(conf))
+ add_to_dict(conf, disabled, bond, 'vif', 'vif')
+ add_to_dict(conf, disabled, bond, 'vif-s', 'vif_s')
return bond
@@ -283,22 +149,38 @@ def get_config():
def verify(bond):
if bond['deleted']:
if bond['is_bridge_member']:
- interface = bond['intf']
- bridge = bond['is_bridge_member']
- raise ConfigError(f'Interface "{interface}" can not be deleted as it belongs to bridge "{bridge}"!')
+ raise ConfigError((
+ f'Cannot delete interface "{bond["intf"]}" as it is a '
+ f'member of bridge "{bond["is_bridge_member"]}"!'))
+
return None
- if len (bond['arp_mon_tgt']) > 16:
- raise ConfigError('The maximum number of targets that can be specified is 16')
+ if len(bond['arp_mon_tgt']) > 16:
+ raise ConfigError('The maximum number of arp-monitor targets is 16')
if bond['primary']:
if bond['mode'] not in ['active-backup', 'balance-tlb', 'balance-alb']:
- raise ConfigError('Mode dependency failed, primary not supported ' \
- 'in mode "{}"!'.format(bond['mode']))
+ raise ConfigError((
+ 'Mode dependency failed, primary not supported in mode '
+ f'"{bond["mode"]}"!'))
+
+ if ( bond['is_bridge_member']
+ and ( bond['address']
+ or bond['ipv6_eui64_prefix']
+ or bond['ipv6_autoconf'] ) ):
+ raise ConfigError((
+ f'Cannot assign address to interface "{bond["intf"]}" '
+ f'as it is a member of bridge "{bond["is_bridge_member"]}"!'))
+
+ if bond['vrf']:
+ if bond['vrf'] not in interfaces():
+ raise ConfigError(f'VRF "{bond["vrf"]}" does not exist')
- vrf_name = bond['vrf']
- if vrf_name and vrf_name not in interfaces():
- raise ConfigError(f'VRF "{vrf_name}" does not exist')
+ if bond['is_bridge_member']:
+ raise ConfigError((
+ f'Interface "{bond["intf"]}" cannot be member of VRF '
+ f'"{bond["vrf"]}" and bridge {bond["is_bridge_member"]} '
+ f'at the same time!'))
# use common function to verify VLAN configuration
verify_vlan_config(bond)
@@ -307,51 +189,55 @@ def verify(bond):
for intf in bond['member']:
# check if member interface is "real"
if intf not in interfaces():
- raise ConfigError('interface {} does not exist!'.format(intf))
+ raise ConfigError(f'Interface {intf} does not exist!')
# a bonding member interface is only allowed to be assigned to one bond!
all_bonds = conf.list_nodes('interfaces bonding')
# We do not need to check our own bond
all_bonds.remove(bond['intf'])
for tmp in all_bonds:
- if conf.exists('interfaces bonding ' + tmp + ' member interface ' + intf):
- raise ConfigError('can not enslave interface {} which already ' \
- 'belongs to {}'.format(intf, tmp))
+ if conf.exists('interfaces bonding {tmp} member interface {intf}'):
+ raise ConfigError((
+ f'Cannot add interface "{intf}" to bond "{bond["intf"]}", '
+ f'it is already a member of bond "{tmp}"!'))
# can not add interfaces with an assigned address to a bond
- if conf.exists('interfaces ethernet ' + intf + ' address'):
- raise ConfigError('can not enslave interface {} which has an address ' \
- 'assigned'.format(intf))
-
- # bond members are not allowed to be bridge members, too
- for tmp in conf.list_nodes('interfaces bridge'):
- if conf.exists('interfaces bridge ' + tmp + ' member interface ' + intf):
- raise ConfigError('can not enslave interface {} which belongs to ' \
- 'bridge {}'.format(intf, tmp))
-
- # bond members are not allowed to be vrrp members, too
+ if has_address_configured(conf, intf):
+ raise ConfigError((
+ f'Cannot add interface "{intf}" to bond "{bond["intf"]}", '
+ f'it has an address assigned!'))
+
+ # bond members are not allowed to be bridge members
+ tmp = is_member(conf, intf, 'bridge')
+ if tmp:
+ raise ConfigError((
+ f'Cannot add interface "{intf}" to bond "{bond["intf"]}", '
+ f'it is already a member of bridge "{tmp}"!'))
+
+ # bond members are not allowed to be vrrp members
for tmp in conf.list_nodes('high-availability vrrp group'):
- if conf.exists('high-availability vrrp group ' + tmp + ' interface ' + intf):
- raise ConfigError('can not enslave interface {} which belongs to ' \
- 'VRRP group {}'.format(intf, tmp))
+ if conf.exists('high-availability vrrp group {tmp} interface {intf}'):
+ raise ConfigError((
+ f'Cannot add interface "{intf}" to bond "{bond["intf"]}", '
+ f'it is already a member of VRRP group "{tmp}"!'))
# bond members are not allowed to be underlaying psuedo-ethernet devices
for tmp in conf.list_nodes('interfaces pseudo-ethernet'):
- if conf.exists('interfaces pseudo-ethernet ' + tmp + ' link ' + intf):
- raise ConfigError('can not enslave interface {} which belongs to ' \
- 'pseudo-ethernet {}'.format(intf, tmp))
+ if conf.exists('interfaces pseudo-ethernet {tmp} link {intf}'):
+ raise ConfigError((
+ f'Cannot add interface "{intf}" to bond "{bond["intf"]}", '
+ f'it is already the link of pseudo-ethernet "{tmp}"!'))
# bond members are not allowed to be underlaying vxlan devices
for tmp in conf.list_nodes('interfaces vxlan'):
- if conf.exists('interfaces vxlan ' + tmp + ' link ' + intf):
- raise ConfigError('can not enslave interface {} which belongs to ' \
- 'vxlan {}'.format(intf, tmp))
-
+ if conf.exists('interfaces vxlan {tmp} link {intf}'):
+ raise ConfigError((
+ f'Cannot add interface "{intf}" to bond "{bond["intf"]}", '
+ f'it is already the link of VXLAN "{tmp}"!'))
if bond['primary']:
if bond['primary'] not in bond['member']:
- raise ConfigError('primary interface must be a member interface of {}' \
- .format(bond['intf']))
+ raise ConfigError(f'Bond "{bond["intf"]}" primary interface must be a member')
if bond['mode'] not in ['active-backup', 'balance-tlb', 'balance-alb']:
raise ConfigError('primary interface only works for mode active-backup, ' \
@@ -364,7 +250,6 @@ def verify(bond):
return None
-
def generate(bond):
return None
@@ -414,6 +299,9 @@ def apply(bond):
if bond['dhcpv6_temporary']:
b.dhcp.v6.options['dhcpv6_temporary'] = True
+ if bond['dhcpv6_pd']:
+ b.dhcp.v6.options['dhcpv6_pd'] = bond['dhcpv6_pd']
+
# ignore link state changes
b.set_link_detect(bond['disable_link_detect'])
# Bonding transmit hash policy
@@ -432,19 +320,27 @@ def apply(bond):
b.set_proxy_arp(bond['ip_proxy_arp'])
# Enable private VLAN proxy ARP on this interface
b.set_proxy_arp_pvlan(bond['ip_proxy_arp_pvlan'])
+ # IPv6 accept RA
+ b.set_ipv6_accept_ra(bond['ipv6_accept_ra'])
# IPv6 address autoconfiguration
b.set_ipv6_autoconf(bond['ipv6_autoconf'])
- # IPv6 EUI-based address
- b.set_ipv6_eui64_address(bond['ipv6_eui64_prefix'])
# IPv6 forwarding
b.set_ipv6_forwarding(bond['ipv6_forwarding'])
# IPv6 Duplicate Address Detection (DAD) tries
b.set_ipv6_dad_messages(bond['ipv6_dup_addr_detect'])
+ # Delete old IPv6 EUI64 addresses before changing MAC
+ for addr in bond['ipv6_eui64_prefix_remove']:
+ b.del_ipv6_eui64_address(addr)
+
# Change interface MAC address
if bond['mac']:
b.set_mac(bond['mac'])
+ # Add IPv6 EUI-based addresses
+ for addr in bond['ipv6_eui64_prefix']:
+ b.add_ipv6_eui64_address(addr)
+
# Maximum Transmission Unit (MTU)
b.set_mtu(bond['mtu'])
@@ -467,6 +363,9 @@ def apply(bond):
# Add (enslave) interfaces to bond
for intf in bond['member']:
+ # if we've come here we already verified the interface doesn't
+ # have addresses configured so just flush any remaining ones
+ cmd(f'ip addr flush dev "{intf}"')
b.add_port(intf)
# As the bond interface is always disabled first when changing
@@ -485,37 +384,17 @@ def apply(bond):
for addr in bond['address']:
b.add_addr(addr)
- # assign/remove VRF
- b.set_vrf(bond['vrf'])
-
- # remove no longer required service VLAN interfaces (vif-s)
- for vif_s in bond['vif_s_remove']:
- b.del_vlan(vif_s)
-
- # create service VLAN interfaces (vif-s)
- for vif_s in bond['vif_s']:
- s_vlan = b.add_vlan(vif_s['id'], ethertype=vif_s['ethertype'])
- apply_vlan_config(s_vlan, vif_s)
-
- # remove no longer required client VLAN interfaces (vif-c)
- # on lower service VLAN interface
- for vif_c in vif_s['vif_c_remove']:
- s_vlan.del_vlan(vif_c)
-
- # create client VLAN interfaces (vif-c)
- # on lower service VLAN interface
- for vif_c in vif_s['vif_c']:
- c_vlan = s_vlan.add_vlan(vif_c['id'])
- apply_vlan_config(c_vlan, vif_c)
-
- # remove no longer required VLAN interfaces (vif)
- for vif in bond['vif_remove']:
- b.del_vlan(vif)
-
- # create VLAN interfaces (vif)
- for vif in bond['vif']:
- vlan = b.add_vlan(vif['id'])
- apply_vlan_config(vlan, vif)
+ # assign/remove VRF (ONLY when not a member of a bridge,
+ # otherwise 'nomaster' removes it from it)
+ if not bond['is_bridge_member']:
+ b.set_vrf(bond['vrf'])
+
+ # re-add ourselves to any bridge we might have fallen out of
+ if bond['is_bridge_member']:
+ b.add_to_bridge(bond['is_bridge_member'])
+
+ # apply all vlans to interface
+ apply_all_vlans(b, bond)
return None
diff --git a/src/conf_mode/interfaces-bridge.py b/src/conf_mode/interfaces-bridge.py
index 93c6db97e..3ff339f0f 100755
--- a/src/conf_mode/interfaces-bridge.py
+++ b/src/conf_mode/interfaces-bridge.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2019 VyOS maintainers and contributors
+# Copyright (C) 2019-2020 VyOS maintainers and contributors
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License version 2 or later as
@@ -20,45 +20,28 @@ from copy import deepcopy
from sys import exit
from netifaces import interfaces
-from vyos.ifconfig import BridgeIf
+from vyos.ifconfig import BridgeIf, Section
from vyos.ifconfig.stp import STP
-from vyos.configdict import list_diff
+from vyos.configdict import list_diff, interface_default_data
+from vyos.validate import is_member, has_address_configured
from vyos.config import Config
+from vyos.util import cmd, get_bridge_member_config
from vyos import ConfigError
default_config_data = {
- 'address': [],
- 'address_remove': [],
+ **interface_default_data,
'aging': 300,
'arp_cache_tmo': 30,
- 'description': '',
'deleted': False,
- 'dhcp_client_id': '',
- 'dhcp_hostname': '',
- 'dhcp_vendor_class_id': '',
- 'dhcpv6_prm_only': False,
- 'dhcpv6_temporary': False,
- 'disable': False,
- 'disable_link_detect': 1,
'forwarding_delay': 14,
'hello_time': 2,
- 'ip_disable_arp_filter': 1,
- 'ip_enable_arp_accept': 0,
- 'ip_enable_arp_announce': 0,
- 'ip_enable_arp_ignore': 0,
- 'ipv6_autoconf': 0,
- 'ipv6_eui64_prefix': '',
- 'ipv6_forwarding': 1,
- 'ipv6_dup_addr_detect': 1,
'igmp_querier': 0,
'intf': '',
- 'mac' : '',
'max_age': 20,
'member': [],
'member_remove': [],
'priority': 32768,
- 'stp': 0,
- 'vrf': ''
+ 'stp': 0
}
def get_config():
@@ -160,9 +143,21 @@ def get_config():
if conf.exists('ipv6 address autoconf'):
bridge['ipv6_autoconf'] = 1
- # Get prefix for IPv6 addressing based on MAC address (EUI-64)
+ # Get prefixes for IPv6 addressing based on MAC address (EUI-64)
if conf.exists('ipv6 address eui64'):
- bridge['ipv6_eui64_prefix'] = conf.return_value('ipv6 address eui64')
+ bridge['ipv6_eui64_prefix'] = conf.return_values('ipv6 address eui64')
+
+ # Determine currently effective EUI64 addresses - to determine which
+ # address is no longer valid and needs to be removed
+ eff_addr = conf.return_effective_values('ipv6 address eui64')
+ bridge['ipv6_eui64_prefix_remove'] = list_diff(eff_addr, bridge['ipv6_eui64_prefix'])
+
+ # Remove the default link-local address if set.
+ if conf.exists('ipv6 address no-default-link-local'):
+ bridge['ipv6_eui64_prefix_remove'].append('fe80::/64')
+ else:
+ # add the link-local by default to make IPv6 work
+ bridge['ipv6_eui64_prefix'].append('fe80::/64')
# Disable IPv6 forwarding on this interface
if conf.exists('ipv6 disable-forwarding'):
@@ -176,28 +171,29 @@ def get_config():
if conf.exists('mac'):
bridge['mac'] = conf.return_value('mac')
+ # Find out if MAC has changed - if so, we need to delete all IPv6 EUI64 addresses
+ # before re-adding them
+ if ( bridge['mac'] and bridge['intf'] in Section.interfaces(section='bridge')
+ and bridge['mac'] != BridgeIf(bridge['intf'], create=False).get_mac() ):
+ bridge['ipv6_eui64_prefix_remove'] += bridge['ipv6_eui64_prefix']
+
+ # to make IPv6 SLAAC and DHCPv6 work with forwarding=1,
+ # accept_ra must be 2
+ if bridge['ipv6_autoconf'] or 'dhcpv6' in bridge['address']:
+ bridge['ipv6_accept_ra'] = 2
+
# Interval at which neighbor bridges are removed
if conf.exists('max-age'):
bridge['max_age'] = int(conf.return_value('max-age'))
# Determine bridge member interface (currently configured)
for intf in conf.list_nodes('member interface'):
- # cost and priority initialized with linux defaults
- # by reading /sys/devices/virtual/net/br0/brif/eth2/{path_cost,priority}
- # after adding interface to bridge after reboot
- iface = {
- 'name': intf,
- 'cost': 100,
- 'priority': 32
- }
-
- if conf.exists('member interface {} cost'.format(intf)):
- iface['cost'] = int(conf.return_value('member interface {} cost'.format(intf)))
-
- if conf.exists('member interface {} priority'.format(intf)):
- iface['priority'] = int(conf.return_value('member interface {} priority'.format(intf)))
-
- bridge['member'].append(iface)
+ # defaults are stored in util.py (they can't be here as all interface
+ # scripts use the function)
+ memberconf = get_bridge_member_config(conf, bridge['intf'], intf)
+ if memberconf:
+ memberconf['name'] = intf
+ bridge['member'].append(memberconf)
# Determine bridge member interface (currently effective) - to determine which
# interfaces is no longer assigend to the bridge and thus can be removed
@@ -228,30 +224,40 @@ def verify(bridge):
raise ConfigError(f'VRF "{vrf_name}" does not exist')
conf = Config()
- for br in conf.list_nodes('interfaces bridge'):
- # it makes no sense to verify ourself in this case
- if br == bridge['intf']:
- continue
-
- for intf in bridge['member']:
- tmp = conf.list_nodes('interfaces bridge {} member interface'.format(br))
- if intf['name'] in tmp:
- raise ConfigError('Interface "{}" belongs to bridge "{}" and can not be enslaved.'.format(intf['name'], bridge['intf']))
-
- # the interface must exist prior adding it to a bridge
for intf in bridge['member']:
+ # the interface must exist prior adding it to a bridge
if intf['name'] not in interfaces():
- raise ConfigError('Can not add non existing interface "{}" to bridge "{}"'.format(intf['name'], bridge['intf']))
+ raise ConfigError((
+ f'Cannot add nonexistent interface "{intf["name"]}" '
+ f'to bridge "{bridge["intf"]}"'))
if intf['name'] == 'lo':
raise ConfigError('Loopback interface "lo" can not be added to a bridge')
- # bridge members are not allowed to be bond members, too
- for intf in bridge['member']:
- for bond in conf.list_nodes('interfaces bonding'):
- if conf.exists('interfaces bonding ' + bond + ' member interface'):
- if intf['name'] in conf.return_values('interfaces bonding ' + bond + ' member interface'):
- raise ConfigError('Interface {} belongs to bond {}, can not add it to {}'.format(intf['name'], bond, bridge['intf']))
+ # bridge members aren't allowed to be members of another bridge
+ for br in conf.list_nodes('interfaces bridge'):
+ # it makes no sense to verify ourself in this case
+ if br == bridge['intf']:
+ continue
+
+ tmp = conf.list_nodes(f'interfaces bridge {br} member interface')
+ if intf['name'] in tmp:
+ raise ConfigError((
+ f'Cannot add interface "{intf["name"]}" to bridge '
+ f'"{bridge["intf"]}", it is already a member of bridge "{br}"!'))
+
+ # bridge members are not allowed to be bond members
+ tmp = is_member(conf, intf['name'], 'bonding')
+ if tmp:
+ raise ConfigError((
+ f'Cannot add interface "{intf["name"]}" to bridge '
+ f'"{bridge["intf"]}", it is already a member of bond "{tmp}"!'))
+
+ # bridge members must not have an assigned address
+ if has_address_configured(conf, intf['name']):
+ raise ConfigError((
+ f'Cannot add interface "{intf["name"]}" to bridge '
+ f'"{bridge["intf"]}", it has an address assigned!'))
return None
@@ -281,10 +287,10 @@ def apply(bridge):
br.set_arp_announce(bridge['ip_enable_arp_announce'])
# configure ARP ignore
br.set_arp_ignore(bridge['ip_enable_arp_ignore'])
+ # IPv6 accept RA
+ br.set_ipv6_accept_ra(bridge['ipv6_accept_ra'])
# IPv6 address autoconfiguration
br.set_ipv6_autoconf(bridge['ipv6_autoconf'])
- # IPv6 EUI-based address
- br.set_ipv6_eui64_address(bridge['ipv6_eui64_prefix'])
# IPv6 forwarding
br.set_ipv6_forwarding(bridge['ipv6_forwarding'])
# IPv6 Duplicate Address Detection (DAD) tries
@@ -315,12 +321,16 @@ def apply(bridge):
if bridge['dhcpv6_temporary']:
br.dhcp.v6.options['dhcpv6_temporary'] = True
+ if bridge['dhcpv6_pd']:
+ br.dhcp.v6.options['dhcpv6_pd'] = br['dhcpv6_pd']
+
# assign/remove VRF
br.set_vrf(bridge['vrf'])
- # Change interface MAC address
- if bridge['mac']:
- br.set_mac(bridge['mac'])
+ # Delete old IPv6 EUI64 addresses before changing MAC
+ # (adding members to a fresh bridge changes its MAC too)
+ for addr in bridge['ipv6_eui64_prefix_remove']:
+ br.del_ipv6_eui64_address(addr)
# remove interface from bridge
for intf in bridge['member_remove']:
@@ -328,8 +338,20 @@ def apply(bridge):
# add interfaces to bridge
for member in bridge['member']:
+ # if we've come here we already verified the interface doesn't
+ # have addresses configured so just flush any remaining ones
+ cmd(f'ip addr flush dev "{member["name"]}"')
br.add_port(member['name'])
+ # Change interface MAC address
+ if bridge['mac']:
+ br.set_mac(bridge['mac'])
+
+ # Add IPv6 EUI-based addresses (must be done after adding the
+ # 1st bridge member or setting its MAC)
+ for addr in bridge['ipv6_eui64_prefix']:
+ br.add_ipv6_eui64_address(addr)
+
# up/down interface
if bridge['disable']:
br.set_admin_state('down')
@@ -347,9 +369,9 @@ def apply(bridge):
for member in bridge['member']:
i = STPBridgeIf(member['name'])
# configure ARP cache timeout
- i.set_arp_cache_tmo(bridge['arp_cache_tmo'])
+ i.set_arp_cache_tmo(member['arp_cache_tmo'])
# ignore link state changes
- i.set_link_detect(bridge['disable_link_detect'])
+ i.set_link_detect(member['disable_link_detect'])
# set bridge port path cost
i.set_path_cost(member['cost'])
# set bridge port path priority
diff --git a/src/conf_mode/interfaces-dummy.py b/src/conf_mode/interfaces-dummy.py
index 23eaa4ecb..4a77b0c1a 100755
--- a/src/conf_mode/interfaces-dummy.py
+++ b/src/conf_mode/interfaces-dummy.py
@@ -23,7 +23,7 @@ from netifaces import interfaces
from vyos.ifconfig import DummyIf
from vyos.configdict import list_diff
from vyos.config import Config
-from vyos.validate import is_bridge_member
+from vyos.validate import is_member
from vyos import ConfigError
default_config_data = {
@@ -47,11 +47,12 @@ def get_config():
dummy['intf'] = os.environ['VYOS_TAGNODE_VALUE']
+ # check if we are a member of any bridge
+ dummy['is_bridge_member'] = is_member(conf, dummy['intf'], 'bridge')
+
# Check if interface has been removed
if not conf.exists('interfaces dummy ' + dummy['intf']):
dummy['deleted'] = True
- # check if interface is member if a bridge
- dummy['is_bridge_member'] = is_bridge_member(conf, dummy['intf'])
return dummy
# set new configuration level
@@ -84,15 +85,26 @@ def get_config():
def verify(dummy):
if dummy['deleted']:
if dummy['is_bridge_member']:
- interface = dummy['intf']
- bridge = dummy['is_bridge_member']
- raise ConfigError(f'Interface "{interface}" can not be deleted as it belongs to bridge "{bridge}"!')
+ raise ConfigError((
+ f'Interface "{dummy["intf"]}" cannot be deleted as it is a '
+ f'member of bridge "{dummy["is_bridge_member"]}"!'))
return None
- vrf_name = dummy['vrf']
- if vrf_name and vrf_name not in interfaces():
- raise ConfigError(f'VRF "{vrf_name}" does not exist')
+ if dummy['vrf']:
+ if dummy['vrf'] not in interfaces():
+ raise ConfigError(f'VRF "{dummy["vrf"]}" does not exist')
+
+ if dummy['is_bridge_member']:
+ raise ConfigError((
+ f'Interface "{dummy["intf"]}" cannot be member of VRF '
+ f'"{dummy["vrf"]}" and bridge "{dummy["is_bridge_member"]}" '
+ f'at the same time!'))
+
+ if dummy['is_bridge_member'] and dummy['address']:
+ raise ConfigError((
+ f'Cannot assign address to interface "{dummy["intf"]}" '
+ f'as it is a member of bridge "{dummy["is_bridge_member"]}"!'))
return None
@@ -117,8 +129,10 @@ def apply(dummy):
for addr in dummy['address']:
d.add_addr(addr)
- # assign/remove VRF
- d.set_vrf(dummy['vrf'])
+ # assign/remove VRF (ONLY when not a member of a bridge,
+ # otherwise 'nomaster' removes it from it)
+ if not dummy['is_bridge_member']:
+ d.set_vrf(dummy['vrf'])
# disable interface on demand
if dummy['disable']:
diff --git a/src/conf_mode/interfaces-ethernet.py b/src/conf_mode/interfaces-ethernet.py
index 5a977d797..f45a77a3e 100755
--- a/src/conf_mode/interfaces-ethernet.py
+++ b/src/conf_mode/interfaces-ethernet.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2019 VyOS maintainers and contributors
+# Copyright (C) 2019-2020 VyOS maintainers and contributors
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License version 2 or later as
@@ -21,66 +21,49 @@ from copy import deepcopy
from netifaces import interfaces
from vyos.ifconfig import EthernetIf
-from vyos.ifconfig_vlan import apply_vlan_config, verify_vlan_config
-from vyos.configdict import list_diff, vlan_to_dict
+from vyos.ifconfig_vlan import apply_all_vlans, verify_vlan_config
+from vyos.configdict import list_diff, intf_to_dict, add_to_dict, interface_default_data
+from vyos.validate import is_member
from vyos.config import Config
from vyos import ConfigError
default_config_data = {
- 'address': [],
- 'address_remove': [],
- 'description': '',
+ **interface_default_data,
'deleted': False,
- 'dhcp_client_id': '',
- 'dhcp_hostname': '',
- 'dhcp_vendor_class_id': '',
- 'dhcpv6_prm_only': False,
- 'dhcpv6_temporary': False,
- 'disable': False,
- 'disable_link_detect': 1,
'duplex': 'auto',
'flow_control': 'on',
'hw_id': '',
'ip_arp_cache_tmo': 30,
- 'ip_disable_arp_filter': 1,
- 'ip_enable_arp_accept': 0,
- 'ip_enable_arp_announce': 0,
- 'ip_enable_arp_ignore': 0,
- 'ip_proxy_arp': 0,
'ip_proxy_arp_pvlan': 0,
- 'ipv6_autoconf': 0,
- 'ipv6_eui64_prefix': '',
- 'ipv6_forwarding': 1,
- 'ipv6_dup_addr_detect': 1,
+ 'is_bond_member': False,
'intf': '',
- 'mac': '',
- 'mtu': 1500,
'offload_gro': 'off',
'offload_gso': 'off',
'offload_sg': 'off',
'offload_tso': 'off',
'offload_ufo': 'off',
'speed': 'auto',
- 'vif_s': [],
+ 'vif_s': {},
'vif_s_remove': [],
- 'vif': [],
+ 'vif': {},
'vif_remove': [],
'vrf': ''
}
-def get_config():
- eth = deepcopy(default_config_data)
- conf = Config()
+def get_config():
# determine tagNode instance
if 'VYOS_TAGNODE_VALUE' not in os.environ:
raise ConfigError('Interface (VYOS_TAGNODE_VALUE) not specified')
- eth['intf'] = os.environ['VYOS_TAGNODE_VALUE']
+ ifname = os.environ['VYOS_TAGNODE_VALUE']
+ conf = Config()
# check if ethernet interface has been removed
- cfg_base = ['interfaces', 'ethernet', eth['intf']]
+ cfg_base = ['interfaces', 'ethernet', ifname]
if not conf.exists(cfg_base):
+ eth = deepcopy(default_config_data)
+ eth['intf'] = ifname
eth['deleted'] = True
# we can not bail out early as ethernet interface can not be removed
# Kernel will complain with: RTNETLINK answers: Operation not supported.
@@ -90,42 +73,7 @@ def get_config():
# set new configuration level
conf.set_level(cfg_base)
- # retrieve configured interface addresses
- if conf.exists('address'):
- eth['address'] = conf.return_values('address')
-
- # get interface addresses (currently effective) - to determine which
- # address is no longer valid and needs to be removed
- eff_addr = conf.return_effective_values('address')
- eth['address_remove'] = list_diff(eff_addr, eth['address'])
-
- # retrieve interface description
- if conf.exists('description'):
- eth['description'] = conf.return_value('description')
-
- # get DHCP client identifier
- if conf.exists('dhcp-options client-id'):
- eth['dhcp_client_id'] = conf.return_value('dhcp-options client-id')
-
- # DHCP client host name (overrides the system host name)
- if conf.exists('dhcp-options host-name'):
- eth['dhcp_hostname'] = conf.return_value('dhcp-options host-name')
-
- # DHCP client vendor identifier
- if conf.exists('dhcp-options vendor-class-id'):
- eth['dhcp_vendor_class_id'] = conf.return_value('dhcp-options vendor-class-id')
-
- # DHCPv6 only acquire config parameters, no address
- if conf.exists('dhcpv6-options parameters-only'):
- eth['dhcpv6_prm_only'] = True
-
- # DHCPv6 temporary IPv6 address
- if conf.exists('dhcpv6-options temporary'):
- eth['dhcpv6_temporary'] = True
-
- # ignore link state changes
- if conf.exists('disable-link-detect'):
- eth['disable_link_detect'] = 2
+ eth, disabled = intf_to_dict(conf, default_config_data)
# disable ethernet flow control (pause frames)
if conf.exists('disable-flow-control'):
@@ -135,10 +83,6 @@ def get_config():
if conf.exists('hw-id'):
eth['hw_id'] = conf.return_value('hw-id')
- # disable interface
- if conf.exists('disable'):
- eth['disable'] = True
-
# interface duplex
if conf.exists('duplex'):
eth['duplex'] = conf.return_value('duplex')
@@ -147,53 +91,12 @@ def get_config():
if conf.exists('ip arp-cache-timeout'):
eth['ip_arp_cache_tmo'] = int(conf.return_value('ip arp-cache-timeout'))
- # ARP filter configuration
- if conf.exists('ip disable-arp-filter'):
- eth['ip_disable_arp_filter'] = 0
-
- # ARP enable accept
- if conf.exists('ip enable-arp-accept'):
- eth['ip_enable_arp_accept'] = 1
-
- # ARP enable announce
- if conf.exists('ip enable-arp-announce'):
- eth['ip_enable_arp_announce'] = 1
-
- # ARP enable ignore
- if conf.exists('ip enable-arp-ignore'):
- eth['ip_enable_arp_ignore'] = 1
-
- # Enable proxy-arp on this interface
- if conf.exists('ip enable-proxy-arp'):
- eth['ip_proxy_arp'] = 1
-
# Enable private VLAN proxy ARP on this interface
if conf.exists('ip proxy-arp-pvlan'):
eth['ip_proxy_arp_pvlan'] = 1
- # Enable acquisition of IPv6 address using stateless autoconfig (SLAAC)
- if conf.exists('ipv6 address autoconf'):
- eth['ipv6_autoconf'] = 1
-
- # Get prefix for IPv6 addressing based on MAC address (EUI-64)
- if conf.exists('ipv6 address eui64'):
- eth['ipv6_eui64_prefix'] = conf.return_value('ipv6 address eui64')
-
- # Disable IPv6 forwarding on this interface
- if conf.exists('ipv6 disable-forwarding'):
- eth['ipv6_forwarding'] = 0
-
- # IPv6 Duplicate Address Detection (DAD) tries
- if conf.exists('ipv6 dup-addr-detect-transmits'):
- eth['ipv6_dup_addr_detect'] = int(conf.return_value('ipv6 dup-addr-detect-transmits'))
-
- # Media Access Control (MAC) address
- if conf.exists('mac'):
- eth['mac'] = conf.return_value('mac')
-
- # Maximum Transmission Unit (MTU)
- if conf.exists('mtu'):
- eth['mtu'] = int(conf.return_value('mtu'))
+ # check if we are a member of any bond
+ eth['is_bond_member'] = is_member(conf, eth['intf'], 'bonding')
# GRO (generic receive offload)
if conf.exists('offload-options generic-receive'):
@@ -219,37 +122,13 @@ def get_config():
if conf.exists('speed'):
eth['speed'] = conf.return_value('speed')
- # retrieve VRF instance
- if conf.exists('vrf'):
- eth['vrf'] = conf.return_value('vrf')
+ # remove default IPv6 link-local address if member of a bond
+ if eth['is_bond_member'] and 'fe80::/64' in eth['ipv6_eui64_prefix']:
+ eth['ipv6_eui64_prefix'].remove('fe80::/64')
+ eth['ipv6_eui64_prefix_remove'].append('fe80::/64')
- # re-set configuration level to parse new nodes
- conf.set_level(cfg_base)
- # get vif-s interfaces (currently effective) - to determine which vif-s
- # interface is no longer present and needs to be removed
- eff_intf = conf.list_effective_nodes('vif-s')
- act_intf = conf.list_nodes('vif-s')
- eth['vif_s_remove'] = list_diff(eff_intf, act_intf)
-
- if conf.exists('vif-s'):
- for vif_s in conf.list_nodes('vif-s'):
- # set config level to vif-s interface
- conf.set_level(cfg_base + ['vif-s', vif_s])
- eth['vif_s'].append(vlan_to_dict(conf))
-
- # re-set configuration level to parse new nodes
- conf.set_level(cfg_base)
- # Determine vif interfaces (currently effective) - to determine which
- # vif interface is no longer present and needs to be removed
- eff_intf = conf.list_effective_nodes('vif')
- act_intf = conf.list_nodes('vif')
- eth['vif_remove'] = list_diff(eff_intf, act_intf)
-
- if conf.exists('vif'):
- for vif in conf.list_nodes('vif'):
- # set config level to vif interface
- conf.set_level(cfg_base + ['vif', vif])
- eth['vif'].append(vlan_to_dict(conf))
+ add_to_dict(conf, disabled, eth, 'vif', 'vif')
+ add_to_dict(conf, disabled, eth, 'vif-s', 'vif_s')
return eth
@@ -272,18 +151,24 @@ def verify(eth):
if eth['dhcpv6_prm_only'] and eth['dhcpv6_temporary']:
raise ConfigError('DHCPv6 temporary and parameters-only options are mutually exclusive!')
- vrf_name = eth['vrf']
- if vrf_name and vrf_name not in interfaces():
- raise ConfigError(f'VRF "{vrf_name}" does not exist')
+ memberof = eth['is_bridge_member'] if eth['is_bridge_member'] else eth['is_bond_member']
- conf = Config()
- # some options can not be changed when interface is enslaved to a bond
- for bond in conf.list_nodes('interfaces bonding'):
- if conf.exists('interfaces bonding ' + bond + ' member interface'):
- bond_member = conf.return_values('interfaces bonding ' + bond + ' member interface')
- if eth['intf'] in bond_member:
- if eth['address']:
- raise ConfigError(f"Can not assign address to interface {eth['intf']} which is a member of {bond}")
+ if ( memberof
+ and ( eth['address']
+ or eth['ipv6_eui64_prefix']
+ or eth['ipv6_autoconf'] ) ):
+ raise ConfigError((
+ f'Cannot assign address to interface "{eth["intf"]}" '
+ f'as it is a member of "{memberof}"!'))
+
+ if eth['vrf']:
+ if eth['vrf'] not in interfaces():
+ raise ConfigError(f'VRF "{eth["vrf"]}" does not exist')
+
+ if memberof:
+ raise ConfigError((
+ f'Interface "{eth["intf"]}" cannot be member of VRF "{eth["vrf"]}" '
+ f'and "{memberof}" at the same time!'))
# use common function to verify VLAN configuration
verify_vlan_config(eth)
@@ -316,6 +201,9 @@ def apply(eth):
if eth['dhcpv6_temporary']:
e.dhcp.v6.options['dhcpv6_temporary'] = True
+ if eth['dhcpv6_pd']:
+ e.dhcp.v6.options['dhcpv6_pd'] = e['dhcpv6_pd']
+
# ignore link state changes
e.set_link_detect(eth['disable_link_detect'])
# disable ethernet flow control (pause frames)
@@ -334,15 +222,19 @@ def apply(eth):
e.set_proxy_arp(eth['ip_proxy_arp'])
# Enable private VLAN proxy ARP on this interface
e.set_proxy_arp_pvlan(eth['ip_proxy_arp_pvlan'])
+ # IPv6 accept RA
+ e.set_ipv6_accept_ra(eth['ipv6_accept_ra'])
# IPv6 address autoconfiguration
e.set_ipv6_autoconf(eth['ipv6_autoconf'])
- # IPv6 EUI-based address
- e.set_ipv6_eui64_address(eth['ipv6_eui64_prefix'])
# IPv6 forwarding
e.set_ipv6_forwarding(eth['ipv6_forwarding'])
# IPv6 Duplicate Address Detection (DAD) tries
e.set_ipv6_dad_messages(eth['ipv6_dup_addr_detect'])
+ # Delete old IPv6 EUI64 addresses before changing MAC
+ for addr in eth['ipv6_eui64_prefix_remove']:
+ e.del_ipv6_eui64_address(addr)
+
# Change interface MAC address - re-set to real hardware address (hw-id)
# if custom mac is removed
if eth['mac']:
@@ -350,6 +242,10 @@ def apply(eth):
elif eth['hw_id']:
e.set_mac(eth['hw_id'])
+ # Add IPv6 EUI-based addresses
+ for addr in eth['ipv6_eui64_prefix']:
+ e.add_ipv6_eui64_address(addr)
+
# Maximum Transmission Unit (MTU)
e.set_mtu(eth['mtu'])
@@ -385,47 +281,17 @@ def apply(eth):
for addr in eth['address']:
e.add_addr(addr)
- # assign/remove VRF
- e.set_vrf(eth['vrf'])
-
- # remove no longer required service VLAN interfaces (vif-s)
- for vif_s in eth['vif_s_remove']:
- e.del_vlan(vif_s)
-
- # create service VLAN interfaces (vif-s)
- for vif_s in eth['vif_s']:
- s_vlan = e.add_vlan(vif_s['id'], ethertype=vif_s['ethertype'])
- apply_vlan_config(s_vlan, vif_s)
-
- # remove no longer required client VLAN interfaces (vif-c)
- # on lower service VLAN interface
- for vif_c in vif_s['vif_c_remove']:
- s_vlan.del_vlan(vif_c)
-
- # create client VLAN interfaces (vif-c)
- # on lower service VLAN interface
- for vif_c in vif_s['vif_c']:
- c_vlan = s_vlan.add_vlan(vif_c['id'])
- apply_vlan_config(c_vlan, vif_c)
-
- # remove no longer required VLAN interfaces (vif)
- for vif in eth['vif_remove']:
- e.del_vlan(vif)
-
- # create VLAN interfaces (vif)
- for vif in eth['vif']:
- # QoS priority mapping can only be set during interface creation
- # so we delete the interface first if required.
- if vif['egress_qos_changed'] or vif['ingress_qos_changed']:
- try:
- # on system bootup the above condition is true but the interface
- # does not exists, which throws an exception, but that's legal
- e.del_vlan(vif['id'])
- except:
- pass
-
- vlan = e.add_vlan(vif['id'], ingress_qos=vif['ingress_qos'], egress_qos=vif['egress_qos'])
- apply_vlan_config(vlan, vif)
+ # assign/remove VRF (ONLY when not a member of a bridge or bond,
+ # otherwise 'nomaster' removes it from it)
+ if not ( eth['is_bridge_member'] or eth['is_bond_member'] ):
+ e.set_vrf(eth['vrf'])
+
+ # re-add ourselves to any bridge we might have fallen out of
+ if eth['is_bridge_member']:
+ e.add_to_bridge(eth['is_bridge_member'])
+
+ # apply all vlans to interface
+ apply_all_vlans(e, eth)
return None
diff --git a/src/conf_mode/interfaces-geneve.py b/src/conf_mode/interfaces-geneve.py
index 708a64474..e4109a221 100755
--- a/src/conf_mode/interfaces-geneve.py
+++ b/src/conf_mode/interfaces-geneve.py
@@ -22,7 +22,7 @@ from netifaces import interfaces
from vyos.config import Config
from vyos.ifconfig import GeneveIf
-from vyos.validate import is_bridge_member
+from vyos.validate import is_member
from vyos import ConfigError
default_config_data = {
@@ -49,11 +49,12 @@ def get_config():
geneve['intf'] = os.environ['VYOS_TAGNODE_VALUE']
+ # check if interface is member if a bridge
+ geneve['is_bridge_member'] = is_member(conf, geneve['intf'], 'bridge')
+
# Check if interface has been removed
if not conf.exists('interfaces geneve ' + geneve['intf']):
geneve['deleted'] = True
- # check if interface is member if a bridge
- geneve['is_bridge_member'] = is_bridge_member(conf, geneve['intf'])
return geneve
# set new configuration level
@@ -97,12 +98,17 @@ def get_config():
def verify(geneve):
if geneve['deleted']:
if geneve['is_bridge_member']:
- interface = geneve['intf']
- bridge = geneve['is_bridge_member']
- raise ConfigError(f'Interface "{interface}" can not be deleted as it belongs to bridge "{bridge}"!')
+ raise ConfigError((
+ f'Cannot delete interface "{geneve["intf"]}" as it is a '
+ f'member of bridge "{geneve["is_bridge_member"]}"!'))
return None
+ if geneve['is_bridge_member'] and geneve['address']:
+ raise ConfigError((
+ f'Cannot assign address to interface "{geneve["intf"]}" '
+ f'as it is a member of bridge "{geneve["is_bridge_member"]}"!'))
+
if not geneve['remote']:
raise ConfigError('GENEVE remote must be configured')
@@ -158,6 +164,10 @@ def apply(geneve):
if not geneve['disable']:
g.set_admin_state('up')
+ # re-add ourselves to any bridge we might have fallen out of
+ if geneve['is_bridge_member']:
+ g.add_to_bridge(geneve['is_bridge_member'])
+
return None
diff --git a/src/conf_mode/interfaces-l2tpv3.py b/src/conf_mode/interfaces-l2tpv3.py
index 8312d6f37..cdfc6ea84 100755
--- a/src/conf_mode/interfaces-l2tpv3.py
+++ b/src/conf_mode/interfaces-l2tpv3.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2019 VyOS maintainers and contributors
+# Copyright (C) 2019-2020 VyOS maintainers and contributors
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License version 2 or later as
@@ -24,7 +24,7 @@ from vyos.config import Config
from vyos.ifconfig import L2TPv3If, Interface
from vyos import ConfigError
from vyos.util import call
-from vyos.validate import is_bridge_member
+from vyos.validate import is_member, is_addr_assigned
default_config_data = {
'address': [],
@@ -35,8 +35,9 @@ default_config_data = {
'local_address': '',
'local_port': 5000,
'intf': '',
+ 'ipv6_accept_ra': 1,
'ipv6_autoconf': 0,
- 'ipv6_eui64_prefix': '',
+ 'ipv6_eui64_prefix': [],
'ipv6_forwarding': 1,
'ipv6_dup_addr_detect': 1,
'is_bridge_member': False,
@@ -66,12 +67,13 @@ def get_config():
l2tpv3['intf'] = os.environ['VYOS_TAGNODE_VALUE']
+ # check if interface is member of a bridge
+ l2tpv3['is_bridge_member'] = is_member(conf, l2tpv3['intf'], 'bridge')
+
# Check if interface has been removed
if not conf.exists('interfaces l2tpv3 ' + l2tpv3['intf']):
l2tpv3['deleted'] = True
interface = l2tpv3['intf']
- # check if interface is member if a bridge
- l2tpv3['is_bridge_member'] = is_bridge_member(conf, interface)
# to delete the l2tpv3 interface we need the current tunnel_id and session_id
if conf.exists_effective(f'interfaces l2tpv3 {interface} tunnel-id'):
@@ -113,9 +115,15 @@ def get_config():
if conf.exists('ipv6 address autoconf'):
l2tpv3['ipv6_autoconf'] = 1
- # Get prefix for IPv6 addressing based on MAC address (EUI-64)
+ # Get prefixes for IPv6 addressing based on MAC address (EUI-64)
if conf.exists('ipv6 address eui64'):
- l2tpv3['ipv6_eui64_prefix'] = conf.return_value('ipv6 address eui64')
+ l2tpv3['ipv6_eui64_prefix'] = conf.return_values('ipv6 address eui64')
+
+ # Remove the default link-local address if set.
+ if not ( conf.exists('ipv6 address no-default-link-local') or
+ l2tpv3['is_bridge_member'] ):
+ # add the link-local by default to make IPv6 work
+ l2tpv3['ipv6_eui64_prefix'].append('fe80::/64')
# Disable IPv6 forwarding on this interface
if conf.exists('ipv6 disable-forwarding'):
@@ -125,6 +133,11 @@ def get_config():
if conf.exists('ipv6 dup-addr-detect-transmits'):
l2tpv3['ipv6_dup_addr_detect'] = int(conf.return_value('ipv6 dup-addr-detect-transmits'))
+ # to make IPv6 SLAAC and DHCPv6 work with forwarding=1,
+ # accept_ra must be 2
+ if l2tpv3['ipv6_autoconf'] or 'dhcpv6' in l2tpv3['address']:
+ l2tpv3['ipv6_accept_ra'] = 2
+
# Maximum Transmission Unit (MTU)
if conf.exists('mtu'):
l2tpv3['mtu'] = int(conf.return_value('mtu'))
@@ -161,15 +174,18 @@ def verify(l2tpv3):
if l2tpv3['deleted']:
if l2tpv3['is_bridge_member']:
- interface = l2tpv3['intf']
- bridge = l2tpv3['is_bridge_member']
- raise ConfigError(f'Interface "{interface}" can not be deleted as it belongs to bridge "{bridge}"!')
+ raise ConfigError((
+ f'Interface "{l2tpv3["intf"]}" cannot be deleted as it is a '
+ f'member of bridge "{l2tpv3["is_bridge_member"]}"!'))
return None
if not l2tpv3['local_address']:
raise ConfigError(f'Must configure the l2tpv3 local-ip for {interface}')
+ if not is_addr_assigned(l2tpv3['local_address']):
+ raise ConfigError(f'Must use a configured IP on l2tpv3 local-ip for {interface}')
+
if not l2tpv3['remote_address']:
raise ConfigError(f'Must configure the l2tpv3 remote-ip for {interface}')
@@ -185,6 +201,14 @@ def verify(l2tpv3):
if not l2tpv3['peer_session_id']:
raise ConfigError(f'Must configure the l2tpv3 peer-session-id for {interface}')
+ if ( l2tpv3['is_bridge_member']
+ and ( l2tpv3['address']
+ or l2tpv3['ipv6_eui64_prefix']
+ or l2tpv3['ipv6_autoconf'] ) ):
+ raise ConfigError((
+ f'Cannot assign address to interface "{l2tpv3["intf"]}" '
+ f'as it is a member of bridge "{l2tpv3["is_bridge_member"]}"!'))
+
return None
@@ -223,10 +247,10 @@ def apply(l2tpv3):
l.set_alias(l2tpv3['description'])
# Maximum Transfer Unit (MTU)
l.set_mtu(l2tpv3['mtu'])
+ # IPv6 accept RA
+ l.set_ipv6_accept_ra(l2tpv3['ipv6_accept_ra'])
# IPv6 address autoconfiguration
l.set_ipv6_autoconf(l2tpv3['ipv6_autoconf'])
- # IPv6 EUI-based address
- l.set_ipv6_eui64_address(l2tpv3['ipv6_eui64_prefix'])
# IPv6 forwarding
l.set_ipv6_forwarding(l2tpv3['ipv6_forwarding'])
# IPv6 Duplicate Address Detection (DAD) tries
@@ -238,12 +262,20 @@ def apply(l2tpv3):
for addr in l2tpv3['address']:
l.add_addr(addr)
+ # IPv6 EUI-based addresses
+ for addr in l2tpv3['ipv6_eui64_prefix']:
+ l.add_ipv6_eui64_address(addr)
+
# As the interface is always disabled first when changing parameters
# we will only re-enable the interface if it is not administratively
# disabled
if not l2tpv3['disable']:
l.set_admin_state('up')
+ # re-add ourselves to any bridge we might have fallen out of
+ if l2tpv3['is_bridge_member']:
+ l.add_to_bridge(l2tpv3['is_bridge_member'])
+
return None
if __name__ == '__main__':
diff --git a/src/conf_mode/interfaces-openvpn.py b/src/conf_mode/interfaces-openvpn.py
index 708ac8f91..ea8e1a7c4 100755
--- a/src/conf_mode/interfaces-openvpn.py
+++ b/src/conf_mode/interfaces-openvpn.py
@@ -25,10 +25,11 @@ from time import sleep
from shutil import rmtree
from vyos.config import Config
+from vyos.configdict import list_diff
from vyos.ifconfig import VTunIf
from vyos.template import render
from vyos.util import call, chown, chmod_600, chmod_755
-from vyos.validate import is_addr_assigned, is_bridge_member, is_ipv4
+from vyos.validate import is_addr_assigned, is_member, is_ipv4
from vyos import ConfigError
user = 'openvpn'
@@ -40,7 +41,6 @@ default_config_data = {
'auth_pass': '',
'auth_user_pass_file': '',
'auth': False,
- 'bridge_member': [],
'compress_lzo': False,
'deleted': False,
'description': '',
@@ -49,8 +49,10 @@ default_config_data = {
'encryption': '',
'hash': '',
'intf': '',
+ 'ipv6_accept_ra': 1,
'ipv6_autoconf': 0,
- 'ipv6_eui64_prefix': '',
+ 'ipv6_eui64_prefix': [],
+ 'ipv6_eui64_prefix_remove': [],
'ipv6_forwarding': 1,
'ipv6_dup_addr_detect': 1,
'ipv6_local_address': [],
@@ -197,21 +199,16 @@ def get_config():
openvpn['intf'] = os.environ['VYOS_TAGNODE_VALUE']
openvpn['auth_user_pass_file'] = f"/run/openvpn/{openvpn['intf']}.pw"
+ # check if interface is member of a bridge
+ openvpn['is_bridge_member'] = is_member(conf, openvpn['intf'], 'bridge')
+
# Check if interface instance has been removed
if not conf.exists('interfaces openvpn ' + openvpn['intf']):
openvpn['deleted'] = True
- # check if interface is member if a bridge
- openvpn['is_bridge_member'] = is_bridge_member(conf, openvpn['intf'])
return openvpn
- # Check if we belong to any bridge interface
- for bridge in conf.list_nodes('interfaces bridge'):
- for intf in conf.list_nodes('interfaces bridge {} member interface'.format(bridge)):
- if intf == openvpn['intf']:
- openvpn['bridge_member'].append(intf)
-
# bridged server should not have a pool by default (but can be specified manually)
- if openvpn['bridge_member']:
+ if openvpn['is_bridge_member']:
openvpn['server_pool'] = False
openvpn['server_ipv6_pool'] = False
@@ -314,9 +311,21 @@ def get_config():
if conf.exists('ipv6 address autoconf'):
openvpn['ipv6_autoconf'] = 1
- # Get prefix for IPv6 addressing based on MAC address (EUI-64)
+ # Get prefixes for IPv6 addressing based on MAC address (EUI-64)
if conf.exists('ipv6 address eui64'):
- openvpn['ipv6_eui64_prefix'] = conf.return_value('ipv6 address eui64')
+ openvpn['ipv6_eui64_prefix'] = conf.return_values('ipv6 address eui64')
+
+ # Determine currently effective EUI64 addresses - to determine which
+ # address is no longer valid and needs to be removed
+ eff_addr = conf.return_effective_values('ipv6 address eui64')
+ openvpn['ipv6_eui64_prefix_remove'] = list_diff(eff_addr, openvpn['ipv6_eui64_prefix'])
+
+ # Remove the default link-local address if set.
+ if conf.exists('ipv6 address no-default-link-local'):
+ openvpn['ipv6_eui64_prefix_remove'].append('fe80::/64')
+ else:
+ # add the link-local by default to make IPv6 work
+ openvpn['ipv6_eui64_prefix'].append('fe80::/64')
# Disable IPv6 forwarding on this interface
if conf.exists('ipv6 disable-forwarding'):
@@ -326,6 +335,11 @@ def get_config():
if conf.exists('ipv6 dup-addr-detect-transmits'):
openvpn['ipv6_dup_addr_detect'] = int(conf.return_value('ipv6 dup-addr-detect-transmits'))
+ # to make IPv6 SLAAC and DHCPv6 work with forwarding=1,
+ # accept_ra must be 2
+ if openvpn['ipv6_autoconf'] or 'dhcpv6' in openvpn['address']:
+ openvpn['ipv6_accept_ra'] = 2
+
# OpenVPN operation mode
if conf.exists('mode'):
openvpn['mode'] = conf.return_value('mode')
@@ -583,7 +597,7 @@ def get_config():
default_server = getDefaultServer(server_network_v4, openvpn['server_topology'], openvpn['type'])
if default_server:
# server-bridge doesn't require a pool so don't set defaults for it
- if openvpn['server_pool'] and not openvpn['bridge_member']:
+ if openvpn['server_pool'] and not openvpn['is_bridge_member']:
if not openvpn['server_pool_start']:
openvpn['server_pool_start'] = default_server['pool_start']
@@ -621,22 +635,15 @@ def get_config():
def verify(openvpn):
if openvpn['deleted']:
if openvpn['is_bridge_member']:
- interface = openvpn['intf']
- bridge = openvpn['is_bridge_member']
- raise ConfigError(f'Interface "{interface}" can not be deleted as it belongs to bridge "{bridge}"!')
-
+ raise ConfigError((
+ f'Cannot delete interface "{openvpn["intf"]}" as it is a '
+ f'member of bridge "{openvpn["is_bridge_menber"]}"!'))
return None
if not openvpn['mode']:
raise ConfigError('Must specify OpenVPN operation mode')
- # Checks which need to be performed on interface rmeoval
- if openvpn['deleted']:
- # OpenVPN interface can not be deleted if it's still member of a bridge
- if openvpn['bridge_member']:
- raise ConfigError('Can not delete {} as it is a member interface of bridge {}!'.format(openvpn['intf'], bridge))
-
# Check if we have disabled ncp and at the same time specified ncp-ciphers
if openvpn['disable_ncp'] and openvpn['ncp_ciphers']:
raise ConfigError('Cannot specify both "encryption disable-ncp" and "encryption ncp-ciphers"')
@@ -666,9 +673,9 @@ def verify(openvpn):
if openvpn['ncp_ciphers']:
raise ConfigError('encryption ncp-ciphers cannot be specified in site-to-site mode, only server or client')
- if openvpn['mode'] == 'site-to-site' and not openvpn['bridge_member']:
+ if openvpn['mode'] == 'site-to-site' and not openvpn['is_bridge_member']:
if not (openvpn['local_address'] or openvpn['ipv6_local_address']):
- raise ConfigError('Must specify "local-address" or "bridge member interface"')
+ raise ConfigError('Must specify "local-address" or add interface to bridge')
if len(openvpn['local_address']) > 1 or len(openvpn['ipv6_local_address']) > 1:
raise ConfigError('Cannot specify more than 1 IPv4 and 1 IPv6 "local-address"')
@@ -747,8 +754,8 @@ def verify(openvpn):
raise ConfigError(f'Client "{client["name"]}" IP {client["ip"][0]} not in server subnet {subnet}')
else:
- if not openvpn['bridge_member']:
- raise ConfigError('Must specify "server subnet" or "bridge member interface" in server mode')
+ if not openvpn['is_bridge_member']:
+ raise ConfigError('Must specify "server subnet" or add interface to bridge in server mode')
if openvpn['server_pool']:
if not (openvpn['server_pool_start'] and openvpn['server_pool_stop']):
@@ -1041,15 +1048,28 @@ def apply(openvpn):
o = VTunIf(interface)
# update interface description used e.g. within SNMP
o.set_alias(openvpn['description'])
+ # IPv6 accept RA
+ o.set_ipv6_accept_ra(openvpn['ipv6_accept_ra'])
# IPv6 address autoconfiguration
o.set_ipv6_autoconf(openvpn['ipv6_autoconf'])
- # IPv6 EUI-based address
- o.set_ipv6_eui64_address(openvpn['ipv6_eui64_prefix'])
# IPv6 forwarding
o.set_ipv6_forwarding(openvpn['ipv6_forwarding'])
# IPv6 Duplicate Address Detection (DAD) tries
o.set_ipv6_dad_messages(openvpn['ipv6_dup_addr_detect'])
+ # IPv6 EUI-based addresses - only in TAP mode (TUN's have no MAC)
+ # If MAC has changed, old EUI64 addresses won't get deleted,
+ # but this isn't easy to solve, so leave them.
+ # This is even more difficult as openvpn uses a random MAC for the
+ # initial interface creation, unless set by 'lladdr'.
+ # NOTE: right now the interface is always deleted. For future
+ # compatibility when tap's are not deleted, leave the del_ in
+ if openvpn['mode'] == 'tap':
+ for addr in openvpn['ipv6_eui64_prefix_remove']:
+ o.del_ipv6_eui64_address(addr)
+ for addr in openvpn['ipv6_eui64_prefix']:
+ o.add_ipv6_eui64_address(addr)
+
except:
pass
diff --git a/src/conf_mode/interfaces-pppoe.py b/src/conf_mode/interfaces-pppoe.py
index f942b7d2f..e46d52d19 100755
--- a/src/conf_mode/interfaces-pppoe.py
+++ b/src/conf_mode/interfaces-pppoe.py
@@ -36,6 +36,7 @@ default_config_data = {
'deleted': False,
'description': '\0',
'disable': False,
+ 'dhcpv6_pd': [],
'intf': '',
'idle_timeout': '',
'ipv6_autoconf': False,
@@ -138,6 +139,27 @@ def get_config():
if conf.exists('vrf'):
pppoe['vrf'] = conf.return_value(['vrf'])
+ if conf.exists(['dhcpv6-options', 'delegate']):
+ for interface in conf.list_nodes(['dhcpv6-options', 'delegate']):
+ pd = {
+ 'ifname': interface,
+ 'sla_id': '',
+ 'sla_len': '',
+ 'if_id': ''
+ }
+ conf.set_level(base_path + [pppoe['intf'], 'dhcpv6-options', 'delegate', interface])
+
+ if conf.exists(['sla-id']):
+ pd['sla_id'] = conf.return_value(['sla-id'])
+
+ if conf.exists(['sla-len']):
+ pd['sla_len'] = conf.return_value(['sla-len'])
+
+ if conf.exists(['interface-id']):
+ pd['if_id'] = conf.return_value(['interface-id'])
+
+ pppoe['dhcpv6_pd'].append(pd)
+
return pppoe
def verify(pppoe):
@@ -169,15 +191,15 @@ def generate(pppoe):
script_pppoe_ip_up = f'/etc/ppp/ip-up.d/1000-vyos-pppoe-{intf}'
script_pppoe_ip_down = f'/etc/ppp/ip-down.d/1000-vyos-pppoe-{intf}'
script_pppoe_ipv6_up = f'/etc/ppp/ipv6-up.d/1000-vyos-pppoe-{intf}'
+ config_wide_dhcp6c = f'/run/dhcp6c/dhcp6c.{intf}.conf'
config_files = [config_pppoe, script_pppoe_pre_up, script_pppoe_ip_up,
- script_pppoe_ip_down, script_pppoe_ipv6_up]
+ script_pppoe_ip_down, script_pppoe_ipv6_up, config_wide_dhcp6c]
+
+ # Shutdown DHCPv6 prefix delegation client
+ if not pppoe['dhcpv6_pd']:
+ cmd(f'systemctl stop dhcp6c@{intf}.service')
- # Ensure directories for config files exist - otherwise create them on demand
- for file in config_files:
- dirname = os.path.dirname(file)
- if not os.path.isdir(dirname):
- os.mkdir(dirname)
# Always hang-up PPPoE connection prior generating new configuration file
cmd(f'systemctl stop ppp@{intf}.service')
@@ -189,27 +211,29 @@ def generate(pppoe):
os.unlink(file)
else:
+ # generated script must be executable
+
# Create PPP configuration files
render(config_pppoe, 'pppoe/peer.tmpl',
- pppoe, trim_blocks=True)
+ pppoe, trim_blocks=True, permission=0o755)
# Create script for ip-pre-up.d
render(script_pppoe_pre_up, 'pppoe/ip-pre-up.script.tmpl',
- pppoe, trim_blocks=True)
+ pppoe, trim_blocks=True, permission=0o755)
# Create script for ip-up.d
render(script_pppoe_ip_up, 'pppoe/ip-up.script.tmpl',
- pppoe, trim_blocks=True)
+ pppoe, trim_blocks=True, permission=0o755)
# Create script for ip-down.d
render(script_pppoe_ip_down, 'pppoe/ip-down.script.tmpl',
- pppoe, trim_blocks=True)
+ pppoe, trim_blocks=True, permission=0o755)
# Create script for ipv6-up.d
render(script_pppoe_ipv6_up, 'pppoe/ipv6-up.script.tmpl',
- pppoe, trim_blocks=True)
+ pppoe, trim_blocks=True, permission=0o755)
- # make generated script file executable
- chmod_755(script_pppoe_pre_up)
- chmod_755(script_pppoe_ip_up)
- chmod_755(script_pppoe_ip_down)
- chmod_755(script_pppoe_ipv6_up)
+ if len(pppoe['dhcpv6_pd']) > 0:
+ # ipv6.tmpl relies on ifname - this should be made consitent in the
+ # future better then double key-ing the same value
+ pppoe['ifname'] = intf
+ render(config_wide_dhcp6c, 'dhcp-client/ipv6.tmpl', pppoe, trim_blocks=True)
return None
diff --git a/src/conf_mode/interfaces-pseudo-ethernet.py b/src/conf_mode/interfaces-pseudo-ethernet.py
index d5f308ed3..3e036a753 100755
--- a/src/conf_mode/interfaces-pseudo-ethernet.py
+++ b/src/conf_mode/interfaces-pseudo-ethernet.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2019 VyOS maintainers and contributors
+# Copyright (C) 2019-2020 VyOS maintainers and contributors
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License version 2 or later as
@@ -21,154 +21,55 @@ from sys import exit
from netifaces import interfaces
from vyos.config import Config
-from vyos.configdict import list_diff, vlan_to_dict
-from vyos.ifconfig import MACVLANIf
-from vyos.ifconfig_vlan import apply_vlan_config, verify_vlan_config
-from vyos.validate import is_bridge_member
+from vyos.configdict import list_diff, intf_to_dict, add_to_dict, interface_default_data
+from vyos.ifconfig import MACVLANIf, Section
+from vyos.ifconfig_vlan import apply_all_vlans, verify_vlan_config
from vyos import ConfigError
default_config_data = {
- 'address': [],
- 'address_remove': [],
- 'description': '',
+ **interface_default_data,
'deleted': False,
- 'dhcp_client_id': '',
- 'dhcp_hostname': '',
- 'dhcp_vendor_class_id': '',
- 'dhcpv6_prm_only': False,
- 'dhcpv6_temporary': False,
- 'disable': False,
- 'disable_link_detect': 1,
'intf': '',
'ip_arp_cache_tmo': 30,
- 'ip_disable_arp_filter': 1,
- 'ip_enable_arp_accept': 0,
- 'ip_enable_arp_announce': 0,
- 'ip_enable_arp_ignore': 0,
- 'ip_proxy_arp': 0,
'ip_proxy_arp_pvlan': 0,
- 'ipv6_autoconf': 0,
- 'ipv6_eui64_prefix': '',
- 'ipv6_forwarding': 1,
- 'ipv6_dup_addr_detect': 1,
- 'is_bridge_member': False,
'source_interface': '',
'source_interface_changed': False,
- 'mac': '',
'mode': 'private',
- 'vif_s': [],
+ 'vif_s': {},
'vif_s_remove': [],
- 'vif': [],
+ 'vif': {},
'vif_remove': [],
'vrf': ''
}
def get_config():
- peth = deepcopy(default_config_data)
- conf = Config()
-
# determine tagNode instance
if 'VYOS_TAGNODE_VALUE' not in os.environ:
raise ConfigError('Interface (VYOS_TAGNODE_VALUE) not specified')
- peth['intf'] = os.environ['VYOS_TAGNODE_VALUE']
- cfg_base = ['interfaces', 'pseudo-ethernet', peth['intf']]
+ ifname = os.environ['VYOS_TAGNODE_VALUE']
+ conf = Config()
# Check if interface has been removed
+ cfg_base = ['interfaces', 'pseudo-ethernet', ifname]
if not conf.exists(cfg_base):
+ peth = deepcopy(default_config_data)
peth['deleted'] = True
- # check if interface is member if a bridge
- peth['is_bridge_member'] = is_bridge_member(conf, peth['intf'])
return peth
# set new configuration level
conf.set_level(cfg_base)
- # retrieve configured interface addresses
- if conf.exists(['address']):
- peth['address'] = conf.return_values(['address'])
-
- # get interface addresses (currently effective) - to determine which
- # address is no longer valid and needs to be removed
- eff_addr = conf.return_effective_values(['address'])
- peth['address_remove'] = list_diff(eff_addr, peth['address'])
-
- # retrieve interface description
- if conf.exists(['description']):
- peth['description'] = conf.return_value(['description'])
-
- # get DHCP client identifier
- if conf.exists(['dhcp-options', 'client-id']):
- peth['dhcp_client_id'] = conf.return_value(['dhcp-options', 'client-id'])
-
- # DHCP client host name (overrides the system host name)
- if conf.exists(['dhcp-options', 'host-name']):
- peth['dhcp_hostname'] = conf.return_value(['dhcp-options', 'host-name'])
-
- # DHCP client vendor identifier
- if conf.exists(['dhcp-options', 'vendor-class-id']):
- peth['dhcp_vendor_class_id'] = conf.return_value(['dhcp-options', 'vendor-class-id'])
-
- # DHCPv6 only acquire config parameters, no address
- if conf.exists(['dhcpv6-options parameters-only']):
- peth['dhcpv6_prm_only'] = True
-
- # DHCPv6 temporary IPv6 address
- if conf.exists(['dhcpv6-options temporary']):
- peth['dhcpv6_temporary'] = True
-
- # disable interface
- if conf.exists(['disable']):
- peth['disable'] = True
-
- # ignore link state changes
- if conf.exists(['disable-link-detect']):
- peth['disable_link_detect'] = 2
+ peth, disabled = intf_to_dict(conf, default_config_data)
# ARP cache entry timeout in seconds
if conf.exists(['ip', 'arp-cache-timeout']):
peth['ip_arp_cache_tmo'] = int(conf.return_value(['ip', 'arp-cache-timeout']))
- # ARP filter configuration
- if conf.exists(['ip', 'disable-arp-filter']):
- peth['ip_disable_arp_filter'] = 0
-
- # ARP enable accept
- if conf.exists(['ip', 'enable-arp-accept']):
- peth['ip_enable_arp_accept'] = 1
-
- # ARP enable announce
- if conf.exists(['ip', 'enable-arp-announce']):
- peth['ip_enable_arp_announce'] = 1
-
- # ARP enable ignore
- if conf.exists(['ip', 'enable-arp-ignore']):
- peth['ip_enable_arp_ignore'] = 1
-
- # Enable proxy-arp on this interface
- if conf.exists(['ip', 'enable-proxy-arp']):
- peth['ip_proxy_arp'] = 1
-
# Enable private VLAN proxy ARP on this interface
if conf.exists(['ip', 'proxy-arp-pvlan']):
peth['ip_proxy_arp_pvlan'] = 1
- # Enable acquisition of IPv6 address using stateless autoconfig (SLAAC)
- if conf.exists('ipv6 address autoconf'):
- peth['ipv6_autoconf'] = 1
-
- # Get prefix for IPv6 addressing based on MAC address (EUI-64)
- if conf.exists('ipv6 address eui64'):
- peth['ipv6_eui64_prefix'] = conf.return_value('ipv6 address eui64')
-
- # Disable IPv6 forwarding on this interface
- if conf.exists('ipv6 disable-forwarding'):
- peth['ipv6_forwarding'] = 0
-
- # IPv6 Duplicate Address Detection (DAD) tries
- if conf.exists('ipv6 dup-addr-detect-transmits'):
- peth['ipv6_dup_addr_detect'] = int(conf.return_value('ipv6 dup-addr-detect-transmits'))
-
# Physical interface
if conf.exists(['source-interface']):
peth['source_interface'] = conf.return_value(['source-interface'])
@@ -176,67 +77,49 @@ def get_config():
if tmp != peth['source_interface']:
peth['source_interface_changed'] = True
- # Media Access Control (MAC) address
- if conf.exists(['mac']):
- peth['mac'] = conf.return_value(['mac'])
-
# MACvlan mode
if conf.exists(['mode']):
peth['mode'] = conf.return_value(['mode'])
- # retrieve VRF instance
- if conf.exists('vrf'):
- peth['vrf'] = conf.return_value('vrf')
-
- # re-set configuration level to parse new nodes
- conf.set_level(cfg_base)
- # get vif-s interfaces (currently effective) - to determine which vif-s
- # interface is no longer present and needs to be removed
- eff_intf = conf.list_effective_nodes('vif-s')
- act_intf = conf.list_nodes('vif-s')
- peth['vif_s_remove'] = list_diff(eff_intf, act_intf)
-
- if conf.exists('vif-s'):
- for vif_s in conf.list_nodes('vif-s'):
- # set config level to vif-s interface
- conf.set_level(cfg_base + ['vif-s', vif_s])
- peth['vif_s'].append(vlan_to_dict(conf))
-
- # re-set configuration level to parse new nodes
- conf.set_level(cfg_base)
- # Determine vif interfaces (currently effective) - to determine which
- # vif interface is no longer present and needs to be removed
- eff_intf = conf.list_effective_nodes('vif')
- act_intf = conf.list_nodes('vif')
- peth['vif_remove'] = list_diff(eff_intf, act_intf)
-
- if conf.exists('vif'):
- for vif in conf.list_nodes('vif'):
- # set config level to vif interface
- conf.set_level(cfg_base + ['vif', vif])
- peth['vif'].append(vlan_to_dict(conf))
-
+ add_to_dict(conf, disabled, peth, 'vif', 'vif')
+ add_to_dict(conf, disabled, peth, 'vif-s', 'vif_s')
return peth
def verify(peth):
if peth['deleted']:
if peth['is_bridge_member']:
- interface = peth['intf']
- bridge = peth['is_bridge_member']
- raise ConfigError(f'Interface "{interface}" can not be deleted as it belongs to bridge "{bridge}"!')
+ raise ConfigError((
+ f'Cannot delete interface "{peth["intf"]}" as it is a '
+ f'member of bridge "{peth["is_bridge_member"]}"!'))
return None
if not peth['source_interface']:
- raise ConfigError('Link device must be set for virtual ethernet {}'.format(peth['intf']))
+ raise ConfigError((
+ f'Link device must be set for pseudo-ethernet "{peth["intf"]}"'))
if not peth['source_interface'] in interfaces():
- raise ConfigError('Pseudo-ethernet source interface does not exist')
+ raise ConfigError((
+ f'Pseudo-ethernet "{peth["intf"]}" link device does not exist'))
+
+ if ( peth['is_bridge_member']
+ and ( peth['address']
+ or peth['ipv6_eui64_prefix']
+ or peth['ipv6_autoconf'] ) ):
+ raise ConfigError((
+ f'Cannot assign address to interface "{peth["intf"]}" '
+ f'as it is a member of bridge "{peth["is_bridge_member"]}"!'))
- vrf_name = peth['vrf']
- if vrf_name and vrf_name not in interfaces():
- raise ConfigError(f'VRF "{vrf_name}" does not exist')
+ if peth['vrf']:
+ if peth['vrf'] not in interfaces():
+ raise ConfigError(f'VRF "{peth["vrf"]}" does not exist')
+
+ if peth['is_bridge_member']:
+ raise ConfigError((
+ f'Interface "{peth["intf"]}" cannot be member of VRF '
+ f'"{peth["vrf"]}" and bridge {peth["is_bridge_member"]} '
+ f'at the same time!'))
# use common function to verify VLAN configuration
verify_vlan_config(peth)
@@ -288,6 +171,9 @@ def apply(peth):
if peth['dhcpv6_temporary']:
p.dhcp.v6.options['dhcpv6_temporary'] = True
+ if peth['dhcpv6_pd']:
+ p.dhcp.v6.options['dhcpv6_pd'] = peth['dhcpv6_pd']
+
# ignore link state changes
p.set_link_detect(peth['disable_link_detect'])
# configure ARP cache timeout in milliseconds
@@ -304,22 +190,32 @@ def apply(peth):
p.set_proxy_arp(peth['ip_proxy_arp'])
# Enable private VLAN proxy ARP on this interface
p.set_proxy_arp_pvlan(peth['ip_proxy_arp_pvlan'])
+ # IPv6 accept RA
+ p.set_ipv6_accept_ra(peth['ipv6_accept_ra'])
# IPv6 address autoconfiguration
p.set_ipv6_autoconf(peth['ipv6_autoconf'])
- # IPv6 EUI-based address
- p.set_ipv6_eui64_address(peth['ipv6_eui64_prefix'])
# IPv6 forwarding
p.set_ipv6_forwarding(peth['ipv6_forwarding'])
# IPv6 Duplicate Address Detection (DAD) tries
p.set_ipv6_dad_messages(peth['ipv6_dup_addr_detect'])
- # assign/remove VRF
- p.set_vrf(peth['vrf'])
+ # assign/remove VRF (ONLY when not a member of a bridge,
+ # otherwise 'nomaster' removes it from it)
+ if not peth['is_bridge_member']:
+ p.set_vrf(peth['vrf'])
+
+ # Delete old IPv6 EUI64 addresses before changing MAC
+ for addr in peth['ipv6_eui64_prefix_remove']:
+ p.del_ipv6_eui64_address(addr)
# Change interface MAC address
if peth['mac']:
p.set_mac(peth['mac'])
+ # Add IPv6 EUI-based addresses
+ for addr in peth['ipv6_eui64_prefix']:
+ p.add_ipv6_eui64_address(addr)
+
# Change interface mode
p.set_mode(peth['mode'])
@@ -337,34 +233,12 @@ def apply(peth):
for addr in peth['address']:
p.add_addr(addr)
- # remove no longer required service VLAN interfaces (vif-s)
- for vif_s in peth['vif_s_remove']:
- p.del_vlan(vif_s)
-
- # create service VLAN interfaces (vif-s)
- for vif_s in peth['vif_s']:
- s_vlan = p.add_vlan(vif_s['id'], ethertype=vif_s['ethertype'])
- apply_vlan_config(s_vlan, vif_s)
-
- # remove no longer required client VLAN interfaces (vif-c)
- # on lower service VLAN interface
- for vif_c in vif_s['vif_c_remove']:
- s_vlan.del_vlan(vif_c)
-
- # create client VLAN interfaces (vif-c)
- # on lower service VLAN interface
- for vif_c in vif_s['vif_c']:
- c_vlan = s_vlan.add_vlan(vif_c['id'])
- apply_vlan_config(c_vlan, vif_c)
-
- # remove no longer required VLAN interfaces (vif)
- for vif in peth['vif_remove']:
- p.del_vlan(vif)
-
- # create VLAN interfaces (vif)
- for vif in peth['vif']:
- vlan = p.add_vlan(vif['id'])
- apply_vlan_config(vlan, vif)
+ # re-add ourselves to any bridge we might have fallen out of
+ if peth['is_bridge_member']:
+ p.add_to_bridge(peth['is_bridge_member'])
+
+ # apply all vlans to interface
+ apply_all_vlans(p, peth)
return None
diff --git a/src/conf_mode/interfaces-tunnel.py b/src/conf_mode/interfaces-tunnel.py
index 9c0c42414..8e9bb069e 100755
--- a/src/conf_mode/interfaces-tunnel.py
+++ b/src/conf_mode/interfaces-tunnel.py
@@ -25,7 +25,7 @@ from vyos.config import Config
from vyos.ifconfig import Interface, GREIf, GRETapIf, IPIPIf, IP6GREIf, IPIP6If, IP6IP6If, SitIf, Sit6RDIf
from vyos.ifconfig.afi import IP4, IP6
from vyos.configdict import list_diff
-from vyos.validate import is_ipv4, is_ipv6, is_bridge_member
+from vyos.validate import is_ipv4, is_ipv6, is_member
from vyos import ConfigError
from vyos.dicts import FixedDict
@@ -222,7 +222,7 @@ class ConfigurationState(Config):
remove all the values which were not changed from the default
"""
for option in options:
- if self.exists(option) and self_return_value(option) != self.default[option]:
+ if self.exists(option) and self.self_return_value(option) != self.default[option]:
continue
del self.options[option]
@@ -251,6 +251,7 @@ default_config_data = {
'ip': False,
'ipv6': False,
'nhrp': [],
+ 'ipv6_accept_ra': 1,
'ipv6_autoconf': 0,
'ipv6_forwarding': 1,
'ipv6_dad_transmits': 1,
@@ -401,6 +402,11 @@ def get_config():
eff_addr = conf.return_effective_values('address')
options['addresses-del'] = list_diff(eff_addr, options['addresses-add'])
+ # to make IPv6 SLAAC and DHCPv6 work with forwarding=1,
+ # accept_ra must be 2
+ if options['ipv6_autoconf'] or 'dhcpv6' in options['addresses-add']:
+ options['ipv6_accept_ra'] = 2
+
# allmulticast fate is linked to multicast
options['allmulticast'] = options['multicast']
@@ -410,7 +416,7 @@ def get_config():
options['tunnel'] = {}
# check for bridges
- options['bridge'] = is_bridge_member(conf, ifname)
+ options['bridge'] = is_member(conf, ifname, 'bridge')
options['interfaces'] = interfaces()
for name in ct:
@@ -436,11 +442,14 @@ def verify(conf):
if changes['section'] == 'delete':
if ifname in options['nhrp']:
- raise ConfigError(f'Can not delete interface tunnel {iftype} {ifname}, it is used by nhrp')
+ raise ConfigError((
+ f'Cannot delete interface tunnel {iftype} {ifname}, '
+ 'it is used by NHRP'))
- bridge = options['bridge']
- if bridge:
- raise ConfigError(f'Interface "{ifname}" can not be deleted as it belongs to bridge "{bridge}"!')
+ if options['bridge']:
+ raise ConfigError((
+ f'Cannot delete interface "{options["ifname"]}" as it is a '
+ f'member of bridge "{options["bridge"]}"!'))
# done, bail out early
return None
@@ -461,7 +470,7 @@ def verify(conf):
# what are the tunnel options we can set / modified / deleted
kls = get_class(options)
- valid = kls.updates + ['alias', 'addresses-add', 'addresses-del', 'vrf']
+ valid = kls.updates + ['alias', 'addresses-add', 'addresses-del', 'vrf', 'state']
if changes['section'] == 'create':
valid.extend(['type',])
@@ -525,15 +534,28 @@ def verify(conf):
print(f'Should not use IPv6 addresses on tunnel {iftype} {ifname}')
# vrf check
-
- vrf = options['vrf']
- if vrf and vrf not in options['interfaces']:
- raise ConfigError(f'VRF "{vrf}" does not exist')
+ if options['vrf']:
+ if options['vrf'] not in options['interfaces']:
+ raise ConfigError(f'VRF "{options["vrf"]}" does not exist')
+
+ if options['bridge']:
+ raise ConfigError((
+ f'Interface "{options["ifname"]}" cannot be member of VRF '
+ f'"{options["vrf"]}" and bridge {options["bridge"]} '
+ f'at the same time!'))
+
+ # bridge and address check
+ if ( options['bridge']
+ and ( options['addresses-add']
+ or options['ipv6_autoconf'] ) ):
+ raise ConfigError((
+ f'Cannot assign address to interface "{options["name"]}" '
+ f'as it is a member of bridge "{options["bridge"]}"!'))
# source-interface check
if tun_dev and tun_dev not in options['interfaces']:
- raise ConfigError(f'device "{dev}" does not exist')
+ raise ConfigError(f'device "{tun_dev}" does not exist')
# tunnel encapsulation check
@@ -620,12 +642,17 @@ def apply(conf):
# set other interface properties
for option in ('alias', 'mtu', 'link_detect', 'multicast', 'allmulticast',
- 'vrf', 'ipv6_autoconf', 'ipv6_forwarding', 'ipv6_dad_transmits'):
+ 'ipv6_accept_ra', 'ipv6_autoconf', 'ipv6_forwarding', 'ipv6_dad_transmits'):
if not options[option]:
# should never happen but better safe
continue
tunnel.set_interface(option, options[option])
+ # assign/remove VRF (ONLY when not a member of a bridge,
+ # otherwise 'nomaster' removes it from it)
+ if not options['bridge']:
+ tunnel.set_vrf(options['vrf'])
+
# Configure interface address(es)
for addr in options['addresses-del']:
tunnel.del_addr(addr)
diff --git a/src/conf_mode/interfaces-vxlan.py b/src/conf_mode/interfaces-vxlan.py
index d238ddb57..84fe3dfc8 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 VyOS maintainers and contributors
+# Copyright (C) 2019-2020 VyOS maintainers and contributors
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License version 2 or later as
@@ -22,7 +22,7 @@ from netifaces import interfaces
from vyos.config import Config
from vyos.ifconfig import VXLANIf, Interface
-from vyos.validate import is_bridge_member
+from vyos.validate import is_member
from vyos import ConfigError
default_config_data = {
@@ -38,8 +38,9 @@ default_config_data = {
'ip_enable_arp_announce': 0,
'ip_enable_arp_ignore': 0,
'ip_proxy_arp': 0,
+ 'ipv6_accept_ra': 1,
'ipv6_autoconf': 0,
- 'ipv6_eui64_prefix': '',
+ 'ipv6_eui64_prefix': [],
'ipv6_forwarding': 1,
'ipv6_dup_addr_detect': 1,
'is_bridge_member': False,
@@ -62,11 +63,12 @@ def get_config():
vxlan['intf'] = os.environ['VYOS_TAGNODE_VALUE']
+ # check if interface is member if a bridge
+ vxlan['is_bridge_member'] = is_member(conf, vxlan['intf'], 'bridge')
+
# Check if interface has been removed
if not conf.exists('interfaces vxlan ' + vxlan['intf']):
vxlan['deleted'] = True
- # check if interface is member if a bridge
- vxlan['is_bridge_member'] = is_bridge_member(conf, vxlan['intf'])
return vxlan
# set new configuration level
@@ -116,9 +118,15 @@ def get_config():
if conf.exists('ipv6 address autoconf'):
vxlan['ipv6_autoconf'] = 1
- # Get prefix for IPv6 addressing based on MAC address (EUI-64)
+ # Get prefixes for IPv6 addressing based on MAC address (EUI-64)
if conf.exists('ipv6 address eui64'):
- vxlan['ipv6_eui64_prefix'] = conf.return_value('ipv6 address eui64')
+ vxlan['ipv6_eui64_prefix'] = conf.return_values('ipv6 address eui64')
+
+ # Remove the default link-local address if set.
+ if not ( conf.exists('ipv6 address no-default-link-local')
+ or vxlan['is_bridge_member'] ):
+ # add the link-local by default to make IPv6 work
+ vxlan['ipv6_eui64_prefix'].append('fe80::/64')
# Disable IPv6 forwarding on this interface
if conf.exists('ipv6 disable-forwarding'):
@@ -128,6 +136,11 @@ def get_config():
if conf.exists('ipv6 dup-addr-detect-transmits'):
vxlan['ipv6_dup_addr_detect'] = int(conf.return_value('ipv6 dup-addr-detect-transmits'))
+ # to make IPv6 SLAAC and DHCPv6 work with forwarding=1,
+ # accept_ra must be 2
+ if vxlan['ipv6_autoconf'] or 'dhcpv6' in vxlan['address']:
+ vxlan['ipv6_accept_ra'] = 2
+
# VXLAN source address
if conf.exists('source-address'):
vxlan['source_address'] = conf.return_value('source-address')
@@ -158,9 +171,9 @@ def get_config():
def verify(vxlan):
if vxlan['deleted']:
if vxlan['is_bridge_member']:
- interface = vxlan['intf']
- bridge = vxlan['is_bridge_member']
- raise ConfigError(f'Interface "{interface}" can not be deleted as it belongs to bridge "{bridge}"!')
+ raise ConfigError((
+ f'Cannot delete interface "{vxlan["intf"]}" as it is a '
+ f'member of bridge "{vxlan["is_bridge_member"]}"!'))
return None
@@ -188,6 +201,14 @@ def verify(vxlan):
raise ConfigError('VXLAN has a 50 byte overhead, underlaying device ' \
'MTU is to small ({})'.format(underlay_mtu))
+ if ( vxlan['is_bridge_member']
+ and ( vxlan['address']
+ or vxlan['ipv6_eui64_prefix']
+ or vxlan['ipv6_autoconf'] ) ):
+ raise ConfigError((
+ f'Cannot assign address to interface "{vxlan["intf"]}" '
+ f'as it is a member of bridge "{vxlan["is_bridge_member"]}"!'))
+
return None
@@ -236,10 +257,10 @@ def apply(vxlan):
v.set_arp_ignore(vxlan['ip_enable_arp_ignore'])
# Enable proxy-arp on this interface
v.set_proxy_arp(vxlan['ip_proxy_arp'])
+ # IPv6 accept RA
+ v.set_ipv6_accept_ra(vxlan['ipv6_accept_ra'])
# IPv6 address autoconfiguration
v.set_ipv6_autoconf(vxlan['ipv6_autoconf'])
- # IPv6 EUI-based address
- v.set_ipv6_eui64_address(vxlan['ipv6_eui64_prefix'])
# IPv6 forwarding
v.set_ipv6_forwarding(vxlan['ipv6_forwarding'])
# IPv6 Duplicate Address Detection (DAD) tries
@@ -251,12 +272,20 @@ def apply(vxlan):
for addr in vxlan['address']:
v.add_addr(addr)
+ # IPv6 EUI-based addresses
+ for addr in vxlan['ipv6_eui64_prefix']:
+ v.add_ipv6_eui64_address(addr)
+
# As the VXLAN interface is always disabled first when changing
# parameters we will only re-enable the interface if it is not
# administratively disabled
if not vxlan['disable']:
v.set_admin_state('up')
+ # re-add ourselves to any bridge we might have fallen out of
+ if vxlan['is_bridge_member']:
+ v.add_to_bridge(vxlan['is_bridge_member'])
+
return None
diff --git a/src/conf_mode/interfaces-wireguard.py b/src/conf_mode/interfaces-wireguard.py
index 423700370..97dcf626b 100755
--- a/src/conf_mode/interfaces-wireguard.py
+++ b/src/conf_mode/interfaces-wireguard.py
@@ -25,7 +25,7 @@ from vyos.config import Config
from vyos.configdict import list_diff
from vyos.ifconfig import WireGuardIf
from vyos.util import chown, chmod_750, call
-from vyos.validate import is_bridge_member
+from vyos.validate import is_member, is_ipv6
from vyos import ConfigError
kdir = r'/config/auth/wireguard'
@@ -35,11 +35,11 @@ default_config_data = {
'address': [],
'address_remove': [],
'description': '',
- 'lport': None,
+ 'listen_port': '',
'deleted': False,
'disable': False,
+ 'fwmark': 0,
'is_bridge_member': False,
- 'fwmark': 0x00,
'mtu': 1420,
'peer': [],
'peer_remove': [], # stores public keys of peers to remove
@@ -78,11 +78,12 @@ def get_config():
wg = deepcopy(default_config_data)
wg['intf'] = os.environ['VYOS_TAGNODE_VALUE']
+ # check if interface is member if a bridge
+ wg['is_bridge_member'] = is_member(conf, wg['intf'], 'bridge')
+
# Check if interface has been removed
if not conf.exists(base + [wg['intf']]):
wg['deleted'] = True
- # check if interface is member if a bridge
- wg['is_bridge_member'] = is_bridge_member(conf, wg['intf'])
return wg
conf.set_level(base + [wg['intf']])
@@ -106,7 +107,7 @@ def get_config():
# local port to listen on
if conf.exists(['port']):
- wg['lport'] = conf.return_value(['port'])
+ wg['listen_port'] = conf.return_value(['port'])
# fwmark value
if conf.exists(['fwmark']):
@@ -189,38 +190,52 @@ def get_config():
def verify(wg):
- interface = wg['intf']
-
if wg['deleted']:
if wg['is_bridge_member']:
- interface = wg['intf']
- bridge = wg['is_bridge_member']
- raise ConfigError(f'Interface "{interface}" can not be deleted as it belongs to bridge "{bridge}"!')
+ raise ConfigError((
+ f'Cannot delete interface "{wg["intf"]}" as it is a member '
+ f'of bridge "{wg["is_bridge_member"]}"!'))
return None
- vrf_name = wg['vrf']
- if vrf_name and vrf_name not in interfaces():
- raise ConfigError(f'VRF "{vrf_name}" does not exist')
+ if wg['is_bridge_member'] and wg['address']:
+ raise ConfigError((
+ f'Cannot assign address to interface "{wg["intf"]}" '
+ f'as it is a member of bridge "{wg["is_bridge_member"]}"!'))
+
+ if wg['vrf']:
+ if wg['vrf'] not in interfaces():
+ raise ConfigError(f'VRF "{wg["vrf"]}" does not exist')
+
+ if wg['is_bridge_member']:
+ raise ConfigError((
+ f'Interface "{wg["intf"]}" cannot be member of VRF '
+ f'"{wg["vrf"]}" and bridge {wg["is_bridge_member"]} '
+ f'at the same time!'))
if not os.path.exists(wg['pk']):
raise ConfigError('No keys found, generate them by executing:\n' \
'"run generate wireguard [keypair|named-keypairs]"')
if not wg['address']:
- raise ConfigError(f'IP address required for interface "{interface}"!')
+ raise ConfigError(f'IP address required for interface "{wg["intf"]}"!')
if not wg['peer']:
- raise ConfigError(f'Peer required for interface "{interface}"!')
+ raise ConfigError(f'Peer required for interface "{wg["intf"]}"!')
# run checks on individual configured WireGuard peer
for peer in wg['peer']:
- peer_name = peer['name']
if not peer['allowed-ips']:
- raise ConfigError(f'Peer allowed-ips required for peer "{peer_name}"!')
+ raise ConfigError(f'Peer allowed-ips required for peer "{peer["name"]}"!')
if not peer['pubkey']:
- raise ConfigError(f'Peer public-key required for peer "{peer_name}"!')
+ raise ConfigError(f'Peer public-key required for peer "{peer["name"]}"!')
+
+ if peer['address'] and not peer['port']:
+ raise ConfigError(f'Peer "{peer["name"]}" port must be defined if address is defined!')
+
+ if not peer['address'] and peer['port']:
+ raise ConfigError(f'Peer "{peer["name"]}" address must be defined if port is defined!')
def apply(wg):
@@ -246,8 +261,10 @@ def apply(wg):
# update interface description used e.g. within SNMP
w.set_alias(wg['description'])
- # assign/remove VRF
- w.set_vrf(wg['vrf'])
+ # assign/remove VRF (ONLY when not a member of a bridge,
+ # otherwise 'nomaster' removes it from it)
+ if not wg['is_bridge_member']:
+ w.set_vrf(wg['vrf'])
# remove peers
for pub_key in wg['peer_remove']:
@@ -263,16 +280,18 @@ def apply(wg):
# peer allowed-ips
w.config['allowed-ips'] = peer['allowed-ips']
# local listen port
- if wg['lport']:
- w.config['port'] = wg['lport']
+ if wg['listen_port']:
+ w.config['port'] = wg['listen_port']
# fwmark
if c['fwmark']:
w.config['fwmark'] = wg['fwmark']
# endpoint
if peer['address'] and peer['port']:
- w.config['endpoint'] = '{}:{}'.format(
- peer['address'], peer['port'])
+ if is_ipv6(peer['address']):
+ w.config['endpoint'] = '[{}]:{}'.format(peer['address'], peer['port'])
+ else:
+ w.config['endpoint'] = '{}:{}'.format(peer['address'], peer['port'])
# persistent-keepalive
if peer['persistent_keepalive']:
diff --git a/src/conf_mode/interfaces-wireless.py b/src/conf_mode/interfaces-wireless.py
index 42842f9bd..f13408fa2 100755
--- a/src/conf_mode/interfaces-wireless.py
+++ b/src/conf_mode/interfaces-wireless.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2019 VyOS maintainers and contributors
+# Copyright (C) 2019-2020 VyOS maintainers and contributors
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License version 2 or later as
@@ -24,17 +24,16 @@ from netifaces import interfaces
from netaddr import EUI, mac_unix_expanded
from vyos.config import Config
-from vyos.configdict import list_diff, vlan_to_dict
-from vyos.ifconfig import WiFiIf
-from vyos.ifconfig_vlan import apply_vlan_config, verify_vlan_config
+from vyos.configdict import list_diff, intf_to_dict, add_to_dict, interface_default_data
+from vyos.ifconfig import WiFiIf, Section
+from vyos.ifconfig_vlan import apply_all_vlans, verify_vlan_config
from vyos.template import render
from vyos.util import chown, call
-from vyos.validate import is_bridge_member
+from vyos.validate import is_member
from vyos import ConfigError
default_config_data = {
- 'address': [],
- 'address_remove': [],
+ **interface_default_data,
'cap_ht' : False,
'cap_ht_40mhz_incapable' : False,
'cap_ht_powersave' : False,
@@ -69,30 +68,13 @@ default_config_data = {
'cap_vht_vht_cf' : False,
'channel': '',
'country_code': '',
- 'description': '',
'deleted': False,
- 'dhcp_client_id': '',
- 'dhcp_hostname': '',
- 'dhcp_vendor_class_id': '',
- 'dhcpv6_prm_only': False,
- 'dhcpv6_temporary': False,
- 'disable': False,
'disable_broadcast_ssid' : False,
'disable_link_detect' : 1,
'expunge_failing_stations' : False,
'hw_id' : '',
'intf': '',
'isolate_stations' : False,
- 'ip_disable_arp_filter': 1,
- 'ip_enable_arp_accept': 0,
- 'ip_enable_arp_announce': 0,
- 'ip_enable_arp_ignore': 0,
- 'ipv6_autoconf': 0,
- 'ipv6_eui64_prefix': '',
- 'ipv6_forwarding': 1,
- 'ipv6_dup_addr_detect': 1,
- 'is_bridge_member': False,
- 'mac' : '',
'max_stations' : '',
'mgmt_frame_protection' : 'disabled',
'mode' : 'g',
@@ -107,9 +89,10 @@ default_config_data = {
'sec_wpa_radius' : [],
'ssid' : '',
'op_mode' : 'monitor',
- 'vif': [],
+ 'vif': {},
'vif_remove': [],
- 'vrf': ''
+ 'vif_s': {},
+ 'vif_s_remove': []
}
def get_conf_file(conf_type, intf):
@@ -124,21 +107,21 @@ def get_conf_file(conf_type, intf):
return cfg_file
def get_config():
- wifi = deepcopy(default_config_data)
- conf = Config()
-
# determine tagNode instance
if 'VYOS_TAGNODE_VALUE' not in os.environ:
raise ConfigError('Interface (VYOS_TAGNODE_VALUE) not specified')
- wifi['intf'] = os.environ['VYOS_TAGNODE_VALUE']
+ ifname = os.environ['VYOS_TAGNODE_VALUE']
+ conf = Config()
# check if wireless interface has been removed
- cfg_base = 'interfaces wireless ' + wifi['intf']
+ cfg_base = ['interfaces', 'wireless ', ifname]
if not conf.exists(cfg_base):
+ wifi = deepcopy(default_config_data)
+ wifi['intf'] = ifname
wifi['deleted'] = True
- # check if interface is member if a bridge
- wifi['is_bridge_member'] = is_bridge_member(conf, wifi['intf'])
+ # we need to know if we're a bridge member so we can refuse deletion
+ wifi['is_bridge_member'] = is_member(conf, wifi['intf'], 'bridge')
# we can not bail out early as wireless interface can not be removed
# Kernel will complain with: RTNETLINK answers: Operation not supported.
# Thus we need to remove individual settings
@@ -147,14 +130,8 @@ def get_config():
# set new configuration level
conf.set_level(cfg_base)
- # retrieve configured interface addresses
- if conf.exists('address'):
- wifi['address'] = conf.return_values('address')
-
- # get interface addresses (currently effective) - to determine which
- # address is no longer valid and needs to be removed
- eff_addr = conf.return_effective_values('address')
- wifi['address_remove'] = list_diff(eff_addr, wifi['address'])
+ # get common interface settings
+ wifi, disabled = intf_to_dict(conf, default_config_data)
# 40MHz intolerance, use 20MHz only
if conf.exists('capabilities ht 40mhz-incapable'):
@@ -308,38 +285,10 @@ def get_config():
if conf.exists('channel'):
wifi['channel'] = conf.return_value('channel')
- # retrieve interface description
- if conf.exists('description'):
- wifi['description'] = conf.return_value('description')
-
- # get DHCP client identifier
- if conf.exists('dhcp-options client-id'):
- wifi['dhcp_client_id'] = conf.return_value('dhcp-options client-id')
-
- # DHCP client host name (overrides the system host name)
- if conf.exists('dhcp-options host-name'):
- wifi['dhcp_hostname'] = conf.return_value('dhcp-options host-name')
-
- # DHCP client vendor identifier
- if conf.exists('dhcp-options vendor-class-id'):
- wifi['dhcp_vendor_class_id'] = conf.return_value('dhcp-options vendor-class-id')
-
- # DHCPv6 only acquire config parameters, no address
- if conf.exists('dhcpv6-options parameters-only'):
- wifi['dhcpv6_prm_only'] = conf.return_value('dhcpv6-options parameters-only')
-
- # DHCPv6 temporary IPv6 address
- if conf.exists('dhcpv6-options temporary'):
- wifi['dhcpv6_temporary'] = conf.return_value('dhcpv6-options temporary')
-
# Disable broadcast of SSID from access-point
if conf.exists('disable-broadcast-ssid'):
wifi['disable_broadcast_ssid'] = True
- # ignore link state changes on this interface
- if conf.exists('disable-link-detect'):
- wifi['disable_link_detect'] = 2
-
# Disassociate stations based on excessive transmission failures
if conf.exists('expunge-failing-stations'):
wifi['expunge_failing_stations'] = True
@@ -352,46 +301,10 @@ def get_config():
if conf.exists('isolate-stations'):
wifi['isolate_stations'] = True
- # ARP filter configuration
- if conf.exists('ip disable-arp-filter'):
- wifi['ip_disable_arp_filter'] = 0
-
- # ARP enable accept
- if conf.exists('ip enable-arp-accept'):
- wifi['ip_enable_arp_accept'] = 1
-
- # ARP enable announce
- if conf.exists('ip enable-arp-announce'):
- wifi['ip_enable_arp_announce'] = 1
-
- # Enable acquisition of IPv6 address using stateless autoconfig (SLAAC)
- if conf.exists('ipv6 address autoconf'):
- wifi['ipv6_autoconf'] = 1
-
- # Get prefix for IPv6 addressing based on MAC address (EUI-64)
- if conf.exists('ipv6 address eui64'):
- wifi['ipv6_eui64_prefix'] = conf.return_value('ipv6 address eui64')
-
- # ARP enable ignore
- if conf.exists('ip enable-arp-ignore'):
- wifi['ip_enable_arp_ignore'] = 1
-
- # Disable IPv6 forwarding on this interface
- if conf.exists('ipv6 disable-forwarding'):
- wifi['ipv6_forwarding'] = 0
-
- # IPv6 Duplicate Address Detection (DAD) tries
- if conf.exists('ipv6 dup-addr-detect-transmits'):
- wifi['ipv6_dup_addr_detect'] = int(conf.return_value('ipv6 dup-addr-detect-transmits'))
-
# Wireless physical device
if conf.exists('physical-device'):
wifi['phy'] = conf.return_value('physical-device')
- # Media Access Control (MAC) address
- if conf.exists('mac'):
- wifi['mac'] = conf.return_value('mac')
-
# Maximum number of wireless radio stations
if conf.exists('max-stations'):
wifi['max_stations'] = conf.return_value('max-stations')
@@ -404,10 +317,6 @@ def get_config():
if conf.exists('mode'):
wifi['mode'] = conf.return_value('mode')
- # retrieve VRF instance
- if conf.exists('vrf'):
- wifi['vrf'] = conf.return_value('vrf')
-
# Transmission power reduction in dBm
if conf.exists('reduce-transmit-power'):
wifi['reduce_transmit_power'] = conf.return_value('reduce-transmit-power')
@@ -503,24 +412,6 @@ def get_config():
wifi['op_mode'] = tmp
- # re-set configuration level to parse new nodes
- conf.set_level(cfg_base)
- # Determine vif interfaces (currently effective) - to determine which
- # vif interface is no longer present and needs to be removed
- eff_intf = conf.list_effective_nodes('vif')
- act_intf = conf.list_nodes('vif')
- wifi['vif_remove'] = list_diff(eff_intf, act_intf)
-
- if conf.exists('vif'):
- for vif in conf.list_nodes('vif'):
- # set config level to vif interface
- conf.set_level(cfg_base + ' vif ' + vif)
- wifi['vif'].append(vlan_to_dict(conf))
-
- # disable interface
- if conf.exists('disable'):
- wifi['disable'] = True
-
# retrieve configured regulatory domain
conf.set_level('system')
if conf.exists('wifi-regulatory-domain'):
@@ -532,9 +423,9 @@ def get_config():
def verify(wifi):
if wifi['deleted']:
if wifi['is_bridge_member']:
- interface = wifi['intf']
- bridge = wifi['is_bridge_member']
- raise ConfigError(f'Interface "{interface}" can not be deleted as it belongs to bridge "{bridge}"!')
+ raise ConfigError((
+ f'Cannot delete interface "{wifi["intf"]}" as it is a '
+ f'member of bridge "{wifi["is_bridge_member"]}"!'))
return None
@@ -579,9 +470,23 @@ def verify(wifi):
if not radius['key']:
raise ConfigError('Misssing RADIUS shared secret key for server: {}'.format(radius['server']))
- vrf_name = wifi['vrf']
- if vrf_name and vrf_name not in interfaces():
- raise ConfigError(f'VRF "{vrf_name}" does not exist')
+ if ( wifi['is_bridge_member']
+ and ( wifi['address']
+ or wifi['ipv6_eui64_prefix']
+ or wifi['ipv6_autoconf'] ) ):
+ raise ConfigError((
+ f'Cannot assign address to interface "{wifi["intf"]}" '
+ f'as it is a member of bridge "{wifi["is_bridge_member"]}"!'))
+
+ if wifi['vrf']:
+ if wifi['vrf'] not in interfaces():
+ raise ConfigError(f'VRF "{wifi["vrf"]}" does not exist')
+
+ if wifi['is_bridge_member']:
+ raise ConfigError((
+ f'Interface "{wifi["intf"]}" cannot be member of VRF '
+ f'"{wifi["vrf"]}" and bridge {wifi["is_bridge_member"]} '
+ f'at the same time!'))
# use common function to verify VLAN configuration
verify_vlan_config(wifi)
@@ -672,8 +577,10 @@ def apply(wifi):
# Finally create the new interface
w = WiFiIf(interface, **conf)
- # assign/remove VRF
- w.set_vrf(wifi['vrf'])
+ # assign/remove VRF (ONLY when not a member of a bridge,
+ # otherwise 'nomaster' removes it from it)
+ if not wifi['is_bridge_member']:
+ w.set_vrf(wifi['vrf'])
# update interface description used e.g. within SNMP
w.set_alias(wifi['description'])
@@ -693,9 +600,16 @@ def apply(wifi):
if wifi['dhcpv6_temporary']:
w.dhcp.v6.options['dhcpv6_temporary'] = True
+ if wifi['dhcpv6_pd']:
+ w.dhcp.v6.options['dhcpv6_pd'] = wifi['dhcpv6_pd']
+
# ignore link state changes
w.set_link_detect(wifi['disable_link_detect'])
+ # Delete old IPv6 EUI64 addresses before changing MAC
+ for addr in wifi['ipv6_eui64_prefix_remove']:
+ w.del_ipv6_eui64_address(addr)
+
# Change interface MAC address - re-set to real hardware address (hw-id)
# if custom mac is removed
if wifi['mac']:
@@ -703,6 +617,10 @@ def apply(wifi):
elif wifi['hw_id']:
w.set_mac(wifi['hw_id'])
+ # Add IPv6 EUI-based addresses
+ for addr in wifi['ipv6_eui64_prefix']:
+ w.add_ipv6_eui64_address(addr)
+
# configure ARP filter configuration
w.set_arp_filter(wifi['ip_disable_arp_filter'])
# configure ARP accept
@@ -711,10 +629,10 @@ def apply(wifi):
w.set_arp_announce(wifi['ip_enable_arp_announce'])
# configure ARP ignore
w.set_arp_ignore(wifi['ip_enable_arp_ignore'])
+ # IPv6 accept RA
+ w.set_ipv6_accept_ra(wifi['ipv6_accept_ra'])
# IPv6 address autoconfiguration
w.set_ipv6_autoconf(wifi['ipv6_autoconf'])
- # IPv6 EUI-based address
- w.set_ipv6_eui64_address(wifi['ipv6_eui64_prefix'])
# IPv6 forwarding
w.set_ipv6_forwarding(wifi['ipv6_forwarding'])
# IPv6 Duplicate Address Detection (DAD) tries
@@ -728,24 +646,8 @@ def apply(wifi):
for addr in wifi['address']:
w.add_addr(addr)
- # remove no longer required VLAN interfaces (vif)
- for vif in wifi['vif_remove']:
- w.del_vlan(vif)
-
- # create VLAN interfaces (vif)
- for vif in wifi['vif']:
- # QoS priority mapping can only be set during interface creation
- # so we delete the interface first if required.
- if vif['egress_qos_changed'] or vif['ingress_qos_changed']:
- try:
- # on system bootup the above condition is true but the interface
- # does not exists, which throws an exception, but that's legal
- w.del_vlan(vif['id'])
- except:
- pass
-
- vlan = w.add_vlan(vif['id'])
- apply_vlan_config(vlan, vif)
+ # apply all vlans to interface
+ apply_all_vlans(w, wifi)
# Enable/Disable interface - interface is always placed in
# administrative down state in WiFiIf class
diff --git a/src/conf_mode/interfaces-wirelessmodem.py b/src/conf_mode/interfaces-wirelessmodem.py
index 163778e22..975e21d9f 100755
--- a/src/conf_mode/interfaces-wirelessmodem.py
+++ b/src/conf_mode/interfaces-wirelessmodem.py
@@ -21,9 +21,10 @@ from copy import deepcopy
from netifaces import interfaces
from vyos.config import Config
+from vyos.ifconfig import BridgeIf, Section
from vyos.template import render
from vyos.util import chown, chmod_755, cmd, call
-from vyos.validate import is_bridge_member
+from vyos.validate import is_member
from vyos import ConfigError
default_config_data = {
@@ -64,11 +65,12 @@ def get_config():
wwan['logfile'] = f"/var/log/vyatta/ppp_{wwan['intf']}.log"
wwan['chat_script'] = f"/etc/ppp/peers/chat.{wwan['intf']}"
+ # check if interface is member if a bridge
+ wwan['is_bridge_member'] = is_member(conf, wwan['intf'], 'bridge')
+
# Check if interface has been removed
if not conf.exists('interfaces wirelessmodem ' + wwan['intf']):
wwan['deleted'] = True
- # check if interface is member if a bridge
- wwan['is_bridge_member'] = is_bridge_member(conf, wwan['intf'])
return wwan
# set new configuration level
@@ -119,9 +121,9 @@ def get_config():
def verify(wwan):
if wwan['deleted']:
if wwan['is_bridge_member']:
- interface = wwan['intf']
- bridge = wwan['is_bridge_member']
- raise ConfigError(f'Interface "{interface}" can not be deleted as it belongs to bridge "{bridge}"!')
+ raise ConfigError((
+ f'Cannot delete interface "{wwan["intf"]}" as it is a '
+ f'member of bridge "{wwan["is_bridge_member"]}"!'))
return None
@@ -133,9 +135,20 @@ def verify(wwan):
if not os.path.exists(f"/dev/{wwan['device']}"):
raise ConfigError(f"Device {wwan['device']} does not exist")
- vrf_name = wwan['vrf']
- if vrf_name and vrf_name not in interfaces():
- raise ConfigError(f'VRF {vrf_name} does not exist')
+ if wwan['is_bridge_member'] and wwan['address']:
+ raise ConfigError((
+ f'Cannot assign address to interface "{wwan["intf"]}" '
+ f'as it is a member of bridge "{wwan["is_bridge_member"]}"!'))
+
+ if wwan['vrf']:
+ if wwan['vrf'] not in interfaces():
+ raise ConfigError(f'VRF "{wwan["vrf"]}" does not exist')
+
+ if wwan['is_bridge_member']:
+ raise ConfigError((
+ f'Interface "{wwan["intf"]}" cannot be member of VRF '
+ f'"{wwan["vrf"]}" and bridge {wwan["is_bridge_member"]} '
+ f'at the same time!'))
return None
@@ -152,12 +165,6 @@ def generate(wwan):
config_files = [config_wwan, config_wwan_chat, script_wwan_pre_up,
script_wwan_ip_up, script_wwan_ip_down]
- # Ensure directories for config files exist - otherwise create them on demand
- for file in config_files:
- dirname = os.path.dirname(file)
- if not os.path.isdir(dirname):
- os.mkdir(dirname)
-
# Always hang-up WWAN connection prior generating new configuration file
cmd(f'systemctl stop ppp@{intf}.service')
@@ -172,17 +179,18 @@ def generate(wwan):
render(config_wwan, 'wwan/peer.tmpl', wwan)
# Create PPP chat script
render(config_wwan_chat, 'wwan/chat.tmpl', wwan)
+
+ # generated script file must be executable
+
# Create script for ip-pre-up.d
- render(script_wwan_pre_up, 'wwan/ip-pre-up.script.tmpl', wwan)
+ render(script_wwan_pre_up, 'wwan/ip-pre-up.script.tmpl',
+ wwan, permission=0o755)
# Create script for ip-up.d
- render(script_wwan_ip_up, 'wwan/ip-up.script.tmpl', wwan)
+ render(script_wwan_ip_up, 'wwan/ip-up.script.tmpl',
+ wwan, permission=0o755)
# Create script for ip-down.d
- render(script_wwan_ip_down, 'wwan/ip-down.script.tmpl', wwan)
-
- # make generated script file executable
- chmod_755(script_wwan_pre_up)
- chmod_755(script_wwan_ip_up)
- chmod_755(script_wwan_ip_down)
+ render(script_wwan_ip_down, 'wwan/ip-down.script.tmpl',
+ wwan, permission=0o755)
return None
@@ -198,6 +206,12 @@ def apply(wwan):
# make logfile owned by root / vyattacfg
chown(wwan['logfile'], 'root', 'vyattacfg')
+ # re-add ourselves to any bridge we might have fallen out of
+ # FIXME: wwan isn't under vyos.ifconfig so we can't call
+ # Interfaces.add_to_bridge() so STP settings won't get applied
+ if wwan['is_bridge_member'] in Section.interfaces('bridge'):
+ BridgeIf(wwan['is_bridge_member'], create=False).add_port(wwan['intf'])
+
return None
if __name__ == '__main__':
diff --git a/src/conf_mode/ipsec-settings.py b/src/conf_mode/ipsec-settings.py
index 3398bcdf2..6282c2cc7 100755
--- a/src/conf_mode/ipsec-settings.py
+++ b/src/conf_mode/ipsec-settings.py
@@ -99,7 +99,7 @@ def get_config():
def write_ipsec_secrets(c):
if c.get("ipsec_l2tp_auth_mode") == "pre-shared-secret":
secret_txt = "{0}\n{1} %any : PSK \"{2}\"\n{3}\n".format(delim_ipsec_l2tp_begin, c['outside_addr'], c['ipsec_l2tp_secret'], delim_ipsec_l2tp_end)
- elif data.get("ipsec_l2tp_auth_mode") == "x509":
+ elif c.get("ipsec_l2tp_auth_mode") == "x509":
secret_txt = "{0}\n: RSA {1}\n{2}\n".format(delim_ipsec_l2tp_begin, c['server_key_file_copied'], delim_ipsec_l2tp_end)
old_umask = os.umask(0o077)
diff --git a/src/conf_mode/nat.py b/src/conf_mode/nat.py
new file mode 100755
index 000000000..d491395ac
--- /dev/null
+++ b/src/conf_mode/nat.py
@@ -0,0 +1,268 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2020 VyOS maintainers and contributors
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License version 2 or later as
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+import jmespath
+import json
+import os
+
+from copy import deepcopy
+from sys import exit
+from netifaces import interfaces
+
+from vyos.config import Config
+from vyos.template import render
+from vyos.util import call, cmd
+from vyos.validate import is_addr_assigned
+from vyos import ConfigError
+
+default_config_data = {
+ 'deleted': False,
+ 'destination': [],
+ 'helper_functions': None,
+ 'pre_ct_helper': '',
+ 'pre_ct_conntrack': '',
+ 'out_ct_helper': '',
+ 'out_ct_conntrack': '',
+ 'source': []
+}
+
+iptables_nat_config = '/tmp/vyos-nat-rules.nft'
+
+def _check_kmod():
+ """ load required Kernel modules """
+ modules = ['nft_nat', 'nft_chain_nat_ipv4']
+ for module in modules:
+ if not os.path.exists(f'/sys/module/{module}'):
+ if call(f'modprobe {module}') != 0:
+ raise ConfigError(f'Loading Kernel module {module} failed')
+
+
+def get_handler(json, chain, target):
+ """ Get nftable rule handler number of given chain/target combination.
+ Handler is required when adding NAT/Conntrack helper targets """
+ for x in json:
+ if x['chain'] != chain:
+ continue
+ if x['target'] != target:
+ continue
+ return x['handle']
+
+ return None
+
+
+def verify_rule(rule, err_msg):
+ """ Common verify steps used for both source and destination NAT """
+ if rule['translation_port'] or rule['dest_port'] or rule['source_port']:
+ if rule['protocol'] not in ['tcp', 'udp', 'tcp_udp']:
+ proto = rule['protocol']
+ raise ConfigError(f'{err_msg} ports can only be specified when protocol is "tcp", "udp" or "tcp_udp" (currently "{proto}")')
+
+ if '/' in rule['translation_address']:
+ raise ConfigError(f'{err_msg}\n' \
+ 'Cannot use ports with an IPv4net type translation address as it\n' \
+ 'statically maps a whole network of addresses onto another\n' \
+ 'network of addresses')
+
+ if not rule['translation_address']:
+ raise ConfigError(f'{err_msg} translation address not specified')
+
+
+def parse_source_destination(conf, source_dest):
+ """ Common wrapper to read in both NAT source and destination CLI """
+ tmp = []
+ base_level = ['nat', source_dest]
+ conf.set_level(base_level)
+ for number in conf.list_nodes(['rule']):
+ rule = {
+ 'description': '',
+ 'dest_address': '',
+ 'dest_port': '',
+ 'disabled': False,
+ 'exclude': False,
+ 'interface_in': '',
+ 'interface_out': '',
+ 'log': False,
+ 'protocol': 'all',
+ 'number': number,
+ 'source_address': '',
+ 'source_prefix': '',
+ 'source_port': '',
+ 'translation_address': '',
+ 'translation_prefix': '',
+ 'translation_port': ''
+ }
+ conf.set_level(base_level + ['rule', number])
+
+ if conf.exists(['description']):
+ rule['description'] = conf.return_value(['description'])
+
+ if conf.exists(['destination', 'address']):
+ rule['dest_address'] = conf.return_value(['destination', 'address'])
+
+ if conf.exists(['destination', 'port']):
+ rule['dest_port'] = conf.return_value(['destination', 'port'])
+
+ if conf.exists(['disable']):
+ rule['disabled'] = True
+
+ if conf.exists(['exclude']):
+ rule['exclude'] = True
+
+ if conf.exists(['inbound-interface']):
+ rule['interface_in'] = conf.return_value(['inbound-interface'])
+
+ if conf.exists(['outbound-interface']):
+ rule['interface_out'] = conf.return_value(['outbound-interface'])
+
+ if conf.exists(['log']):
+ rule['log'] = True
+
+ if conf.exists(['protocol']):
+ rule['protocol'] = conf.return_value(['protocol'])
+
+ if conf.exists(['source', 'address']):
+ rule['source_address'] = conf.return_value(['source', 'address'])
+
+ if conf.exists(['source', 'prefix']):
+ rule['source_prefix'] = conf.return_value(['source', 'prefix'])
+
+ if conf.exists(['source', 'port']):
+ rule['source_port'] = conf.return_value(['source', 'port'])
+
+ if conf.exists(['translation', 'address']):
+ rule['translation_address'] = conf.return_value(['translation', 'address'])
+
+ if conf.exists(['translation', 'prefix']):
+ rule['translation_prefix'] = conf.return_value(['translation', 'prefix'])
+
+ if conf.exists(['translation', 'port']):
+ rule['translation_port'] = conf.return_value(['translation', 'port'])
+
+ tmp.append(rule)
+
+ return tmp
+
+def get_config():
+ nat = deepcopy(default_config_data)
+ conf = Config()
+
+ # read in current nftable (once) for further processing
+ tmp = cmd('nft -j list table raw')
+ nftable_json = json.loads(tmp)
+
+ # condense the full JSON table into a list with only relevand informations
+ pattern = 'nftables[?rule].rule[?expr[].jump].{chain: chain, handle: handle, target: expr[].jump.target | [0]}'
+ condensed_json = jmespath.search(pattern, nftable_json)
+
+ if not conf.exists(['nat']):
+ nat['helper_functions'] = 'remove'
+
+ # Retrieve current table handler positions
+ nat['pre_ct_ignore'] = get_handler(condensed_json, 'PREROUTING', 'VYATTA_CT_HELPER')
+ nat['pre_ct_conntrack'] = get_handler(condensed_json, 'PREROUTING', 'NAT_CONNTRACK')
+ nat['out_ct_ignore'] = get_handler(condensed_json, 'OUTPUT', 'VYATTA_CT_HELPER')
+ nat['out_ct_conntrack'] = get_handler(condensed_json, 'OUTPUT', 'NAT_CONNTRACK')
+
+ nat['deleted'] = True
+
+ return nat
+
+ # check if NAT connection tracking helpers need to be set up - this has to
+ # be done only once
+ if not get_handler(condensed_json, 'PREROUTING', 'NAT_CONNTRACK'):
+ nat['helper_functions'] = 'add'
+
+ # Retrieve current table handler positions
+ nat['pre_ct_ignore'] = get_handler(condensed_json, 'PREROUTING', 'VYATTA_CT_IGNORE')
+ nat['pre_ct_conntrack'] = get_handler(condensed_json, 'PREROUTING', 'VYATTA_CT_PREROUTING_HOOK')
+ nat['out_ct_ignore'] = get_handler(condensed_json, 'OUTPUT', 'VYATTA_CT_IGNORE')
+ nat['out_ct_conntrack'] = get_handler(condensed_json, 'OUTPUT', 'VYATTA_CT_OUTPUT_HOOK')
+
+ # set config level for parsing in NAT configuration
+ conf.set_level(['nat'])
+
+ # use a common wrapper function to read in the source / destination
+ # tree from the config - thus we do not need to replicate almost the
+ # same code :-)
+ for tgt in ['source', 'destination', 'nptv6']:
+ nat[tgt] = parse_source_destination(conf, tgt)
+
+ return nat
+
+def verify(nat):
+ if nat['deleted']:
+ # no need to verify the CLI as NAT is going to be deactivated
+ return None
+
+ if nat['helper_functions']:
+ if not (nat['pre_ct_ignore'] or nat['pre_ct_conntrack'] or nat['out_ct_ignore'] or nat['out_ct_conntrack']):
+ raise Exception('could not determine nftable ruleset handlers')
+
+ for rule in nat['source']:
+ interface = rule['interface_out']
+ err_msg = f"Source NAT configuration error in rule {rule['number']}:"
+
+ if interface and interface not in interfaces():
+ print(f'NAT configuration warning: interface {interface} does not exist on this system')
+
+ if not rule['interface_out']:
+ raise ConfigError(f'{err_msg} outbound-interface not specified')
+
+ if rule['translation_address']:
+ addr = rule['translation_address']
+ if addr != 'masquerade' and not is_addr_assigned(addr):
+ print(f'Warning: IP address {addr} does not exist on the system!')
+
+ # common rule verification
+ verify_rule(rule, err_msg)
+
+ for rule in nat['destination']:
+ interface = rule['interface_in']
+ err_msg = f"Destination NAT configuration error in rule {rule['number']}:"
+
+ if interface and interface not in interfaces():
+ print(f'NAT configuration warning: interface {interface} does not exist on this system')
+
+ if not rule['interface_in']:
+ raise ConfigError(f'{err_msg} inbound-interface not specified')
+
+ # common rule verification
+ verify_rule(rule, err_msg)
+
+ return None
+
+def generate(nat):
+ render(iptables_nat_config, 'firewall/nftables-nat.tmpl', nat, trim_blocks=True, permission=0o755)
+
+ return None
+
+def apply(nat):
+ cmd(f'{iptables_nat_config}')
+ if os.path.isfile(iptables_nat_config):
+ os.unlink(iptables_nat_config)
+
+ return None
+
+if __name__ == '__main__':
+ try:
+ _check_kmod()
+ c = get_config()
+ verify(c)
+ generate(c)
+ apply(c)
+ except ConfigError as e:
+ print(e)
+ exit(1)
diff --git a/src/conf_mode/protocols_bfd.py b/src/conf_mode/protocols_bfd.py
index ed8c3637b..d6577579e 100755
--- a/src/conf_mode/protocols_bfd.py
+++ b/src/conf_mode/protocols_bfd.py
@@ -190,7 +190,7 @@ def generate(bfd):
if bfd is None:
return None
- render(config_file, 'frr-bfd/bfd.frr.tmpl', bfd)
+ render(config_file, 'frr/bfd.frr.tmpl', bfd)
return None
def apply(bfd):
diff --git a/src/conf_mode/protocols_igmp.py b/src/conf_mode/protocols_igmp.py
index 9b338c5b9..821ccb0fc 100755
--- a/src/conf_mode/protocols_igmp.py
+++ b/src/conf_mode/protocols_igmp.py
@@ -87,7 +87,7 @@ def generate(igmp):
if igmp is None:
return None
- render(config_file, 'igmp/igmp.frr.tmpl', igmp)
+ render(config_file, 'frr/igmp.frr.tmpl', igmp)
return None
def apply(igmp):
diff --git a/src/conf_mode/protocols_mpls.py b/src/conf_mode/protocols_mpls.py
index 0a241277d..9b946b43a 100755
--- a/src/conf_mode/protocols_mpls.py
+++ b/src/conf_mode/protocols_mpls.py
@@ -127,7 +127,7 @@ def generate(mpls):
if mpls is None:
return None
- render(config_file, 'mpls/ldpd.frr.tmpl', mpls)
+ render(config_file, 'frr/ldpd.frr.tmpl', mpls)
return None
def apply(mpls):
diff --git a/src/conf_mode/protocols_pim.py b/src/conf_mode/protocols_pim.py
index f12de4a72..15c4a2b0f 100755
--- a/src/conf_mode/protocols_pim.py
+++ b/src/conf_mode/protocols_pim.py
@@ -114,7 +114,7 @@ def generate(pim):
if pim is None:
return None
- render(config_file, 'pim/pimd.frr.tmpl', pim)
+ render(config_file, 'frr/pimd.frr.tmpl', pim)
return None
def apply(pim):
diff --git a/src/conf_mode/protocols_static_multicast.py b/src/conf_mode/protocols_static_multicast.py
index 411a130ec..ba6324393 100755
--- a/src/conf_mode/protocols_static_multicast.py
+++ b/src/conf_mode/protocols_static_multicast.py
@@ -91,7 +91,7 @@ def generate(mroute):
if mroute is None:
return None
- render(config_file, 'frr-mcast/static_mcast.frr.tmpl', mroute)
+ render(config_file, 'frr/static_mcast.frr.tmpl', mroute)
return None
def apply(mroute):
diff --git a/src/conf_mode/salt-minion.py b/src/conf_mode/salt-minion.py
index 236480854..8bc35bb45 100755
--- a/src/conf_mode/salt-minion.py
+++ b/src/conf_mode/salt-minion.py
@@ -17,117 +17,102 @@
import os
from copy import deepcopy
-from pwd import getpwnam
from socket import gethostname
from sys import exit
from urllib3 import PoolManager
from vyos.config import Config
-from vyos import ConfigError
-from vyos.util import call
from vyos.template import render
-
+from vyos.util import call, chown
+from vyos import ConfigError
config_file = r'/etc/salt/minion'
+master_keyfile = r'/opt/vyatta/etc/config/salt/pki/minion/master_sign.pub'
default_config_data = {
- 'hash_type': 'sha256',
- 'log_file': '/var/log/salt/minion',
+ 'hash': 'sha256',
'log_level': 'warning',
'master' : 'salt',
'user': 'minion',
+ 'group': 'vyattacfg',
'salt_id': gethostname(),
'mine_interval': '60',
- 'verify_master_pubkey_sign': 'false'
+ 'verify_master_pubkey_sign': 'false',
+ 'master_key': ''
}
def get_config():
salt = deepcopy(default_config_data)
conf = Config()
- if not conf.exists('service salt-minion'):
+ base = ['service', 'salt-minion']
+
+ if not conf.exists(base):
return None
else:
- conf.set_level('service salt-minion')
-
- if conf.exists('hash_type'):
- salt['hash_type'] = conf.return_value('hash_type')
-
- if conf.exists('log_file'):
- salt['log_file'] = conf.return_value('log_file')
+ conf.set_level(base)
- if conf.exists('log_level'):
- salt['log_level'] = conf.return_value('log_level')
+ if conf.exists(['hash']):
+ salt['hash'] = conf.return_value(['hash'])
- if conf.exists('master'):
- master = conf.return_values('master')
- salt['master'] = master
+ if conf.exists(['master']):
+ salt['master'] = conf.return_values(['master'])
- if conf.exists('id'):
- salt['salt_id'] = conf.return_value('id')
+ if conf.exists(['id']):
+ salt['salt_id'] = conf.return_value(['id'])
- if conf.exists('user'):
- salt['user'] = conf.return_value('user')
+ if conf.exists(['user']):
+ salt['user'] = conf.return_value(['user'])
- if conf.exists('mine_interval'):
- salt['mine_interval'] = conf.return_value('mine_interval')
+ if conf.exists(['interval']):
+ salt['interval'] = conf.return_value(['interval'])
- salt['master-key'] = None
- if conf.exists('master-key'):
- salt['master-key'] = conf.return_value('master-key')
+ if conf.exists(['master-key']):
+ salt['master_key'] = conf.return_value(['master-key'])
salt['verify_master_pubkey_sign'] = 'true'
return salt
-def generate(salt):
- paths = ['/etc/salt/','/var/run/salt','/opt/vyatta/etc/config/salt/']
- directory = '/opt/vyatta/etc/config/salt/pki/minion'
- uid = getpwnam(salt['user']).pw_uid
- http = PoolManager()
+def verify(salt):
+ return None
- if salt is None:
+def generate(salt):
+ if not salt:
return None
- if not os.path.exists(directory):
- os.makedirs(directory)
-
- render(config_file, 'salt-minion/minion.tmpl', salt)
-
- path = "/etc/salt/"
- for path in paths:
- for root, dirs, files in os.walk(path):
- for usgr in dirs:
- os.chown(os.path.join(root, usgr), uid, 100)
- for usgr in files:
- os.chown(os.path.join(root, usgr), uid, 100)
+ render(config_file, 'salt-minion/minion.tmpl', salt,
+ user=salt['user'], group=salt['group'])
- if not os.path.exists('/opt/vyatta/etc/config/salt/pki/minion/master_sign.pub'):
- if not salt['master-key'] is None:
- r = http.request('GET', salt['master-key'], preload_content=False)
+ if not os.path.exists(master_keyfile):
+ if salt['master_key']:
+ req = PoolManager().request('GET', salt['master_key'], preload_content=False)
- with open('/opt/vyatta/etc/config/salt/pki/minion/master_sign.pub', 'wb') as out:
+ with open(master_keyfile, 'wb') as f:
while True:
- data = r.read(1024)
+ data = req.read(1024)
if not data:
break
- out.write(data)
+ f.write(data)
- r.release_conn()
+ req.release_conn()
+ chown(master_keyfile, salt['user'], salt['group'])
return None
def apply(salt):
- if salt is not None:
- call("sudo systemctl restart salt-minion")
+ if not salt:
+ # Salt removed from running config
+ call('systemctl stop salt-minion.service')
+ if os.path.exists(config_file):
+ os.unlink(config_file)
else:
- # Salt access is removed in the commit
- call("sudo systemctl stop salt-minion")
- os.unlink(config_file)
+ call('systemctl restart salt-minion.service')
return None
if __name__ == '__main__':
try:
c = get_config()
+ verify(c)
generate(c)
apply(c)
except ConfigError as e:
diff --git a/src/conf_mode/service_ipoe-server.py b/src/conf_mode/service_ipoe-server.py
index 17fa2c3f0..84443ade3 100755
--- a/src/conf_mode/service_ipoe-server.py
+++ b/src/conf_mode/service_ipoe-server.py
@@ -112,28 +112,30 @@ def get_config():
'name': interface,
'mac': []
}
- for client in conf.list_nodes(base_path + ['authentication', 'interface', interface, 'mac-address']):
- mac = {
+ for mac in conf.list_nodes(['authentication', 'interface', interface, 'mac-address']):
+ client = {
'address': mac,
'rate_download': '',
'rate_upload': '',
'vlan_id': ''
}
- conf.set_level(base_path + ['authentication', 'interface', interface, 'mac-address', client])
+ conf.set_level(base_path + ['authentication', 'interface', interface, 'mac-address', mac])
if conf.exists(['rate-limit', 'download']):
- mac['rate_download'] = conf.return_value(['rate-limit', 'download'])
+ client['rate_download'] = conf.return_value(['rate-limit', 'download'])
if conf.exists(['rate-limit', 'upload']):
- mac['rate_upload'] = conf.return_value(['rate-limit', 'upload'])
+ client['rate_upload'] = conf.return_value(['rate-limit', 'upload'])
if conf.exists(['vlan-id']):
- mac['vlan'] = conf.return_value(['vlan-id'])
+ client['vlan'] = conf.return_value(['vlan-id'])
- tmp['mac'].append(mac)
+ tmp['mac'].append(client)
ipoe['auth_interfaces'].append(tmp)
+ conf.set_level(base_path)
+
#
# authentication mode radius servers and settings
if conf.exists(['authentication', 'mode', 'radius']):
@@ -265,10 +267,6 @@ def generate(ipoe):
if not ipoe:
return None
- dirname = os.path.dirname(ipoe_conf)
- if not os.path.exists(dirname):
- os.mkdir(dirname)
-
render(ipoe_conf, 'accel-ppp/ipoe.config.tmpl', ipoe, trim_blocks=True)
if ipoe['auth_mode'] == 'local':
diff --git a/src/conf_mode/service_pppoe-server.py b/src/conf_mode/service_pppoe-server.py
index 95cb066d8..e05b0ab2a 100755
--- a/src/conf_mode/service_pppoe-server.py
+++ b/src/conf_mode/service_pppoe-server.py
@@ -23,7 +23,7 @@ from sys import exit
from vyos.config import Config
from vyos.template import render
-from vyos.util import call, get_half_cpus()
+from vyos.util import call, get_half_cpus
from vyos.validate import is_ipv4
from vyos import ConfigError
@@ -32,6 +32,7 @@ pppoe_chap_secrets = r'/run/accel-pppd/pppoe.chap-secrets'
default_config_data = {
'auth_mode': 'local',
+ 'auth_proto': ['auth_mschap_v2', 'auth_mschap_v1', 'auth_chap_md5', 'auth_pap'],
'chap_secrets_file': pppoe_chap_secrets, # used in Jinja2 template
'client_ip_pool': '',
'client_ip_subnets': [],
@@ -216,6 +217,19 @@ def get_config():
pppoe['local_users'].append(user)
conf.set_level(base_path)
+
+ if conf.exists(['authentication', 'protocols']):
+ auth_mods = {
+ 'mschap-v2': 'auth_mschap_v2',
+ 'mschap': 'auth_mschap_v1',
+ 'chap': 'auth_chap_md5',
+ 'pap': 'auth_pap'
+ }
+
+ pppoe['auth_proto'] = []
+ for proto in conf.return_values(['authentication', 'protocols']):
+ pppoe['auth_proto'].append(auth_mods[proto])
+
#
# authentication mode radius servers and settings
if conf.exists(['authentication', 'mode', 'radius']):
@@ -301,7 +315,7 @@ def get_config():
pppoe['mtu'] = conf.return_value(['mtu'])
if conf.exists(['session-control']):
- pppoe['session_control'] = conf.return_value(['session-control'])
+ pppoe['sesscrtl'] = conf.return_value(['session-control'])
# ppp_options
if conf.exists(['ppp-options']):
@@ -415,10 +429,6 @@ def generate(pppoe):
if not pppoe:
return None
- dirname = os.path.dirname(pppoe_conf)
- if not os.path.exists(dirname):
- os.mkdir(dirname)
-
render(pppoe_conf, 'accel-ppp/pppoe.config.tmpl', pppoe, trim_blocks=True)
if pppoe['local_users']:
diff --git a/src/conf_mode/system-login.py b/src/conf_mode/system-login.py
index 91e2b369f..09c5422eb 100755
--- a/src/conf_mode/system-login.py
+++ b/src/conf_mode/system-login.py
@@ -17,6 +17,7 @@
import os
from crypt import crypt, METHOD_SHA512
+from netifaces import interfaces
from psutil import users
from pwd import getpwall, getpwnam
from stat import S_IRUSR, S_IWUSR, S_IRWXU, S_IRGRP, S_IXGRP
@@ -39,6 +40,7 @@ default_config_data = {
'del_users': [],
'radius_server': [],
'radius_source_address': '',
+ 'radius_vrf': ''
}
def get_local_users():
@@ -127,6 +129,10 @@ def get_config():
if conf.exists(['source-address']):
login['radius_source_address'] = conf.return_value(['source-address'])
+ # retrieve VRF instance
+ if conf.exists(['vrf']):
+ login['radius_vrf'] = conf.return_value(['vrf'])
+
# Read in all RADIUS servers and store to list
for server in conf.list_nodes(['server']):
server_cfg = {
@@ -193,6 +199,9 @@ def verify(login):
if fail:
raise ConfigError('At least one RADIUS server must be active.')
+ vrf_name = login['radius_vrf']
+ if vrf_name and vrf_name not in interfaces():
+ raise ConfigError(f'VRF "{vrf_name}" does not exist')
return None
@@ -217,7 +226,7 @@ def generate(login):
# env=env)
if len(login['radius_server']) > 0:
- render(radius_config_file, 'system-login/pam_radius_auth.conf.tmpl', login)
+ render(radius_config_file, 'system-login/pam_radius_auth.conf.tmpl', login, trim_blocks=True)
uid = getpwnam('root').pw_uid
gid = getpwnam('root').pw_gid
diff --git a/src/conf_mode/vpn_l2tp.py b/src/conf_mode/vpn_l2tp.py
index a4ef99d45..f312f2a17 100755
--- a/src/conf_mode/vpn_l2tp.py
+++ b/src/conf_mode/vpn_l2tp.py
@@ -340,10 +340,6 @@ def generate(l2tp):
if not l2tp:
return None
- dirname = os.path.dirname(l2tp_conf)
- if not os.path.exists(dirname):
- os.mkdir(dirname)
-
render(l2tp_conf, 'accel-ppp/l2tp.config.tmpl', l2tp, trim_blocks=True)
if l2tp['auth_mode'] == 'local':
diff --git a/src/conf_mode/vpn_pptp.py b/src/conf_mode/vpn_pptp.py
index 046fc8f9c..085c9c2c6 100755
--- a/src/conf_mode/vpn_pptp.py
+++ b/src/conf_mode/vpn_pptp.py
@@ -247,10 +247,6 @@ def generate(pptp):
if not pptp:
return None
- dirname = os.path.dirname(pptp_conf)
- if not os.path.exists(dirname):
- os.mkdir(dirname)
-
render(pptp_conf, 'accel-ppp/pptp.config.tmpl', pptp, trim_blocks=True)
if pptp['local_users']:
diff --git a/src/conf_mode/vpn_sstp.py b/src/conf_mode/vpn_sstp.py
index e6ce94709..7c3e3f515 100755
--- a/src/conf_mode/vpn_sstp.py
+++ b/src/conf_mode/vpn_sstp.py
@@ -22,10 +22,10 @@ from copy import deepcopy
from stat import S_IRUSR, S_IWUSR, S_IRGRP
from vyos.config import Config
-from vyos import ConfigError
-from vyos.util import call, run, get_half_cpus
from vyos.template import render
-
+from vyos.util import call, run, get_half_cpus
+from vyos.validate import is_ipv4
+from vyos import ConfigError
sstp_conf = '/run/accel-pppd/sstp.conf'
sstp_chap_secrets = '/run/accel-pppd/sstp.chap-secrets'
@@ -35,7 +35,12 @@ default_config_data = {
'auth_mode' : 'local',
'auth_proto' : ['auth_mschap_v2'],
'chap_secrets_file': sstp_chap_secrets, # used in Jinja2 template
+ 'client_ip_pool' : [],
+ 'client_ipv6_pool': [],
+ 'client_ipv6_delegate_prefix': [],
'client_gateway': '',
+ 'dnsv4' : [],
+ 'dnsv6' : [],
'radius_server' : [],
'radius_acct_tmo' : '3',
'radius_max_try' : '3',
@@ -49,8 +54,6 @@ default_config_data = {
'ssl_ca' : '',
'ssl_cert' : '',
'ssl_key' : '',
- 'client_ip_pool' : [],
- 'dnsv4' : [],
'mtu' : '',
'ppp_mppe' : 'prefer',
'ppp_echo_failure' : '',
@@ -210,7 +213,7 @@ def get_config():
#
- # read in client ip pool settings
+ # read in client IPv4 pool
conf.set_level(base_path + ['network-settings', 'client-ip-settings'])
if conf.exists(['subnet']):
sstp['client_ip_pool'] = conf.return_values(['subnet'])
@@ -219,10 +222,41 @@ def get_config():
sstp['client_gateway'] = conf.return_value(['gateway-address'])
#
+ # read in client IPv6 pool
+ conf.set_level(base_path + ['network-settings', 'client-ipv6-pool'])
+ if conf.exists(['prefix']):
+ for prefix in conf.list_nodes(['prefix']):
+ tmp = {
+ 'prefix': prefix,
+ 'mask': '64'
+ }
+
+ if conf.exists(['prefix', prefix, 'mask']):
+ tmp['mask'] = conf.return_value(['prefix', prefix, 'mask'])
+
+ sstp['client_ipv6_pool'].append(tmp)
+
+ if conf.exists(['delegate']):
+ for prefix in conf.list_nodes(['delegate']):
+ tmp = {
+ 'prefix': prefix,
+ 'mask': ''
+ }
+
+ if conf.exists(['delegate', prefix, 'delegation-prefix']):
+ tmp['mask'] = conf.return_value(['delegate', prefix, 'delegation-prefix'])
+
+ sstp['client_ipv6_delegate_prefix'].append(tmp)
+
+ #
# read in network settings
conf.set_level(base_path + ['network-settings'])
if conf.exists(['name-server']):
- sstp['dnsv4'] = conf.return_values(['name-server'])
+ for name_server in conf.return_values(['name-server']):
+ if is_ipv4(name_server):
+ sstp['dnsv4'].append(name_server)
+ else:
+ sstp['dnsv6'].append(name_server)
if conf.exists(['mtu']):
sstp['mtu'] = conf.return_value(['mtu'])
@@ -275,6 +309,14 @@ def verify(sstp):
if len(sstp['dnsv4']) > 2:
raise ConfigError('Not more then two IPv4 DNS name-servers can be configured')
+ # check ipv6
+ if sstp['client_ipv6_delegate_prefix'] and not sstp['client_ipv6_pool']:
+ raise ConfigError('IPv6 prefix delegation requires client-ipv6-pool prefix')
+
+ for prefix in sstp['client_ipv6_delegate_prefix']:
+ if not prefix['mask']:
+ raise ConfigError('Delegation-prefix required for individual delegated networks')
+
if not sstp['ssl_ca'] or not sstp['ssl_cert'] or not sstp['ssl_key']:
raise ConfigError('One or more SSL certificates missing')
@@ -303,10 +345,6 @@ def generate(sstp):
if not sstp:
return None
- dirname = os.path.dirname(sstp_conf)
- if not os.path.exists(dirname):
- os.mkdir(dirname)
-
# accel-cmd reload doesn't work so any change results in a restart of the daemon
render(sstp_conf, 'accel-ppp/sstp.config.tmpl', sstp, trim_blocks=True)
diff --git a/src/etc/dhcp/dhclient-enter-hooks.d/03-vyos-ipwrapper b/src/etc/dhcp/dhclient-enter-hooks.d/03-vyos-ipwrapper
index 59f92703c..f1167fcd2 100644
--- a/src/etc/dhcp/dhclient-enter-hooks.d/03-vyos-ipwrapper
+++ b/src/etc/dhcp/dhclient-enter-hooks.d/03-vyos-ipwrapper
@@ -15,8 +15,11 @@ function frr_alive () {
# convert ip route command to vtysh
function iptovtysh () {
# prepare variables for vtysh command
- VTYSH_DISTANCE="210"
- VTYSH_TAG="210"
+ local VTYSH_DISTANCE="210"
+ local VTYSH_TAG="210"
+ local VTYSH_NETADDR=""
+ local VTYSH_GATEWAY=""
+ local VTYSH_DEV=""
# convert default route to 0.0.0.0/0
if [ "$4" == "default" ] ; then
VTYSH_NETADDR="0.0.0.0/0"
@@ -74,3 +77,4 @@ function ip () {
fi
fi
}
+
diff --git a/src/etc/dhcp/dhclient-exit-hooks.d/01-vyos-cleanup b/src/etc/dhcp/dhclient-exit-hooks.d/01-vyos-cleanup
index ce846f6c3..88a4d9db9 100644
--- a/src/etc/dhcp/dhclient-exit-hooks.d/01-vyos-cleanup
+++ b/src/etc/dhcp/dhclient-exit-hooks.d/01-vyos-cleanup
@@ -1,12 +1,74 @@
+# NOTE: here we use 'ip' wrapper, therefore a route will be actually deleted via /usr/sbin/ip or vtysh, according to the system state
+
if [[ $reason =~ (EXPIRE|FAIL|RELEASE|STOP) ]]; then
# delete dynamic nameservers from a configuration if lease was deleted
logmsg info "Deleting nameservers with tag \"dhcp-${interface}\" via vyos-hostsd-client"
vyos-hostsd-client --delete-name-servers --tag dhcp-${interface}
- # try to delete default ip route (NOTE: here we use 'ip' wrapper, therefore a route will be actually deleted via /usr/sbin/ip or vtysh, according to the system state)
+ # try to delete default ip route
for router in $old_routers; do
logmsg info "Deleting default route: via $router dev ${interface}"
ip -4 route del default via $router dev ${interface}
done
+ # delete rfc3442 routes
+ if [ -n "$old_rfc3442_classless_static_routes" ]; then
+ set -- $old_rfc3442_classless_static_routes
+ while [ $# -gt 0 ]; do
+ net_length=$1
+ via_arg=''
+ case $net_length in
+ 32|31|30|29|28|27|26|25)
+ if [ $# -lt 9 ]; then
+ return 1
+ fi
+ net_address="${2}.${3}.${4}.${5}"
+ gateway="${6}.${7}.${8}.${9}"
+ shift 9
+ ;;
+ 24|23|22|21|20|19|18|17)
+ if [ $# -lt 8 ]; then
+ return 1
+ fi
+ net_address="${2}.${3}.${4}.0"
+ gateway="${5}.${6}.${7}.${8}"
+ shift 8
+ ;;
+ 16|15|14|13|12|11|10|9)
+ if [ $# -lt 7 ]; then
+ return 1
+ fi
+ net_address="${2}.${3}.0.0"
+ gateway="${4}.${5}.${6}.${7}"
+ shift 7
+ ;;
+ 8|7|6|5|4|3|2|1)
+ if [ $# -lt 6 ]; then
+ return 1
+ fi
+ net_address="${2}.0.0.0"
+ gateway="${3}.${4}.${5}.${6}"
+ shift 6
+ ;;
+ 0) # default route
+ if [ $# -lt 5 ]; then
+ return 1
+ fi
+ net_address="0.0.0.0"
+ gateway="${2}.${3}.${4}.${5}"
+ shift 5
+ ;;
+ *) # error
+ return 1
+ ;;
+ esac
+ # take care of link-local routes
+ if [ "${gateway}" != '0.0.0.0' ]; then
+ via_arg="via ${gateway}"
+ fi
+ # delete route (ip detects host routes automatically)
+ ip -4 route del "${net_address}/${net_length}" \
+ ${via_arg} dev "${interface}" >/dev/null 2>&1
+ done
+ fi
fi
if [[ $reason =~ (EXPIRE6|RELEASE6|STOP6) ]]; then
diff --git a/src/etc/dhcp/dhclient-exit-hooks.d/02-vyos-dhcp-renew-rfc3442 b/src/etc/dhcp/dhclient-exit-hooks.d/02-vyos-dhcp-renew-rfc3442
new file mode 100644
index 000000000..9202fe72d
--- /dev/null
+++ b/src/etc/dhcp/dhclient-exit-hooks.d/02-vyos-dhcp-renew-rfc3442
@@ -0,0 +1,148 @@
+# support for RFC3442 routes in DHCP RENEW
+
+function convert_to_cidr () {
+ cidr=""
+ set -- $1
+ while [ $# -gt 0 ]; do
+ net_length=$1
+
+ case $net_length in
+ 32|31|30|29|28|27|26|25)
+ if [ $# -lt 9 ]; then
+ return 1
+ fi
+ net_address="${2}.${3}.${4}.${5}"
+ gateway="${6}.${7}.${8}.${9}"
+ shift 9
+ ;;
+ 24|23|22|21|20|19|18|17)
+ if [ $# -lt 8 ]; then
+ return 1
+ fi
+ net_address="${2}.${3}.${4}.0"
+ gateway="${5}.${6}.${7}.${8}"
+ shift 8
+ ;;
+ 16|15|14|13|12|11|10|9)
+ if [ $# -lt 7 ]; then
+ return 1
+ fi
+ net_address="${2}.${3}.0.0"
+ gateway="${4}.${5}.${6}.${7}"
+ shift 7
+ ;;
+ 8|7|6|5|4|3|2|1)
+ if [ $# -lt 6 ]; then
+ return 1
+ fi
+ net_address="${2}.0.0.0"
+ gateway="${3}.${4}.${5}.${6}"
+ shift 6
+ ;;
+ 0) # default route
+ if [ $# -lt 5 ]; then
+ return 1
+ fi
+ net_address="0.0.0.0"
+ gateway="${2}.${3}.${4}.${5}"
+ shift 5
+ ;;
+ *) # error
+ return 1
+ ;;
+ esac
+
+ cidr+="${net_address}/${net_length}:${gateway} "
+ done
+}
+
+# main script starts here
+
+RUN="yes"
+
+if [ "$RUN" = "yes" ]; then
+ convert_to_cidr "$old_rfc3442_classless_static_routes"
+ old_cidr=$cidr
+ convert_to_cidr "$new_rfc3442_classless_static_routes"
+ new_cidr=$cidr
+
+ if [ "$reason" = "RENEW" ]; then
+ if [ "$new_rfc3442_classless_static_routes" != "$old_rfc3442_classless_static_routes" ]; then
+ logmsg info "RFC3442 route change detected, old_routes: $old_rfc3442_classless_static_routes"
+ logmsg info "RFC3442 route change detected, new_routes: $new_rfc3442_classless_static_routes"
+ if [ -z "$new_rfc3442_classless_static_routes" ]; then
+ # delete all routes from the old_rfc3442_classless_static_routes
+ for route in $old_cidr; do
+ network=$(printf "${route}" | awk -F ":" '{print $1}')
+ gateway=$(printf "${route}" | awk -F ":" '{print $2}')
+ # take care of link-local routes
+ if [ "${gateway}" != '0.0.0.0' ]; then
+ via_arg="via ${gateway}"
+ else
+ via_arg=""
+ fi
+ ip -4 route del "${network}" "${via_arg}" dev "${interface}" >/dev/null 2>&1
+ done
+ elif [ -z "$old_rfc3442_classless_static_routes" ]; then
+ # add all routes from the new_rfc3442_classless_static_routes
+ for route in $new_cidr; do
+ network=$(printf "${route}" | awk -F ":" '{print $1}')
+ gateway=$(printf "${route}" | awk -F ":" '{print $2}')
+ # take care of link-local routes
+ if [ "${gateway}" != '0.0.0.0' ]; then
+ via_arg="via ${gateway}"
+ else
+ via_arg=""
+ fi
+ ip -4 route add "${network}" "${via_arg}" dev "${interface}" >/dev/null 2>&1
+ done
+ else
+ # update routes
+ # delete old
+ for old_route in $old_cidr; do
+ match="false"
+ for new_route in $new_cidr; do
+ if [[ "$old_route" == "$new_route" ]]; then
+ match="true"
+ break
+ fi
+ done
+ if [[ "$match" == "false" ]]; then
+ # delete old_route
+ network=$(printf "${old_route}" | awk -F ":" '{print $1}')
+ gateway=$(printf "${old_route}" | awk -F ":" '{print $2}')
+ # take care of link-local routes
+ if [ "${gateway}" != '0.0.0.0' ]; then
+ via_arg="via ${gateway}"
+ else
+ via_arg=""
+ fi
+ ip -4 route del "${network}" "${via_arg}" dev "${interface}" >/dev/null 2>&1
+ fi
+ done
+ # add new
+ for new_route in $new_cidr; do
+ match="false"
+ for old_route in $old_cidr; do
+ if [[ "$new_route" == "$old_route" ]]; then
+ match="true"
+ break
+ fi
+ done
+ if [[ "$match" == "false" ]]; then
+ # add new_route
+ network=$(printf "${new_route}" | awk -F ":" '{print $1}')
+ gateway=$(printf "${new_route}" | awk -F ":" '{print $2}')
+ # take care of link-local routes
+ if [ "${gateway}" != '0.0.0.0' ]; then
+ via_arg="via ${gateway}"
+ else
+ via_arg=""
+ fi
+ ip -4 route add "${network}" "${via_arg}" dev "${interface}" >/dev/null 2>&1
+ fi
+ done
+ fi
+ fi
+ fi
+fi
diff --git a/src/etc/sysctl.d/31-vyos-addr_gen_mode.conf b/src/etc/sysctl.d/31-vyos-addr_gen_mode.conf
new file mode 100644
index 000000000..07a0d1584
--- /dev/null
+++ b/src/etc/sysctl.d/31-vyos-addr_gen_mode.conf
@@ -0,0 +1,14 @@
+### Added by vyos-1x ###
+#
+# addr_gen_mode - INTEGER
+# Defines how link-local and autoconf addresses are generated.
+#
+# 0: generate address based on EUI64 (default)
+# 1: do no generate a link-local address, use EUI64 for addresses generated
+# from autoconf
+# 2: generate stable privacy addresses, using the secret from
+# stable_secret (RFC7217)
+# 3: generate stable privacy addresses, using a random secret if unset
+#
+net.ipv6.conf.all.addr_gen_mode = 1
+net.ipv6.conf.default.addr_gen_mode = 1
diff --git a/src/etc/systemd/system/pdns-recursor.service.d/override.conf b/src/etc/systemd/system/pdns-recursor.service.d/override.conf
index ef4dec303..750bc9972 100644
--- a/src/etc/systemd/system/pdns-recursor.service.d/override.conf
+++ b/src/etc/systemd/system/pdns-recursor.service.d/override.conf
@@ -1,5 +1,7 @@
[Service]
WorkingDirectory=
WorkingDirectory=/run/powerdns
+RuntimeDirectory=
+RuntimeDirectory=/run/powerdns
ExecStart=
ExecStart=/usr/sbin/pdns_recursor --daemon=no --write-pid=no --disable-syslog --log-timestamp=no --config-dir=/run/powerdns --socket-dir=/run/powerdns
diff --git a/src/helpers/validate-value.py b/src/helpers/validate-value.py
deleted file mode 100755
index a58ba61d1..000000000
--- a/src/helpers/validate-value.py
+++ /dev/null
@@ -1,45 +0,0 @@
-#!/usr/bin/env python3
-
-import re
-import os
-import sys
-import argparse
-
-from vyos.util import call
-
-parser = argparse.ArgumentParser()
-parser.add_argument('--regex', action='append')
-parser.add_argument('--exec', action='append')
-parser.add_argument('--value', action='store')
-
-args = parser.parse_args()
-
-debug = False
-
-# Multiple arguments work like logical OR
-
-try:
- for r in args.regex:
- if re.fullmatch(r, args.value):
- sys.exit(0)
-except Exception as exn:
- if debug:
- print(exn)
- else:
- pass
-
-try:
- for cmd in args.exec:
- cmd = "{0} {1}".format(cmd, args.value)
- if debug:
- print(cmd)
- res = call(cmd)
- if res == 0:
- sys.exit(0)
-except Exception as exn:
- if debug:
- print(exn)
- else:
- pass
-
-sys.exit(1)
diff --git a/src/migration-scripts/dhcpv6-server/0-to-1 b/src/migration-scripts/dhcpv6-server/0-to-1
new file mode 100755
index 000000000..6f1150da1
--- /dev/null
+++ b/src/migration-scripts/dhcpv6-server/0-to-1
@@ -0,0 +1,61 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2020 VyOS maintainers and contributors
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License version 2 or later as
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+# combine both sip-server-address and sip-server-name nodes to common sip-server
+
+from sys import argv, exit
+from vyos.configtree import ConfigTree
+
+if (len(argv) < 1):
+ print("Must specify file name!")
+ exit(1)
+
+file_name = argv[1]
+
+with open(file_name, 'r') as f:
+ config_file = f.read()
+
+config = ConfigTree(config_file)
+base = ['service', 'dhcpv6-server', 'shared-network-name']
+if not config.exists(base):
+ # Nothing to do
+ exit(0)
+else:
+ # we need to run this for every configured network
+ for network in config.list_nodes(base):
+ for subnet in config.list_nodes(base + [network, 'subnet']):
+ sip_server = []
+
+ # Do we have 'sip-server-address' configured?
+ if config.exists(base + [network, 'subnet', subnet, 'sip-server-address']):
+ sip_server += config.return_values(base + [network, 'subnet', subnet, 'sip-server-address'])
+ config.delete(base + [network, 'subnet', subnet, 'sip-server-address'])
+
+ # Do we have 'sip-server-name' configured?
+ if config.exists(base + [network, 'subnet', subnet, 'sip-server-name']):
+ sip_server += config.return_values(base + [network, 'subnet', subnet, 'sip-server-name'])
+ config.delete(base + [network, 'subnet', subnet, 'sip-server-name'])
+
+ # Write new CLI value for sip-server
+ for server in sip_server:
+ config.set(base + [network, 'subnet', subnet, 'sip-server'], value=server, 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/migration-scripts/nat/4-to-5 b/src/migration-scripts/nat/4-to-5
new file mode 100755
index 000000000..dda191719
--- /dev/null
+++ b/src/migration-scripts/nat/4-to-5
@@ -0,0 +1,58 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2020 VyOS maintainers and contributors
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License version 2 or later as
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+# Drop the enable/disable from the nat "log" node. If log node is specified
+# it is "enabled"
+
+from sys import argv,exit
+from vyos.configtree import ConfigTree
+
+if (len(argv) < 1):
+ print("Must specify file name!")
+ exit(1)
+
+file_name = argv[1]
+
+with open(file_name, 'r') as f:
+ config_file = f.read()
+
+config = ConfigTree(config_file)
+
+if not config.exists(['nat']):
+ # Nothing to do
+ exit(0)
+else:
+ for direction in ['source', 'destination']:
+ if not config.exists(['nat', direction]):
+ continue
+
+ for rule in config.list_nodes(['nat', direction, 'rule']):
+ base = ['nat', direction, 'rule', rule]
+
+ # Check if the log node exists and if log is enabled,
+ # migrate it to the new valueless 'log' node
+ if config.exists(base + ['log']):
+ tmp = config.return_value(base + ['log'])
+ config.delete(base + ['log'])
+ if tmp == 'enable':
+ config.set(base + ['log'])
+
+ try:
+ with open(file_name, 'w') as f:
+ f.write(config.to_string())
+ except OSError as e:
+ print("Failed to save the modified config: {}".format(e))
+ exit(1)
diff --git a/src/migration-scripts/salt/0-to-1 b/src/migration-scripts/salt/0-to-1
new file mode 100755
index 000000000..79053c056
--- /dev/null
+++ b/src/migration-scripts/salt/0-to-1
@@ -0,0 +1,58 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2020 VyOS maintainers and contributors
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License version 2 or later as
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+# Delete log_file, log_level and user nodes
+# rename hash_type to hash
+# rename mine_interval to interval
+
+from sys import argv,exit
+
+from vyos.configtree import ConfigTree
+
+if (len(argv) < 1):
+ print("Must specify file name!")
+ exit(1)
+
+file_name = argv[1]
+
+with open(file_name, 'r') as f:
+ config_file = f.read()
+
+config = ConfigTree(config_file)
+
+base = ['service', 'salt-minion']
+if not config.exists(base):
+ # Nothing to do
+ exit(0)
+else:
+
+ # delete nodes which are now populated with sane defaults
+ for node in ['log_file', 'log_level', 'user']:
+ if config.exists(base + [node]):
+ config.delete(base + [node])
+
+ if config.exists(base + ['hash_type']):
+ config.rename(base + ['hash_type'], 'hash')
+
+ if config.exists(base + ['mine_interval']):
+ config.rename(base + ['mine_interval'], 'interval')
+
+ 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/flow_accounting_op.py b/src/op_mode/flow_accounting_op.py
index bf8c39fd6..219ad6316 100755
--- a/src/op_mode/flow_accounting_op.py
+++ b/src/op_mode/flow_accounting_op.py
@@ -195,7 +195,7 @@ if not _uacctd_running():
# restart pmacct daemon
if cmd_args.action == 'restart':
# run command to restart flow-accounting
- cmd('systemctl restart uacctd.service',
+ cmd('sudo systemctl restart uacctd.service',
message='Failed to restart flow-accounting')
# clear in-memory collected flows
diff --git a/src/op_mode/powerctrl.py b/src/op_mode/powerctrl.py
index 4ab91384b..69af427ec 100755
--- a/src/op_mode/powerctrl.py
+++ b/src/op_mode/powerctrl.py
@@ -15,168 +15,179 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import os
-import sys
-import argparse
import re
+from argparse import ArgumentParser
from datetime import datetime, timedelta, time as type_time, date as type_date
-from vyos.util import ask_yes_no
-from vyos.util import cmd
-from vyos.util import call
-from vyos.util import run
-from vyos.util import STDOUT
+from sys import exit
+from time import time
+
+from vyos.util import ask_yes_no, cmd, call, run, STDOUT
systemd_sched_file = "/run/systemd/shutdown/scheduled"
-def parse_time(s):
- try:
- if re.match(r'^\d{1,2}$', s):
- return datetime.strptime(s, "%M").time()
- else:
- return datetime.strptime(s, "%H:%M").time()
- except ValueError:
- return None
+def utc2local(datetime):
+ now = time()
+ offs = datetime.fromtimestamp(now) - datetime.utcfromtimestamp(now)
+ return datetime + offs
-def parse_date(s):
- for fmt in ["%d%m%Y", "%d/%m/%Y", "%d.%m.%Y", "%d:%m:%Y", "%Y-%m-%d"]:
+def parse_time(s):
try:
- return datetime.strptime(s, fmt).date()
+ if re.match(r'^\d{1,2}$', s):
+ return datetime.strptime(s, "%M").time()
+ else:
+ return datetime.strptime(s, "%H:%M").time()
except ValueError:
- continue
- # If nothing matched...
- return None
+ return None
+
+
+def parse_date(s):
+ for fmt in ["%d%m%Y", "%d/%m/%Y", "%d.%m.%Y", "%d:%m:%Y", "%Y-%m-%d"]:
+ try:
+ return datetime.strptime(s, fmt).date()
+ except ValueError:
+ continue
+ # If nothing matched...
+ return None
+
def get_shutdown_status():
- if os.path.exists(systemd_sched_file):
- # Get scheduled from systemd file
- with open(systemd_sched_file, 'r') as f:
- data = f.read().rstrip('\n')
- r_data = {}
- for line in data.splitlines():
- tmp_split = line.split("=")
- if tmp_split[0] == "USEC":
- # Convert USEC to human readable format
- r_data['DATETIME'] = datetime.utcfromtimestamp(int(tmp_split[1])/1000000).strftime('%Y-%m-%d %H:%M:%S')
- else:
- r_data[tmp_split[0]] = tmp_split[1]
- return r_data
- return None
+ if os.path.exists(systemd_sched_file):
+ # Get scheduled from systemd file
+ with open(systemd_sched_file, 'r') as f:
+ data = f.read().rstrip('\n')
+ r_data = {}
+ for line in data.splitlines():
+ tmp_split = line.split("=")
+ if tmp_split[0] == "USEC":
+ # Convert USEC to human readable format
+ r_data['DATETIME'] = datetime.utcfromtimestamp(
+ int(tmp_split[1])/1000000).strftime('%Y-%m-%d %H:%M:%S')
+ else:
+ r_data[tmp_split[0]] = tmp_split[1]
+ return r_data
+ return None
+
def check_shutdown():
- output = get_shutdown_status()
- if output and 'MODE' in output:
- if output['MODE'] == 'reboot':
- print("Reboot is scheduled", output['DATETIME'])
- elif output['MODE'] == 'poweroff':
- print("Poweroff is scheduled", output['DATETIME'])
- else:
- print("Reboot or poweroff is not scheduled")
+ output = get_shutdown_status()
+ if output and 'MODE' in output:
+ dt = datetime.strptime(output['DATETIME'], '%Y-%m-%d %H:%M:%S')
+ if output['MODE'] == 'reboot':
+ print("Reboot is scheduled", utc2local(dt))
+ elif output['MODE'] == 'poweroff':
+ print("Poweroff is scheduled", utc2local(dt))
+ else:
+ print("Reboot or poweroff is not scheduled")
+
def cancel_shutdown():
- output = get_shutdown_status()
- if output and 'MODE' in output:
- timenow = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
- try:
- cmd('/sbin/shutdown -c --no-wall')
- except OSError as e:
- sys.exit("Could not cancel a reboot or poweroff: %s" % e)
- message = "Scheduled %s has been cancelled %s" % (output['MODE'], timenow)
- run(f'wall {message}')
- else:
- print("Reboot or poweroff is not scheduled")
-
-def execute_shutdown(time, reboot = True, ask=True):
- if not ask:
- action = "reboot" if reboot else "poweroff"
- if not ask_yes_no("Are you sure you want to %s this system?" % action):
- sys.exit(0)
-
- action = "-r" if reboot else "-P"
-
- if len(time) == 0:
- ### T870 legacy reboot job support
- chk_vyatta_based_reboots()
- ###
-
- out = cmd(f'/sbin/shutdown {action} now', stderr=STDOUT)
- print(out.split(",",1)[0])
- return
- elif len(time) == 1:
- # Assume the argument is just time
- ts = parse_time(time[0])
- if ts:
- cmd(f'/sbin/shutdown {action} {time[0]}', stderr=STDOUT)
+ output = get_shutdown_status()
+ if output and 'MODE' in output:
+ timenow = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
+ try:
+ run('/sbin/shutdown -c --no-wall')
+ except OSError as e:
+ exit("Could not cancel a reboot or poweroff: %s" % e)
+
+ message = 'Scheduled {} has been cancelled {}'.format(output['MODE'], timenow)
+ run(f'wall {message} > /dev/null 2>&1')
else:
- sys.exit("Invalid time \"{0}\". The valid format is HH:MM".format(time[0]))
- elif len(time) == 2:
- # Assume it's date and time
- ts = parse_time(time[0])
- ds = parse_date(time[1])
- if ts and ds:
- t = datetime.combine(ds, ts)
- td = t - datetime.now()
- t2 = 1 + int(td.total_seconds())//60 # Get total minutes
- cmd('/sbin/shutdown {action} {t2}', stderr=STDOUT)
+ print("Reboot or poweroff is not scheduled")
+
+
+def execute_shutdown(time, reboot=True, ask=True):
+ if not ask:
+ action = "reboot" if reboot else "poweroff"
+ if not ask_yes_no("Are you sure you want to %s this system?" % action):
+ exit(0)
+
+ action = "-r" if reboot else "-P"
+
+ if len(time) == 0:
+ # T870 legacy reboot job support
+ chk_vyatta_based_reboots()
+ ###
+
+ out = cmd(f'/sbin/shutdown {action} now', stderr=STDOUT)
+ print(out.split(",", 1)[0])
+ return
+ elif len(time) == 1:
+ # Assume the argument is just time
+ ts = parse_time(time[0])
+ if ts:
+ cmd(f'/sbin/shutdown {action} {time[0]}', stderr=STDOUT)
+ else:
+ exit("Invalid time \"{0}\". The valid format is HH:MM".format(time[0]))
+ elif len(time) == 2:
+ # Assume it's date and time
+ ts = parse_time(time[0])
+ ds = parse_date(time[1])
+ if ts and ds:
+ t = datetime.combine(ds, ts)
+ td = t - datetime.now()
+ t2 = 1 + int(td.total_seconds())//60 # Get total minutes
+ cmd('/sbin/shutdown {action} {t2}', stderr=STDOUT)
+ else:
+ if not ts:
+ exit("Invalid time \"{0}\". The valid format is HH:MM".format(time[0]))
+ else:
+ exit("Invalid time \"{0}\". A valid format is YYYY-MM-DD [HH:MM]".format(time[1]))
else:
- if not ts:
- sys.exit("Invalid time \"{0}\". The valid format is HH:MM".format(time[0]))
- else:
- sys.exit("Invalid time \"{0}\". A valid format is YYYY-MM-DD [HH:MM]".format(time[1]))
- else:
- sys.exit("Could not decode date and time. Valids formats are HH:MM or YYYY-MM-DD HH:MM")
- check_shutdown()
+ exit("Could not decode date and time. Valids formats are HH:MM or YYYY-MM-DD HH:MM")
+ check_shutdown()
+
def chk_vyatta_based_reboots():
- ### T870 commit-confirm is still using the vyatta code base, once gone, the code below can be removed
- ### legacy scheduled reboot s are using at and store the is as /var/run/<name>.job
- ### name is the node of scheduled the job, commit-confirm checks for that
+ # T870 commit-confirm is still using the vyatta code base, once gone, the code below can be removed
+ # legacy scheduled reboot s are using at and store the is as /var/run/<name>.job
+ # name is the node of scheduled the job, commit-confirm checks for that
+
+ f = r'/var/run/confirm.job'
+ if os.path.exists(f):
+ jid = open(f).read().strip()
+ if jid != 0:
+ call(f'sudo atrm {jid}')
+ os.remove(f)
- f = r'/var/run/confirm.job'
- if os.path.exists(f):
- jid = open(f).read().strip()
- if jid != 0:
- call(f'sudo atrm {jid}')
- os.remove(f)
def main():
- parser = argparse.ArgumentParser()
- parser.add_argument("--yes", "-y",
- help="Do not ask for confirmation",
- action="store_true",
- dest="yes")
- action = parser.add_mutually_exclusive_group(required=True)
- action.add_argument("--reboot", "-r",
- help="Reboot the system",
- nargs="*",
- metavar="Minutes|HH:MM")
-
- action.add_argument("--poweroff", "-p",
- help="Poweroff the system",
- nargs="*",
- metavar="Minutes|HH:MM")
-
- action.add_argument("--cancel", "-c",
- help="Cancel pending shutdown",
- action="store_true")
-
- action.add_argument("--check",
- help="Check pending chutdown",
- action="store_true")
- args = parser.parse_args()
-
- try:
- if args.reboot is not None:
- execute_shutdown(args.reboot, reboot=True, ask=args.yes)
- if args.poweroff is not None:
- execute_shutdown(args.poweroff, reboot=False,ask=args.yes)
- if args.cancel:
- cancel_shutdown()
- if args.check:
- check_shutdown()
- except KeyboardInterrupt:
- sys.exit("Interrupted")
+ parser = ArgumentParser()
+ parser.add_argument("--yes", "-y",
+ help="Do not ask for confirmation",
+ action="store_true",
+ dest="yes")
+ action = parser.add_mutually_exclusive_group(required=True)
+ action.add_argument("--reboot", "-r",
+ help="Reboot the system",
+ nargs="*",
+ metavar="Minutes|HH:MM")
+
+ action.add_argument("--poweroff", "-p",
+ help="Poweroff the system",
+ nargs="*",
+ metavar="Minutes|HH:MM")
+
+ action.add_argument("--cancel", "-c",
+ help="Cancel pending shutdown",
+ action="store_true")
+
+ action.add_argument("--check",
+ help="Check pending chutdown",
+ action="store_true")
+ args = parser.parse_args()
+ try:
+ if args.reboot is not None:
+ execute_shutdown(args.reboot, reboot=True, ask=args.yes)
+ if args.poweroff is not None:
+ execute_shutdown(args.poweroff, reboot=False, ask=args.yes)
+ if args.cancel:
+ cancel_shutdown()
+ if args.check:
+ check_shutdown()
+ except KeyboardInterrupt:
+ exit("Interrupted")
if __name__ == "__main__":
- main()
-
+ main()
diff --git a/src/op_mode/show_interfaces.py b/src/op_mode/show_interfaces.py
index 8b6690b7d..2f0f8a1c9 100755
--- a/src/op_mode/show_interfaces.py
+++ b/src/op_mode/show_interfaces.py
@@ -18,6 +18,7 @@
import os
import re
import sys
+import glob
import datetime
import argparse
import netifaces
@@ -146,9 +147,20 @@ def run_allowed(**kwarg):
sys.stdout.write(' '.join(Section.interfaces()))
+def pppoe(ifname):
+ out = cmd(f'ps -C pppd -f')
+ if ifname in out:
+ return 'C'
+ elif ifname in [_.split('/')[-1] for _ in glob.glob('/etc/ppp/peers/pppoe*')]:
+ return 'D'
+ return ''
+
+
@register('show')
def run_show_intf(ifnames, iftypes, vif, vrrp):
+ handled = []
for interface in filtered_interfaces(ifnames, iftypes, vif, vrrp):
+ handled.append(interface.ifname)
cache = interface.operational.load_counters()
out = cmd(f'ip addr show {interface.ifname}')
@@ -173,6 +185,17 @@ def run_show_intf(ifnames, iftypes, vif, vrrp):
print()
print(interface.operational.formated_stats())
+ for ifname in ifnames:
+ if ifname not in handled and ifname.startswith('pppoe'):
+ state = pppoe(ifname)
+ if not state:
+ continue
+ string = {
+ 'C': 'Coming up',
+ 'D': 'Link down',
+ }[state]
+ print('{}: {}'.format(ifname, string))
+
@register('show-brief')
def run_show_intf_brief(ifnames, iftypes, vif, vrrp):
@@ -183,7 +206,10 @@ def run_show_intf_brief(ifnames, iftypes, vif, vrrp):
print(format1 % ("Interface", "IP Address", "S/L", "Description"))
print(format1 % ("---------", "----------", "---", "-----------"))
+ handled = []
for interface in filtered_interfaces(ifnames, iftypes, vif, vrrp):
+ handled.append(interface.ifname)
+
oper_state = interface.operational.get_state()
admin_state = interface.get_admin_state()
@@ -206,6 +232,17 @@ def run_show_intf_brief(ifnames, iftypes, vif, vrrp):
print(format2 % (i, a))
print(format1 % ('', '', '/'.join(s+l), d))
+ for ifname in ifnames:
+ if ifname not in handled and ifname.startswith('pppoe'):
+ state = pppoe(ifname)
+ if not state:
+ continue
+ string = {
+ 'C': 'u/D',
+ 'D': 'A/D',
+ }[state]
+ print(format1 % (ifname, '', string, ''))
+
@register('show-count')
def run_show_counters(ifnames, iftypes, vif, vrrp):
@@ -230,17 +267,15 @@ def run_show_counters(ifnames, iftypes, vif, vrrp):
@register('clear')
-def run_clear_intf(intf, iftypes, vif, vrrp):
+def run_clear_intf(ifnames, iftypes, vif, vrrp):
for interface in filtered_interfaces(ifnames, iftypes, vif, vrrp):
print(f'Clearing {interface.ifname}')
- interface = Interface(ifname, create=False, debug=False)
interface.operational.clear_counters()
@register('reset')
-def run_reset_intf(intf, iftypes, vif, vrrp):
+def run_reset_intf(ifnames, iftypes, vif, vrrp):
for interface in filtered_interfaces(ifnames, iftypes, vif, vrrp):
- interface = Interface(ifname, create=False, debug=False)
interface.operational.reset_counters()
diff --git a/src/op_mode/show_nat_statistics.py b/src/op_mode/show_nat_statistics.py
new file mode 100755
index 000000000..0b53112f2
--- /dev/null
+++ b/src/op_mode/show_nat_statistics.py
@@ -0,0 +1,63 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2018 VyOS maintainers and contributors
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License version 2 or later as
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+import jmespath
+import json
+
+from argparse import ArgumentParser
+from jinja2 import Template
+from sys import exit
+from vyos.util import cmd
+
+OUT_TMPL_SRC="""
+rule pkts bytes interface
+---- ---- ----- ---------
+{% for r in output %}
+{%- if r.comment -%}
+{%- set packets = r.counter.packets -%}
+{%- set bytes = r.counter.bytes -%}
+{%- set interface = r.interface -%}
+{# remove rule comment prefix #}
+{%- set comment = r.comment | replace('SRC-NAT-', '') | replace('DST-NAT-', '') | replace(' tcp_udp', '') -%}
+{{ "%-4s" | format(comment) }} {{ "%9s" | format(packets) }} {{ "%12s" | format(bytes) }} {{ interface }}
+{%- endif %}
+{% endfor %}
+"""
+
+parser = ArgumentParser()
+group = parser.add_mutually_exclusive_group()
+group.add_argument("--source", help="Show statistics for configured source NAT rules", action="store_true")
+group.add_argument("--destination", help="Show statistics for configured destination NAT rules", action="store_true")
+args = parser.parse_args()
+
+if args.source or args.destination:
+ tmp = cmd('sudo nft -j list table nat')
+ tmp = json.loads(tmp)
+
+ source = r"nftables[?rule.chain=='POSTROUTING'].rule.{chain: chain, handle: handle, comment: comment, counter: expr[].counter | [0], interface: expr[].match.right | [0] }"
+ destination = r"nftables[?rule.chain=='PREROUTING'].rule.{chain: chain, handle: handle, comment: comment, counter: expr[].counter | [0], interface: expr[].match.right | [0] }"
+ data = {
+ 'output' : jmespath.search(source if args.source else destination, tmp),
+ 'direction' : 'source' if args.source else 'destination'
+ }
+
+ tmpl = Template(OUT_TMPL_SRC, lstrip_blocks=True)
+ print(tmpl.render(data))
+ exit(0)
+else:
+ parser.print_help()
+ exit(1)
+
diff --git a/src/op_mode/to_be_migrated/vyatta-nat-translations.pl b/src/op_mode/to_be_migrated/vyatta-nat-translations.pl
new file mode 100755
index 000000000..94ed74bad
--- /dev/null
+++ b/src/op_mode/to_be_migrated/vyatta-nat-translations.pl
@@ -0,0 +1,267 @@
+#!/usr/bin/perl
+#
+# Module: vyatta-nat-translate.pl
+#
+# **** License ****
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License version 2 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.
+#
+# This code was originally developed by Vyatta, Inc.
+# Portions created by Vyatta are Copyright (C) 2007 Vyatta, Inc.
+# All Rights Reserved.
+#
+# Author: Stig Thormodsrud
+# Date: July 2008
+# Description: Script to display nat translations
+#
+# **** End License ****
+#
+
+use Getopt::Long;
+use XML::Simple;
+use Data::Dumper;
+use POSIX;
+
+use warnings;
+use strict;
+
+my $dump = 0;
+my ($xml_file, $verbose, $proto, $stats, $ipaddr, $pipe);
+my $type;
+my $verbose_format = "%-20s %-18s %-20s %-18s\n";
+my $format = "%-20s %-20s %-4s %-8s";
+
+sub add_xml_root {
+ my $xml = shift;
+
+ $xml = '<data>' . $xml . '</data>';
+ return $xml;
+}
+
+
+sub read_xml_file {
+ my $file = shift;
+
+ local($/, *FD); # slurp mode
+ open FD, "<", $file or die "Couldn't open $file\n";
+ my $xml = <FD>;
+ close FD;
+ return $xml;
+}
+
+sub print_xml {
+ my $data = shift;
+ print Dumper($data);
+}
+
+sub guess_snat_dnat {
+ my ($src, $dst) = @_;
+
+ if ($src->{original} eq $dst->{reply}) {
+ return "dnat";
+ }
+ if ($dst->{original} eq $src->{reply}) {
+ return "snat";
+ }
+ return "unkn";
+}
+
+sub nat_print_xml {
+ my ($data, $type) = @_;
+
+ my $flow = 0;
+
+ my %flowh;
+ while (1) {
+ my $meta = 0;
+ last if ! defined $data->{flow}[$flow];
+ my $flow_ref = $data->{flow}[$flow];
+ my $flow_type = $flow_ref->{type};
+ my (%src, %dst, %sport, %dport, %proto);
+ my (%packets, %bytes);
+ my $timeout = undef;
+ my $uses = undef;
+ while (1) {
+ my $meta_ref = $flow_ref->{meta}[$meta];
+ last if ! defined $meta_ref;
+ my $dir = $meta_ref->{direction};
+ if ($dir eq 'original' or $dir eq 'reply') {
+ my $l3_ref = $meta_ref->{layer3}[0];
+ my $l4_ref = $meta_ref->{layer4}[0];
+ my $count_ref = $meta_ref->{counters}[0];
+ if (defined $l3_ref) {
+ $src{$dir} = $l3_ref->{src}[0];
+ $dst{$dir} = $l3_ref->{dst}[0];
+ if (defined $l4_ref) {
+ $sport{$dir} = $l4_ref->{sport}[0];
+ $dport{$dir} = $l4_ref->{dport}[0];
+ $proto{$dir} = $l4_ref->{protoname};
+ }
+ }
+ if (defined $stats and defined $count_ref) {
+ $packets{$dir} = $count_ref->{packets}[0];
+ $bytes{$dir} = $count_ref->{bytes}[0];
+ }
+ } elsif ($dir eq 'independent') {
+ $timeout = $meta_ref->{timeout}[0];
+ $uses = $meta_ref->{'use'}[0];
+ }
+ $meta++;
+ }
+ my ($proto, $in_src, $in_dst, $out_src, $out_dst);
+ $proto = $proto{original};
+ $in_src = "$src{original}";
+ $in_src .= ":$sport{original}" if defined $sport{original};
+ $in_dst = "$dst{original}";
+ $in_dst .= ":$dport{original}" if defined $dport{original};
+ $out_src = "$dst{reply}";
+ $out_src .= ":$dport{reply}" if defined $dport{reply};
+ $out_dst = "$src{reply}";
+ $out_dst .= ":$sport{reply}" if defined $sport{reply};
+ if (defined $verbose) {
+ printf($verbose_format, $in_src, $in_dst, $out_src, $out_dst);
+ }
+# if (! defined $type) {
+# $type = guess_snat_dnat(\%src, \%dst);
+# }
+ if (defined $type) {
+ my ($from, $to);
+ if ($type eq 'source') {
+ $from = "$src{original}";
+ $to = "$dst{reply}";
+ if (defined $sport{original} and defined $dport{reply}) {
+ if ($sport{original} ne $dport{reply}) {
+ $from .= ":$sport{original}";
+ $to .= ":$dport{reply}";
+ }
+ }
+ } else {
+ $from = "$dst{original}";
+ $to = "$src{reply}";
+ if (defined $dport{original} and defined $sport{reply}) {
+ if ($dport{original} ne $sport{reply}) {
+ $from .= ":$dport{original}";
+ $to .= ":$sport{reply}";
+ }
+ }
+ }
+ if (defined $verbose) {
+ print " $proto: $from ==> $to";
+ } else {
+ my $timeout2 = "";
+ if (defined $timeout) {
+ $timeout2 = $timeout;
+ }
+ printf($format, $from, $to, $proto, $timeout2);
+ print " $flow_type" if defined $flow_type;
+ print "\n";
+ }
+ }
+ if (defined $verbose) {
+ print " timeout: $timeout" if defined $timeout;
+ print " use: $uses " if defined $uses;
+ print " type: $flow_type" if defined $flow_type;
+ print "\n";
+ }
+ if (defined $stats) {
+ foreach my $dir ('original', 'reply') {
+ if (defined $packets{$dir}) {
+ printf(" %-8s: packets %s, bytes %s\n",
+ $dir, $packets{$dir}, $bytes{$dir});
+ }
+ }
+ }
+ $flow++;
+ }
+ return $flow;
+}
+
+
+#
+# main
+#
+GetOptions("verbose" => \$verbose,
+ "proto=s" => \$proto,
+ "file=s" => \$xml_file,
+ "stats" => \$stats,
+ "type=s" => \$type,
+ "ipaddr=s" => \$ipaddr,
+ "pipe" => \$pipe,
+);
+
+my $conntrack = '/usr/sbin/conntrack';
+if (! -f $conntrack) {
+ die "Package [conntrack] not installed";
+}
+
+die "Must specify NAT type!" if !defined($type);
+die "Unknown NAT type!" if (($type ne 'source') && ($type ne 'destination'));
+
+my $xs = XML::Simple->new(ForceArray => 1, KeepRoot => 0);
+my ($xml, $data);
+
+# flush stdout after every write for pipe mode
+$| = 1 if defined $pipe;
+
+if (defined $verbose) {
+ printf($verbose_format, 'Pre-NAT src', 'Pre-NAT dst',
+ 'Post-NAT src', 'Post-NAT dst');
+} else {
+ printf($format, 'Pre-NAT', 'Post-NAT', 'Prot', 'Timeout');
+ print " Type" if defined $pipe;
+ print "\n";
+}
+
+if (defined $xml_file) {
+ $xml = read_xml_file($xml_file);
+ $data = $xs->XMLin($xml);
+ if ($dump) {
+ print_xml($data);
+ exit;
+ }
+ nat_print_xml($data, 'snat');
+
+} elsif (defined $pipe) {
+ while ($xml = <STDIN>) {
+ $xml =~ s/\<\?xml version=\"1\.0\" encoding=\"utf-8\"\?\>//;
+ $xml =~ s/\<conntrack\>//;
+ $xml = add_xml_root($xml);
+ $data = $xs->XMLin($xml);
+ nat_print_xml($data, $type);
+ }
+} else {
+ if (defined $proto) {
+ $proto = "-p $proto"
+ } else {
+ $proto = "";
+ }
+ if ($type eq 'source') {
+ my $ipopt = "";
+ if (defined $ipaddr) {
+ $ipopt = "--orig-src $ipaddr";
+ }
+ $xml = `sudo $conntrack -L -n $ipopt -o xml $proto 2>/dev/null`;
+ chomp $xml;
+ $data = undef;
+ $data = $xs->XMLin($xml) if ! $xml eq '';
+ }
+ if ($type eq 'destination') {
+ my $ipopt = "";
+ if (defined $ipaddr) {
+ $ipopt = "--orig-dst $ipaddr";
+ }
+ $xml = `sudo $conntrack -L -g $ipopt -o xml $proto 2>/dev/null`;
+ chomp $xml;
+ $data = undef;
+ $data = $xs->XMLin($xml) if ! $xml eq '';
+ }
+ nat_print_xml($data, $type) if defined $data;
+}
+
+# end of file
diff --git a/src/services/vyos-hostsd b/src/services/vyos-hostsd
index a655762e9..647cbc8c1 100755
--- a/src/services/vyos-hostsd
+++ b/src/services/vyos-hostsd
@@ -25,6 +25,7 @@ import traceback
import re
import logging
import zmq
+import collections
import jinja2
@@ -79,8 +80,18 @@ resolv_tmpl_source = """
### Autogenerated by VyOS ###
### Do not edit, your changes will get overwritten ###
+# name server from static configuration
{% for ns in name_servers -%}
+{%- if name_servers[ns]['tag'] == "static" %}
nameserver {{ns}}
+{%- endif %}
+{% endfor -%}
+
+{% for ns in name_servers -%}
+{%- if name_servers[ns]['tag'] != "static" %}
+# name server from {{name_servers[ns]['tag']}}
+nameserver {{ns}}
+{%- endif %}
{% endfor -%}
{%- if domain_name %}
@@ -110,7 +121,7 @@ resolv_tmpl = jinja2.Template(resolv_tmpl_source)
# and re-created without having to track what needs
# to be changed
STATE = {
- "name_servers": {},
+ "name_servers": collections.OrderedDict({}),
"hosts": {},
"host_name": "vyos",
"domain_name": "",
diff --git a/src/services/vyos-http-api-server b/src/services/vyos-http-api-server
index c36cbd640..4c41fa96d 100755
--- a/src/services/vyos-http-api-server
+++ b/src/services/vyos-http-api-server
@@ -26,7 +26,8 @@ import signal
import vyos.config
-import bottle
+from flask import Flask, request
+from waitress import serve
from functools import wraps
@@ -37,7 +38,7 @@ from vyos.config import VyOSError
DEFAULT_CONFIG_FILE = '/etc/vyos/http-api.conf'
CFG_GROUP = 'vyattacfg'
-app = bottle.default_app()
+app = Flask(__name__)
# Giant lock!
lock = threading.Lock()
@@ -55,18 +56,31 @@ def check_auth(key_list, key):
return id
def error(code, msg):
- bottle.response.status = code
resp = {"success": False, "error": msg, "data": None}
- return json.dumps(resp)
+ return json.dumps(resp), code
def success(data):
resp = {"success": True, "data": data, "error": None}
return json.dumps(resp)
+def get_command(f):
+ @wraps(f)
+ def decorated_function(*args, **kwargs):
+ cmd = request.form.get("data")
+ if not cmd:
+ return error(400, "Non-empty data field is required")
+ try:
+ cmd = json.loads(cmd)
+ except Exception as e:
+ return error(400, "Failed to parse JSON: {0}".format(e))
+ return f(cmd, *args, **kwargs)
+
+ return decorated_function
+
def auth_required(f):
@wraps(f)
def decorated_function(*args, **kwargs):
- key = bottle.request.forms.get("key")
+ key = request.form.get("key")
api_keys = app.config['vyos_keys']
id = check_auth(api_keys, key)
if not id:
@@ -75,28 +89,20 @@ def auth_required(f):
return decorated_function
-@app.route('/configure', method='POST')
+@app.route('/configure', methods=['POST'])
+@get_command
@auth_required
-def configure():
+def configure_op(commands):
session = app.config['vyos_session']
env = session.get_session_env()
config = vyos.config.Config(session_env=env)
- strict_field = bottle.request.forms.get("strict")
+ strict_field = request.form.get("strict")
if strict_field == "true":
strict = True
else:
strict = False
- commands = bottle.request.forms.get("data")
- if not commands:
- return error(400, "Non-empty data field is required")
- else:
- try:
- commands = json.loads(commands)
- except Exception as e:
- return error(400, "Failed to parse JSON: {0}".format(e))
-
# Allow users to pass just one command
if not isinstance(commands, list):
commands = [commands]
@@ -186,16 +192,14 @@ def configure():
else:
return success(None)
-@app.route('/retrieve', method='POST')
+@app.route('/retrieve', methods=['POST'])
+@get_command
@auth_required
-def get_value():
+def retrieve_op(command):
session = app.config['vyos_session']
env = session.get_session_env()
config = vyos.config.Config(session_env=env)
- command = bottle.request.forms.get("data")
- command = json.loads(command)
-
try:
op = command['op']
path = " ".join(command['path'])
@@ -229,20 +233,20 @@ def get_value():
return error(400, "\"{0}\" is not a valid operation".format(op))
except VyOSError as e:
return error(400, str(e))
+ except ConfigSessionError as e:
+ return error(400, str(e))
except Exception as e:
print(traceback.format_exc(), file=sys.stderr)
return error(500, "An internal error occured. Check the logs for details.")
return success(res)
-@app.route('/config-file', method='POST')
+@app.route('/config-file', methods=['POST'])
+@get_command
@auth_required
-def config_file_op():
+def config_file_op(command):
session = app.config['vyos_session']
- command = bottle.request.forms.get("data")
- command = json.loads(command)
-
try:
op = command['op']
except KeyError:
@@ -264,7 +268,7 @@ def config_file_op():
res = session.commit()
else:
return error(400, "\"{0}\" is not a valid operation".format(op))
- except VyOSError as e:
+ except ConfigSessionError as e:
return error(400, str(e))
except Exception as e:
print(traceback.format_exc(), file=sys.stderr)
@@ -272,14 +276,12 @@ def config_file_op():
return success(res)
-@app.route('/image', method='POST')
+@app.route('/image', methods=['POST'])
+@get_command
@auth_required
-def config_file_op():
+def image_op(command):
session = app.config['vyos_session']
- command = bottle.request.forms.get("data")
- command = json.loads(command)
-
try:
op = command['op']
except KeyError:
@@ -300,7 +302,7 @@ def config_file_op():
res = session.remove_image(name)
else:
return error(400, "\"{0}\" is not a valid operation".format(op))
- except VyOSError as e:
+ except ConfigSessionError as e:
return error(400, str(e))
except Exception as e:
print(traceback.format_exc(), file=sys.stderr)
@@ -309,14 +311,12 @@ def config_file_op():
return success(res)
-@app.route('/generate', method='POST')
+@app.route('/generate', methods=['POST'])
+@get_command
@auth_required
-def generate_op():
+def generate_op(command):
session = app.config['vyos_session']
- command = bottle.request.forms.get("data")
- command = json.loads(command)
-
try:
op = command['op']
path = command['path']
@@ -339,14 +339,12 @@ def generate_op():
return success(res)
-@app.route('/show', method='POST')
+@app.route('/show', methods=['POST'])
+@get_command
@auth_required
-def show_op():
+def show_op(command):
session = app.config['vyos_session']
- command = bottle.request.forms.get("data")
- command = json.loads(command)
-
try:
op = command['op']
path = command['path']
@@ -398,4 +396,8 @@ if __name__ == '__main__':
signal.signal(signal.SIGTERM, sig_handler)
- bottle.run(app, host=server_config["listen_address"], port=server_config["port"], debug=True)
+ try:
+ serve(app, host=server_config["listen_address"],
+ port=server_config["port"])
+ except OSError as e:
+ print(f"OSError {e}")
diff --git a/src/system/on-dhcp-event.sh b/src/system/on-dhcp-event.sh
index 385ae460f..3e2b6430d 100755
--- a/src/system/on-dhcp-event.sh
+++ b/src/system/on-dhcp-event.sh
@@ -43,13 +43,13 @@ case "$action" in
exit 1
fi
# add host
- /usr/bin/vyos-hostsd-client --add-hosts --tag "DHCP-$client_ip" --host "$client_fqdn_name,$client_ip"
+ sudo /usr/bin/vyos-hostsd-client --add-hosts --tag "DHCP-$client_ip" --host "$client_fqdn_name,$client_ip"
((changes++))
;;
release) # delete mapping for released address
# delete host
- /usr/bin/vyos-hostsd-client --delete-hosts --tag "DHCP-$client_ip"
+ sudo /usr/bin/vyos-hostsd-client --delete-hosts --tag "DHCP-$client_ip"
((changes++))
;;
diff --git a/src/systemd/dhclient@.service b/src/systemd/dhclient@.service
new file mode 100644
index 000000000..2ced1038a
--- /dev/null
+++ b/src/systemd/dhclient@.service
@@ -0,0 +1,18 @@
+[Unit]
+Description=DHCP client on %i
+Documentation=man:dhclient(8)
+ConditionPathExists=/var/lib/dhcp/dhclient_%i.conf
+ConditionPathExists=/var/lib/dhcp/dhclient_%i.options
+After=vyos-router.service
+
+[Service]
+WorkingDirectory=/var/lib/dhcp
+Type=exec
+EnvironmentFile=-/var/lib/dhcp/dhclient_%i.options
+PIDFile=/var/lib/dhcp/dhclient_%i.pid
+ExecStart=/sbin/dhclient -4 $DHCLIENT_OPTS
+ExecStop=/sbin/dhclient -4 $DHCLIENT_OPTS -r
+Restart=always
+
+[Install]
+WantedBy=multi-user.target
diff --git a/src/systemd/dhcp6c@.service b/src/systemd/dhcp6c@.service
new file mode 100644
index 000000000..1a4175461
--- /dev/null
+++ b/src/systemd/dhcp6c@.service
@@ -0,0 +1,16 @@
+[Unit]
+Description=WIDE DHCPv6 client on %i
+Documentation=man:dhcp6c(8) man:dhcp6c.conf(5)
+ConditionPathExists=/run/dhcp6c/dhcp6c.%i.conf
+After=vyos-router.service
+
+[Service]
+WorkingDirectory=/run/dhcp6c
+Type=forking
+PIDFile=/run/dhcp6c/dhcp6c.%i.pid
+ExecStart=/usr/sbin/dhcp6c -D -k /run/dhcp6c/dhcp6c.%i.sock -c /run/dhcp6c/dhcp6c.%i.conf -p /run/dhcp6c/dhcp6c.%i.pid %i
+
+Restart=always
+
+[Install]
+WantedBy=multi-user.target
diff --git a/src/systemd/isc-dhcp-relay.service b/src/systemd/isc-dhcp-relay.service
index ebf4d234e..56bcec840 100644
--- a/src/systemd/isc-dhcp-relay.service
+++ b/src/systemd/isc-dhcp-relay.service
@@ -2,13 +2,19 @@
Description=ISC DHCP IPv4 relay
Documentation=man:dhcrelay(8)
Wants=network-online.target
+RequiresMountsFor=/run
ConditionPathExists=/run/dhcp-relay/dhcp.conf
After=vyos-router.service
[Service]
+Type=forking
WorkingDirectory=/run/dhcp-relay
+RuntimeDirectory=dhcp-relay
+RuntimeDirectoryPreserve=yes
EnvironmentFile=/run/dhcp-relay/dhcp.conf
-ExecStart=/usr/sbin/dhcrelay -d -4 $OPTIONS
+PIDFile=/run/dhcp-relay/dhcrelay.pid
+ExecStart=/usr/sbin/dhcrelay -4 -pf /run/dhcp-relay/dhcrelay.pid $OPTIONS
+Restart=always
[Install]
WantedBy=multi-user.target
diff --git a/src/systemd/isc-dhcp-relay6.service b/src/systemd/isc-dhcp-relay6.service
index a477618b1..85ff16e41 100644
--- a/src/systemd/isc-dhcp-relay6.service
+++ b/src/systemd/isc-dhcp-relay6.service
@@ -2,13 +2,19 @@
Description=ISC DHCP IPv6 relay
Documentation=man:dhcrelay(8)
Wants=network-online.target
+RequiresMountsFor=/run
ConditionPathExists=/run/dhcp-relay/dhcpv6.conf
After=vyos-router.service
[Service]
+Type=forking
WorkingDirectory=/run/dhcp-relay
+RuntimeDirectory=dhcp-relay
+RuntimeDirectoryPreserve=yes
EnvironmentFile=/run/dhcp-relay/dhcpv6.conf
-ExecStart=/usr/sbin/dhcrelay -d -6 $OPTIONS
+PIDFile=/run/dhcp-relay/dhcrelayv6.pid
+ExecStart=/usr/sbin/dhcrelay -6 -pf /run/dhcp-relay/dhcrelayv6.pid $OPTIONS
+Restart=always
[Install]
WantedBy=multi-user.target
diff --git a/src/systemd/isc-dhcp-server.service b/src/systemd/isc-dhcp-server.service
index d848e3df1..e13c66dc6 100644
--- a/src/systemd/isc-dhcp-server.service
+++ b/src/systemd/isc-dhcp-server.service
@@ -6,14 +6,19 @@ ConditionPathExists=/run/dhcp-server/dhcpd.conf
After=vyos-router.service
[Service]
+Type=forking
WorkingDirectory=/run/dhcp-server
-# The leases files need to be root:vyattacfg even when dropping privileges
-ExecStart=/bin/sh -ec '\
- CONFIG_FILE=/run/dhcp-server/dhcpd.conf; \
- [ -e /config/dhcpd.leases ] || touch /config/dhcpd.leases; \
- chown root:vyattacfg /config/dhcpd.leases; \
- chmod 664 /config/dhcpd.leases; \
- exec /usr/sbin/dhcpd -user nobody -group nogroup -f -4 -pf /run/dhcp-server/dhcpd.pid -cf $CONFIG_FILE -lf /config/dhcpd.leases'
+RuntimeDirectory=dhcp-server
+RuntimeDirectoryPreserve=yes
+Environment=PID_FILE=/run/dhcp-server/dhcpd.pid CONFIG_FILE=/run/dhcp-server/dhcpd.conf LEASE_FILE=/config/dhcpd.leases
+PIDFile=/run/dhcp-server/dhcpd.pid
+ExecStartPre=/bin/sh -ec '\
+touch ${LEASE_FILE}; \
+chown nobody:nogroup ${LEASE_FILE}* ; \
+chmod 664 ${LEASE_FILE}* ; \
+/usr/sbin/dhcpd -4 -t -T -q -user nobody -group nogroup -pf ${PID_FILE} -cf ${CONFIG_FILE} -lf ${LEASE_FILE} '
+ExecStart=/usr/sbin/dhcpd -4 -q -user nobody -group nogroup -pf ${PID_FILE} -cf ${CONFIG_FILE} -lf ${LEASE_FILE}
+Restart=always
[Install]
WantedBy=multi-user.target
diff --git a/src/systemd/isc-dhcp-server6.service b/src/systemd/isc-dhcp-server6.service
index 743f16840..8ac861d7a 100644
--- a/src/systemd/isc-dhcp-server6.service
+++ b/src/systemd/isc-dhcp-server6.service
@@ -2,17 +2,23 @@
Description=ISC DHCP IPv6 server
Documentation=man:dhcpd(8)
RequiresMountsFor=/run
-ConditionPathExists=/run/dhcp-server/dhcpd.conf
+ConditionPathExists=/run/dhcp-server/dhcpdv6.conf
After=vyos-router.service
[Service]
+Type=forking
WorkingDirectory=/run/dhcp-server
-# The leases files need to be root:vyattacfg even when dropping privileges
-ExecStart=/bin/sh -ec '\
- [ -e /config/dhcpdv6.leases ] || touch /config/dhcpdv6.leases; \
- chown root:vyattacfg /config/dhcpdv6.leases; \
- chmod 664 /config/dhcpdv6.leases; \
- exec /usr/sbin/dhcpd -user nobody -group nogroup -f -6 -pf /run/dhcp-server/dhcpdv6.pid -cf /run/dhcp-server/dhcpdv6.conf -lf /config/dhcpdv6.leases'
+RuntimeDirectory=dhcp-server
+RuntimeDirectoryPreserve=yes
+Environment=PID_FILE=/run/dhcp-server/dhcpdv6.pid CONFIG_FILE=/run/dhcp-server/dhcpdv6.conf LEASE_FILE=/config/dhcpdv6.leases
+PIDFile=/run/dhcp-server/dhcpdv6.pid
+ExecStartPre=/bin/sh -ec '\
+touch ${LEASE_FILE}; \
+chown nobody:nogroup ${LEASE_FILE}* ; \
+chmod 664 ${LEASE_FILE}* ; \
+/usr/sbin/dhcpd -6 -t -T -q -user nobody -group nogroup -pf ${PID_FILE} -cf ${CONFIG_FILE} -lf ${LEASE_FILE} '
+ExecStart=/usr/sbin/dhcpd -6 -q -user nobody -group nogroup -pf ${PID_FILE} -cf ${CONFIG_FILE} -lf ${LEASE_FILE}
+Restart=always
[Install]
WantedBy=multi-user.target
diff --git a/src/validators/file-exists b/src/validators/file-exists
index e179805ed..5cef6b199 100755
--- a/src/validators/file-exists
+++ b/src/validators/file-exists
@@ -23,40 +23,39 @@ import os
import sys
import argparse
-parser = argparse.ArgumentParser()
-parser.add_argument("-d", "--directory", type=str, help="File must be present in this directory.")
-parser.add_argument("-e", "--error", action="store_true", help="Tread warnings as errors - change exit code to '1'")
-parser.add_argument("file", type=str, help="Path of file to validate")
-args = parser.parse_args()
-
-msg_prefix = "WARNING: "
-if args.error:
- msg_prefix = "ERROR: "
-
-#
-# Always check if the given file exists
-#
-if not os.path.exists(args.file):
- print(msg_prefix + "File '{}' not found".format(args.file))
- if args.error:
- sys.exit(1)
- else:
- sys.exit(0)
-
-#
-# Optional check if the file is under a certain directory path
-#
-if args.directory:
- # remove directory path from path to verify
- rel_filename = args.file.replace(args.directory, '').lstrip('/')
-
- if not os.path.exists(args.directory + '/' + rel_filename):
- print(msg_prefix + "'{}' lies outside of '{}' directory.\n" \
- "It will not get preserved during image upgrade!".format(args.file, args.directory))
- if args.error:
- sys.exit(1)
- else:
- sys.exit(0)
-
-sys.exit(0)
+def exit(strict, message):
+ if strict:
+ sys.exit(f'ERROR: {message}')
+ print(f'WARNING: {message}', file=sys.stderr)
+ sys.exit()
+
+
+if __name__ == '__main__':
+ parser = argparse.ArgumentParser()
+ parser.add_argument("-d", "--directory", type=str, help="File must be present in this directory.")
+ parser.add_argument("-e", "--error", action="store_true", help="Tread warnings as errors - change exit code to '1'")
+ parser.add_argument("file", type=str, help="Path of file to validate")
+
+ args = parser.parse_args()
+
+ #
+ # Always check if the given file exists
+ #
+ if not os.path.exists(args.file):
+ exit(args.error, f"File '{args.file}' not found")
+
+ #
+ # Optional check if the file is under a certain directory path
+ #
+ if args.directory:
+ # remove directory path from path to verify
+ rel_filename = args.file.replace(args.directory, '').lstrip('/')
+
+ if not os.path.exists(args.directory + '/' + rel_filename):
+ exit(args.error,
+ f"'{args.file}' lies outside of '{args.directory}' directory.\n"
+ "It will not get preserved during image upgrade!"
+ )
+
+ sys.exit()
diff --git a/src/validators/fqdn b/src/validators/fqdn
index 9f4ed764f..347ffda42 100755
--- a/src/validators/fqdn
+++ b/src/validators/fqdn
@@ -14,14 +14,17 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
-from re import match
-from sys import argv,exit
+import re
+import sys
-if len(argv) == 2:
- # pattern copied from: https://www.regextester.com/103452
- pattern = "(?=^.{4,253}$)(^((?!-)[a-zA-Z0-9-]{0,62}[a-zA-Z0-9]\.)+[a-zA-Z]{2,63}$)"
- if match(pattern, argv[1]):
- exit(0)
- else:
- exit(1)
+# pattern copied from: https://www.regextester.com/103452
+pattern = "(?=^.{4,253}$)(^((?!-)[a-zA-Z0-9-]{0,62}[a-zA-Z0-9]\.)+[a-zA-Z]{2,63}$)"
+
+
+if __name__ == '__main__':
+ if len(sys.argv) != 2:
+ sys.exit(1)
+ if not re.match(pattern, sys.argv[1]):
+ sys.exit(1)
+ sys.exit(0)
diff --git a/src/validators/ip-protocol b/src/validators/ip-protocol
new file mode 100755
index 000000000..078f8e319
--- /dev/null
+++ b/src/validators/ip-protocol
@@ -0,0 +1,41 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2020 VyOS maintainers and contributors
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License version 2 or later as
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+import re
+from sys import argv,exit
+
+if __name__ == '__main__':
+ if len(argv) != 2:
+ exit(1)
+
+ input = argv[1]
+ try:
+ # IP protocol can be in the range 0 - 255, thus the range must end with 256
+ if int(input) in range(0, 256):
+ exit(0)
+ except ValueError:
+ pass
+
+ pattern = "!?\\b(all|ip|hopopt|icmp|igmp|ggp|ipencap|st|tcp|egp|igp|pup|udp|" \
+ "tcp_udp|hmp|xns-idp|rdp|iso-tp4|dccp|xtp|ddp|idpr-cmtp|ipv6|" \
+ "ipv6-route|ipv6-frag|idrp|rsvp|gre|esp|ah|skip|ipv6-icmp|" \
+ "ipv6-nonxt|ipv6-opts|rspf|vmtp|eigrp|ospf|ax.25|ipip|etherip|" \
+ "encap|99|pim|ipcomp|vrrp|l2tp|isis|sctp|fc|mobility-header|" \
+ "udplite|mpls-in-ip|manet|hip|shim6|wesp|rohc)\\b"
+ if re.match(pattern, input):
+ exit(0)
+
+ exit(1)
diff --git a/src/validators/ipv4-address-exclude b/src/validators/ipv4-address-exclude
new file mode 100755
index 000000000..80ad17d45
--- /dev/null
+++ b/src/validators/ipv4-address-exclude
@@ -0,0 +1,7 @@
+#!/bin/sh
+arg="$1"
+if [ "${arg:0:1}" != "!" ]; then
+ exit 1
+fi
+path=$(dirname "$0")
+${path}/ipv4-address "${arg:1}"
diff --git a/src/validators/ipv4-prefix-exclude b/src/validators/ipv4-prefix-exclude
new file mode 100755
index 000000000..4f7de400a
--- /dev/null
+++ b/src/validators/ipv4-prefix-exclude
@@ -0,0 +1,7 @@
+#!/bin/sh
+arg="$1"
+if [ "${arg:0:1}" != "!" ]; then
+ exit 1
+fi
+path=$(dirname "$0")
+${path}/ipv4-prefix "${arg:1}"
diff --git a/src/validators/ipv4-range b/src/validators/ipv4-range
new file mode 100755
index 000000000..ae3f3f163
--- /dev/null
+++ b/src/validators/ipv4-range
@@ -0,0 +1,33 @@
+#!/bin/bash
+
+# snippet from https://stackoverflow.com/questions/10768160/ip-address-converter
+ip2dec () {
+ local a b c d ip=$@
+ IFS=. read -r a b c d <<< "$ip"
+ printf '%d\n' "$((a * 256 ** 3 + b * 256 ** 2 + c * 256 + d))"
+}
+
+# Only run this if there is a hypen present in $1
+if [[ "$1" =~ "-" ]]; then
+ # This only works with real bash (<<<) - split IP addresses into array with
+ # hyphen as delimiter
+ readarray -d - -t strarr <<< $1
+
+ ipaddrcheck --is-ipv4-single ${strarr[0]}
+ if [ $? -gt 0 ]; then
+ exit 1
+ fi
+
+ ipaddrcheck --is-ipv4-single ${strarr[1]}
+ if [ $? -gt 0 ]; then
+ exit 1
+ fi
+
+ start=$(ip2dec ${strarr[0]})
+ stop=$(ip2dec ${strarr[1]})
+ if [ $start -ge $stop ]; then
+ exit 1
+ fi
+fi
+
+exit 0
diff --git a/src/validators/ipv4-range-exclude b/src/validators/ipv4-range-exclude
new file mode 100755
index 000000000..3787b4dec
--- /dev/null
+++ b/src/validators/ipv4-range-exclude
@@ -0,0 +1,7 @@
+#!/bin/sh
+arg="$1"
+if [ "${arg:0:1}" != "!" ]; then
+ exit 1
+fi
+path=$(dirname "$0")
+${path}/ipv4-range "${arg:1}"
diff --git a/src/validators/mac-address b/src/validators/mac-address
index 435920b84..b2d3496f4 100755
--- a/src/validators/mac-address
+++ b/src/validators/mac-address
@@ -15,12 +15,15 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import re
-from sys import exit, argv
+import sys
-if len(argv) == 2:
- pattern = "^([0-9A-Fa-f]{2}[:]){5}([0-9A-Fa-f]{2})$"
- if re.match(pattern, argv[1]):
- exit(0)
- else:
- exit(1)
+pattern = "^([0-9A-Fa-f]{2}[:]){5}([0-9A-Fa-f]{2})$"
+
+
+if __name__ == '__main__':
+ if len(sys.argv) != 2:
+ sys.exit(1)
+ if not re.match(pattern, sys.argv[1]):
+ sys.exit(1)
+ sys.exit(0)
diff --git a/src/validators/numeric b/src/validators/numeric
deleted file mode 100755
index 0a2d83d14..000000000
--- a/src/validators/numeric
+++ /dev/null
@@ -1,78 +0,0 @@
-#!/usr/bin/env python3
-#
-# numeric value validator
-#
-# Copyright (C) 2017 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
-# as published by the Free Software Foundation; either version 2
-# of the License, or (at your option) any later version.
-#
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-# GNU General Public License for more details.
-#
-# You should have received a copy of the GNU General Public License
-# along with this program; If not, see <http://www.gnu.org/licenses/>.
-
-import sys
-import argparse
-import re
-
-parser = argparse.ArgumentParser()
-parser.add_argument("-f", "--float", action="store_true", help="Accept floating point values")
-group = parser.add_mutually_exclusive_group()
-group.add_argument("-r", "--range", type=str, help="Check if the number is within range (inclusive), example: 1024-65535", action='append')
-group.add_argument("-n", "--non-negative", action="store_true", help="Check if the number is non-negative (>= 0)")
-group.add_argument("-p", "--positive", action="store_true", help="Check if the number is positive (> 0)")
-parser.add_argument("number", type=str, help="Number to validate")
-
-args = parser.parse_args()
-
-# Try to load the argument
-number = None
-if args.float:
- try:
- number = float(args.number)
- except:
- print("{0} is not a valid floating point number".format(args.number), file=sys.stderr)
- sys.exit(1)
-else:
- try:
- number = int(args.number)
- except:
- print("{0} is not a valid integer number".format(args.number), file=sys.stderr)
- sys.exit(1)
-
-if args.range:
- valid = False
- for r in args.range:
- try:
- lower, upper = re.match(r'(\d+)\s*\-\s*(\d+)', r).groups()
- lower, upper = int(lower), int(upper)
- except:
- print("{0} is not a valid number range",format(args.range), file=sys.stderr)
- sys.exit(1)
-
- if (number >= lower) and (number <= upper):
- valid = True
- # end for
-
- if not valid:
- if len(args.range) > 1:
- err_msg = "Number {0} is not in any of the ranges {1}".format(number, args.range)
- else:
- err_msg = "Number {0} is not in the range {1}".format(number, args.range[0])
- print(err_msg, file=sys.stderr)
- sys.exit(1)
-elif args.non_negative:
- if number < 0:
- print("Number should be non-negative", file=sys.stderr)
- sys.exit(1)
-elif args.positive:
- if number <= 0:
- print("Number should be positive", file=sys.stderr)
- sys.exit(1)
-
diff --git a/src/validators/script b/src/validators/script
index 689296a73..2665ec1f6 100755
--- a/src/validators/script
+++ b/src/validators/script
@@ -17,7 +17,6 @@
# 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 sys
import shlex
@@ -25,22 +24,22 @@ import shlex
import vyos.util
-if len(sys.argv) < 2:
- print("Please specify script file to check")
- sys.exit(1)
+if __name__ == '__main__':
+ if len(sys.argv) < 2:
+ sys.exit('Please specify script file to check')
-#if the "script" is a script+ stowaway arugments, this removes the aguements
-script = shlex.split(sys.argv[1])[0]
+ # if the "script" is a script+ stowaway arguments, this removes the aguements
+ script = shlex.split(sys.argv[1])[0]
-if not os.path.exists(script):
- print("File {0} does not exist".format(script))
- sys.exit(1)
+ if not os.path.exists(script):
+ sys.exit(f'File {script} does not exist')
-if not (os.path.isfile(script) and os.access(script, os.X_OK)):
- print("File {0} is not an executable file".format(script))
- sys.exit(1)
+ if not (os.path.isfile(script) and os.access(script, os.X_OK)):
+ sys.exit('File {script} is not an executable file')
-# File outside the config dir is just a warning
-res, warning = vyos.util.file_is_persistent(script)
-if not res:
- print(warning)
+ # File outside the config dir is just a warning
+ if not vyos.util.file_is_persistent(script):
+ sys.exit(
+ 'Warning: file {path} is outside the / config directory\n'
+ 'It will not be automatically migrated to a new image on system update'
+ )
diff --git a/src/validators/timezone b/src/validators/timezone
index ec845e755..baf5abca2 100755
--- a/src/validators/timezone
+++ b/src/validators/timezone
@@ -15,25 +15,19 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import argparse
-
-from sys import exit
+import sys
from vyos.util import cmd
-parser = argparse.ArgumentParser()
-parser.add_argument("--validate", action="store", help="Check if timezone is valid")
if __name__ == '__main__':
+ parser = argparse.ArgumentParser()
+ parser.add_argument("--validate", action="store", required=True, help="Check if timezone is valid")
args = parser.parse_args()
- if args.validate:
- tz_data = cmd('find /usr/share/zoneinfo/posix -type f -or -type l | sed -e s:/usr/share/zoneinfo/posix/::')
- tz_data = tz_data.split('\n')
- # if timezone can't be found in list it's invalid
- if args.validate not in tz_data:
- exit(1)
- else:
- parser.print_help()
- exit(1)
+ tz_data = cmd('find /usr/share/zoneinfo/posix -type f -or -type l | sed -e s:/usr/share/zoneinfo/posix/::')
+ tz_data = tz_data.split('\n')
- exit(0)
+ if args.validate not in tz_data:
+ sys.exit("the timezone can't be found in the timezone list")
+ sys.exit()
diff --git a/src/validators/vrf-name b/src/validators/vrf-name
index 11c453f4d..878893c46 100755
--- a/src/validators/vrf-name
+++ b/src/validators/vrf-name
@@ -14,27 +14,30 @@
# 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
-from sys import exit, argv
+import sys
+
+
+if __name__ == '__main__':
+ if len(sys.argv) != 2:
+ sys.exit(1)
+
+ vrf = sys.argv[1]
+ length = len(vrf)
-if len(argv) == 2:
- len = len(argv[1])
- # VRF instance name must be 16 characters or less, python range needs to be
- # extended by one
- if not len in range(1, 17):
- exit(1)
+ if length not in range(1, 17):
+ sys.exit('VRF instance name must be 16 characters or less')
# Treat loopback interface "lo" explicitly. Adding "lo" explicitly to the
# following regex pattern would deny any VRF name starting with lo - thuse
# local-vrf would be illegal - and that we do not want.
- if argv[1] == "lo":
- exit(1)
+ if vrf == "lo":
+ exit(f'"{vrf}" is invalid as VRF name as it is an interface name')
- # VRF instances should not be named after regular interface names like bond0,
- # br10 and so on - this can cause a lot of confusion/trouble
pattern = "^(?!(bond|br|dum|eth|lan|eno|ens|enp|enx|gnv|ipoe|l2tp|l2tpeth|" \
"vtun|ppp|pppoe|peth|tun|vti|vxlan|wg|wlan|wlm)\d+(\.\d+(v.+)?)?$).*$"
- if re.match(pattern, argv[1]):
- exit(0)
+ if not re.match(pattern, vrf):
+ sys.exit(f'"{vrf}" is invalid as VRF name as it is an interface name')
-exit(1)
+ sys.exit(0)