diff options
291 files changed, 15744 insertions, 8028 deletions
diff --git a/Jenkinsfile b/Jenkinsfile index ed98477f2..7a79b0f43 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -36,7 +36,7 @@ def isCustomBuild() { def gitURI = 'git@github.com:vyos/' + getGitRepoName() def httpURI = 'https://github.com/vyos/' + getGitRepoName() - return ! ((getGitRepoURL() == gitURI) || (getGitRepoURL() == httpURI)) + return !((getGitRepoURL() == gitURI) || (getGitRepoURL() == httpURI)) || env.CHANGE_ID } def setDescription() { @@ -74,7 +74,13 @@ node('Docker') { script { // create container name on demand def branchName = getGitBranchName() - if (branchName == "master") { + // Adjust PR target branch name so we can re-map it to the proper + // Docker image. CHANGE_ID is set only for pull requests, so it is + // safe to access the pullRequest global variable + if (env.CHANGE_ID) { + branchName = "${env.CHANGE_TARGET}".toLowerCase() + } + if (branchName.equals("master")) { branchName = "current" } env.DOCKER_IMAGE = "vyos/vyos-build:" + branchName @@ -92,7 +98,6 @@ pipeline { } options { disableConcurrentBuilds() - skipDefaultCheckout() timeout(time: 30, unit: 'MINUTES') timestamps() } @@ -101,8 +106,7 @@ pipeline { steps { script { dir('build') { - git branch: getGitBranchName(), - url: getGitRepoURL() + checkout scm } } } @@ -36,22 +36,41 @@ interface_definitions: $(BUILD_DIR) $(obj) rm -f $(TMPL_DIR)/firewall/node.def rm -f $(TMPL_DIR)/interfaces/node.def rm -f $(TMPL_DIR)/interfaces/bonding/node.tag/ip/node.def + rm -f $(TMPL_DIR)/interfaces/bonding/node.tag/ipv6/node.def rm -f $(TMPL_DIR)/interfaces/bonding/node.tag/vif/node.tag/ip/node.def + rm -f $(TMPL_DIR)/interfaces/bonding/node.tag/vif/node.tag/ipv6/node.def rm -f $(TMPL_DIR)/interfaces/bonding/node.tag/vif-s/node.tag/ip/node.def + rm -f $(TMPL_DIR)/interfaces/bonding/node.tag/vif-s/node.tag/ipv6/node.def rm -f $(TMPL_DIR)/interfaces/bridge/node.tag/ip/node.def + rm -f $(TMPL_DIR)/interfaces/bridge/node.tag/ipv6/node.def rm -f $(TMPL_DIR)/interfaces/ethernet/node.tag/ip/node.def + rm -f $(TMPL_DIR)/interfaces/ethernet/node.tag/ipv6/node.def rm -f $(TMPL_DIR)/interfaces/ethernet/node.tag/vif/node.tag/ip/node.def + rm -f $(TMPL_DIR)/interfaces/ethernet/node.tag/vif/node.tag/ipv6/node.def rm -f $(TMPL_DIR)/interfaces/ethernet/node.tag/vif-s/node.tag/ip/node.def + rm -f $(TMPL_DIR)/interfaces/ethernet/node.tag/vif-s/node.tag/ipv6/node.def rm -f $(TMPL_DIR)/interfaces/ethernet/node.tag/vif-s/node.tag/vif-c/node.tag/ip/node.def + rm -f $(TMPL_DIR)/interfaces/ethernet/node.tag/vif-s/node.tag/vif-c/node.tag/ipv6/node.def + rm -f $(TMPL_DIR)/interfaces/l2tpv3/node.tag/ipv6/node.def + rm -f $(TMPL_DIR)/interfaces/openvpn/node.tag/ipv6/node.def rm -f $(TMPL_DIR)/interfaces/pppoe/node.tag/ip/node.def rm -f $(TMPL_DIR)/interfaces/pppoe/node.tag/ipv6/node.def rm -f $(TMPL_DIR)/interfaces/pseudo-ethernet/node.tag/ip/node.def + rm -f $(TMPL_DIR)/interfaces/pseudo-ethernet/node.tag/ipv6/node.def rm -f $(TMPL_DIR)/interfaces/pseudo-ethernet/node.tag/vif/node.tag/ip/node.def + rm -f $(TMPL_DIR)/interfaces/pseudo-ethernet/node.tag/vif/node.tag/ipv6/node.def rm -f $(TMPL_DIR)/interfaces/pseudo-ethernet/node.tag/vif-s/node.tag/ip/node.def + rm -f $(TMPL_DIR)/interfaces/pseudo-ethernet/node.tag/vif-s/node.tag/ipv6/node.def rm -f $(TMPL_DIR)/interfaces/pseudo-ethernet/node.tag/vif-s/node.tag/vif-c/node.tag/ip/node.def + rm -f $(TMPL_DIR)/interfaces/pseudo-ethernet/node.tag/vif-s/node.tag/vif-c/node.tag/ipv6/node.def + rm -f $(TMPL_DIR)/interfaces/tunnel/node.tag/ipv6/node.def rm -f $(TMPL_DIR)/interfaces/vxlan/node.tag/ip/node.def - rm -f $(TMPL_DIR)/interfaces/wireless/node.tag/vif/node.tag/ip/node.def + rm -f $(TMPL_DIR)/interfaces/vxlan/node.tag/ipv6/node.def rm -f $(TMPL_DIR)/interfaces/wireless/node.tag/ip/node.def + rm -f $(TMPL_DIR)/interfaces/wireless/node.tag/ipv6/node.def + rm -f $(TMPL_DIR)/interfaces/wireless/node.tag/vif/node.tag/ip/node.def + rm -f $(TMPL_DIR)/interfaces/wireless/node.tag/vif/node.tag/ipv6/node.def + rm -f $(TMPL_DIR)/interfaces/wirelessmodem/node.tag/ipv6/node.def rm -f $(TMPL_DIR)/protocols/node.def rm -f $(TMPL_DIR)/protocols/static/node.def rm -f $(TMPL_DIR)/system/node.def diff --git a/data/interface-types.json b/data/interface-types.json deleted file mode 100644 index f174d3c39..000000000 --- a/data/interface-types.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "loopback": "lo", - "dummy": "dum", - "ethernet": "eth", - "bonding": "bond", - "bridge": "br", - "pseudo-ethernet": "peth", - "openvpn": "vtun", - "tunnel": "tun", - "vti": "vti", - "l2tpv3": "l2tpeth", - "vxlan": "vxlan", - "wireguard": "wg", - "wireless": "wlan", - "wirelessmodem": "wlm", - "input": "ifb", - "pppoe": "pppoe", - "geneve": "gnv" -} diff --git a/data/templates/bcast-relay/udp-broadcast-relay.tmpl b/data/templates/bcast-relay/udp-broadcast-relay.tmpl new file mode 100644 index 000000000..3d8c3fe94 --- /dev/null +++ b/data/templates/bcast-relay/udp-broadcast-relay.tmpl @@ -0,0 +1,7 @@ +### Autogenerated by bcast_relay.py ### + +# UDP broadcast relay configuration for instance {{ id }} +{%- if description %} +# Comment: {{ description }} +{% endif %} +DAEMON_ARGS="{% if address %}-s {{ address }} {% endif %}{{ id }} {{ port }} {{ interfaces | join(' ') }}" diff --git a/data/templates/dhcp-relay/config.tmpl b/data/templates/dhcp-relay/config.tmpl new file mode 100644 index 000000000..7203ae9fb --- /dev/null +++ b/data/templates/dhcp-relay/config.tmpl @@ -0,0 +1,17 @@ +### Autogenerated by dhcp_relay.py ### + +# Defaults for isc-dhcp-relay initscript +# sourced by /etc/init.d/isc-dhcp-relay + +# +# This is a POSIX shell fragment +# + +# What servers should the DHCP relay forward requests to? +SERVERS="{{ server | join(' ') }}" + +# On what interfaces should the DHCP relay (dhrelay) serve DHCP requests? +INTERFACES="{{ interface | join(' ') }}" + +# Additional options that are passed to the DHCP relay daemon? +OPTIONS="-4 {{ options | join(' ') }}" diff --git a/data/templates/dhcp-server/daemon.tmpl b/data/templates/dhcp-server/daemon.tmpl new file mode 100644 index 000000000..f88032d38 --- /dev/null +++ b/data/templates/dhcp-server/daemon.tmpl @@ -0,0 +1,8 @@ +### Autogenerated by dhcp_server.py ### + +# sourced by /etc/init.d/isc-dhcpv4-server + +DHCPD_CONF={{ config_file }} +DHCPD_PID={{ pid_file }} +OPTIONS="-4 -lf {{ lease_file }}" +INTERFACES="" diff --git a/data/templates/dhcp-server/dhcpd.conf.tmpl b/data/templates/dhcp-server/dhcpd.conf.tmpl new file mode 100644 index 000000000..5f5129451 --- /dev/null +++ b/data/templates/dhcp-server/dhcpd.conf.tmpl @@ -0,0 +1,195 @@ + +### Autogenerated by dhcp_server.py ### + +# For options please consult the following website: +# https://www.isc.org/wp-content/uploads/2017/08/dhcp43options.html +# +# log-facility local7; + +{% if hostfile_update %} +on release { + set ClientName = pick-first-value(host-decl-name, option fqdn.hostname, option host-name); + set ClientIp = binary-to-ascii(10, 8, ".",leased-address); + set ClientMac = binary-to-ascii(16, 8, ":",substring(hardware, 1, 6)); + set ClientDomain = pick-first-value(config-option domain-name, "..YYZ!"); + execute("/usr/libexec/vyos/system/on-dhcp-event.sh", "release", ClientName, ClientIp, ClientMac, ClientDomain); +} + +on expiry { + set ClientName = pick-first-value(host-decl-name, option fqdn.hostname, option host-name); + set ClientIp = binary-to-ascii(10, 8, ".",leased-address); + set ClientMac = binary-to-ascii(16, 8, ":",substring(hardware, 1, 6)); + set ClientDomain = pick-first-value(config-option domain-name, "..YYZ!"); + execute("/usr/libexec/vyos/system/on-dhcp-event.sh", "release", ClientName, ClientIp, ClientMac, ClientDomain); +} +{% endif %} +{%- if host_decl_name %} +use-host-decl-names on; +{%- endif %} +ddns-update-style {% if ddns_enable -%} interim {%- else -%} none {%- endif %}; +{% if static_route -%} +option rfc3442-static-route code 121 = array of integer 8; +option windows-static-route code 249 = array of integer 8; +{%- endif %} +{% if wpad -%} +option wpad-url code 252 = text; +{% endif %} + +{%- if global_parameters %} +# The following {{ global_parameters | length }} line(s) were added as global-parameters in the CLI and have not been validated +{%- for param in global_parameters %} +{{ param }} +{%- endfor -%} +{%- endif %} + +# Failover configuration +{% for network in shared_network %} +{%- if not network.disabled -%} +{%- for subnet in network.subnet %} +{%- if subnet.failover_name -%} +failover peer "{{ subnet.failover_name }}" { +{%- if subnet.failover_status == 'primary' %} + primary; + mclt 1800; + split 128; +{%- elif subnet.failover_status == 'secondary' %} + secondary; +{%- endif %} + address {{ subnet.failover_local_addr }}; + port 520; + peer address {{ subnet.failover_peer_addr }}; + peer port 520; + max-response-delay 30; + max-unacked-updates 10; + load balance max seconds 3; +} +{% endif -%} +{% endfor -%} +{% endif -%} +{% endfor %} + +# Shared network configration(s) +{% for network in shared_network %} +{%- if not network.disabled -%} +shared-network {{ network.name }} { + {%- if network.authoritative %} + authoritative; + {%- endif %} + {%- if network.network_parameters %} + # The following {{ network.network_parameters | length }} line(s) were added as shared-network-parameters in the CLI and have not been validated + {%- for param in network.network_parameters %} + {{ param }} + {%- endfor %} + {%- endif %} + {%- for subnet in network.subnet %} + subnet {{ subnet.address }} netmask {{ subnet.netmask }} { + {%- if subnet.dns_server %} + option domain-name-servers {{ subnet.dns_server | join(', ') }}; + {%- endif %} + {%- if subnet.domain_search %} + option domain-search {{ subnet.domain_search | join(', ') }}; + {%- endif %} + {%- if subnet.ntp_server %} + option ntp-servers {{ subnet.ntp_server | join(', ') }}; + {%- endif %} + {%- if subnet.pop_server %} + option pop-server {{ subnet.pop_server | join(', ') }}; + {%- endif %} + {%- if subnet.smtp_server %} + option smtp-server {{ subnet.smtp_server | join(', ') }}; + {%- endif %} + {%- if subnet.time_server %} + option time-servers {{ subnet.time_server | join(', ') }}; + {%- endif %} + {%- if subnet.wins_server %} + option netbios-name-servers {{ subnet.wins_server | join(', ') }}; + {%- endif %} + {%- if subnet.static_route %} + option rfc3442-static-route {{ subnet.static_route }}{% if subnet.rfc3442_default_router %}, {{ subnet.rfc3442_default_router }}{% endif %}; + option windows-static-route {{ subnet.static_route }}; + {%- endif %} + {%- if subnet.ip_forwarding %} + option ip-forwarding true; + {%- endif -%} + {%- if subnet.default_router %} + option routers {{ subnet.default_router }}; + {%- endif -%} + {%- if subnet.server_identifier %} + option dhcp-server-identifier {{ subnet.server_identifier }}; + {%- endif -%} + {%- if subnet.domain_name %} + option domain-name "{{ subnet.domain_name }}"; + {%- endif -%} + {%- if subnet.subnet_parameters %} + # The following {{ subnet.subnet_parameters | length }} line(s) were added as subnet-parameters in the CLI and have not been validated + {%- for param in subnet.subnet_parameters %} + {{ param }} + {%- endfor -%} + {%- endif %} + {%- if subnet.tftp_server %} + option tftp-server-name "{{ subnet.tftp_server }}"; + {%- endif -%} + {%- if subnet.bootfile_name %} + option bootfile-name "{{ subnet.bootfile_name }}"; + filename "{{ subnet.bootfile_name }}"; + {%- endif -%} + {%- if subnet.bootfile_server %} + next-server {{ subnet.bootfile_server }}; + {%- endif -%} + {%- if subnet.time_offset %} + option time-offset {{ subnet.time_offset }}; + {%- endif -%} + {%- if subnet.wpad_url %} + option wpad-url "{{ subnet.wpad_url }}"; + {%- endif -%} + {%- if subnet.client_prefix_length %} + option subnet-mask {{ subnet.client_prefix_length }}; + {%- endif -%} + {% if subnet.lease %} + default-lease-time {{ subnet.lease }}; + max-lease-time {{ subnet.lease }}; + {%- endif -%} + {%- for host in subnet.static_mapping %} + {% if not host.disabled -%} + host {% if host_decl_name -%} {{ host.name }} {%- else -%} {{ network.name }}_{{ host.name }} {%- endif %} { + {%- if host.ip_address %} + fixed-address {{ host.ip_address }}; + {%- endif %} + hardware ethernet {{ host.mac_address }}; + {%- if host.static_parameters %} + # The following {{ host.static_parameters | length }} line(s) were added as static-mapping-parameters in the CLI and have not been validated + {%- for param in host.static_parameters %} + {{ param }} + {%- endfor -%} + {%- endif %} + } + {%- endif %} + {%- endfor %} + {%- if subnet.failover_name %} + pool { + failover peer "{{ subnet.failover_name }}"; + deny dynamic bootp clients; + {%- for range in subnet.range %} + range {{ range.start }} {{ range.stop }}; + {%- endfor %} + } + {%- else %} + {%- for range in subnet.range %} + range {{ range.start }} {{ range.stop }}; + {%- endfor %} + {%- endif %} + } + {%- endfor %} + on commit { + set shared-networkname = "{{ network.name }}"; + {% if hostfile_update -%} + set ClientName = pick-first-value(host-decl-name, option fqdn.hostname, option host-name); + set ClientIp = binary-to-ascii(10, 8, ".", leased-address); + set ClientMac = binary-to-ascii(16, 8, ":", substring(hardware, 1, 6)); + set ClientDomain = pick-first-value(config-option domain-name, "..YYZ!"); + execute("/usr/libexec/vyos/system/on-dhcp-event.sh", "commit", ClientName, ClientIp, ClientMac, ClientDomain); + {%- endif %} + } +} +{%- endif %} +{% endfor %} diff --git a/data/templates/dhcpv6-relay/config.tmpl b/data/templates/dhcpv6-relay/config.tmpl new file mode 100644 index 000000000..28f7a1a58 --- /dev/null +++ b/data/templates/dhcpv6-relay/config.tmpl @@ -0,0 +1,4 @@ +### Autogenerated by dhcpv6_relay.py ### + +# Defaults for isc-dhcpv6-relay initscript sourced by /etc/init.d/isc-dhcpv6-relay +OPTIONS="-6 -l {{ listen_addr | join(' -l ') }} -u {{ upstream_addr | join(' -u ') }} {{ options | join(' ') }}" diff --git a/data/templates/dhcpv6-server/daemon.tmpl b/data/templates/dhcpv6-server/daemon.tmpl new file mode 100644 index 000000000..a4967e7c3 --- /dev/null +++ b/data/templates/dhcpv6-server/daemon.tmpl @@ -0,0 +1,8 @@ +### Autogenerated by dhcpv6_server.py ### + +# sourced by /etc/init.d/isc-dhcpv6-server + +DHCPD_CONF={{ config_file }} +DHCPD_PID={{ pid_file }} +OPTIONS="-6 -lf {{ lease_file }}" +INTERFACES="" diff --git a/data/templates/dhcpv6-server/dhcpdv6.conf.tmpl b/data/templates/dhcpv6-server/dhcpdv6.conf.tmpl new file mode 100644 index 000000000..80d620fcf --- /dev/null +++ b/data/templates/dhcpv6-server/dhcpdv6.conf.tmpl @@ -0,0 +1,78 @@ +### Autogenerated by dhcpv6_server.py ### + +# For options please consult the following website: +# https://www.isc.org/wp-content/uploads/2017/08/dhcp43options.html + +log-facility local7; +{%- if preference %} +option dhcp6.preference {{ preference }}; +{%- endif %} + +# Shared network configration(s) +{% for network in shared_network %} +{%- if not network.disabled -%} +shared-network {{ network.name }} { + {%- for subnet in network.subnet %} + subnet6 {{ subnet.network }} { + {%- for range in subnet.range6_prefix %} + range6 {{ range.prefix }}{{ " temporary" if range.temporary }}; + {%- endfor %} + {%- for range in subnet.range6 %} + range6 {{ range.start }} {{ range.stop }}; + {%- endfor %} + {%- if subnet.domain_search %} + option dhcp6.domain-search {{ subnet.domain_search | join(', ') }}; + {%- endif %} + {%- if subnet.lease_def %} + default-lease-time {{ subnet.lease_def }}; + {%- endif %} + {%- if subnet.lease_max %} + max-lease-time {{ subnet.lease_max }}; + {%- endif %} + {%- if subnet.lease_min %} + min-lease-time {{ subnet.lease_min }}; + {%- endif %} + {%- if subnet.dns_server %} + option dhcp6.name-servers {{ subnet.dns_server | join(', ') }}; + {%- endif %} + {%- if subnet.nis_domain %} + option dhcp6.nis-domain-name "{{ subnet.nis_domain }}"; + {%- endif %} + {%- if subnet.nis_server %} + option dhcp6.nis-servers {{ subnet.nis_server | join(', ') }}; + {%- endif %} + {%- if subnet.nisp_domain %} + option dhcp6.nisp-domain-name "{{ subnet.nisp_domain }}"; + {%- endif %} + {%- if subnet.nisp_server %} + option dhcp6.nisp-servers {{ subnet.nisp_server | join(', ') }}; + {%- endif %} + {%- if subnet.sip_address %} + option dhcp6.sip-servers-addresses {{ subnet.sip_address | join(', ') }}; + {%- endif %} + {%- if subnet.sip_hostname %} + option dhcp6.sip-servers-names {{ subnet.sip_hostname | join(', ') }}; + {%- endif %} + {%- if subnet.sntp_server %} + option dhcp6.sntp-servers {{ subnet.sntp_server | join(', ') }}; + {%- endif %} + {%- for host in subnet.static_mapping %} + {% if not host.disabled -%} + host {{ network.name }}_{{ host.name }} { + {%- if host.client_identifier %} + host-identifier option dhcp6.client-id {{ host.client_identifier }}; + {%- endif %} + {%- if host.ipv6_address %} + fixed-address6 {{ host.ipv6_address }}; + {%- endif %} + } + {%- endif %} + {%- endfor %} + } + {%- endfor %} + on commit { + set shared-networkname = "{{ network.name }}"; + } +} +{%- endif %} +{% endfor %} diff --git a/data/templates/dns-forwarding/recursor.conf.tmpl b/data/templates/dns-forwarding/recursor.conf.tmpl new file mode 100644 index 000000000..9d1e019fa --- /dev/null +++ b/data/templates/dns-forwarding/recursor.conf.tmpl @@ -0,0 +1,44 @@ +### Autogenerated by dns_forwarding.py ### + +# XXX: pdns recursor doesn't like whitespace near entry separators, +# especially in the semicolon-separated lists of name servers. +# Please be careful if you edit the template. + +# Non-configurable defaults +daemon=yes +threads=1 +allow-from={{ allow_from | join(',') }} +log-common-errors=yes +non-local-bind=yes +query-local-address=0.0.0.0 +query-local-address6=:: + +# cache-size +max-cache-entries={{ cache_size }} + +# negative TTL for NXDOMAIN +max-negative-ttl={{ negative_ttl }} + +# ignore-hosts-file +export-etc-hosts={{ export_hosts_file }} + +# listen-on +local-address={{ listen_on | join(',') }} + +# dnssec +dnssec={{ dnssec }} + +# forward-zones / recursion +# +# statement is only inserted if either one forwarding domain or nameserver is configured +# if nothing is given at all, powerdns will act as a real recursor and resolve all requests by its own +# +{% if name_servers or domains %}forward-zones-recurse= +{%- for d in domains %} +{{ d.name }}={{ d.servers | join(";") }} +{{- ", " if not loop.last -}} +{%- endfor -%} +{%- if name_servers -%} +{%- if domains -%}, {% endif -%}.={{ name_servers | join(';') }} +{% endif %} +{% endif %} diff --git a/data/templates/dynamic-dns/ddclient.conf.tmpl b/data/templates/dynamic-dns/ddclient.conf.tmpl new file mode 100644 index 000000000..22cb38f4e --- /dev/null +++ b/data/templates/dynamic-dns/ddclient.conf.tmpl @@ -0,0 +1,49 @@ + +### Autogenerated by dynamic_dns.py ### +daemon=1m +syslog=yes +ssl=yes +pid={{ pid_file }} +cache={{ cache_file }} + +{% for interface in interfaces -%} + +# +# ddclient configuration for interface "{{ interface.interface }}": +# +{% if interface.web_url -%} +use=web, web='{{ interface.web_url}}' {%- if interface.web_skip %}, web-skip='{{ interface.web_skip }}'{% endif %} +{% else -%} +use=if, if={{ interface.interface }} +{% endif -%} + +{% for rfc in interface.rfc2136 -%} +{% for record in rfc.record %} +# RFC2136 dynamic DNS configuration for {{ record }}.{{ rfc.zone }} +server={{ rfc.server }} +protocol=nsupdate +password={{ rfc.keyfile }} +ttl={{ rfc.ttl }} +zone={{ rfc.zone }} +{{ record }} +{% endfor -%} +{% endfor -%} + +{% for srv in interface.service %} +{% for host in srv.host %} +# DynDNS provider configuration for {{ host }} +protocol={{ srv.protocol }}, +max-interval=28d, +login={{ srv.login }}, +password='{{ srv.password }}', +{% if srv.server -%} +server={{ srv.server }}, +{% endif -%} +{% if srv.zone -%} +zone={{ srv.zone }}, +{% endif -%} +{{ host }} +{% endfor %} +{% endfor %} + +{% endfor %} diff --git a/data/templates/frr-bfd/bfd.frr.tmpl b/data/templates/frr-bfd/bfd.frr.tmpl new file mode 100644 index 000000000..7df4bfd01 --- /dev/null +++ b/data/templates/frr-bfd/bfd.frr.tmpl @@ -0,0 +1,16 @@ +! +bfd +{% for peer in old_peers -%} + no peer {{ peer.remote }}{% if peer.multihop %} multihop{% endif %}{% if peer.src_addr %} local-address {{ peer.src_addr }}{% endif %}{% if peer.src_if %} interface {{ peer.src_if }}{% endif %} +{% endfor -%} +! +{% for peer in new_peers -%} + peer {{ peer.remote }}{% if peer.multihop %} multihop{% endif %}{% if peer.src_addr %} local-address {{ peer.src_addr }}{% endif %}{% if peer.src_if %} interface {{ peer.src_if }}{% endif %} + detect-multiplier {{ peer.multiplier }} + receive-interval {{ peer.rx_interval }} + transmit-interval {{ peer.tx_interval }} + {% if peer.echo_mode %}echo-mode{% endif %} + {% if peer.echo_interval != '' %}echo-interval {{ peer.echo_interval }}{% endif %} + {% if not peer.shutdown %}no {% endif %}shutdown +{% endfor -%} +! diff --git a/data/templates/https/nginx.default.tmpl b/data/templates/https/nginx.default.tmpl new file mode 100644 index 000000000..f4f2c1848 --- /dev/null +++ b/data/templates/https/nginx.default.tmpl @@ -0,0 +1,69 @@ +### Autogenerated by https.py ### +# Default server configuration +# +server { + listen 80 default_server; + listen [::]:80 default_server; + server_name _; + return 301 https://$server_name$request_uri; +} + +{% for server in server_block_list %} +server { + + # SSL configuration + # +{% if server.address == '*' %} + listen {{ server.port }} ssl; + listen [::]:{{ server.port }} ssl; +{% else %} + listen {{ server.address }}:{{ server.port }} ssl; +{% endif %} + +{% for name in server.name %} + server_name {{ name }}; +{% endfor %} + +{% if server.certbot %} + ssl_certificate /etc/letsencrypt/live/{{ server.certbot_dir }}/fullchain.pem; + ssl_certificate_key /etc/letsencrypt/live/{{ server.certbot_dir }}/privkey.pem; + include /etc/letsencrypt/options-ssl-nginx.conf; + ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; +{% elif server.vyos_cert %} + include {{ server.vyos_cert.conf }}; +{% else %} + # + # Self signed certs generated by the ssl-cert package + # Don't use them in a production server! + # + include snippets/snakeoil.conf; +{% endif %} + + # proxy settings for HTTP API, if enabled; 503, if not + location ~ /(retrieve|configure|config-file|image|generate|show) { +{% if server.api %} + proxy_pass http://localhost:{{ server.api.port }}; + proxy_read_timeout 600; + proxy_buffering off; +{% else %} + return 503; +{% endif %} + } + + error_page 501 502 503 =200 @50*_json; + +{% if api_somewhere %} + location @50*_json { + default_type application/json; + return 200 '{"error": "service https api unavailable at this proxy address: set service https api-restrict virtual-host"}'; + } +{% else %} + location @50*_json { + default_type application/json; + return 200 '{"error": "Start service in configuration mode: set service https api"}'; + } +{% endif %} + +} + +{% endfor %} diff --git a/data/templates/igmp-proxy/igmpproxy.conf.tmpl b/data/templates/igmp-proxy/igmpproxy.conf.tmpl new file mode 100644 index 000000000..c7fc5cef5 --- /dev/null +++ b/data/templates/igmp-proxy/igmpproxy.conf.tmpl @@ -0,0 +1,37 @@ +######################################################## +# +# autogenerated by igmp_proxy.py +# +# The configuration file must define one upstream +# interface, and one or more downstream interfaces. +# +# If multicast traffic originates outside the +# upstream subnet, the "altnet" option can be +# used in order to define legal multicast sources. +# (Se example...) +# +# The "quickleave" should be used to avoid saturation +# of the upstream link. The option should only +# be used if it's absolutely nessecary to +# accurately imitate just one Client. +# +######################################################## + +{% if not disable_quickleave -%} +quickleave +{% endif -%} + +{% for interface in interfaces %} +# Configuration for {{ interface.name }} ({{ interface.role }} interface) +{% if interface.role == 'disabled' -%} +phyint {{ interface.name }} disabled +{%- else -%} +phyint {{ interface.name }} {{ interface.role }} ratelimit 0 threshold {{ interface.threshold }} +{%- endif -%} +{%- for subnet in interface.alt_subnet %} + altnet {{ subnet }} +{%- endfor %} +{%- for subnet in interface.whitelist %} + whitelist {{ subnet }} +{%- endfor %} +{% endfor %} diff --git a/data/templates/igmp/igmp.frr.tmpl b/data/templates/igmp/igmp.frr.tmpl new file mode 100644 index 000000000..de4696c1f --- /dev/null +++ b/data/templates/igmp/igmp.frr.tmpl @@ -0,0 +1,41 @@ +! +{% for iface in old_ifaces -%} +interface {{ iface }} +{% for group in old_ifaces[iface].gr_join -%} +{% if old_ifaces[iface].gr_join[group] -%} +{% for source in old_ifaces[iface].gr_join[group] -%} +no ip igmp join {{ group }} {{ source }} +{% endfor -%} +{% else -%} +no ip igmp join {{ group }} +{% endif -%} +{% endfor -%} +no ip igmp +! +{% endfor -%} +{% for iface in ifaces -%} +interface {{ iface }} +{% if ifaces[iface].version -%} +ip igmp version {{ ifaces[iface].version }} +{% else -%} +{# IGMP default version 3 #} +ip igmp +{% endif -%} +{% if ifaces[iface].query_interval -%} +ip igmp query-interval {{ ifaces[iface].query_interval }} +{% endif -%} +{% if ifaces[iface].query_max_resp_time -%} +ip igmp query-max-response-time {{ ifaces[iface].query_max_resp_time }} +{% endif -%} +{% for group in ifaces[iface].gr_join -%} +{% if ifaces[iface].gr_join[group] -%} +{% for source in ifaces[iface].gr_join[group] -%} +ip igmp join {{ group }} {{ source }} +{% endfor -%} +{% else -%} +ip igmp join {{ group }} +{% endif -%} +{% endfor -%} +! +{% endfor -%} +! diff --git a/data/templates/ipoe-server/chap-secrets.tmpl b/data/templates/ipoe-server/chap-secrets.tmpl new file mode 100644 index 000000000..707718e94 --- /dev/null +++ b/data/templates/ipoe-server/chap-secrets.tmpl @@ -0,0 +1,18 @@ +# username server password acceptable local IP addresses shaper +{% for aifc in auth['auth_if'] %} +{% for mac in auth['auth_if'][aifc] %} +{% if (auth['auth_if'][aifc][mac]['up']) and (auth['auth_if'][aifc][mac]['down']) %} +{% if auth['auth_if'][aifc][mac]['vlan'] %} +{{aifc}}.{{auth['auth_if'][aifc][mac]['vlan']}}\t*\t{{mac.lower()}}\t*\t{{auth['auth_if'][aifc][mac]['down']}}/{{auth['auth_if'][aifc][mac]['up']}} +{% else %} +{{aifc}}\t*\t{{mac.lower()}}\t*\t{{auth['auth_if'][aifc][mac]['down']}}/{{auth['auth_if'][aifc][mac]['up']}} +{% endif %} +{% else %} +{% if auth['auth_if'][aifc][mac]['vlan'] %} +{{aifc}}.{{auth['auth_if'][aifc][mac]['vlan']}}\t*\t{{mac.lower()}}\t* +{% else %} +{{aifc}}\t*\t{{mac.lower()}}\t* +{% endif %} +{% endif %} +{% endfor %} +{% endfor %} diff --git a/data/templates/ipoe-server/ipoe.config.tmpl b/data/templates/ipoe-server/ipoe.config.tmpl new file mode 100644 index 000000000..109bc0d92 --- /dev/null +++ b/data/templates/ipoe-server/ipoe.config.tmpl @@ -0,0 +1,123 @@ +### generated by ipoe.py ### +[modules] +log_syslog +ipoe +shaper +ipv6pool +ipv6_nd +ipv6_dhcp +{% if auth['mech'] == 'radius' %} +radius +{% endif -%} +ippool +{% if auth['mech'] == 'local' %} +chap-secrets +{% endif %} + +[core] +thread-count={{thread_cnt}} + +[log] +syslog=accel-ipoe,daemon +copy=1 +level=5 + +[ipoe] +verbose=1 +{% for intfc in interfaces %} +{% if interfaces[intfc]['vlan_mon'] %} +interface=re:{{intfc}}\.\d+,\ +{% else %} +interface={{intfc}},\ +{% endif %} +shared={{interfaces[intfc]['shared']}},\ +mode={{interfaces[intfc]['mode']}},\ +ifcfg={{interfaces[intfc]['ifcfg']}},\ +range={{interfaces[intfc]['range']}},\ +start={{interfaces[intfc]['sess_start']}},\ +ipv6=1 +{% endfor %} +{% if auth['mech'] == 'noauth' %} +noauth=1 +{% endif %} +{% if auth['mech'] == 'local' %} +username=ifname +password=csid +{% endif %} + +{%- for intfc in interfaces %} +{% if (interfaces[intfc]['shared'] == '0') and (interfaces[intfc]['vlan_mon']) %} +vlan-mon={{intfc}},{{interfaces[intfc]['vlan_mon']|join(',')}} +{% endif %} +{% endfor %} + +{% if (dns['server1']) or (dns['server2']) %} +[dns] +{% if dns['server1'] %} +dns1={{dns['server1']}} +{% endif -%} +{% if dns['server2'] %} +dns2={{dns['server2']}} +{% endif -%} +{% endif -%} + +{% if (dnsv6['server1']) or (dnsv6['server2']) or (dnsv6['server3']) %} +[dnsv6] +dns={{dnsv6['server1']}} +dns={{dnsv6['server2']}} +dns={{dnsv6['server3']}} +{% endif %} + +[ipv6-nd] +verbose=1 + +[ipv6-dhcp] +verbose=1 + +{% if ipv6['prfx'] %} +[ipv6-pool] +{% for prfx in ipv6['prfx'] %} +{{prfx}} +{% endfor %} +{% for pd in ipv6['pd'] %} +delegate={{pd}} +{% endfor %} +{% endif %} + +{% if auth['mech'] == 'local' %} +[chap-secrets] +chap-secrets=/etc/accel-ppp/ipoe/chap-secrets +{% endif %} + +{% if auth['mech'] == 'radius' %} +[radius] +verbose=1 +{% for srv in auth['radius'] %} +server={{srv}},{{auth['radius'][srv]['secret']}},\ +req-limit={{auth['radius'][srv]['req-limit']}},\ +fail-time={{auth['radius'][srv]['fail-time']}} +{% endfor %} +{% if auth['radsettings']['dae-server']['ip-address'] %} +dae-server={{auth['radsettings']['dae-server']['ip-address']}}:\ +{{auth['radsettings']['dae-server']['port']}},\ +{{auth['radsettings']['dae-server']['secret']}} +{% endif -%} +{% if auth['radsettings']['acct-timeout'] %} +acct-timeout={{auth['radsettings']['acct-timeout']}} +{% endif -%} +{% if auth['radsettings']['max-try'] %} +max-try={{auth['radsettings']['max-try']}} +{% endif -%} +{% if auth['radsettings']['timeout'] %} +timeout={{auth['radsettings']['timeout']}} +{% endif -%} +{% if auth['radsettings']['nas-ip-address'] %} +nas-ip-address={{auth['radsettings']['nas-ip-address']}} +{% endif -%} +{% if auth['radsettings']['nas-identifier'] %} +nas-identifier={{auth['radsettings']['nas-identifier']}} +{% endif -%} +{% endif %} + +[cli] +tcp=127.0.0.1:2002 diff --git a/data/templates/ipsec/ipsec.conf.tmpl b/data/templates/ipsec/ipsec.conf.tmpl new file mode 100644 index 000000000..d0b60765b --- /dev/null +++ b/data/templates/ipsec/ipsec.conf.tmpl @@ -0,0 +1,3 @@ +{{delim_ipsec_l2tp_begin}} +include {{ipsec_ra_conn_file}} +{{delim_ipsec_l2tp_end}} diff --git a/data/templates/ipsec/ipsec.secrets.tmpl b/data/templates/ipsec/ipsec.secrets.tmpl new file mode 100644 index 000000000..55c010a3b --- /dev/null +++ b/data/templates/ipsec/ipsec.secrets.tmpl @@ -0,0 +1,7 @@ +{{delim_ipsec_l2tp_begin}} +{% if ipsec_l2tp_auth_mode == 'pre-shared-secret' %} +{{outside_addr}} %any : PSK "{{ipsec_l2tp_secret}}" +{% elif ipsec_l2tp_auth_mode == 'x509' %} +: RSA {{server_key_file_copied}} +{% endif%} +{{delim_ipsec_l2tp_end}} diff --git a/data/templates/ipsec/remote-access.tmpl b/data/templates/ipsec/remote-access.tmpl new file mode 100644 index 000000000..fae48232f --- /dev/null +++ b/data/templates/ipsec/remote-access.tmpl @@ -0,0 +1,28 @@ +{{delim_ipsec_l2tp_begin}} +conn {{ra_conn_name}} + type=transport + left={{outside_addr}} + leftsubnet=%dynamic[/1701] + rightsubnet=%dynamic + mark_in=%unique + auto=add + ike=aes256-sha1-modp1024,3des-sha1-modp1024,3des-sha1-modp1024! + dpddelay=15 + dpdtimeout=45 + dpdaction=clear + esp=aes256-sha1,3des-sha1! + rekey=no +{% if ipsec_l2tp_auth_mode == 'pre-shared-secret' %} + authby=secret + leftauth=psk + rightauth=psk +{% elif ipsec_l2tp_auth_mode == 'x509' %} + authby=rsasig + leftrsasigkey=%cert + rightrsasigkey=%cert + rightca=%same + leftcert={{server_cert_file_copied}} +{% endif %} + ikelifetime={{ipsec_l2tp_ike_lifetime}} + keylife={{ipsec_l2tp_lifetime}} +{{delim_ipsec_l2tp_end}} diff --git a/data/templates/l2tp/chap-secrets.tmpl b/data/templates/l2tp/chap-secrets.tmpl new file mode 100644 index 000000000..0db295fdc --- /dev/null +++ b/data/templates/l2tp/chap-secrets.tmpl @@ -0,0 +1,10 @@ +# username server password acceptable local IP addresses shaper +{% for user in authentication['local-users'] %} +{% if authentication['local-users'][user]['state'] == 'enabled' %} +{% if authentication['local-users'][user]['upload'] and authentication['local-users'][user]['download'] %} +{{ "%-12s" | format(user) }} * {{ "%-16s" | format(authentication['local-users'][user]['passwd']) }} {{ "%-16s" | format(authentication['local-users'][user]['ip']) }} {{ authentication['local-users'][user]['download'] }} / {{ authentication['local-users'][user]['upload'] }} +{% else %} +{{ "%-12s" | format(user) }} * {{ "%-16s" | format(authentication['local-users'][user]['passwd']) }} {{ "%-16s" | format(authentication['local-users'][user]['ip']) }} +{% endif %} +{% endif %} +{% endfor %} diff --git a/data/templates/l2tp/l2tp.config.tmpl b/data/templates/l2tp/l2tp.config.tmpl new file mode 100644 index 000000000..b8637e256 --- /dev/null +++ b/data/templates/l2tp/l2tp.config.tmpl @@ -0,0 +1,173 @@ +### generated by accel_l2tp.py ### +[modules] +log_syslog +l2tp +chap-secrets +{% for proto in authentication['auth_proto']: %} +{{proto}} +{% endfor%} +{% if authentication['mode'] == 'radius' %} +radius +{% endif -%} +ippool +shaper +ipv6pool +ipv6_nd +ipv6_dhcp + +[core] +thread-count={{thread_cnt}} + +[log] +syslog=accel-l2tp,daemon +copy=1 +level=5 + +{% if dns %} +[dns] +{% if dns[0] %} +dns1={{dns[0]}} +{% endif %} +{% if dns[1] %} +dns2={{dns[1]}} +{% endif %} +{% endif -%} + +{% if dnsv6 %} +[ipv6-dns] +{% for srv in dnsv6: %} +{{srv}} +{% endfor %} +{% endif %} + +{% if wins %} +[wins] +{% if wins[0] %} +wins1={{wins[0]}} +{% endif %} +{% if wins[1] %} +wins2={{wins[1]}} +{% endif %} +{% endif -%} + +[l2tp] +verbose=1 +ifname=l2tp%d +ppp-max-mtu={{mtu}} +mppe={{authentication['mppe']}} +{% if outside_addr %} +bind={{outside_addr}} +{% endif %} +{% if lns_shared_secret %} +secret={{lns_shared_secret}} +{% endif %} + +[client-ip-range] +0.0.0.0/0 + +{% if (client_ip_pool) or (client_ip_subnets) %} +[ip-pool] +{% if client_ip_pool %} +{{client_ip_pool}} +{% endif -%} +{% if client_ip_subnets %} +{% for sn in client_ip_subnets %} +{{sn}} +{% endfor -%} +{% endif %} +{% endif %} +{% if gateway_address %} +gw-ip-address={{gateway_address}} +{% endif %} + +{% if authentication['mode'] == 'local' %} +[chap-secrets] +chap-secrets=/etc/accel-ppp/l2tp/chap-secrets +{% if gateway_address %} +gw-ip-address={{gateway_address}} +{% endif %} +{% endif %} + +[ppp] +verbose=1 +check-ip=1 +single-session=replace +{% if idle_timeout %} +lcp-echo-timeout={{idle_timeout}} +{% endif %} +{% if ppp_options['lcp-echo-interval'] %} +lcp-echo-interval={{ppp_options['lcp-echo-interval']}} +{% else %} +lcp-echo-interval=30 +{% endif %} +{% if ppp_options['lcp-echo-failure'] %} +lcp-echo-failure={{ppp_options['lcp-echo-failure']}} +{% else %} +lcp-echo-failure=3 +{% endif %} +{% if ccp_disable %} +ccp=0 +{% endif %} +{% if client_ipv6_pool %} +ipv6=allow +{% endif %} + +{% if authentication['mode'] == 'radius' %} +[radius] +{% for rsrv in authentication['radiussrv']: %} +server={{rsrv}},{{authentication['radiussrv'][rsrv]['secret']}},\ +req-limit={{authentication['radiussrv'][rsrv]['req-limit']}},\ +fail-time={{authentication['radiussrv'][rsrv]['fail-time']}} +{% endfor %} +{% if authentication['radiusopt']['timeout'] %} +timeout={{authentication['radiusopt']['timeout']}} +{% endif %} +{% if authentication['radiusopt']['acct-timeout'] %} +acct-timeout={{authentication['radiusopt']['acct-timeout']}} +{% endif %} +{% if authentication['radiusopt']['max-try'] %} +max-try={{authentication['radiusopt']['max-try']}} +{% endif %} +{% if authentication['radiusopt']['nas-id'] %} +nas-identifier={{authentication['radiusopt']['nas-id']}} +{% endif %} +{% if authentication['radius_source_address'] %} +nas-ip-address={{authentication['radius_source_address']}} +{% endif -%} +{% if authentication['radiusopt']['dae-srv'] %} +dae-server={{authentication['radiusopt']['dae-srv']['ip-addr']}}:\ +{{authentication['radiusopt']['dae-srv']['port']}},\ +{{authentication['radiusopt']['dae-srv']['secret']}} +{% endif -%} +gw-ip-address={{gateway_address}} +verbose=1 +{% endif -%} + +{% if client_ipv6_pool %} +[ipv6-pool] +{% for prfx in client_ipv6_pool.prefix: %} +{{prfx}} +{% endfor %} +{% for prfx in client_ipv6_pool.delegate_prefix: %} +delegate={{prfx}} +{% endfor %} +{% endif %} + +{% if client_ipv6_pool['delegate_prefix'] %} +[ipv6-dhcp] +verbose=1 +{% endif %} + +{% if authentication['radiusopt']['shaper'] %} +[shaper] +verbose=1 +attr={{authentication['radiusopt']['shaper']['attr']}} +{% if authentication['radiusopt']['shaper']['vendor'] %} +vendor={{authentication['radiusopt']['shaper']['vendor']}} +{% endif -%} +{% endif %} + +[cli] +tcp=127.0.0.1:2004 +sessions-columns=ifname,username,calling-sid,ip,{{ip6_column}}{{ip6_dp_column}}rate-limit,type,comp,state,rx-bytes,tx-bytes,uptime + diff --git a/data/templates/lldp/lldpd.tmpl b/data/templates/lldp/lldpd.tmpl new file mode 100644 index 000000000..3db955b48 --- /dev/null +++ b/data/templates/lldp/lldpd.tmpl @@ -0,0 +1,3 @@ +### Autogenerated by lldp.py ### +DAEMON_ARGS="-M 4{% if options.snmp %} -x{% endif %}{% if options.cdp %} -c{% endif %}{% if options.edp %} -e{% endif %}{% if options.fdp %} -f{% endif %}{% if options.sonmp %} -s{% endif %}" + diff --git a/data/templates/lldp/vyos.conf.tmpl b/data/templates/lldp/vyos.conf.tmpl new file mode 100644 index 000000000..e724f42c6 --- /dev/null +++ b/data/templates/lldp/vyos.conf.tmpl @@ -0,0 +1,20 @@ +### Autogenerated by lldp.py ### + +configure system platform VyOS +configure system description "VyOS {{ options.description }}" +{% if options.listen_on -%} +configure system interface pattern "{{ ( options.listen_on | select('equalto','all') | map('replace','all','*') | list + options.listen_on | select('equalto','!all') | map('replace','!all','!*') | list + options.listen_on | reject('equalto','all') | reject('equalto','!all') | list ) | unique | join(",") }}" +{%- endif %} +{% if options.mgmt_addr -%} +configure system ip management pattern {{ options.mgmt_addr | join(",") }} +{%- endif %} +{%- for loc in location -%} +{%- if loc.elin %} +configure ports {{ loc.name }} med location elin "{{ loc.elin }}" +{%- endif %} +{%- if loc.coordinate_based %} +configure ports {{ loc.name }} med location coordinate {% if loc.coordinate_based.latitude %}latitude {{ loc.coordinate_based.latitude }}{% endif %} {% if loc.coordinate_based.longitude %}longitude {{ loc.coordinate_based.longitude }}{% endif %} {% if loc.coordinate_based.altitude %}altitude {{ loc.coordinate_based.altitude }} m{% endif %} {% if loc.coordinate_based.datum %}datum {{ loc.coordinate_based.datum }}{% endif %} +{%- endif %} + + +{% endfor %} diff --git a/data/templates/mdns-repeater/mdns-repeater.tmpl b/data/templates/mdns-repeater/mdns-repeater.tmpl new file mode 100644 index 000000000..3fc4db67e --- /dev/null +++ b/data/templates/mdns-repeater/mdns-repeater.tmpl @@ -0,0 +1,2 @@ +### Autogenerated by mdns_repeater.py ### +DAEMON_ARGS="{{ interfaces | join(' ') }}" diff --git a/data/templates/mpls/ldpd.frr.tmpl b/data/templates/mpls/ldpd.frr.tmpl new file mode 100644 index 000000000..bbff88ae5 --- /dev/null +++ b/data/templates/mpls/ldpd.frr.tmpl @@ -0,0 +1,58 @@ +! +{% if mpls_ldp -%} +mpls ldp +{% if old_router_id -%} +no router-id {{ old_router_id }} +{% endif -%} +{% if router_id -%} +router-id {{ router_id }} +{% endif -%} +{% for neighbor_id in old_ldp.neighbors -%} +no neighbor {{neighbor_id}} password {{old_ldp.neighbors[neighbor_id].password}} +{% endfor -%} +{% for neighbor_id in ldp.neighbors -%} +neighbor {{neighbor_id}} password {{ldp.neighbors[neighbor_id].password}} +{% endfor -%} +address-family ipv4 +label local allocate host-routes +{% if old_ldp.d_transp_ipv4 -%} +no discovery transport-address {{ old_ldp.d_transp_ipv4 }} +{% endif -%} +{% if ldp.d_transp_ipv4 -%} +discovery transport-address {{ ldp.d_transp_ipv4 }} +{% endif -%} +{% for interface in old_ldp.interfaces -%} +no interface {{interface}} +{% endfor -%} +{% for interface in ldp.interfaces -%} +interface {{interface}} +{% endfor -%} +! +! +exit-address-family +! +{% if ldp.d_transp_ipv6 -%} +address-family ipv6 +label local allocate host-routes +{% if old_ldp.d_transp_ipv6 -%} +no discovery transport-address {{ old_ldp.d_transp_ipv6 }} +{% endif -%} +{% if ldp.d_transp_ipv6 -%} +discovery transport-address {{ ldp.d_transp_ipv6 }} +{% endif -%} +{% for interface in old_ldp.interfaces -%} +no interface {{interface}} +{% endfor -%} +{% for interface in ldp.interfaces -%} +interface {{interface}} +{% endfor -%} +! +exit-address-family +{% else -%} +no address-family ipv6 +{% endif -%} +! +{% else -%} +no mpls ldp +{% endif -%} +! diff --git a/data/templates/netflow/uacctd.conf.tmpl b/data/templates/netflow/uacctd.conf.tmpl new file mode 100644 index 000000000..d8615566f --- /dev/null +++ b/data/templates/netflow/uacctd.conf.tmpl @@ -0,0 +1,69 @@ +# Genereated from VyOS configuration +daemonize: true +promisc: false +pidfile: /var/run/uacctd.pid +uacctd_group: 2 +uacctd_nl_size: 2097152 +snaplen: {{ snaplen }} +aggregate: in_iface,src_mac,dst_mac,vlan,src_host,dst_host,src_port,dst_port,proto,tos,flows +plugin_pipe_size: {{ templatecfg['plugin_pipe_size'] }} +plugin_buffer_size: {{ templatecfg['plugin_buffer_size'] }} +{%- if templatecfg['syslog-facility'] != none %} +syslog: {{ templatecfg['syslog-facility'] }} +{%- endif %} +{%- if templatecfg['disable-imt'] == none %} +imt_path: /tmp/uacctd.pipe +imt_mem_pools_number: 169 +{%- endif %} +plugins: +{%- if templatecfg['netflow']['servers'] != none -%} + {% for server in templatecfg['netflow']['servers'] %} + {%- if loop.last -%}nfprobe[nf_{{ server['address'] }}]{%- else %}nfprobe[nf_{{ server['address'] }}],{%- endif %} + {%- endfor -%} + {% set plugins_presented = true %} +{%- endif %} +{%- if templatecfg['sflow']['servers'] != none -%} + {% if plugins_presented -%} + {%- for server in templatecfg['sflow']['servers'] -%} + ,sfprobe[sf_{{ server['address'] }}] + {%- endfor %} + {%- else %} + {%- for server in templatecfg['sflow']['servers'] %} + {%- if loop.last -%}sfprobe[sf_{{ server['address'] }}]{%- else %}sfprobe[sf_{{ server['address'] }}],{%- endif %} + {%- endfor %} + {%- endif -%} + {% set plugins_presented = true %} +{%- endif %} +{%- if templatecfg['disable-imt'] == none %} + {%- if plugins_presented -%},memory{%- else %}memory{%- endif %} +{%- endif %} +{%- if templatecfg['netflow']['servers'] != none %} +{%- for server in templatecfg['netflow']['servers'] %} +nfprobe_receiver[nf_{{ server['address'] }}]: {{ server['address'] }}:{{ server['port'] }} +nfprobe_version[nf_{{ server['address'] }}]: {{ templatecfg['netflow']['version'] }} +{%- if templatecfg['netflow']['engine-id'] != none %} +nfprobe_engine[nf_{{ server['address'] }}]: {{ templatecfg['netflow']['engine-id'] }} +{%- endif %} +{%- if templatecfg['netflow']['max-flows'] != none %} +nfprobe_maxflows[nf_{{ server['address'] }}]: {{ templatecfg['netflow']['max-flows'] }} +{%- endif %} +{%- if templatecfg['netflow']['sampling-rate'] != none %} +sampling_rate[nf_{{ server['address'] }}]: {{ templatecfg['netflow']['sampling-rate'] }} +{%- endif %} +{%- if templatecfg['netflow']['source-ip'] != none %} +nfprobe_source_ip[nf_{{ server['address'] }}]: {{ templatecfg['netflow']['source-ip'] }} +{%- endif %} +{%- if templatecfg['netflow']['timeout_string'] != '' %} +nfprobe_timeouts[nf_{{ server['address'] }}]: {{ templatecfg['netflow']['timeout_string'] }} +{%- endif %} +{%- endfor %} +{%- endif %} +{%- if templatecfg['sflow']['servers'] != none %} +{%- for server in templatecfg['sflow']['servers'] %} +sfprobe_receiver[sf_{{ server['address'] }}]: {{ server['address'] }}:{{ server['port'] }} +sfprobe_agentip[sf_{{ server['address'] }}]: {{ templatecfg['sflow']['agent-address'] }} +{%- if templatecfg['sflow']['sampling-rate'] != none %} +sampling_rate[sf_{{ server['address'] }}]: {{ templatecfg['sflow']['sampling-rate'] }} +{%- endif %} +{%- endfor %} +{% endif %} diff --git a/data/templates/ntp/ntp.conf.tmpl b/data/templates/ntp/ntp.conf.tmpl new file mode 100644 index 000000000..52042d218 --- /dev/null +++ b/data/templates/ntp/ntp.conf.tmpl @@ -0,0 +1,38 @@ +### Autogenerated by ntp.py ### + +# +# Non-configurable defaults +# +driftfile /var/lib/ntp/ntp.drift +# By default, only allow ntpd to query time sources, ignore any incoming requests +restrict default noquery nopeer notrap nomodify +# Local users have unrestricted access, allowing reconfiguration via ntpdc +restrict 127.0.0.1 +restrict -6 ::1 + +# +# Configurable section +# + +{% if servers -%} +{% for s in servers -%} +# Server configuration for: {{ s.name }} +server {{ s.name }} iburst {{ s.options | join(" ") }} +{% endfor -%} +{% endif %} + +{% if allowed_networks -%} +{% for n in allowed_networks -%} +# Client configuration for network: {{ n.network }} +restrict {{ n.address }} mask {{ n.netmask }} nomodify notrap nopeer + +{% endfor -%} +{% endif %} + +{% if listen_address -%} +# NTP should listen on configured addresses only +interface ignore wildcard +{% for a in listen_address -%} +interface listen {{ a }} +{% endfor -%} +{% endif %} diff --git a/data/templates/openvpn/client.conf.tmpl b/data/templates/openvpn/client.conf.tmpl new file mode 100644 index 000000000..3099f2ca7 --- /dev/null +++ b/data/templates/openvpn/client.conf.tmpl @@ -0,0 +1,16 @@ +### Autogenerated by interfaces-openvpn.py ### + +{% if ip -%} +ifconfig-push {{ ip }} {{ remote_netmask }} +{% endif -%} +{% for route in push_route -%} +push "route {{ route }}" +{% endfor -%} + +{% for net in subnet -%} +iroute {{ net }} +{% endfor -%} + +{% if disable -%} +disable +{% endif -%} diff --git a/data/templates/openvpn/server.conf.tmpl b/data/templates/openvpn/server.conf.tmpl new file mode 100644 index 000000000..d0af3d2e3 --- /dev/null +++ b/data/templates/openvpn/server.conf.tmpl @@ -0,0 +1,236 @@ +### Autogenerated by interfaces-openvpn.py ### +# +# See https://community.openvpn.net/openvpn/wiki/Openvpn24ManPage +# for individual keyword definition + +{% if description -%} +# {{ description }} + +{% endif -%} + +verb 3 +status /opt/vyatta/etc/openvpn/status/{{ intf }}.status 30 +writepid /var/run/openvpn/{{ intf }}.pid + +user {{ uid }} +group {{ gid }} + +dev-type {{ type }} +dev {{ intf }} +persist-key +iproute /usr/libexec/vyos/system/unpriv-ip + +proto {% if 'tcp-active' in protocol -%}tcp-client{% elif 'tcp-passive' in protocol -%}tcp-server{% else %}udp{% endif %} + +{%- if local_host %} +local {{ local_host }} +{%- endif %} + +{%- if mode == 'server' and protocol == 'udp' and not local_host %} +multihome +{%- endif %} + +{%- if local_port %} +lport {{ local_port }} +{%- endif %} + +{% if remote_port -%} +rport {{ remote_port }} +{% endif %} + +{%- if remote_host %} +{%- for remote in remote_host -%} +remote {{ remote }} +{% endfor -%} +{% endif -%} + +{% if shared_secret_file %} +secret {{ shared_secret_file }} +{%- endif %} + +{%- if persistent_tunnel %} +persist-tun +{%- endif %} + +{%- if redirect_gateway %} +push "redirect-gateway {{ redirect_gateway }}" +{%- endif %} + +{%- if compress_lzo %} +compress lzo +{%- endif %} + +{% if 'client' in mode -%} +# +# OpenVPN Client mode +# +client +nobind + +{% elif 'server' in mode -%} +# +# OpenVPN Server mode +# + +{%- if server_topology %} +topology {% if 'point-to-point' in server_topology %}p2p{% else %}subnet{% endif %} +{%- endif %} + +{%- if bridge_member %} +server-bridge nogw +{%- else %} +server {{ server_subnet }} +{%- endif %} + +{%- if server_max_conn %} +max-clients {{ server_max_conn }} +{%- endif %} + +{%- if client %} +client-config-dir /opt/vyatta/etc/openvpn/ccd/{{ intf }} +{%- endif %} + +{%- if server_reject_unconfigured %} +ccd-exclusive +{%- endif %} + +keepalive {{ ping_interval }} {{ ping_restart }} +management /tmp/openvpn-mgmt-intf unix + +{% for route in server_push_route -%} +push "route {{ route }}" +{% endfor -%} + +{% for ns in server_dns_nameserver -%} +push "dhcp-option DNS {{ ns }}" +{% endfor -%} + +{%- if server_domain -%} +push "dhcp-option DOMAIN {{ server_domain }}" +{% endif %} + +{% else -%} +# +# OpenVPN site-2-site mode +# +ping {{ ping_interval }} +ping-restart {{ ping_restart }} + +{% if local_address_subnet -%} +ifconfig {{ local_address }} {{ local_address_subnet }} +{%- elif remote_address -%} +ifconfig {{ local_address }} {{ remote_address }} +{%- endif %} + +{% endif -%} + +{% if tls -%} +# TLS options +{%- if tls_ca_cert %} +ca {{ tls_ca_cert }} +{%- endif %} + +{%- if tls_cert %} +cert {{ tls_cert }} +{%- endif %} + +{%- if tls_key %} +key {{ tls_key }} +{%- endif %} + +{%- if tls_crypt %} +tls-crypt {{ tls_crypt }} +{%- endif %} + +{%- if tls_crl %} +crl-verify {{ tls_crl }} +{%- endif %} + +{%- if tls_version_min %} +tls-version-min {{tls_version_min}} +{%- endif %} + +{%- if tls_dh %} +dh {{ tls_dh }} +{%- endif %} + +{%- if tls_auth %} +tls-auth {{tls_auth}} +{%- endif %} + +{%- if tls_role %} +{%- if 'active' in tls_role %} +tls-client +{%- elif 'passive' in tls_role %} +tls-server +{%- endif %} +{%- endif %} + +{%- endif %} + +# Encryption options +{%- if encryption %} +{% if encryption == 'des' -%} +cipher des-cbc +{%- elif encryption == '3des' -%} +cipher des-ede3-cbc +{%- elif encryption == 'bf128' -%} +cipher bf-cbc +keysize 128 +{%- elif encryption == 'bf256' -%} +cipher bf-cbc +keysize 25 +{%- elif encryption == 'aes128gcm' -%} +cipher aes-128-gcm +{%- elif encryption == 'aes128' -%} +cipher aes-128-cbc +{%- elif encryption == 'aes192gcm' -%} +cipher aes-192-gcm +{%- elif encryption == 'aes192' -%} +cipher aes-192-cbc +{%- elif encryption == 'aes256gcm' -%} +cipher aes-256-gcm +{%- elif encryption == 'aes256' -%} +cipher aes-256-cbc +{%- endif -%} +{%- endif %} + +{%- if ncp_ciphers %} +ncp-ciphers {{ncp_ciphers}} +{%- endif %} +{%- if disable_ncp %} +ncp-disable +{%- endif %} + +{% if hash -%} +auth {{ hash }} +{%- endif -%} + +{%- if auth %} +auth-user-pass /tmp/openvpn-{{ intf }}-pw +auth-retry nointeract +{%- endif %} + +# DEPRECATED This option will be removed in OpenVPN 2.5 +# Until OpenVPN v2.3 the format of the X.509 Subject fields was formatted like this: +# /C=US/L=Somewhere/CN=John Doe/emailAddress=john@example.com In addition the old +# behaviour was to remap any character other than alphanumeric, underscore ('_'), +# dash ('-'), dot ('.'), and slash ('/') to underscore ('_'). The X.509 Subject +# string as returned by the tls_id environmental variable, could additionally +# contain colon (':') or equal ('='). When using the --compat-names option, this +# old formatting and remapping will be re-enabled again. This is purely implemented +# for compatibility reasons when using older plug-ins or scripts which does not +# handle the new formatting or UTF-8 characters. +# +# See https://phabricator.vyos.net/T1512 +compat-names + +{% if options -%} +# +# Custom options added by user (not validated) +# + +{% for option in options -%} +{{ option }} +{% endfor -%} +{%- endif %} diff --git a/data/templates/pim/pimd.frr.tmpl b/data/templates/pim/pimd.frr.tmpl new file mode 100644 index 000000000..1d1532c60 --- /dev/null +++ b/data/templates/pim/pimd.frr.tmpl @@ -0,0 +1,34 @@ +! +{% for rp_addr in old_pim.rp -%} +{% for group in old_pim.rp[rp_addr] -%} +no ip pim rp {{ rp_addr }} {{ group }} +{% endfor -%} +{% endfor -%} +{% if old_pim.rp_keep_alive -%} +no ip pim rp keep-alive-timer {{ old_pim.rp_keep_alive }} +{% endif -%} +{% for iface in old_pim.ifaces -%} +interface {{ iface }} +no ip pim +! +{% endfor -%} +{% for iface in pim.ifaces -%} +interface {{ iface }} +ip pim +{% if pim.ifaces[iface].dr_prio -%} +ip pim drpriority {{ pim.ifaces[iface].dr_prio }} +{% endif -%} +{% if pim.ifaces[iface].hello -%} +ip pim hello {{ pim.ifaces[iface].hello }} +{% endif -%} +! +{% endfor -%} +{% for rp_addr in pim.rp -%} +{% for group in pim.rp[rp_addr] -%} +ip pim rp {{ rp_addr }} {{ group }} +{% endfor -%} +{% endfor -%} +{% if pim.rp_keep_alive -%} +ip pim rp keep-alive-timer {{ pim.rp_keep_alive }} +{% endif -%} +! diff --git a/data/templates/pppoe-server/chap-secrets.tmpl b/data/templates/pppoe-server/chap-secrets.tmpl new file mode 100644 index 000000000..907ac6ed7 --- /dev/null +++ b/data/templates/pppoe-server/chap-secrets.tmpl @@ -0,0 +1,10 @@ +# username server password acceptable local IP addresses shaper +{% for user in authentication['local-users'] %} +{% if authentication['local-users'][user]['state'] == 'enabled' %} +{% if (authentication['local-users'][user]['upload']) and (authentication['local-users'][user]['download']) %} +{{ "%-12s" | format(user) }} * {{ "%-16s" | format(authentication['local-users'][user]['passwd']) }} {{ "%-16s" | format(authentication['local-users'][user]['ip']) }} {{ authentication['local-users'][user]['download'] }} / {{ authentication['local-users'][user]['upload'] }} +{% else %} +{{ "%-12s" | format(user) }} * {{ "%-16s" | format(authentication['local-users'][user]['passwd']) }} {{ "%-16s" | format(authentication['local-users'][user]['ip']) }} +{% endif %} +{% endif %} +{% endfor %} diff --git a/data/templates/pppoe-server/pppoe.config.tmpl b/data/templates/pppoe-server/pppoe.config.tmpl new file mode 100644 index 000000000..d44c0aa93 --- /dev/null +++ b/data/templates/pppoe-server/pppoe.config.tmpl @@ -0,0 +1,228 @@ + +### generated by accel_pppoe.py ### +[modules] +log_syslog +pppoe +{% if authentication['mode'] == 'radius' %} +radius +{% endif %} +ippool +{% if ppp_options['ipv6'] != 'deny' %} +ipv6pool +ipv6_nd +ipv6_dhcp +{% endif %} +chap-secrets +auth_pap +auth_chap_md5 +auth_mschap_v1 +auth_mschap_v2 +#pppd_compat +shaper +{% if snmp == 'enable' or snmp == 'enable-ma' %} +net-snmp +{% endif %} +{% if limits %} +connlimit +{% endif %} + +[core] +thread-count={{thread_cnt}} + +[log] +syslog=accel-pppoe,daemon +copy=1 +level=5 + +{% if snmp == 'enable-ma' %} +[snmp] +master=1 +{% endif -%} + +[client-ip-range] +disable + +{% if ppp_gw %} +[ip-pool] +gw-ip-address={{ppp_gw}} +{% if client_ip_pool %} +{{client_ip_pool}} +{% endif -%} + +{% if client_ip_subnets %} +{% for sn in client_ip_subnets %} +{{sn}} +{% endfor %} +{% endif %} +{% endif -%} + +{% if client_ipv6_pool %} +[ipv6-pool] +{% for prfx in client_ipv6_pool['prefix']: %} +{{prfx}} +{% endfor %} +{% for prfx in client_ipv6_pool['delegate-prefix']: %} +delegate={{prfx}} +{% endfor %} +{% endif %} + +{% if dns %} +[dns] +{% if dns[0] %} +dns1={{dns[0]}} +{% endif -%} +{% if dns[1] %} +dns2={{dns[1]}} +{% endif -%} +{% endif %} + +{% if dnsv6 %} +[ipv6-dns] +{% for srv in dnsv6: %} +{{srv}} +{% endfor %} +{% endif %} + +{% if wins %} +[wins] +{% if wins[0] %} +wins1={{wins[0]}} +{% endif %} +{% if wins[1] %} +wins2={{wins[1]}} +{% endif -%} +{% endif -%} + +{% if authentication['mode'] == 'local' %} +[chap-secrets] +chap-secrets=/etc/accel-ppp/pppoe/chap-secrets +{% endif -%} + +{% if authentication['mode'] == 'radius' %} +[radius] +{% for rsrv in authentication['radiussrv']: %} +server={{rsrv}},{{authentication['radiussrv'][rsrv]['secret']}},\ +req-limit={{authentication['radiussrv'][rsrv]['req-limit']}},\ +fail-time={{authentication['radiussrv'][rsrv]['fail-time']}} +{% endfor %} +{% if authentication['radiusopt']['timeout'] %} +timeout={{authentication['radiusopt']['timeout']}} +{% endif %} +{% if authentication['radiusopt']['acct-timeout'] %} +acct-timeout={{authentication['radiusopt']['acct-timeout']}} +{% endif %} +{% if authentication['radiusopt']['max-try'] %} +max-try={{authentication['radiusopt']['max-try']}} +{% endif %} +{% if authentication['radiusopt']['nas-id'] %} +nas-identifier={{authentication['radiusopt']['nas-id']}} +{% endif %} +{% if authentication['radiusopt']['nas-ip'] %} +nas-ip-address={{authentication['radiusopt']['nas-ip']}} +{% endif -%} +{% if authentication['radiusopt']['dae-srv'] %} +dae-server={{authentication['radiusopt']['dae-srv']['ip-addr']}}:\ +{{authentication['radiusopt']['dae-srv']['port']}},\ +{{authentication['radiusopt']['dae-srv']['secret']}} +{% endif -%} +gw-ip-address={{ppp_gw}} +verbose=1 + +{% if authentication['radiusopt']['shaper'] %} +[shaper] +verbose=1 +attr={{authentication['radiusopt']['shaper']['attr']}} +{% if authentication['radiusopt']['shaper']['vendor'] %} +vendor={{authentication['radiusopt']['shaper']['vendor']}} +{% endif -%} +{% endif -%} +{% endif %} + +[ppp] +verbose=1 +check-ip=1 +{% if not sesscrtl == 'disable' %} +single-session={{sesscrtl}} +{% endif -%} +{% if ppp_options['ccp'] %} +ccp=1 +{% endif %} +{% if ppp_options['min-mtu'] %} +min-mtu={{ppp_options['min-mtu']}} +{% else %} +min-mtu={{mtu}} +{% endif %} +{% if ppp_options['mru'] %} +mru={{ppp_options['mru']}} +{% endif %} +{% if ppp_options['mppe'] %} +mppe={{ppp_options['mppe']}} +{% else %} +mppe=prefer +{% endif %} +{% if ppp_options['lcp-echo-interval'] %} +lcp-echo-interval={{ppp_options['lcp-echo-interval']}} +{% else %} +lcp-echo-interval=30 +{% endif %} +{% if ppp_options['lcp-echo-timeout'] %} +lcp-echo-timeout={{ppp_options['lcp-echo-timeout']}} +{% endif %} +{% if ppp_options['lcp-echo-failure'] %} +lcp-echo-failure={{ppp_options['lcp-echo-failure']}} +{% else %} +lcp-echo-failure=3 +{% endif %} +{% if ppp_options['ipv4'] %} +ipv4={{ppp_options['ipv4']}} +{% endif %} +{% if client_ipv6_pool %} +ipv6=allow +{% endif %} + +{% if ppp_options['ipv6'] %} +ipv6={{ppp_options['ipv6']}} +{% if ppp_options['ipv6-intf-id'] %} +ipv6-intf-id={{ppp_options['ipv6-intf-id']}} +{% endif %} +{% if ppp_options['ipv6-peer-intf-id'] %} +ipv6-peer-intf-id={{ppp_options['ipv6-peer-intf-id']}} +{% endif %} +{% if ppp_options['ipv6-accept-peer-intf-id'] %} +ipv6-accept-peer-intf-id={{ppp_options['ipv6-accept-peer-intf-id']}} +{% endif %} +{% endif %} +mtu={{mtu}} + +[pppoe] +verbose=1 +{% if concentrator %} +ac-name={{concentrator}} +{% endif %} +{% if interface %} +{% for int in interface %} +interface={{int}} +{% if interface[int]['vlans'] %} +vlan-mon={{int}},{{interface[int]['vlans']|join(',')}} +interface=re:{{int}}\.\d+ +{% endif %} +{% endfor -%} +{% endif -%} + +{% if svc_name %} +service-name={{svc_name|join(',')}} +{% endif -%} + +{% if pado_delay %} +pado-delay={{pado_delay}} +{% endif %} + +{% if limits %} +[connlimit] +limit={{limits['conn-limit']}} +burst={{limits['burst']}} +timeout={{limits['timeout']}} +{% endif %} + +[cli] +tcp=127.0.0.1:2001 diff --git a/data/templates/pppoe/ip-down.script.tmpl b/data/templates/pppoe/ip-down.script.tmpl new file mode 100644 index 000000000..a68fc099c --- /dev/null +++ b/data/templates/pppoe/ip-down.script.tmpl @@ -0,0 +1,28 @@ +#!/bin/sh + +# As PPPoE is an "on demand" interface we need to re-configure it when it +# becomes up +if [ "$6" != "{{ intf }}" ]; then + exit +fi + +# add some info to syslog +DIALER_PID=$(cat /var/run/{{ intf }}.pid) +logger -t pppd[$DIALER_PID] "executing $0" + +{% if not on_demand %} +# See https://phabricator.vyos.net/T2248. Determine if we are enslaved to a +# VRF, this is needed to properly insert the default route. +VRF_NAME="" +if [ -d /sys/class/net/{{ intf }}/upper_* ]; then + # Determine upper (VRF) interface + VRF=$(basename $(ls -d /sys/class/net/{{ intf }}/upper_*)) + # Remove upper_ prefix from result string + VRF=${VRF#"upper_"} + # Populate variable to run in VR context + VRF_NAME="vrf ${VRF_NAME}" +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 %} diff --git a/data/templates/pppoe/ip-pre-up.script.tmpl b/data/templates/pppoe/ip-pre-up.script.tmpl new file mode 100644 index 000000000..9a7ba7d57 --- /dev/null +++ b/data/templates/pppoe/ip-pre-up.script.tmpl @@ -0,0 +1,19 @@ +#!/bin/sh + +# As PPPoE is an "on demand" interface we need to re-configure it when it +# becomes up + +if [ "$6" != "{{ intf }}" ]; then + exit +fi + +# add some info to syslog +DIALER_PID=$(cat /var/run/{{ intf }}.pid) +logger -t pppd[$DIALER_PID] "executing $0" + +echo "{{ description }}" > /sys/class/net/{{ intf }}/ifalias + +{% if vrf -%} +logger -t pppd[$DIALER_PID] "configuring dialer interface $6 for VRF {{ vrf }}" +ip link set dev {{ intf }} master {{ vrf }} +{% endif %} diff --git a/data/templates/pppoe/ip-up.script.tmpl b/data/templates/pppoe/ip-up.script.tmpl new file mode 100644 index 000000000..697ebcc20 --- /dev/null +++ b/data/templates/pppoe/ip-up.script.tmpl @@ -0,0 +1,47 @@ +#!/bin/sh + +# As PPPoE is an "on demand" interface we need to re-configure it when it +# becomes up +if [ "$6" != "{{ intf }}" ]; then + exit +fi + +{% if not on_demand %} +# See https://phabricator.vyos.net/T2248 & T2220. Determine if we are enslaved +# to a VRF, this is needed to properly insert the default route. + +# add some info to syslog +DIALER_PID=$(cat /var/run/{{ intf }}.pid) +logger -t pppd[$DIALER_PID] "executing $0" + +SED_OPT="^ip route" +VRF_NAME="" +if [ -d /sys/class/net/{{ intf }}/upper_* ]; then + # Determine upper (VRF) interface + VRF=$(basename $(ls -d /sys/class/net/{{ intf }}/upper_*)) + # Remove upper_ prefix from result string + VRF=${VRF#"upper_"} + # generate new SED command + SED_OPT="vrf ${VRF}" + # generate vtysh option + VRF_NAME="vrf ${VRF}" +fi + +{% if 'auto' in default_route -%} +# Only insert a new default route if there is no default route configured +routes=$(vtysh -c "show running-config" | sed -n "/${SED_OPT}/,/!/p" | grep 0.0.0.0/0 | wc -l) +if [ "$routes" -ne 0 ]; then + exit 1 +fi + +{% elif 'force' in default_route -%} +# Retrieve current static default routes and remove it from the routing table +vtysh -c "show running-config" | sed -n "/${SED_OPT}/,/!/p" | grep 0.0.0.0/0 | while read route ; do + vtysh -c "conf t" ${VTY_OPT} -c "no ${route} ${VRF_NAME}" +done +{% endif %} + +# Add default route to default or VRF routing table +vtysh -c "conf t" ${VTY_OPT} -c "ip route 0.0.0.0/0 {{ intf }} ${VRF_NAME}" +logger -t pppd[$DIALER_PID] "added default route via {{ intf }} ${VRF_NAME}" +{% endif %} diff --git a/data/templates/pppoe/ipv6-up.script.tmpl b/data/templates/pppoe/ipv6-up.script.tmpl new file mode 100644 index 000000000..a4b08ddaf --- /dev/null +++ b/data/templates/pppoe/ipv6-up.script.tmpl @@ -0,0 +1,41 @@ +#!/bin/sh + +# As PPPoE is an "on demand" interface we need to re-configure it when it +# becomes up + +if [ "$6" != "{{ intf }}" ]; then + exit +fi + +{% if ipv6_autoconf -%} +# add some info to syslog +DIALER_PID=$(cat /var/run/{{ intf }}.pid) +logger -t pppd[$DIALER_PID] "executing $0" +logger -t pppd[$DIALER_PID] "configuring interface {{ intf }} via {{ source_interface }}" + +# Configure interface-specific Host/Router behaviour. +# Note: It is recommended to have the same setting on all interfaces; mixed +# router/host scenarios are rather uncommon. Possible values are: +# +# 0 Forwarding disabled +# 1 Forwarding enabled +# +echo 1 > /proc/sys/net/ipv6/conf/{{ intf }}/forwarding + +# 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. Possible values are: +# +# 0 Do not accept Router Advertisements. +# 1 Accept Router Advertisements if forwarding is disabled. +# 2 Overrule forwarding behaviour. Accept Router Advertisements +# even if forwarding is enabled. +# +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 %} diff --git a/data/templates/pppoe/peer.tmpl b/data/templates/pppoe/peer.tmpl new file mode 100644 index 000000000..36d108cee --- /dev/null +++ b/data/templates/pppoe/peer.tmpl @@ -0,0 +1,72 @@ +### Autogenerated by interfaces-pppoe.py ### + +{% if description %} +# {{ description }} +{% endif %} + +# Require peer to provide the local IP address if it is not +# specified explicitly in the config file. +noipdefault + +# Don't show the password in logfiles: +hide-password + +# Standard Link Control Protocol (LCP) parameters: +lcp-echo-interval 20 +lcp-echo-failure 3 + +# RFC 2516, paragraph 7 mandates that the following options MUST NOT be +# requested and MUST be rejected if requested by the peer: +# Address-and-Control-Field-Compression (ACFC) +noaccomp + +# Asynchronous-Control-Character-Map (ACCM) +default-asyncmap + +# Override any connect script that may have been set in /etc/ppp/options. +connect /bin/true + +# Don't try to authenticate the remote node +noauth + +# Don't try to proxy ARP for the remote endpoint. User can set proxy +# arp entries up manually if they wish. More importantly, having +# the "proxyarp" parameter set disables the "defaultroute" option. +noproxyarp + +# Unlimited connection attempts +maxfail 0 + +plugin rp-pppoe.so +{{ source_interface }} +persist +ifname {{ intf }} +ipparam {{ intf }} +debug +logfile {{ logfile }} +mtu {{ mtu }} +mru {{ mtu }} +user "{{ auth_username }}" +password "{{ auth_password }}" +{% if name_server -%} +usepeerdns +{% endif %} +{% if ipv6_enable -%} ++ipv6 +ipv6cp-use-ipaddr +{% endif %} +{% if service_name -%} +rp_pppoe_service "{{ service_name }}" +{% endif %} +{% if on_demand %} +demand +# See T2249. PPP default route options should only be set when in on-demand +# mode. As soon as we are not in on-demand mode the default-route handling is +# passed to the ip-up.d/ip-down.s scripts which is required for VRF support. +{% if 'auto' in default_route -%} +defaultroute +{% elif 'force' in default_route -%} +defaultroute +replacedefaultroute +{% endif %} +{% endif %} diff --git a/data/templates/pptp/chap-secrets.tmpl b/data/templates/pptp/chap-secrets.tmpl new file mode 100644 index 000000000..f93f4607b --- /dev/null +++ b/data/templates/pptp/chap-secrets.tmpl @@ -0,0 +1,6 @@ +# username server password acceptable local IP addresses +{% for user in authentication['local-users'] %} +{% if authentication['local-users'][user]['state'] == 'enabled' %} +{{ "%-12s" | format(user) }} * {{ "%-16s" | format(authentication['local-users'][user]['passwd']) }} {{ "%-16s" | format(authentication['local-users'][user]['ip']) }} +{% endif %} +{% endfor %} diff --git a/data/templates/pptp/pptp.config.tmpl b/data/templates/pptp/pptp.config.tmpl new file mode 100644 index 000000000..2596507af --- /dev/null +++ b/data/templates/pptp/pptp.config.tmpl @@ -0,0 +1,87 @@ + +### generated by accel_pptp.py ### +[modules] +log_syslog +pptp +ippool +chap-secrets +{% if authentication['auth_proto'] %} +{{ authentication['auth_proto'] }} +{% else %} +auth_mschap_v2 +{% endif %} +{% if authentication['mode'] == 'radius' %} +radius +{% endif -%} + +[core] +thread-count={{thread_cnt}} + +[log] +syslog=accel-pptp,daemon +copy=1 +level=5 + +{% if dns %} +[dns] +{% if dns[0] %} +dns1={{dns[0]}} +{% endif %} +{% if dns[1] %} +dns2={{dns[1]}} +{% endif %} +{% endif %} + +{% if wins %} +[wins] +{% if wins[0] %} +wins1={{wins[0]}} +{% endif %} +{% if wins[1] %} +wins2={{wins[1]}} +{% endif %} +{% endif %} + +[pptp] +ifname=pptp%d +{% if outside_addr %} +bind={{outside_addr}} +{% endif %} +verbose=1 +ppp-max-mtu={{mtu}} +mppe={{authentication['mppe']}} +echo-interval=10 +echo-failure=3 + + +[client-ip-range] +0.0.0.0/0 + +[ip-pool] +tunnel={{client_ip_pool}} +gw-ip-address={{gw_ip}} + +{% if authentication['mode'] == 'local' %} +[chap-secrets] +chap-secrets=/etc/accel-ppp/pptp/chap-secrets +{% endif %} + +[ppp] +verbose=5 +check-ip=1 +single-session=replace + +{% if authentication['mode'] == 'radius' %} +[radius] +{% for rsrv in authentication['radiussrv']: %} +server={{rsrv}},{{authentication['radiussrv'][rsrv]['secret']}},\ +req-limit={{authentication['radiussrv'][rsrv]['req-limit']}},\ +fail-time={{authentication['radiussrv'][rsrv]['fail-time']}} +{% endfor %} +timeout=30 +acct-timeout=30 +max-try=3 +{%endif %} + +[cli] +tcp=127.0.0.1:2003 diff --git a/data/templates/router-advert/radvd.conf.tmpl b/data/templates/router-advert/radvd.conf.tmpl new file mode 100644 index 000000000..2768f6f2e --- /dev/null +++ b/data/templates/router-advert/radvd.conf.tmpl @@ -0,0 +1,37 @@ +### Autogenerated by service-router-advert.py ### + +{% for i in interfaces -%} +interface {{ i.name }} { + IgnoreIfMissing on; + AdvDefaultPreference {{ i.default_preference }}; + AdvManagedFlag {{ i.managed_flag }}; + MaxRtrAdvInterval {{ i.interval_max }}; +{% if i.interval_min %} + MinRtrAdvInterval {{ i.interval_min }}; +{% endif %} + AdvReachableTime {{ i.reachable_time }}; + AdvIntervalOpt {{ i.send_advert }}; + AdvSendAdvert {{ i.send_advert }}; +{% if i.default_lifetime %} + AdvDefaultLifetime {{ i.default_lifetime }}; +{% endif %} +{% if i.link_mtu %} + AdvLinkMTU {{ i.link_mtu }}; +{% endif %} + AdvOtherConfigFlag {{ i.other_config_flag }}; + AdvRetransTimer {{ i.retrans_timer }}; + AdvCurHopLimit {{ i.hop_limit }}; +{% for p in i.prefixes %} + prefix {{ p.prefix }} { + AdvAutonomous {{ p.autonomous_flag }}; + AdvValidLifetime {{ p.valid_lifetime }}; + AdvOnLink {{ p.on_link }}; + AdvPreferredLifetime {{ p.preferred_lifetime }}; + }; +{% endfor %} +{% if i.name_server %} + RDNSS {{ i.name_server | join(" ") }} { + }; +{% endif %} +}; +{% endfor -%} diff --git a/data/templates/salt-minion/minion.tmpl b/data/templates/salt-minion/minion.tmpl new file mode 100644 index 000000000..5e50d588c --- /dev/null +++ b/data/templates/salt-minion/minion.tmpl @@ -0,0 +1,63 @@ +### Autogenerated by salt-minion.py ### + +##### Primary configuration settings ##### +########################################## + +# The hash_type is the hash to use when discovering the hash of a file on +# the master server. The default is sha256, but md5, sha1, sha224, sha384 and +# sha512 are also supported. +# +# WARNING: While md5 and sha1 are also supported, do not use them due to the +# high chance of possible collisions and thus security breach. +# +# Prior to changing this value, the master should be stopped and all Salt +# caches should be cleared. +hash_type: {{ hash_type }} + +##### Logging settings ##### +########################################## +# The location of the minion log file +# The minion log can be sent to a regular file, local path name, or network +# 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 }} + +# The level of messages to send to the console. +# One of 'garbage', 'trace', 'debug', info', 'warning', 'error', 'critical'. +# +# The following log levels are considered INSECURE and may log sensitive data: +# ['garbage', 'trace', 'debug'] +# +# Default: 'warning' +log_level: {{ log_level }} + +# Set the location of the salt master server, if the master server cannot be +# resolved, then the minion will fail to start. +master: +{% for host in master -%} +- {{ host }} +{% endfor %} + +# The user to run salt +user: {{ user }} + +# The directory to store the pki information in +pki_dir: /config/salt/pki/minion + +# Explicitly declare the id for this minion to use, if left commented the id +# will be the hostname as returned by the python call: socket.getfqdn() +# Since salt uses detached ids it is possible to run multiple minions on the +# same machine but with different ids, this can be useful for salt compute +# clusters. +id: {{ salt_id }} + + +# The number of minutes between mine updates. +mine_interval: {{ mine_interval }} + +verify_master_pubkey_sign: {{ verify_master_pubkey_sign }} diff --git a/data/templates/snmp/etc.snmp.conf.tmpl b/data/templates/snmp/etc.snmp.conf.tmpl new file mode 100644 index 000000000..159578906 --- /dev/null +++ b/data/templates/snmp/etc.snmp.conf.tmpl @@ -0,0 +1,4 @@ +### Autogenerated by snmp.py ### +{% if trap_source -%} +clientaddr {{ trap_source }} +{% endif %} diff --git a/data/templates/snmp/etc.snmpd.conf.tmpl b/data/templates/snmp/etc.snmpd.conf.tmpl new file mode 100644 index 000000000..1659abf93 --- /dev/null +++ b/data/templates/snmp/etc.snmpd.conf.tmpl @@ -0,0 +1,118 @@ +### Autogenerated by snmp.py ### + +# non configurable defaults +sysObjectID 1.3.6.1.4.1.44641 +sysServices 14 +master agentx +agentXPerms 0777 0777 +pass .1.3.6.1.2.1.31.1.1.1.18 /opt/vyatta/sbin/if-mib-alias +smuxpeer .1.3.6.1.2.1.83 +smuxpeer .1.3.6.1.2.1.157 +smuxsocket localhost + +# linkUp/Down configure the Event MIB tables to monitor +# the ifTable for network interfaces being taken up or down +# for making internal queries to retrieve any necessary information +iquerySecName {{ vyos_user }} + +# Modified from the default linkUpDownNotification +# to include more OIDs and poll more frequently +notificationEvent linkUpTrap linkUp ifIndex ifDescr ifType ifAdminStatus ifOperStatus +notificationEvent linkDownTrap linkDown ifIndex ifDescr ifType ifAdminStatus ifOperStatus +monitor -r 10 -e linkUpTrap "Generate linkUp" ifOperStatus != 2 +monitor -r 10 -e linkDownTrap "Generate linkDown" ifOperStatus == 2 + +######################## +# configurable section # +######################## + +# Default system description is VyOS version +sysDescr VyOS {{ version }} + +{% if description %} +# Description +SysDescr {{ description }} +{%- endif %} + +# Listen +agentaddress unix:/run/snmpd.socket{% if listen_on %}{% for li in listen_on %},{{ li }}{% endfor %}{% else %},udp:161{% if ipv6_enabled %},udp6:161{% endif %}{% endif %} + +# SNMP communities +{%- for c in communities %} + +{%- if c.network_v4 %} +{%- for network in c.network_v4 %} +{{ c.authorization }}community {{ c.name }} {{ network }} +{%- endfor %} +{%- elif not c.has_source %} +{{ c.authorization }}community {{ c.name }} +{%- endif %} + +{%- if c.network_v6 %} +{%- for network in c.network_v6 %} +{{ c.authorization }}community6 {{ c.name }} {{ network }} +{%- endfor %} +{%- elif not c.has_source %} +{{ c.authorization }}community6 {{ c.name }} +{%- endif %} + +{%- endfor %} + +{% if contact %} +# system contact information +SysContact {{ contact }} +{%- endif %} + +{% if location %} +# system location information +SysLocation {{ location }} +{%- endif %} + +{% if smux_peers -%} +# additional smux peers +{%- for sp in smux_peers %} +smuxpeer {{ sp }} +{%- endfor %} +{%- endif %} + +{% if trap_targets -%} +# if there is a problem - tell someone! +{%- for t in trap_targets %} +trap2sink {{ t.target }}{% if t.port -%}:{{ t.port }}{% endif %} {{ t.community }} +{%- endfor %} +{%- endif %} + +{%- if v3_enabled %} +# +# SNMPv3 stuff goes here +# +# views +{%- for v in v3_views %} +{%- for oid in v.oids %} +view {{ v.name }} included .{{ oid.oid }} +{%- endfor %} +{%- endfor %} + +# access +# context sec.model sec.level match read write notif +{%- for g in v3_groups %} +access {{ g.name }} "" usm {{ g.seclevel }} exact {{ g.view }} {% if g.mode == 'ro' %}none{% else %}{{ g.view }}{% endif %} none +{%- endfor %} + +# trap-target +{%- for t in v3_traps %} +trapsess -v 3 {{ '-Ci' if t.type == 'inform' }} -e {{ v3_engineid }} -u {{ t.secName }} -l {{ t.secLevel }} -a {{ t.authProtocol }} {% if t.authPassword %}-A {{ t.authPassword }}{% elif t.authMasterKey %}-3m {{ t.authMasterKey }}{% endif %} -x {{ t.privProtocol }} {% if t.privPassword %}-X {{ t.privPassword }}{% elif t.privMasterKey %}-3M {{ t.privMasterKey }}{% endif %} {{ t.ipProto }}:{{ t.ipAddr }}:{{ t.ipPort }} +{%- endfor %} + +# group +{%- for u in v3_users %} +group {{ u.group }} usm {{ u.name }} +{% endfor %} +{%- endif %} + +{% if script_ext %} +# extension scripts +{%- for ext in script_ext|sort(attribute='name') %} +extend {{ ext.name }} {{ ext.script }} +{%- endfor %} +{% endif %} diff --git a/data/templates/snmp/usr.snmpd.conf.tmpl b/data/templates/snmp/usr.snmpd.conf.tmpl new file mode 100644 index 000000000..9c0337fa8 --- /dev/null +++ b/data/templates/snmp/usr.snmpd.conf.tmpl @@ -0,0 +1,6 @@ +### Autogenerated by snmp.py ### +{%- for u in v3_users %} +{{ u.mode }}user {{ u.name }} +{%- endfor %} + +rwuser {{ vyos_user }} diff --git a/data/templates/snmp/var.snmpd.conf.tmpl b/data/templates/snmp/var.snmpd.conf.tmpl new file mode 100644 index 000000000..0b8e9f291 --- /dev/null +++ b/data/templates/snmp/var.snmpd.conf.tmpl @@ -0,0 +1,16 @@ +### Autogenerated by snmp.py ### +# user +{%- for u in v3_users %} +{%- if u.authOID == 'none' %} +createUser {{ u.name }} +{%- elif u.authPassword %} +createUser {{ u.name }} {{ u.authProtocol | upper }} "{{ u.authPassword }}" {{ u.privProtocol | upper }} {{ u.privPassword }} +{%- else %} +usmUser 1 3 {{ v3_engineid }} "{{ u.name }}" "{{ u.name }}" NULL {{ u.authOID }} {{ u.authMasterKey }} {{ u.privOID }} {{ u.privMasterKey }} 0x +{%- endif %} +{%- endfor %} + +createUser {{ vyos_user }} MD5 "{{ vyos_user_pass }}" DES +{%- if v3_engineid %} +oldEngineID {{ v3_engineid }} +{%- endif %} diff --git a/data/templates/ssh/sshd_config.tmpl b/data/templates/ssh/sshd_config.tmpl new file mode 100644 index 000000000..5deb5232a --- /dev/null +++ b/data/templates/ssh/sshd_config.tmpl @@ -0,0 +1,125 @@ +### Autogenerated by ssh.py ### + +# Non-configurable defaults +Protocol 2 +HostKey /etc/ssh/ssh_host_rsa_key +HostKey /etc/ssh/ssh_host_dsa_key +HostKey /etc/ssh/ssh_host_ecdsa_key +HostKey /etc/ssh/ssh_host_ed25519_key +SyslogFacility AUTH +LoginGraceTime 120 +StrictModes yes +PubkeyAuthentication yes +IgnoreRhosts yes +HostbasedAuthentication no +PermitEmptyPasswords no +ChallengeResponseAuthentication no +X11Forwarding yes +X11DisplayOffset 10 +PrintMotd no +PrintLastLog yes +TCPKeepAlive yes +Banner /etc/issue.net +Subsystem sftp /usr/lib/openssh/sftp-server +UsePAM yes +HostKey /etc/ssh/ssh_host_rsa_key + +# Specifies whether sshd should look up the remote host name, +# and to check that the resolved host name for the remote IP +# address maps back to the very same IP address. +UseDNS {{ host_validation }} + +# Specifies the port number that sshd listens on. The default is 22. +# Multiple options of this type are permitted. +{% if mport|length != 0 %} +{% for p in mport %} +Port {{ p }} +{% endfor %} +{% else %} +Port {{ port }} +{% endif %} + +# Gives the verbosity level that is used when logging messages from sshd +LogLevel {{ log_level }} + +# Specifies whether root can log in using ssh +PermitRootLogin no + +# Specifies whether password authentication is allowed +PasswordAuthentication {{ password_authentication }} + +{% if listen_on %} +# Specifies the local addresses sshd should listen on +{% for a in listen_on %} +ListenAddress {{ a }} +{% endfor %} +{{ "\n" }} +{% endif %} + +{%- if ciphers %} +# Specifies the ciphers allowed. Multiple ciphers must be comma-separated. +# +# NOTE: As of now, there is no 'multi' node for 'ciphers', thus we have only one :/ +Ciphers {{ ciphers | join(",") }} +{{ "\n" }} +{% endif %} + +{%- if mac %} +# Specifies the available MAC (message authentication code) algorithms. The MAC +# algorithm is used for data integrity protection. Multiple algorithms must be +# comma-separated. +# +# NOTE: As of now, there is no 'multi' node for 'mac', thus we have only one :/ +MACs {{ mac | join(",") }} +{{ "\n" }} +{% endif %} + +{%- if key_exchange %} +# Specifies the available KEX (Key Exchange) algorithms. Multiple algorithms must +# be comma-separated. +# +# NOTE: As of now, there is no 'multi' node for 'key-exchange', thus we have only one :/ +KexAlgorithms {{ key_exchange | join(",") }} +{{ "\n" }} +{% endif %} + +{%- if allow_users %} +# This keyword can be followed by a list of user name patterns, separated by spaces. +# If specified, login is allowed only for user names that match one of the patterns. +# Only user names are valid, a numerical user ID is not recognized. +AllowUsers {{ allow_users | join(" ") }} +{{ "\n" }} +{% endif %} + +{%- if allow_groups %} +# This keyword can be followed by a list of group name patterns, separated by spaces. +# If specified, login is allowed only for users whose primary group or supplementary +# group list matches one of the patterns. Only group names are valid, a numerical group +# ID is not recognized. +AllowGroups {{ allow_groups | join(" ") }} +{{ "\n" }} +{% endif %} + +{%- if deny_users %} +# This keyword can be followed by a list of user name patterns, separated by spaces. +# Login is disallowed for user names that match one of the patterns. Only user names +# are valid, a numerical user ID is not recognized. +DenyUsers {{ deny_users | join(" ") }} +{{ "\n" }} +{% endif %} + +{%- if deny_groups %} +# This keyword can be followed by a list of group name patterns, separated by spaces. +# Login is disallowed for users whose primary group or supplementary group list matches +# one of the patterns. Only group names are valid, a numerical group ID is not recognized. +DenyGroups {{ deny_groups | join(" ") }} +{{ "\n" }} +{% endif %} + +{%- if client_keepalive %} +# Sets a timeout interval in seconds after which if no data has been received from the client, +# sshd will send a message through the encrypted channel to request a response from the client. +# The default is 0, indicating that these messages will not be sent to the client. +# This option applies to protocol version 2 only. +ClientAliveInterval {{ client_keepalive }} +{% endif %} diff --git a/data/templates/sstp/chap-secrets.tmpl b/data/templates/sstp/chap-secrets.tmpl new file mode 100644 index 000000000..dd00d7bd0 --- /dev/null +++ b/data/templates/sstp/chap-secrets.tmpl @@ -0,0 +1,10 @@ +# username server password acceptable local IP addresses shaper +{% for user in local_users %} +{% if user.state == 'enabled' %} +{% if user.upload and user.download %} +{{ "%-12s" | format(user.name) }} * {{ "%-16s" | format(user.password) }} {{ "%-16s" | format(user.ip) }} {{ user.download }} / {{ user.upload }} +{% else %} +{{ "%-12s" | format(user.name) }} * {{ "%-16s" | format(user.password) }} {{ "%-16s" | format(user.ip) }} +{% endif %} +{% endif %} +{% endfor %} diff --git a/data/templates/sstp/sstp.config.tmpl b/data/templates/sstp/sstp.config.tmpl new file mode 100644 index 000000000..19805358e --- /dev/null +++ b/data/templates/sstp/sstp.config.tmpl @@ -0,0 +1,114 @@ +### generated by vpn_sstp.py ### +[modules] +log_syslog +sstp +shaper +{% if auth_mode == 'local' %} +chap-secrets +{% elif auth_mode == 'radius' %} +radius +{% endif -%} +ippool + +{% for proto in auth_proto %} +{{proto}} +{% endfor %} + +[core] +thread-count={{thread_cnt}} + +[common] +single-session=replace + +[log] +syslog=accel-sstp,daemon +copy=1 +level=5 + +[client-ip-range] +disable + +[sstp] +verbose=1 +accept=ssl +ssl-ca-file={{ ssl_ca }} +ssl-pemfile={{ ssl_cert }} +ssl-keyfile={{ ssl_key }} + +{% if client_ip_pool %} +[ip-pool] +gw-ip-address={{ client_gateway }} +{% for subnet in client_ip_pool %} +{{ subnet }} +{% endfor %} +{% endif %} + +{% if dnsv4 %} +[dns] +{% for dns in dnsv4 -%} +dns{{ loop.index }}={{ dns }} +{% endfor -%} +{% endif %} + +{% if auth_mode == 'local' %} +[chap-secrets] +chap-secrets=/etc/accel-ppp/sstp/chap-secrets +{% elif auth_mode == 'radius' %} +[radius] +verbose=1 +{% for r in radius_server %} +server={{ r.server }},{{ r.key }},auth-port={{ r.port }},req-limit=0,fail-time={{ r.fail_time }} +{% endfor -%} + +acct-timeout={{ radius_acct_tmo }} +timeout={{ radius_timeout }} +max-try={{ radius_max_try }} + +{% if radius_nas_id %} +nas-identifier={{ radius_nas_id }} +{% endif -%} +{% if radius_nas_ip %} +nas-ip-address={{ radius_nas_ip }} +{% endif -%} +{% if radius_source_address %} +bind={{ radius_source_address }} +{% endif -%} + + +{% if radius_dynamic_author %} +dae-server={{ radius_dynamic_author.server }}:{{ radius_dynamic_author.port }},{{ radius_dynamic_author.key }} +{% endif -%} +{% endif %} + +[ppp] +verbose=1 +check-ip=1 +{% if mtu %} +mtu={{ mtu }} +{% endif -%} + +{% if ppp_mppe %} +mppe={{ ppp_mppe }} +{% endif -%} +{% if ppp_echo_interval %} +lcp-echo-interval={{ ppp_echo_interval }} +{% endif -%} +{% if ppp_echo_failure %} +lcp-echo-failure={{ ppp_echo_failure }} +{% endif -%} +{% if ppp_echo_timeout %} +lcp-echo-timeout={{ ppp_echo_timeout }} +{% endif %} + +{% if radius_shaper_attr %} +[shaper] +verbose=1 +attr={{ radius_shaper_attr }} +{% if radius_shaper_vendor %} +vendor={{ radius_shaper_vendor }} +{% endif -%} +{% endif %} + +[cli] +tcp=127.0.0.1:2005 + diff --git a/data/templates/syslog/logrotate.tmpl b/data/templates/syslog/logrotate.tmpl new file mode 100644 index 000000000..f758265e4 --- /dev/null +++ b/data/templates/syslog/logrotate.tmpl @@ -0,0 +1,12 @@ +{% for file in files %} +{{files[file]['log-file']}} { + missingok + notifempty + create + rotate {{files[file]['max-files']}} + size={{files[file]['max-size']//1024}}k + postrotate + invoke-rc.d rsyslog rotate > /dev/null + endscript +} +{% endfor %} diff --git a/data/templates/syslog/rsyslog.conf.tmpl b/data/templates/syslog/rsyslog.conf.tmpl new file mode 100644 index 000000000..bc3f7667b --- /dev/null +++ b/data/templates/syslog/rsyslog.conf.tmpl @@ -0,0 +1,44 @@ +## generated by syslog.py ## +## file based logging +{% if files['global']['marker'] -%} +$ModLoad immark +{% if files['global']['marker-interval'] %} +$MarkMessagePeriod {{files['global']['marker-interval']}} +{% endif %} +{% endif -%} +{% if files['global']['preserver_fqdn'] -%} +$PreserveFQDN on +{% endif -%} +{% for file in files %} +$outchannel {{file}},{{files[file]['log-file']}},{{files[file]['max-size']}},{{files[file]['action-on-max-size']}} +{{files[file]['selectors']}} :omfile:${{file}} +{% endfor %} +{% if console %} +## console logging +{% for con in console %} +{{console[con]['selectors']}} /dev/console +{% endfor %} +{% endif %} +{% if hosts %} +## remote logging +{% for host in hosts %} +{% if hosts[host]['proto'] == 'tcp' %} +{% if hosts[host]['port'] %} +{{hosts[host]['selectors']}} @@{{host}}:{{hosts[host]['port']}} +{% else %} +{{hosts[host]['selectors']}} @@{{host}} +{% endif %} +{% else %} +{% if hosts[host]['port'] %} +{{hosts[host]['selectors']}} @{{host}}:{{hosts[host]['port']}} +{% else %} +{{hosts[host]['selectors']}} @{{host}} +{% endif %} +{% endif %} +{% endfor %} +{% endif %} +{% if user %} +{% for u in user %} +{{user[u]['selectors']}} :omusrmsg:{{u}} +{% endfor %} +{% endif %} diff --git a/data/templates/system-login/pam_radius_auth.conf.tmpl b/data/templates/system-login/pam_radius_auth.conf.tmpl new file mode 100644 index 000000000..6cff67867 --- /dev/null +++ b/data/templates/system-login/pam_radius_auth.conf.tmpl @@ -0,0 +1,13 @@ +# Automatically generated by VyOS +# RADIUS configuration file +{%- if radius_server %} +# server[:port] shared_secret timeout (s) source_ip +{% for s in radius_server %} +{%- if not s.disabled -%} +{{ s.address }}:{{ s.port }} {{ s.key }} {{ s.timeout }} {% if radius_source_address -%}{{ radius_source_address }}{% endif %} +{% endif %} +{%- endfor %} + +priv-lvl 15 +mapped_priv_user radius_priv_user +{% endif %} diff --git a/data/templates/tftp-server/default.tmpl b/data/templates/tftp-server/default.tmpl new file mode 100644 index 000000000..18fee35d1 --- /dev/null +++ b/data/templates/tftp-server/default.tmpl @@ -0,0 +1,2 @@ +### Autogenerated by tftp_server.py ### +DAEMON_ARGS="--listen --user tftp --address {% for a in listen-%}{{ a }}{% endfor %}{% if allow_upload %} --create --umask 000{% endif %} --secure {{ directory }}" diff --git a/data/templates/vrf/vrf.conf.tmpl b/data/templates/vrf/vrf.conf.tmpl new file mode 100644 index 000000000..761b0bb6f --- /dev/null +++ b/data/templates/vrf/vrf.conf.tmpl @@ -0,0 +1,8 @@ +### Autogenerated by vrf.py ### +# +# Routing table ID to name mapping reference + +# id vrf name comment +{% for vrf in vrf_add -%} +{{ "%-10s" | format(vrf.table) }} {{ "%-16s" | format(vrf.name) }} # {{ vrf.description }} +{% endfor -%} diff --git a/data/templates/vrrp/daemon.tmpl b/data/templates/vrrp/daemon.tmpl new file mode 100644 index 000000000..62d0ba63b --- /dev/null +++ b/data/templates/vrrp/daemon.tmpl @@ -0,0 +1,5 @@ +# Autogenerated by VyOS
+# Options to pass to keepalived
+
+# DAEMON_ARGS are appended to the keepalived command-line
+DAEMON_ARGS="--snmp"
diff --git a/data/templates/vrrp/keepalived.conf.tmpl b/data/templates/vrrp/keepalived.conf.tmpl new file mode 100644 index 000000000..08b821f70 --- /dev/null +++ b/data/templates/vrrp/keepalived.conf.tmpl @@ -0,0 +1,97 @@ +# Autogenerated by VyOS +# Do not edit this file, all your changes will be lost +# on next commit or reboot + +global_defs { + dynamic_interfaces + script_user root + notify_fifo /run/keepalived_notify_fifo + notify_fifo_script /usr/libexec/vyos/system/keepalived-fifo.py +} + +{% for group in groups -%} + +{% if group.health_check_script -%} +vrrp_script healthcheck_{{ group.name }} { + script "{{ group.health_check_script }}" + interval {{ group.health_check_interval }} + fall {{ group.health_check_count }} + rise 1 + +} +{% endif %} + +vrrp_instance {{ group.name }} { + {% if group.description -%} + # {{ group.description }} + {% endif -%} + + state BACKUP + interface {{ group.interface }} + virtual_router_id {{ group.vrid }} + priority {{ group.priority }} + advert_int {{ group.advertise_interval }} + + {% if group.preempt -%} + preempt_delay {{ group.preempt_delay }} + {% else -%} + nopreempt + {% endif -%} + + {% if group.peer_address -%} + unicast_peer { {{ group.peer_address }} } + {% endif -%} + + {% if group.hello_source -%} + {%- if group.peer_address -%} + unicast_src_ip {{ group.hello_source }} + {%- else -%} + mcast_src_ip {{ group.hello_source }} + {%- endif %} + {% endif -%} + + {% if group.use_vmac and group.peer_address -%} + use_vmac {{group.interface}}v{{group.vrid}} + vmac_xmit_base + {% elif group.use_vmac -%} + use_vmac {{group.interface}}v{{group.vrid}} + {% endif -%} + + {% if group.auth_password -%} + authentication { + auth_pass "{{ group.auth_password }}" + auth_type {{ group.auth_type }} + } + {% endif -%} + + virtual_ipaddress { + {% for addr in group.virtual_addresses -%} + {{ addr }} + {% endfor -%} + } + + {% if group.health_check_script -%} + track_script { + healthcheck_{{ group.name }} + } + {% endif -%} +} + +{% endfor -%} + +{% for sync_group in sync_groups -%} +vrrp_sync_group {{ sync_group.name }} { + group { + {% for member in sync_group.members -%} + {{ member }} + {% endfor -%} + } + + {% if sync_group.conntrack_sync -%} + notify_master "/opt/vyatta/sbin/vyatta-vrrp-conntracksync.sh master {{ sync_group.name }}" + notify_backup "/opt/vyatta/sbin/vyatta-vrrp-conntracksync.sh backup {{ sync_group.name }}" + notify_fault "/opt/vyatta/sbin/vyatta-vrrp-conntracksync.sh fault {{ sync_group.name }}" + {% endif -%} +} + +{% endfor -%} diff --git a/data/templates/wifi/cfg80211.conf.tmpl b/data/templates/wifi/cfg80211.conf.tmpl new file mode 100644 index 000000000..b21bacc1e --- /dev/null +++ b/data/templates/wifi/cfg80211.conf.tmpl @@ -0,0 +1,3 @@ +{%- if regdom -%} +options cfg80211 ieee80211_regdom={{ regdom }} +{% endif %} diff --git a/data/templates/wifi/crda.tmpl b/data/templates/wifi/crda.tmpl new file mode 100644 index 000000000..750ad86ee --- /dev/null +++ b/data/templates/wifi/crda.tmpl @@ -0,0 +1,3 @@ +{%- if regdom -%} +REGDOMAIN={{ regdom }} +{% endif %} diff --git a/data/templates/wifi/hostapd.conf.tmpl b/data/templates/wifi/hostapd.conf.tmpl new file mode 100644 index 000000000..031fb6c90 --- /dev/null +++ b/data/templates/wifi/hostapd.conf.tmpl @@ -0,0 +1,728 @@ +### Autogenerated by interfaces-wireless.py ### +{% if description %} +# Description: {{ description }} +# User-friendly description of device; up to 32 octets encoded in UTF-8 +device_name={{ description | truncate(32, True) }} +{% endif %} + +# AP netdevice name (without 'ap' postfix, i.e., wlan0 uses wlan0ap for +# management frames with the Host AP driver); wlan0 with many nl80211 drivers +# Note: This attribute can be overridden by the values supplied with the '-i' +# command line parameter. +interface={{ intf }} + +# Driver interface type (hostap/wired/none/nl80211/bsd); +# default: hostap). nl80211 is used with all Linux mac80211 drivers. +# Use driver=none if building hostapd as a standalone RADIUS server that does +# not control any wireless/wired driver. +driver=nl80211 + +# Levels (minimum value for logged events): +# 0 = verbose debugging +# 1 = debugging +# 2 = informational messages +# 3 = notification +# 4 = warning +logger_syslog=-1 +logger_syslog_level=0 +logger_stdout=-1 +logger_stdout_level=0 + +{%- if country_code %} + +# Country code (ISO/IEC 3166-1). Used to set regulatory domain. +# Set as needed to indicate country in which device is operating. +# This can limit available channels and transmit power. +country_code={{ country_code }} + +# Enable IEEE 802.11d. This advertises the country_code and the set of allowed +# channels and transmit power levels based on the regulatory limits. The +# country_code setting must be configured with the correct country for +# IEEE 802.11d functions. +ieee80211d=1 +{% endif %} + +{%- if ssid %} + +# SSID to be used in IEEE 802.11 management frames +ssid={{ ssid }} +{% endif %} + +{%- if channel %} + +# Channel number (IEEE 802.11) +# (default: 0, i.e., not set) +# Please note that some drivers do not use this value from hostapd and the +# channel will need to be configured separately with iwconfig. +# +# If CONFIG_ACS build option is enabled, the channel can be selected +# automatically at run time by setting channel=acs_survey or channel=0, both of +# which will enable the ACS survey based algorithm. +channel={{ channel }} +{% endif %} + +{%- if mode %} + +# Operation mode (a = IEEE 802.11a (5 GHz), b = IEEE 802.11b (2.4 GHz), +# g = IEEE 802.11g (2.4 GHz), ad = IEEE 802.11ad (60 GHz); a/g options are used +# with IEEE 802.11n (HT), too, to specify band). For IEEE 802.11ac (VHT), this +# needs to be set to hw_mode=a. For IEEE 802.11ax (HE) on 6 GHz this needs +# to be set to hw_mode=a. When using ACS (see channel parameter), a +# special value "any" can be used to indicate that any support band can be used. +# This special case is currently supported only with drivers with which +# offloaded ACS is used. +{% if 'n' in mode -%} +hw_mode=g +ieee80211n=1 +{% elif 'ac' in mode -%} +hw_mode=a +ieee80211h=1 +ieee80211ac=1 +{% else -%} +hw_mode={{ mode }} +{% endif %} +{% endif %} + +# ieee80211w: Whether management frame protection (MFP) is enabled +# 0 = disabled (default) +# 1 = optional +# 2 = required +{% if 'disabled' in mgmt_frame_protection -%} +ieee80211w=0 +{% elif 'optional' in mgmt_frame_protection -%} +ieee80211w=1 +{% elif 'required' in mgmt_frame_protection -%} +ieee80211w=2 +{% endif %} + +# ht_capab: HT capabilities (list of flags) +# LDPC coding capability: [LDPC] = supported +# Supported channel width set: [HT40-] = both 20 MHz and 40 MHz with secondary +# channel below the primary channel; [HT40+] = both 20 MHz and 40 MHz +# with secondary channel above the primary channel +# (20 MHz only if neither is set) +# Note: There are limits on which channels can be used with HT40- and +# HT40+. Following table shows the channels that may be available for +# HT40- and HT40+ use per IEEE 802.11n Annex J: +# freq HT40- HT40+ +# 2.4 GHz 5-13 1-7 (1-9 in Europe/Japan) +# 5 GHz 40,48,56,64 36,44,52,60 +# (depending on the location, not all of these channels may be available +# for use) +# Please note that 40 MHz channels may switch their primary and secondary +# channels if needed or creation of 40 MHz channel maybe rejected based +# on overlapping BSSes. These changes are done automatically when hostapd +# is setting up the 40 MHz channel. +# Spatial Multiplexing (SM) Power Save: [SMPS-STATIC] or [SMPS-DYNAMIC] +# (SMPS disabled if neither is set) +# HT-greenfield: [GF] (disabled if not set) +# Short GI for 20 MHz: [SHORT-GI-20] (disabled if not set) +# Short GI for 40 MHz: [SHORT-GI-40] (disabled if not set) +# Tx STBC: [TX-STBC] (disabled if not set) +# Rx STBC: [RX-STBC1] (one spatial stream), [RX-STBC12] (one or two spatial +# streams), or [RX-STBC123] (one, two, or three spatial streams); Rx STBC +# disabled if none of these set +# HT-delayed Block Ack: [DELAYED-BA] (disabled if not set) +# Maximum A-MSDU length: [MAX-AMSDU-7935] for 7935 octets (3839 octets if not +# set) +# DSSS/CCK Mode in 40 MHz: [DSSS_CCK-40] = allowed (not allowed if not set) +# 40 MHz intolerant [40-INTOLERANT] (not advertised if not set) +# L-SIG TXOP protection support: [LSIG-TXOP-PROT] (disabled if not set) +{% if cap_ht %} +ht_capab= +{%- endif -%} + +{%- if cap_ht_40mhz_incapable -%} +[40-INTOLERANT] +{%- endif -%} + +{%- if cap_ht_delayed_block_ack -%} +[DELAYED-BA] +{%- endif -%} + +{%- if cap_ht_dsss_cck_40 -%} +[DSSS_CCK-40] +{%- endif -%} + +{%- if cap_ht_greenfield -%} +[GF] +{%- endif -%} + +{%- if cap_ht_ldpc -%} +[LDPC] +{%- endif -%} + +{%- if cap_ht_lsig_protection -%} +[LSIG-TXOP-PROT] +{%- endif -%} + +{%- if cap_ht_max_amsdu -%} +[MAX-AMSDU-{{ cap_ht_max_amsdu }}] +{%- endif -%} + +{%- if cap_ht_smps -%} +[SMPS-{{ cap_ht_smps | upper }}] +{%- endif -%} + +{%- if cap_ht_chan_set_width -%} +{%- for csw in cap_ht_chan_set_width -%} +[{{ csw | upper }}] +{%- endfor -%} +{%- endif -%} + +{%- if cap_ht_short_gi -%} +{%- for gi in cap_ht_short_gi -%} +[SHORT-GI-{{ gi }}] +{%- endfor -%} +{%- endif -%} + +{%- if cap_ht_stbc_tx -%} +[TX-STBC] +{%- endif -%} +{%- if cap_ht_stbc_rx -%} +[RX-STBC{{ cap_ht_stbc_rx }}] +{%- endif %} + +# Required for full HT and VHT functionality +wme_enabled=1 + +{% if cap_ht_powersave -%} +# WMM-PS Unscheduled Automatic Power Save Delivery [U-APSD] +# Enable this flag if U-APSD supported outside hostapd (eg., Firmware/driver) +uapsd_advertisement_enabled=1 +{%- endif %} + +{% if cap_req_ht -%} +# Require stations to support HT PHY (reject association if they do not) +require_ht=1 +{% endif %} + +{%- if cap_vht_chan_set_width -%} +vht_oper_chwidth={{ cap_vht_chan_set_width }} +{%- endif %} + +# vht_capab: VHT capabilities (list of flags) +# +# vht_max_mpdu_len: [MAX-MPDU-7991] [MAX-MPDU-11454] +# Indicates maximum MPDU length +# 0 = 3895 octets (default) +# 1 = 7991 octets +# 2 = 11454 octets +# 3 = reserved +# +# supported_chan_width: [VHT160] [VHT160-80PLUS80] +# Indicates supported Channel widths +# 0 = 160 MHz & 80+80 channel widths are not supported (default) +# 1 = 160 MHz channel width is supported +# 2 = 160 MHz & 80+80 channel widths are supported +# 3 = reserved +# +# Rx LDPC coding capability: [RXLDPC] +# Indicates support for receiving LDPC coded pkts +# 0 = Not supported (default) +# 1 = Supported +# +# Short GI for 80 MHz: [SHORT-GI-80] +# Indicates short GI support for reception of packets transmitted with TXVECTOR +# params format equal to VHT and CBW = 80Mhz +# 0 = Not supported (default) +# 1 = Supported +# +# Short GI for 160 MHz: [SHORT-GI-160] +# Indicates short GI support for reception of packets transmitted with TXVECTOR +# params format equal to VHT and CBW = 160Mhz +# 0 = Not supported (default) +# 1 = Supported +# +# Tx STBC: [TX-STBC-2BY1] +# Indicates support for the transmission of at least 2x1 STBC +# 0 = Not supported (default) +# 1 = Supported +# +# Rx STBC: [RX-STBC-1] [RX-STBC-12] [RX-STBC-123] [RX-STBC-1234] +# Indicates support for the reception of PPDUs using STBC +# 0 = Not supported (default) +# 1 = support of one spatial stream +# 2 = support of one and two spatial streams +# 3 = support of one, two and three spatial streams +# 4 = support of one, two, three and four spatial streams +# 5,6,7 = reserved +# +# SU Beamformer Capable: [SU-BEAMFORMER] +# Indicates support for operation as a single user beamformer +# 0 = Not supported (default) +# 1 = Supported +# +# SU Beamformee Capable: [SU-BEAMFORMEE] +# Indicates support for operation as a single user beamformee +# 0 = Not supported (default) +# 1 = Supported +# +# Compressed Steering Number of Beamformer Antennas Supported: +# [BF-ANTENNA-2] [BF-ANTENNA-3] [BF-ANTENNA-4] +# Beamformee's capability indicating the maximum number of beamformer +# antennas the beamformee can support when sending compressed beamforming +# feedback +# If SU beamformer capable, set to maximum value minus 1 +# else reserved (default) +# +# Number of Sounding Dimensions: +# [SOUNDING-DIMENSION-2] [SOUNDING-DIMENSION-3] [SOUNDING-DIMENSION-4] +# Beamformer's capability indicating the maximum value of the NUM_STS parameter +# in the TXVECTOR of a VHT NDP +# If SU beamformer capable, set to maximum value minus 1 +# else reserved (default) +# +# MU Beamformer Capable: [MU-BEAMFORMER] +# Indicates support for operation as an MU beamformer +# 0 = Not supported or sent by Non-AP STA (default) +# 1 = Supported +# +# VHT TXOP PS: [VHT-TXOP-PS] +# Indicates whether or not the AP supports VHT TXOP Power Save Mode +# or whether or not the STA is in VHT TXOP Power Save mode +# 0 = VHT AP doesn't support VHT TXOP PS mode (OR) VHT STA not in VHT TXOP PS +# mode +# 1 = VHT AP supports VHT TXOP PS mode (OR) VHT STA is in VHT TXOP power save +# mode +# +# +HTC-VHT Capable: [HTC-VHT] +# Indicates whether or not the STA supports receiving a VHT variant HT Control +# field. +# 0 = Not supported (default) +# 1 = supported +# +# Maximum A-MPDU Length Exponent: [MAX-A-MPDU-LEN-EXP0]..[MAX-A-MPDU-LEN-EXP7] +# Indicates the maximum length of A-MPDU pre-EOF padding that the STA can recv +# This field is an integer in the range of 0 to 7. +# The length defined by this field is equal to +# 2 pow(13 + Maximum A-MPDU Length Exponent) -1 octets +# +# VHT Link Adaptation Capable: [VHT-LINK-ADAPT2] [VHT-LINK-ADAPT3] +# Indicates whether or not the STA supports link adaptation using VHT variant +# HT Control field +# If +HTC-VHTcapable is 1 +# 0 = (no feedback) if the STA does not provide VHT MFB (default) +# 1 = reserved +# 2 = (Unsolicited) if the STA provides only unsolicited VHT MFB +# 3 = (Both) if the STA can provide VHT MFB in response to VHT MRQ and if the +# STA provides unsolicited VHT MFB +# Reserved if +HTC-VHTcapable is 0 +# +# Rx Antenna Pattern Consistency: [RX-ANTENNA-PATTERN] +# Indicates the possibility of Rx antenna pattern change +# 0 = Rx antenna pattern might change during the lifetime of an association +# 1 = Rx antenna pattern does not change during the lifetime of an association +# +# Tx Antenna Pattern Consistency: [TX-ANTENNA-PATTERN] +# Indicates the possibility of Tx antenna pattern change +# 0 = Tx antenna pattern might change during the lifetime of an association +# 1 = Tx antenna pattern does not change during the lifetime of an association +{% if cap_vht %} +vht_capab= +{%- endif -%} + +{%- if cap_vht_max_mpdu -%} +[MAX-MPDU-{{ cap_vht_max_mpdu }}] +{%- endif -%} + +{%- if cap_vht_max_mpdu_exp -%} +[MAX-A-MPDU-LEN-EXP{{ cap_vht_max_mpdu_exp }}] +{%- endif -%} + +{%- if cap_vht_chan_set_width -%} +{%- if '2' in cap_vht_chan_set_width -%} +[VHT160] +{%- elif '3' in cap_vht_chan_set_width -%} +[VHT160-80PLUS80] +{%- endif -%} +{%- endif -%} + +{%- if cap_vht_stbc_tx -%} +[TX-STBC-2BY1] +{%- endif -%} + +{%- if cap_vht_stbc_rx -%} +[RX-STBC-{{ cap_vht_stbc_rx }}] +{%- endif -%} + +{%- if cap_vht_link_adaptation -%} +{%- if 'unsolicited' in cap_vht_link_adaptation -%} +[VHT-LINK-ADAPT2] +{%- elif 'both' in cap_vht_link_adaptation -%} +[VHT-LINK-ADAPT3] +{%- endif -%} +{%- endif -%} + +{%- if cap_vht_short_gi -%} +{%- for gi in cap_vht_short_gi -%} +[SHORT-GI-{{ gi }}] +{%- endfor -%} +{%- endif -%} + +{%- if cap_vht_ldpc -%} +[RXLDPC] +{%- endif -%} + +{%- if cap_vht_tx_powersave -%} +[VHT-TXOP-PS] +{%- endif -%} + +{%- if cap_vht_vht_cf -%} +[HTC-VHT] +{%- endif -%} + +{%- if cap_vht_beamform -%} +{%- for beamform in cap_vht_beamform -%} +{%- if 'single-user-beamformer' in beamform -%} +[SU-BEAMFORMER] +{%- elif 'single-user-beamformee' in beamform -%} +[SU-BEAMFORMEE] +{%- elif 'multi-user-beamformer' in beamform -%} +[MU-BEAMFORMER] +{%- elif 'multi-user-beamformee' in beamform -%} +[MU-BEAMFORMEE] +{%- endif -%} +{%- endfor -%} +{%- endif -%} + +{%- if cap_vht_antenna_fixed -%} +[RX-ANTENNA-PATTERN][TX-ANTENNA-PATTERN] +{%- endif -%} + +{%- if cap_vht_antenna_cnt -%} +{%- if cap_vht_antenna_cnt|int > 1 -%} +{%- if cap_vht_beamform -%} +{%- for beamform in cap_vht_beamform -%} +{%- if 'single-user-beamformer' in beamform -%} +{%- if cap_vht_antenna_cnt|int < 6 -%} +[BF-ANTENNA-{{ cap_vht_antenna_cnt|int -1 }}][SOUNDING-DIMENSION-{{ cap_vht_antenna_cnt|int -1}}] +{%- endif -%} +{%- else -%} +{%- if cap_vht_antenna_cnt|int < 5 -%} +[BF-ANTENNA-{{ cap_vht_antenna_cnt }}][SOUNDING-DIMENSION-{{ cap_vht_antenna_cnt }}] +{%- endif -%} +{%- endif -%} +{%- endfor -%} +{%- else -%} +{%- if cap_vht_antenna_cnt|int < 5 -%} +[BF-ANTENNA-{{ cap_vht_antenna_cnt }}][SOUNDING-DIMENSION-{{ cap_vht_antenna_cnt }}] +{%- endif -%} +{%- endif -%} +{%- endif -%} +{%- endif %} + +# ieee80211n: Whether IEEE 802.11n (HT) is enabled +# 0 = disabled (default) +# 1 = enabled +# Note: You will also need to enable WMM for full HT functionality. +# Note: hw_mode=g (2.4 GHz) and hw_mode=a (5 GHz) is used to specify the band. +{% if cap_req_vht -%} +ieee80211n=0 +# Require stations to support VHT PHY (reject association if they do not) +require_vht=1 +{% endif %} + +{% if cap_vht_center_freq_1 -%} +# center freq = 5 GHz + (5 * index) +# So index 42 gives center freq 5.210 GHz +# which is channel 42 in 5G band +vht_oper_centr_freq_seg0_idx={{ cap_vht_center_freq_1 }} +{% endif %} + +{% if cap_vht_center_freq_2 -%} +# center freq = 5 GHz + (5 * index) +# So index 159 gives center freq 5.795 GHz +# which is channel 159 in 5G band +vht_oper_centr_freq_seg1_idx={{ cap_vht_center_freq_2 }} +{% endif %} + +{% if disable_broadcast_ssid -%} +# Send empty SSID in beacons and ignore probe request frames that do not +# specify full SSID, i.e., require stations to know SSID. +# default: disabled (0) +# 1 = send empty (length=0) SSID in beacon and ignore probe request for +# broadcast SSID +# 2 = clear SSID (ASCII 0), but keep the original length (this may be required +# with some clients that do not support empty SSID) and ignore probe +# requests for broadcast SSID +ignore_broadcast_ssid=1 +{% endif %} + +# Station MAC address -based authentication +# Please note that this kind of access control requires a driver that uses +# hostapd to take care of management frame processing and as such, this can be +# used with driver=hostap or driver=nl80211, but not with driver=atheros. +# 0 = accept unless in deny list +# 1 = deny unless in accept list +# 2 = use external RADIUS server (accept/deny lists are searched first) +macaddr_acl=0 + +{% if max_stations -%} +# Maximum number of stations allowed in station table. New stations will be +# rejected after the station table is full. IEEE 802.11 has a limit of 2007 +# different association IDs, so this number should not be larger than that. +# (default: 2007) +max_num_sta={{ max_stations }} +{% endif %} + +{% if isolate_stations -%} +# Client isolation can be used to prevent low-level bridging of frames between +# associated stations in the BSS. By default, this bridging is allowed. +ap_isolate=1 +{% endif %} + +{% if reduce_transmit_power -%} +# Add Power Constraint element to Beacon and Probe Response frames +# This config option adds Power Constraint element when applicable and Country +# element is added. Power Constraint element is required by Transmit Power +# Control. This can be used only with ieee80211d=1. +# Valid values are 0..255. +local_pwr_constraint={{ reduce_transmit_power }} +{% endif %} + +{% if expunge_failing_stations -%} +# Disassociate stations based on excessive transmission failures or other +# indications of connection loss. This depends on the driver capabilities and +# may not be available with all drivers. +disassoc_low_ack=1 +{% endif %} + +{% if sec_wep -%} +# IEEE 802.11 specifies two authentication algorithms. hostapd can be +# configured to allow both of these or only one. Open system authentication +# should be used with IEEE 802.1X. +# Bit fields of allowed authentication algorithms: +# bit 0 = Open System Authentication +# bit 1 = Shared Key Authentication (requires WEP) +auth_algs=2 + +# WEP rekeying (disabled if key lengths are not set or are set to 0) +# Key lengths for default/broadcast and individual/unicast keys: +# 5 = 40-bit WEP (also known as 64-bit WEP with 40 secret bits) +# 13 = 104-bit WEP (also known as 128-bit WEP with 104 secret bits) +wep_key_len_broadcast=5 +wep_key_len_unicast=5 + +# Static WEP key configuration +# +# The key number to use when transmitting. +# It must be between 0 and 3, and the corresponding key must be set. +# default: not set +wep_default_key=0 + +# The WEP keys to use. +# A key may be a quoted string or unquoted hexadecimal digits. +# The key length should be 5, 13, or 16 characters, or 10, 26, or 32 +# digits, depending on whether 40-bit (64-bit), 104-bit (128-bit), or +# 128-bit (152-bit) WEP is used. +# Only the default key must be supplied; the others are optional. +{% if sec_wep_key -%} +{% for key in sec_wep_key -%} +wep_key{{ loop.index -1 }}={{ key}} +{% endfor %} +{%- endif %} + +{% elif sec_wpa -%} +##### WPA/IEEE 802.11i configuration ########################################## + +# Enable WPA. Setting this variable configures the AP to require WPA (either +# WPA-PSK or WPA-RADIUS/EAP based on other configuration). For WPA-PSK, either +# wpa_psk or wpa_passphrase must be set and wpa_key_mgmt must include WPA-PSK. +# Instead of wpa_psk / wpa_passphrase, wpa_psk_radius might suffice. +# For WPA-RADIUS/EAP, ieee8021x must be set (but without dynamic WEP keys), +# RADIUS authentication server must be configured, and WPA-EAP must be included +# in wpa_key_mgmt. +# This field is a bit field that can be used to enable WPA (IEEE 802.11i/D3.0) +# and/or WPA2 (full IEEE 802.11i/RSN): +# bit0 = WPA +# bit1 = IEEE 802.11i/RSN (WPA2) (dot11RSNAEnabled) +{% if 'both' in sec_wpa_mode -%} +wpa=3 +{%- elif 'wpa2' in sec_wpa_mode -%} +wpa=2 +{%- elif 'wpa' in sec_wpa_mode -%} +wpa=1 +{%- endif %} + +{% if sec_wpa_cipher -%} +# Set of accepted cipher suites (encryption algorithms) for pairwise keys +# (unicast packets). This is a space separated list of algorithms: +# CCMP = AES in Counter mode with CBC-MAC (CCMP-128) +# TKIP = Temporal Key Integrity Protocol +# CCMP-256 = AES in Counter mode with CBC-MAC with 256-bit key +# GCMP = Galois/counter mode protocol (GCMP-128) +# GCMP-256 = Galois/counter mode protocol with 256-bit key +# Group cipher suite (encryption algorithm for broadcast and multicast frames) +# is automatically selected based on this configuration. If only CCMP is +# allowed as the pairwise cipher, group cipher will also be CCMP. Otherwise, +# TKIP will be used as the group cipher. The optional group_cipher parameter can +# be used to override this automatic selection. +{% if 'wpa2' in sec_wpa_mode -%} +# Pairwise cipher for RSN/WPA2 (default: use wpa_pairwise value) +rsn_pairwise={{ sec_wpa_cipher | join(" ") }} +{% else -%} +# Pairwise cipher for WPA (v1) (default: TKIP) +wpa_pairwise={{ sec_wpa_cipher | join(" ") }} +{%- endif -%} +{% endif %} + +{% if sec_wpa_passphrase -%} +# IEEE 802.11 specifies two authentication algorithms. hostapd can be +# configured to allow both of these or only one. Open system authentication +# should be used with IEEE 802.1X. +# Bit fields of allowed authentication algorithms: +# bit 0 = Open System Authentication +# bit 1 = Shared Key Authentication (requires WEP) +auth_algs=1 + +# WPA pre-shared keys for WPA-PSK. This can be either entered as a 256-bit +# secret in hex format (64 hex digits), wpa_psk, or as an ASCII passphrase +# (8..63 characters) that will be converted to PSK. This conversion uses SSID +# so the PSK changes when ASCII passphrase is used and the SSID is changed. +wpa_passphrase={{ sec_wpa_passphrase }} + +# Set of accepted key management algorithms (WPA-PSK, WPA-EAP, or both). The +# entries are separated with a space. WPA-PSK-SHA256 and WPA-EAP-SHA256 can be +# added to enable SHA256-based stronger algorithms. +# WPA-PSK = WPA-Personal / WPA2-Personal +# WPA-PSK-SHA256 = WPA2-Personal using SHA256 +wpa_key_mgmt=WPA-PSK + +{% elif sec_wpa_radius -%} +##### IEEE 802.1X-2004 related configuration ################################## +# Require IEEE 802.1X authorization +ieee8021x=1 + +# Set of accepted key management algorithms (WPA-PSK, WPA-EAP, or both). The +# entries are separated with a space. WPA-PSK-SHA256 and WPA-EAP-SHA256 can be +# added to enable SHA256-based stronger algorithms. +# WPA-EAP = WPA-Enterprise / WPA2-Enterprise +# WPA-EAP-SHA256 = WPA2-Enterprise using SHA256 +wpa_key_mgmt=WPA-EAP + +{% if sec_wpa_radius_source -%} +# RADIUS client forced local IP address for the access point +# Normally the local IP address is determined automatically based on configured +# IP addresses, but this field can be used to force a specific address to be +# used, e.g., when the device has multiple IP addresses. +radius_client_addr={{ sec_wpa_radius_source }} + +# The own IP address of the access point (used as NAS-IP-Address) +own_ip_addr={{ sec_wpa_radius_source }} +{% else %} +# The own IP address of the access point (used as NAS-IP-Address) +own_ip_addr=127.0.0.1 +{% endif %} + +{% for radius in sec_wpa_radius -%} +{%- if not radius.disabled -%} +# RADIUS authentication server +auth_server_addr={{ radius.server }} +auth_server_port={{ radius.port }} +auth_server_shared_secret={{ radius.key }} +{% if radius.acc_port -%} +# RADIUS accounting server +acct_server_addr={{ radius.server }} +acct_server_port={{ radius.acc_port }} +acct_server_shared_secret={{ radius.key }} +{% endif %} +{% endif %} +{% endfor %} + +{% endif %} + +{% else %} +# Open system +auth_algs=1 +{% endif %} + +# TX queue parameters (EDCF / bursting) +# tx_queue_<queue name>_<param> +# queues: data0, data1, data2, data3 +# (data0 is the highest priority queue) +# parameters: +# aifs: AIFS (default 2) +# cwmin: cwMin (1, 3, 7, 15, 31, 63, 127, 255, 511, 1023, 2047, 4095, 8191, +# 16383, 32767) +# cwmax: cwMax (same values as cwMin, cwMax >= cwMin) +# burst: maximum length (in milliseconds with precision of up to 0.1 ms) for +# bursting +# +# Default WMM parameters (IEEE 802.11 draft; 11-03-0504-03-000e): +# These parameters are used by the access point when transmitting frames +# to the clients. +# +# Low priority / AC_BK = background +tx_queue_data3_aifs=7 +tx_queue_data3_cwmin=15 +tx_queue_data3_cwmax=1023 +tx_queue_data3_burst=0 +# Note: for IEEE 802.11b mode: cWmin=31 cWmax=1023 burst=0 +# +# Normal priority / AC_BE = best effort +tx_queue_data2_aifs=3 +tx_queue_data2_cwmin=15 +tx_queue_data2_cwmax=63 +tx_queue_data2_burst=0 +# Note: for IEEE 802.11b mode: cWmin=31 cWmax=127 burst=0 +# +# High priority / AC_VI = video +tx_queue_data1_aifs=1 +tx_queue_data1_cwmin=7 +tx_queue_data1_cwmax=15 +tx_queue_data1_burst=3.0 +# Note: for IEEE 802.11b mode: cWmin=15 cWmax=31 burst=6.0 +# +# Highest priority / AC_VO = voice +tx_queue_data0_aifs=1 +tx_queue_data0_cwmin=3 +tx_queue_data0_cwmax=7 +tx_queue_data0_burst=1.5 + +# Default WMM parameters (IEEE 802.11 draft; 11-03-0504-03-000e): +# for 802.11a or 802.11g networks +# These parameters are sent to WMM clients when they associate. +# The parameters will be used by WMM clients for frames transmitted to the +# access point. +# +# note - txop_limit is in units of 32microseconds +# note - acm is admission control mandatory flag. 0 = admission control not +# required, 1 = mandatory +# note - Here cwMin and cmMax are in exponent form. The actual cw value used +# will be (2^n)-1 where n is the value given here. The allowed range for these +# wmm_ac_??_{cwmin,cwmax} is 0..15 with cwmax >= cwmin. +# +wmm_enabled=1 + +# Low priority / AC_BK = background +wmm_ac_bk_cwmin=4 +wmm_ac_bk_cwmax=10 +wmm_ac_bk_aifs=7 +wmm_ac_bk_txop_limit=0 +wmm_ac_bk_acm=0 +# Note: for IEEE 802.11b mode: cWmin=5 cWmax=10 +# +# Normal priority / AC_BE = best effort +wmm_ac_be_aifs=3 +wmm_ac_be_cwmin=4 +wmm_ac_be_cwmax=10 +wmm_ac_be_txop_limit=0 +wmm_ac_be_acm=0 +# Note: for IEEE 802.11b mode: cWmin=5 cWmax=7 +# +# High priority / AC_VI = video +wmm_ac_vi_aifs=2 +wmm_ac_vi_cwmin=3 +wmm_ac_vi_cwmax=4 +wmm_ac_vi_txop_limit=94 +wmm_ac_vi_acm=0 +# Note: for IEEE 802.11b mode: cWmin=4 cWmax=5 txop_limit=188 +# +# Highest priority / AC_VO = voice +wmm_ac_vo_aifs=2 +wmm_ac_vo_cwmin=2 +wmm_ac_vo_cwmax=3 +wmm_ac_vo_txop_limit=47 +wmm_ac_vo_acm=0 + diff --git a/data/templates/wifi/wpa_supplicant.conf.tmpl b/data/templates/wifi/wpa_supplicant.conf.tmpl new file mode 100644 index 000000000..2784883f1 --- /dev/null +++ b/data/templates/wifi/wpa_supplicant.conf.tmpl @@ -0,0 +1,9 @@ +# WPA supplicant config +network={ + ssid="{{ ssid }}" +{%- if sec_wpa_passphrase %} + psk="{{ sec_wpa_passphrase }}" +{% else %} + key_mgmt=NONE +{% endif %} +} diff --git a/data/templates/wwan/chat.tmpl b/data/templates/wwan/chat.tmpl new file mode 100644 index 000000000..a3395c057 --- /dev/null +++ b/data/templates/wwan/chat.tmpl @@ -0,0 +1,6 @@ +ABORT 'NO DIAL TONE' ABORT 'NO ANSWER' ABORT 'NO CARRIER' ABORT DELAYED +'' AT +OK ATZ +OK 'AT+CGDCONT=1,"IP","{{ apn }}"' +OK ATD*99# +CONNECT '' diff --git a/data/templates/wwan/ip-down.script.tmpl b/data/templates/wwan/ip-down.script.tmpl new file mode 100644 index 000000000..194f8d863 --- /dev/null +++ b/data/templates/wwan/ip-down.script.tmpl @@ -0,0 +1,26 @@ +#!/bin/sh + +tty=$2 + +# Only applicable for Wireless Modems (WWAN) +if [ -z "$(echo $tty | egrep "tty(USB|ACM)")" ]; then + exit 0 +fi + +# Determine if we are enslaved to a VRF, this is needed to properly insert +# the default route +VRF_NAME="" +if [ -d /sys/class/net/{{ intf }}/upper_* ]; then + # Determine upper (VRF) interface + VRF=$(basename $(ls -d /sys/class/net/{{ intf }}/upper_*)) + # Remove upper_ prefix from result string + VRF=${VRF#"upper_"} + # Populate variable to run in VR context + VRF_NAME=" -c vrf ${VRF_NAME} " +fi + +# Remove default route to either default or VRF routing table +vtysh -c "conf t" ${VRF_NAME} -c "no ip route 0.0.0.0/0 {{ intf }} {{ metric }}" + +DIALER_PID=$(cat /var/run/{{ intf }}.pid) +logger -t pppd[$DIALER_PID] "removed default route via {{ intf }} metric {{ metric }}" diff --git a/data/templates/wwan/ip-pre-up.script.tmpl b/data/templates/wwan/ip-pre-up.script.tmpl new file mode 100644 index 000000000..7a17a1c71 --- /dev/null +++ b/data/templates/wwan/ip-pre-up.script.tmpl @@ -0,0 +1,23 @@ +#!/bin/sh +# As WWAN is an "on demand" interface we need to re-configure it when it +# becomes 'up' + +ipparam=$6 + +# device name and metric are received using ipparam +device=`echo "$ipparam"|awk '{ print $1 }'` + +if [ "$device" != "{{ intf }}" ]; then + exit +fi + +# add some info to syslog +DIALER_PID=$(cat /var/run/{{ intf }}.pid) +logger -t pppd[$DIALER_PID] "executing $0" + +echo "{{ description }}" > /sys/class/net/{{ intf }}/ifalias + +{% if vrf -%} +logger -t pppd[$DIALER_PID] "configuring interface {{ intf }} for VRF {{ vrf }}" +ip link set dev {{ intf }} master {{ vrf }} +{% endif %} diff --git a/data/templates/wwan/ip-up.script.tmpl b/data/templates/wwan/ip-up.script.tmpl new file mode 100644 index 000000000..89e42a23a --- /dev/null +++ b/data/templates/wwan/ip-up.script.tmpl @@ -0,0 +1,25 @@ +#!/bin/sh + +tty=$2 + +# Only applicable for Wireless Modems (WWAN) +if [ -z "$(echo $tty | egrep "tty(USB|ACM)")" ]; then + exit 0 +fi + +DIALER_PID=$(cat /var/run/{{ intf }}.pid) + +# Determine if we are enslaved to a VRF, this is needed to properly insert +# the default route +VRF_NAME="" +if [ -d /sys/class/net/{{ intf }}/upper_* ]; then + # Determine upper (VRF) interface + VRF=$(basename $(ls -d /sys/class/net/{{ intf }}/upper_*)) + # Remove upper_ prefix from result string + VRF=${VRF#"upper_"} + VRF_NAME="vrf ${VRF}" +fi + +# Apply default route to either default or VRF routing table +vtysh -c "conf t" -c "ip route 0.0.0.0/0 {{ intf }} ${VRF_NAME} {{ metric }}" +logger -t pppd[$DIALER_PID] "added default route via {{ intf }} metric {{ metric }} ${VRF_NAME}" diff --git a/data/templates/wwan/peer.tmpl b/data/templates/wwan/peer.tmpl new file mode 100644 index 000000000..04ab4f844 --- /dev/null +++ b/data/templates/wwan/peer.tmpl @@ -0,0 +1,30 @@ +### Autogenerated by interfaces-wirelessmodem.py ### + +{% if description %} +# {{ description }} +{% endif %} +ifname {{ intf }} +ipparam {{ intf }} +linkname {{ intf }} +{% if name_server -%} +usepeerdns +{%- endif %} +# physical device +/dev/{{ device }} +lcp-echo-failure 0 +115200 +debug +logfile {{ logfile }} +nodefaultroute +ipcp-max-failure 4 +ipcp-accept-local +ipcp-accept-remote +noauth +crtscts +lock +persist +{% if on_demand -%} +demand +{%- endif %} + +connect '/usr/sbin/chat -v -t6 -f {{ chat_script }}' diff --git a/debian/changelog b/debian/changelog index 00ed8ea6d..80d8bf425 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,9 @@ +vyos-1x (1.3.0-17) 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' diff --git a/debian/control b/debian/control index fbcc3fa2e..7b95b2c75 100644 --- a/debian/control +++ b/debian/control @@ -29,8 +29,10 @@ Depends: python3, python3-hurry.filesize, python3-vici (>= 5.7.2), python3-bottle, + python3-netaddr, python3-zmq, cron, + easy-rsa, ipaddrcheck, tcpdump, tshark, @@ -76,7 +78,9 @@ Depends: python3, iperf, iperf3, frr, + radvd, dbus, + usb-modeswitch, hostapd (>= 0.6.8), wpasupplicant (>= 0.6.7), iw, @@ -95,6 +99,7 @@ Architecture: amd64 i386 Depends: ${misc:Depends}, ${shlibs:Depends}, + vyos-1x, open-vm-tools Description: VyOS configuration scripts and data for VMware Adds configuration files required for VyOS running on VMware hosts. diff --git a/debian/vyos-1x.install b/debian/vyos-1x.install index eb19dafeb..5004d111f 100644 --- a/debian/vyos-1x.install +++ b/debian/vyos-1x.install @@ -1,6 +1,9 @@ etc/dhcp etc/init.d +etc/ppp +etc/rsyslog.d etc/systemd +etc/udev etc/vyos lib/ opt/ diff --git a/interface-definitions/flow-accounting-conf.xml.in b/interface-definitions/flow-accounting-conf.xml.in index 5cf866b8e..239269235 100644 --- a/interface-definitions/flow-accounting-conf.xml.in +++ b/interface-definitions/flow-accounting-conf.xml.in @@ -217,7 +217,7 @@ </valueHelp> <valueHelp> <format>10</format> - <description>IPFIX</description> + <description>Internet Protocol Flow Information Export (IPFIX)</description> </valueHelp> </properties> </leafNode> diff --git a/interface-definitions/https.xml.in b/interface-definitions/https.xml.in index 4f940f7f6..9bb96f1f0 100644 --- a/interface-definitions/https.xml.in +++ b/interface-definitions/https.xml.in @@ -1,6 +1,7 @@ <?xml version="1.0"?> <!-- HTTPS configuration --> <interfaceDefinition> + <syntaxVersion component='https' version='2'></syntaxVersion> <node name="service"> <children> <node name="https" owner="${vyos_conf_scripts_dir}/https.py"> @@ -9,28 +10,37 @@ <priority>1001</priority> </properties> <children> - <tagNode name="listen-address"> + <tagNode name="virtual-host"> <properties> - <help>Addresses to listen for HTTPS requests</help> - <valueHelp> - <format>ipv4</format> - <description>HTTPS IPv4 address</description> - </valueHelp> - <valueHelp> - <format>ipv6</format> - <description>HTTPS IPv6 address</description> - </valueHelp> - <valueHelp> - <format>'*'</format> - <description>any</description> - </valueHelp> + <help>Identifier for virtual host</help> <constraint> - <validator name="ipv4-address"/> - <validator name="ipv6-address"/> - <regex>\*$</regex> + <regex>[a-zA-Z0-9-_.:]{1,255}</regex> </constraint> + <constraintErrorMessage>illegal characters in identifier or identifier longer than 255 characters</constraintErrorMessage> </properties> <children> + <leafNode name="listen-address"> + <properties> + <help>Address to listen for HTTPS requests</help> + <valueHelp> + <format>ipv4</format> + <description>HTTPS IPv4 address</description> + </valueHelp> + <valueHelp> + <format>ipv6</format> + <description>HTTPS IPv6 address</description> + </valueHelp> + <valueHelp> + <format>'*'</format> + <description>any</description> + </valueHelp> + <constraint> + <validator name="ipv4-address"/> + <validator name="ipv6-address"/> + <regex>\*$</regex> + </constraint> + </properties> + </leafNode> <leafNode name='listen-port'> <properties> <help>Port to listen for HTTPS requests; default 443</help> @@ -45,7 +55,7 @@ </leafNode> <leafNode name="server-name"> <properties> - <help>Server names: exact, wildcard, regex, or '_' (any)</help> + <help>Server names: exact, wildcard, or regex</help> <multi/> </properties> </leafNode> @@ -103,6 +113,19 @@ </leafNode> </children> </node> + <node name="api-restrict"> + <properties> + <help>Restrict api proxy to subset of virtual hosts</help> + </properties> + <children> + <leafNode name="virtual-host"> + <properties> + <help>Restrict proxy to virtual host(s)</help> + <multi/> + </properties> + </leafNode> + </children> + </node> <node name="certificates"> <properties> <help>TLS certificates</help> diff --git a/interface-definitions/include/interface-description.xml.i b/interface-definitions/include/interface-description.xml.i index 7a7a37871..961533e26 100644 --- a/interface-definitions/include/interface-description.xml.i +++ b/interface-definitions/include/interface-description.xml.i @@ -1,9 +1,9 @@ <leafNode name="description"> <properties> - <help>Interface description</help> + <help>Interface specific description</help> <constraint> <regex>.{1,256}$</regex> </constraint> - <constraintErrorMessage>Interface description too long (limit 256 characters)</constraintErrorMessage> + <constraintErrorMessage>Description too long (limit 256 characters)</constraintErrorMessage> </properties> </leafNode> diff --git a/interface-definitions/include/interface-mtu-64-8024.xml.i b/interface-definitions/include/interface-mtu-64-8024.xml.i new file mode 100644 index 000000000..e917c816f --- /dev/null +++ b/interface-definitions/include/interface-mtu-64-8024.xml.i @@ -0,0 +1,13 @@ +<leafNode name="mtu"> + <properties> + <help>Maximum Transmission Unit (MTU)</help> + <valueHelp> + <format>64-8024</format> + <description>Maximum Transmission Unit</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 64-8024"/> + </constraint> + <constraintErrorMessage>MTU must be between 64 and 8024</constraintErrorMessage> + </properties> +</leafNode> diff --git a/interface-definitions/include/interface-mtu-68-1500.xml.i b/interface-definitions/include/interface-mtu-68-1500.xml.i new file mode 100644 index 000000000..81223c332 --- /dev/null +++ b/interface-definitions/include/interface-mtu-68-1500.xml.i @@ -0,0 +1,13 @@ +<leafNode name="mtu"> + <properties> + <help>Maximum Transmission Unit (MTU)</help> + <valueHelp> + <format>68-1500</format> + <description>Maximum Transmission Unit</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 68-1500"/> + </constraint> + <constraintErrorMessage>MTU must be between 68 and 1500</constraintErrorMessage> + </properties> +</leafNode> diff --git a/interface-definitions/include/interface-vrf.xml.i b/interface-definitions/include/interface-vrf.xml.i new file mode 100644 index 000000000..355e7f0f3 --- /dev/null +++ b/interface-definitions/include/interface-vrf.xml.i @@ -0,0 +1,12 @@ +<leafNode name="vrf"> + <properties> + <help>VRF instance name</help> + <valueHelp> + <format>text</format> + <description>VRF instance name</description> + </valueHelp> + <completionHelp> + <path>vrf name</path> + </completionHelp> + </properties> +</leafNode> diff --git a/interface-definitions/include/ipv6-address.xml.i b/interface-definitions/include/ipv6-address.xml.i new file mode 100644 index 000000000..507d5dcc1 --- /dev/null +++ b/interface-definitions/include/ipv6-address.xml.i @@ -0,0 +1,22 @@ +<node name="address"> + <children> + <leafNode name="autoconf"> + <properties> + <help>Enable acquisition of IPv6 address using stateless autoconfig (SLAAC)</help> + <valueless/> + </properties> + </leafNode> + <leafNode name="eui64"> + <properties> + <help>ssign IPv6 address using EUI-64 based on MAC address</help> + <valueHelp> + <format>ipv6net</format> + <description>IPv6 address and prefix length</description> + </valueHelp> + <constraint> + <validator name="ipv6-prefix"/> + </constraint> + </properties> + </leafNode> + </children> +</node> diff --git a/interface-definitions/include/ipv6-disable-forwarding.xml.i b/interface-definitions/include/ipv6-disable-forwarding.xml.i new file mode 100644 index 000000000..3f90c7e34 --- /dev/null +++ b/interface-definitions/include/ipv6-disable-forwarding.xml.i @@ -0,0 +1,6 @@ +<leafNode name="disable-forwarding"> + <properties> + <help>Disable IPv6 forwarding on this interface</help> + <valueless/> + </properties> +</leafNode> diff --git a/interface-definitions/include/ipv6-dup-addr-detect-transmits.xml.i b/interface-definitions/include/ipv6-dup-addr-detect-transmits.xml.i new file mode 100644 index 000000000..728187560 --- /dev/null +++ b/interface-definitions/include/ipv6-dup-addr-detect-transmits.xml.i @@ -0,0 +1,16 @@ +<leafNode name="dup-addr-detect-transmits"> + <properties> + <help>Number of NS messages to send while performing DAD (default: 1)</help> + <valueHelp> + <format>1-n</format> + <description>Number of NS messages to send while performing DAD</description> + </valueHelp> + <valueHelp> + <format>0</format> + <description>Disable Duplicate Address Dectection (DAD)</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--non-negative"/> + </constraint> + </properties> +</leafNode> diff --git a/interface-definitions/include/port-number.xml.i b/interface-definitions/include/port-number.xml.i new file mode 100644 index 000000000..78eb4b7af --- /dev/null +++ b/interface-definitions/include/port-number.xml.i @@ -0,0 +1,12 @@ +<leafNode name="port">
+ <properties>
+ <help>Port number used to establish connection</help>
+ <valueHelp>
+ <format>1-65535</format>
+ <description>Numeric IP port</description>
+ </valueHelp>
+ <constraint>
+ <validator name="numeric" argument="--range 1-65535"/>
+ </constraint>
+ </properties>
+</leafNode>
diff --git a/interface-definitions/include/radius-server.xml.i b/interface-definitions/include/radius-server.xml.i new file mode 100644 index 000000000..047728233 --- /dev/null +++ b/interface-definitions/include/radius-server.xml.i @@ -0,0 +1,56 @@ +<node name="radius"> + <properties> + <help>RADIUS based user authentication</help> + </properties> + <children> + <leafNode name="source-address"> + <properties> + <help>RADIUS client source address</help> + <valueHelp> + <format>ipv4</format> + <description>IPv4 source-address of RADIUS queries</description> + </valueHelp> + <constraint> + <validator name="ipv4-address"/> + </constraint> + </properties> + </leafNode> + <tagNode name="server"> + <properties> + <help>RADIUS server configuration</help> + <valueHelp> + <format>ipv4</format> + <description>RADIUS server IPv4 address</description> + </valueHelp> + <constraint> + <validator name="ipv4-address"/> + </constraint> + </properties> + <children> + <leafNode name="disable"> + <properties> + <help>Temporary disable this server</help> + <valueless/> + </properties> + </leafNode> + <leafNode name="key"> + <properties> + <help>Shared secret key</help> + </properties> + </leafNode> + <leafNode name="port"> + <properties> + <help>Authentication port</help> + <valueHelp> + <format>1-65535</format> + <description>Numeric IP port (default: 1812)</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 1-65535"/> + </constraint> + </properties> + </leafNode> + </children> + </tagNode> + </children> +</node> diff --git a/interface-definitions/include/vif-s.xml.i b/interface-definitions/include/vif-s.xml.i index 2120aa32d..ab2dcd955 100644 --- a/interface-definitions/include/vif-s.xml.i +++ b/interface-definitions/include/vif-s.xml.i @@ -58,6 +58,7 @@ #include <include/interface-disable.xml.i> #include <include/interface-mac.xml.i> #include <include/interface-mtu-68-9000.xml.i> + #include <include/interface-vrf.xml.i> </children> </tagNode> </children> diff --git a/interface-definitions/include/vif.xml.i b/interface-definitions/include/vif.xml.i index 85e901852..819534dc1 100644 --- a/interface-definitions/include/vif.xml.i +++ b/interface-definitions/include/vif.xml.i @@ -16,6 +16,7 @@ #include <include/dhcp-dhcpv6-options.xml.i> #include <include/interface-disable-link-detect.xml.i> #include <include/interface-disable.xml.i> + #include <include/interface-vrf.xml.i> <leafNode name="egress-qos"> <properties> <help>VLAN egress QoS</help> @@ -50,6 +51,13 @@ #include <include/interface-enable-proxy-arp.xml.i> </children> </node> + <node name="ipv6"> + <children> + #include <include/ipv6-address.xml.i> + #include <include/ipv6-disable-forwarding.xml.i> + #include <include/ipv6-dup-addr-detect-transmits.xml.i> + </children> + </node> #include <include/interface-mac.xml.i> #include <include/interface-mtu-68-9000.xml.i> </children> diff --git a/interface-definitions/interfaces-bonding.xml.in b/interface-definitions/interfaces-bonding.xml.in index 586a8437d..07a6abd30 100644 --- a/interface-definitions/interfaces-bonding.xml.in +++ b/interface-definitions/interfaces-bonding.xml.in @@ -7,7 +7,7 @@ <help>Bonding Interface/Link Aggregation</help> <priority>320</priority> <constraint> - <regex>bond[0-9]+$</regex> + <regex>^bond[0-9]+$</regex> </constraint> <constraintErrorMessage>Bonding interface must be named bondN</constraintErrorMessage> <valueHelp> @@ -53,6 +53,7 @@ #include <include/dhcp-dhcpv6-options.xml.i> #include <include/interface-disable-link-detect.xml.i> #include <include/interface-disable.xml.i> + #include <include/interface-vrf.xml.i> <leafNode name="hash-policy"> <properties> <help>Bonding transmit hash policy</help> @@ -88,6 +89,13 @@ #include <include/interface-proxy-arp-pvlan.xml.i> </children> </node> + <node name="ipv6"> + <children> + #include <include/ipv6-address.xml.i> + #include <include/ipv6-disable-forwarding.xml.i> + #include <include/ipv6-dup-addr-detect-transmits.xml.i> + </children> + </node> #include <include/interface-mac.xml.i> <leafNode name="mode"> <properties> diff --git a/interface-definitions/interfaces-bridge.xml.in b/interface-definitions/interfaces-bridge.xml.in index e8285b16c..818bc9c0e 100644 --- a/interface-definitions/interfaces-bridge.xml.in +++ b/interface-definitions/interfaces-bridge.xml.in @@ -5,9 +5,9 @@ <tagNode name="bridge" owner="${vyos_conf_scripts_dir}/interfaces-bridge.py"> <properties> <help>Bridge Interface</help> - <priority>470</priority> + <priority>489</priority> <constraint> - <regex>br[0-9]+$</regex> + <regex>^br[0-9]+$</regex> </constraint> <constraintErrorMessage>Bridge interface must be named brN</constraintErrorMessage> <valueHelp> @@ -37,6 +37,7 @@ #include <include/dhcp-dhcpv6-options.xml.i> #include <include/interface-disable-link-detect.xml.i> #include <include/interface-disable.xml.i> + #include <include/interface-vrf.xml.i> <leafNode name="forwarding-delay"> <properties> <help>Forwarding delay</help> @@ -85,6 +86,13 @@ #include <include/interface-disable-arp-filter.xml.i> </children> </node> + <node name="ipv6"> + <children> + #include <include/ipv6-address.xml.i> + #include <include/ipv6-disable-forwarding.xml.i> + #include <include/ipv6-dup-addr-detect-transmits.xml.i> + </children> + </node> #include <include/interface-mac.xml.i> <leafNode name="max-age"> <properties> diff --git a/interface-definitions/interfaces-dummy.xml.in b/interface-definitions/interfaces-dummy.xml.in index 39809a610..135adfc10 100644 --- a/interface-definitions/interfaces-dummy.xml.in +++ b/interface-definitions/interfaces-dummy.xml.in @@ -7,7 +7,7 @@ <help>Dummy Interface</help> <priority>300</priority> <constraint> - <regex>dum[0-9]+$</regex> + <regex>^dum[0-9]+$</regex> </constraint> <constraintErrorMessage>Dummy interface must be named dumN</constraintErrorMessage> <valueHelp> @@ -19,6 +19,7 @@ #include <include/address-ipv4-ipv6.xml.i> #include <include/interface-description.xml.i> #include <include/interface-disable.xml.i> + #include <include/interface-vrf.xml.i> </children> </tagNode> </children> diff --git a/interface-definitions/interfaces-ethernet.xml.in b/interface-definitions/interfaces-ethernet.xml.in index 8f5d7355b..f8ec26d04 100644 --- a/interface-definitions/interfaces-ethernet.xml.in +++ b/interface-definitions/interfaces-ethernet.xml.in @@ -7,7 +7,7 @@ <help>Ethernet Interface</help> <priority>318</priority> <constraint> - <regex>((eth|lan)[0-9]+|(eno|ens|enp|enx).+)$</regex> + <regex>^((eth|lan)[0-9]+|(eno|ens|enp|enx).+)$</regex> </constraint> <constraintErrorMessage>Invalid Ethernet interface name</constraintErrorMessage> <valueHelp> @@ -31,6 +31,7 @@ </leafNode> #include <include/interface-disable-link-detect.xml.i> #include <include/interface-disable.xml.i> + #include <include/interface-vrf.xml.i> <leafNode name="duplex"> <properties> <help>Duplex mode</help> @@ -78,6 +79,13 @@ #include <include/interface-proxy-arp-pvlan.xml.i> </children> </node> + <node name="ipv6"> + <children> + #include <include/ipv6-address.xml.i> + #include <include/ipv6-disable-forwarding.xml.i> + #include <include/ipv6-dup-addr-detect-transmits.xml.i> + </children> + </node> #include <include/interface-mac.xml.i> #include <include/interface-mtu-68-9000.xml.i> <node name="offload-options"> diff --git a/interface-definitions/interfaces-geneve.xml.in b/interface-definitions/interfaces-geneve.xml.in index a6406ffc9..31a3ebb7a 100644 --- a/interface-definitions/interfaces-geneve.xml.in +++ b/interface-definitions/interfaces-geneve.xml.in @@ -7,7 +7,7 @@ <help>Generic Network Virtualization Encapsulation (GENEVE) Interface</help> <priority>460</priority> <constraint> - <regex>gnv[0-9]+$</regex> + <regex>^gnv[0-9]+$</regex> </constraint> <constraintErrorMessage>GENEVE interface must be named gnvN</constraintErrorMessage> <valueHelp> diff --git a/interface-definitions/interfaces-l2tpv3.xml.in b/interface-definitions/interfaces-l2tpv3.xml.in index a408e58c1..30dd9b604 100644 --- a/interface-definitions/interfaces-l2tpv3.xml.in +++ b/interface-definitions/interfaces-l2tpv3.xml.in @@ -5,9 +5,9 @@ <tagNode name="l2tpv3" owner="${vyos_conf_scripts_dir}/interfaces-l2tpv3.py"> <properties> <help>Layer 2 Tunnel Protocol Version 3 (L2TPv3) Interface</help> - <priority>800</priority> + <priority>485</priority> <constraint> - <regex>l2tpeth[0-9]+$</regex> + <regex>^l2tpeth[0-9]+$</regex> </constraint> <constraintErrorMessage>L2TPv3 interface must be named l2tpethN</constraintErrorMessage> <valueHelp> @@ -51,6 +51,13 @@ <constraintErrorMessage>Encapsulation must be UDP or IP</constraintErrorMessage> </properties> </leafNode> + <node name="ipv6"> + <children> + #include <include/ipv6-address.xml.i> + #include <include/ipv6-disable-forwarding.xml.i> + #include <include/ipv6-dup-addr-detect-transmits.xml.i> + </children> + </node> <leafNode name="local-ip"> <properties> <help>Local IP address for L2TPv3 tunnel</help> diff --git a/interface-definitions/interfaces-loopback.xml.in b/interface-definitions/interfaces-loopback.xml.in index ddbfad763..97d5bab90 100644 --- a/interface-definitions/interfaces-loopback.xml.in +++ b/interface-definitions/interfaces-loopback.xml.in @@ -7,7 +7,7 @@ <help>Loopback Interface</help> <priority>300</priority> <constraint> - <regex>lo$</regex> + <regex>^lo$</regex> </constraint> <constraintErrorMessage>Loopback interface must be named lo</constraintErrorMessage> <valueHelp> diff --git a/interface-definitions/interfaces-openvpn.xml.in b/interface-definitions/interfaces-openvpn.xml.in index bc1a159a9..92bac3fab 100644 --- a/interface-definitions/interfaces-openvpn.xml.in +++ b/interface-definitions/interfaces-openvpn.xml.in @@ -7,7 +7,7 @@ <help>OpenVPN Tunnel Interface</help> <priority>460</priority> <constraint> - <regex>vtun[0-9]+$</regex> + <regex>^vtun[0-9]+$</regex> </constraint> <constraintErrorMessage>OpenVPN tunnel interface must be named vtunN</constraintErrorMessage> <valueHelp> @@ -162,6 +162,13 @@ </leafNode> </children> </node> + <node name="ipv6"> + <children> + #include <include/ipv6-address.xml.i> + #include <include/ipv6-disable-forwarding.xml.i> + #include <include/ipv6-dup-addr-detect-transmits.xml.i> + </children> + </node> <leafNode name="hash"> <properties> <help>Hashing Algorithm</help> @@ -611,6 +618,18 @@ </constraint> </properties> </leafNode> + <leafNode name="crypt-file"> + <properties> + <help>File containing encryption key to authenticate control channel</help> + <valueHelp> + <format>file</format> + <description>File in /config/auth directory</description> + </valueHelp> + <constraint> + <validator name="file-exists" argument="--directory /config/auth"/> + </constraint> + </properties> + </leafNode> <leafNode name="tls-version-min"> <properties> <help>Specify the minimum required TLS version</help> diff --git a/interface-definitions/interfaces-pppoe.xml.in b/interface-definitions/interfaces-pppoe.xml.in index b6b54c915..d69e0b42c 100644 --- a/interface-definitions/interfaces-pppoe.xml.in +++ b/interface-definitions/interfaces-pppoe.xml.in @@ -7,13 +7,12 @@ <help>Point-to-Point Protocol over Ethernet (PPPoE)</help> <priority>321</priority> <constraint> - <regex>pppoe[0-9]+$</regex> - <validator name="numeric" argument="--range 1-99"/> + <regex>^pppoe[0-9]+$</regex> </constraint> <constraintErrorMessage>PPPoE interface must be named pppoeN</constraintErrorMessage> <valueHelp> <format>pppoeN</format> - <description>PPPoE interface name (1-15)</description> + <description>PPPoE dialer interface name</description> </valueHelp> </properties> <children> @@ -75,6 +74,7 @@ </leafNode> #include <include/interface-description.xml.i> #include <include/interface-disable.xml.i> + #include <include/interface-vrf.xml.i> <leafNode name="idle-timeout"> <properties> <help>Delay before disconnecting idle session (in seconds)</help> diff --git a/interface-definitions/interfaces-pseudo-ethernet.xml.in b/interface-definitions/interfaces-pseudo-ethernet.xml.in index c2dea438a..c6e61d19a 100644 --- a/interface-definitions/interfaces-pseudo-ethernet.xml.in +++ b/interface-definitions/interfaces-pseudo-ethernet.xml.in @@ -7,7 +7,7 @@ <help>Pseudo Ethernet</help> <priority>319</priority> <constraint> - <regex>peth[0-9]+$</regex> + <regex>^peth[0-9]+$</regex> </constraint> <constraintErrorMessage>Pseudo Ethernet interface must be named pethN</constraintErrorMessage> <valueHelp> @@ -21,6 +21,7 @@ #include <include/dhcp-dhcpv6-options.xml.i> #include <include/interface-disable-link-detect.xml.i> #include <include/interface-disable.xml.i> + #include <include/interface-vrf.xml.i> <node name="ip"> <children> #include <include/interface-arp-cache-timeout.xml.i> @@ -32,9 +33,16 @@ #include <include/interface-proxy-arp-pvlan.xml.i> </children> </node> - <leafNode name="link"> + <node name="ipv6"> + <children> + #include <include/ipv6-address.xml.i> + #include <include/ipv6-disable-forwarding.xml.i> + #include <include/ipv6-dup-addr-detect-transmits.xml.i> + </children> + </node> + <leafNode name="source-interface"> <properties> - <help>Lower link device</help> + <help>Physical Interface used for this device</help> <valueHelp> <format>interface</format> <description>Interface used for VXLAN underlay</description> diff --git a/interface-definitions/interfaces-tunnel.xml.in b/interface-definitions/interfaces-tunnel.xml.in new file mode 100644 index 000000000..e1ac60319 --- /dev/null +++ b/interface-definitions/interfaces-tunnel.xml.in @@ -0,0 +1,280 @@ +<?xml version="1.0"?> +<interfaceDefinition> + <node name="interfaces"> + <children> + <tagNode name="tunnel" owner="${vyos_conf_scripts_dir}/interfaces-tunnel.py"> + <properties> + <help>Tunnel interface</help> + <priority>380</priority> + <constraint> + <regex>^tun[0-9]+$</regex> + </constraint> + <constraintErrorMessage>tunnel interface must be named tunN</constraintErrorMessage> + <valueHelp> + <format>tunN</format> + <description>Tunnel interface name</description> + </valueHelp> + </properties> + <children> + #include <include/interface-description.xml.i> + #include <include/address-ipv4-ipv6.xml.i> + #include <include/interface-disable.xml.i> + #include <include/interface-disable-link-detect.xml.i> + #include <include/interface-vrf.xml.i> + #include <include/interface-mtu-64-8024.xml.i> + <node name="ipv6"> + <children> + #include <include/ipv6-address.xml.i> + #include <include/ipv6-disable-forwarding.xml.i> + #include <include/ipv6-dup-addr-detect-transmits.xml.i> + </children> + </node> + <leafNode name="local-ip"> + <properties> + <help>Local IP address for this tunnel</help> + <valueHelp> + <format>ipv4</format> + <description>Local IPv4 address for this tunnel</description> + </valueHelp> + <valueHelp> + <format>ipv6</format> + <description>Local IPv6 address for this tunnel [NOTICE: unavailable for mGRE tunnels]</description> + </valueHelp> + <completionHelp> + <script>${vyos_completion_dir}/list_local.py</script> + </completionHelp> + <constraint> + <!-- does it need fixing/changing to be more restrictive ? --> + <validator name="ip-address"/> + </constraint> + </properties> + </leafNode> + <leafNode name="remote-ip"> + <properties> + <help>Remote IP address for this tunnel</help> + <valueHelp> + <format>ipv4</format> + <description>Remote IPv4 address for this tunnel</description> + </valueHelp> + <valueHelp> + <format>ipv6</format> + <description>Remote IPv6 address for this tunnel</description> + </valueHelp> + <constraint> + <!-- does it need fixing/changing to be more restrictive ? --> + <validator name="ip-address"/> + </constraint> + </properties> + </leafNode> + <leafNode name="6rd-prefix"> + <properties> + <help>6rd network prefix</help> + <valueHelp> + <format>ipv6</format> + <description>IPv6 address and prefix length</description> + </valueHelp> + <constraint> + <validator name="ipv6-prefix"/> + </constraint> + </properties> + </leafNode> + <leafNode name="6rd-relay-prefix"> + <properties> + <help>6rd relay prefix</help> + <valueHelp> + <format>ipv4net</format> + <description>IPv4 prefix of interface for 6rd</description> + </valueHelp> + <constraint> + <validator name="ipv4-prefix"/> + </constraint> + </properties> + </leafNode> + <leafNode name="dhcp-interface"> + <properties> + <help>dhcp interface</help> + <valueHelp> + <format>interface</format> + <description>DHCP interface that supplies the local IP address for this tunnel</description> + </valueHelp> + <completionHelp> + <script>${vyos_completion_dir}/list_interfaces.py</script> + </completionHelp> + <constraint> + <regex>(en|eth|br|bond|gnv|vxlan|wg|tun)[0-9]+</regex> + </constraint> + </properties> + </leafNode> + <leafNode name="encapsulation"> + <properties> + <help>Encapsulation of this tunnel interface</help> + <completionHelp> + <list>gre gre-bridge ipip sit ipip6 ip6ip6 ip6gre</list> + </completionHelp> + <valueHelp> + <format>gre-bridge</format> + <description>Generic Routing Encapsulation bridge interface</description> + </valueHelp> + <valueHelp> + <format>ipip</format> + <description>IP in IP encapsulation</description> + </valueHelp> + <valueHelp> + <format>sit</format> + <description>Simple Internet Transition encapsulation</description> + </valueHelp> + <valueHelp> + <format>ipip6</format> + <description>IP in IP6 encapsulation</description> + </valueHelp> + <valueHelp> + <format>ip6ip6</format> + <description>IP6 in IP6 encapsulation</description> + </valueHelp> + <valueHelp> + <format>ip6gre</format> + <description>GRE over IPv6 network</description> + </valueHelp> + <constraint> + <regex>(gre|gre-bridge|ipip|sit|ipip6|ip6ip6|ip6gre)</regex> + </constraint> + <constraintErrorMessage>Must be one of 'gre' 'gre-bridge' 'ipip' 'sit' 'ipip6' 'ip6ip6' 'ip6gre'</constraintErrorMessage> + </properties> + </leafNode> + <leafNode name="multicast"> + <properties> + <help>Multicast operation over tunnel</help> + <completionHelp> + <list>enable disable</list> + </completionHelp> + <valueHelp> + <format>enable</format> + <description>Enable Multicast</description> + </valueHelp> + <valueHelp> + <format>disable</format> + <description>Disable Multicast (default)</description> + </valueHelp> + <constraint> + <regex>(enable|disable)</regex> + </constraint> + <constraintErrorMessage>Must be 'disable' or 'enable'</constraintErrorMessage> + </properties> + </leafNode> + <node name="parameters"> + <properties> + <help>Tunnel parameters</help> + </properties> + <children> + <node name="ip"> + <properties> + <help>IPv4 specific tunnel parameters</help> + </properties> + <children> + <leafNode name="ttl"> + <properties> + <help>Time to live field</help> + <valueHelp> + <format>0-255</format> + <description>Time to live (default 255)</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 0-255"/> + </constraint> + <constraintErrorMessage>TTL must be between 0 and 255</constraintErrorMessage> + </properties> + </leafNode> + <leafNode name="tos"> + <properties> + <help>Type of Service (TOS)</help> + <valueHelp> + <format>0-99</format> + <description>Type of Service (TOS)</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 0-99"/> + </constraint> + <constraintErrorMessage>TOS must be between 0 and 99</constraintErrorMessage> + </properties> + </leafNode> + <leafNode name="key"> + <properties> + <help>Tunnel key</help> + <valueHelp> + <format>0-4294967295</format> + <description>Tunnel key</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 0-4294967295"/> + </constraint> + <constraintErrorMessage>key must be between 0-4294967295</constraintErrorMessage> + </properties> + </leafNode> + </children> + </node> + <node name="ipv6"> + <properties> + <help>IPv6 specific tunnel parameters</help> + </properties> + <children> + <leafNode name="encaplimit"> + <properties> + <help>Encaplimit field</help> + <valueHelp> + <format>0-255</format> + <description>Encaplimit (default 4)</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 0-255"/> + </constraint> + <constraintErrorMessage>key must be between 0-255</constraintErrorMessage> + </properties> + </leafNode> + <leafNode name="flowlabel"> + <properties> + <help>Flowlabel</help> + <valueHelp> + <format>0x0-0x0FFFFF</format> + <description>Tunnel key, 'inherit' or hex value</description> + </valueHelp> + <constraint> + <regex>(0x){0,1}(0?[0-9A-Fa-f]{1,5})</regex> + </constraint> + <constraintErrorMessage>Must be 'inherit' or a number</constraintErrorMessage> + </properties> + </leafNode> + <leafNode name="hoplimit"> + <properties> + <help>Hoplimit</help> + <valueHelp> + <format>0-255</format> + <description>Hoplimit (default 64)</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 0-255"/> + </constraint> + <constraintErrorMessage>hoplimit must be between 0-255</constraintErrorMessage> + </properties> + </leafNode> + <leafNode name="tclass"> + <properties> + <help>Traffic class (Tclass)</help> + <valueHelp> + <format>0x0-0x0FFFFF</format> + <description>Traffic class, 'inherit' or hex value</description> + </valueHelp> + <constraint> + <regex>(0x){0,1}(0?[0-9A-Fa-f]{1,2})</regex> + </constraint> + <constraintErrorMessage>Must be 'inherit' or a number</constraintErrorMessage> + </properties> + </leafNode> + </children> + </node> + </children> + </node> + </children> + </tagNode> + </children> + </node> +</interfaceDefinition> diff --git a/interface-definitions/interfaces-vxlan.xml.in b/interface-definitions/interfaces-vxlan.xml.in index 16cb2c169..fdde57525 100644 --- a/interface-definitions/interfaces-vxlan.xml.in +++ b/interface-definitions/interfaces-vxlan.xml.in @@ -7,7 +7,7 @@ <help>Virtual Extensible LAN (VXLAN) Interface</help> <priority>460</priority> <constraint> - <regex>vxlan[0-9]+$</regex> + <regex>^vxlan[0-9]+$</regex> </constraint> <constraintErrorMessage>VXLAN interface must be named vxlanN</constraintErrorMessage> <valueHelp> @@ -45,9 +45,28 @@ #include <include/interface-enable-proxy-arp.xml.i> </children> </node> - <leafNode name="link"> + <node name="ipv6"> + <children> + #include <include/ipv6-address.xml.i> + #include <include/ipv6-disable-forwarding.xml.i> + #include <include/ipv6-dup-addr-detect-transmits.xml.i> + </children> + </node> + <leafNode name="source-address"> + <properties> + <help>VXLAN source address</help> + <valueHelp> + <format>ipv4</format> + <description>IPv4 source-address of VXLAN tunnel</description> + </valueHelp> + <constraint> + <validator name="ipv4-address"/> + </constraint> + </properties> + </leafNode> + <leafNode name="source-interface"> <properties> - <help>Underlay device of VXLAN interface</help> + <help>Physical Interface used for this connection</help> <valueHelp> <format>interface</format> <description>Interface used for VXLAN underlay</description> diff --git a/interface-definitions/interfaces-wireguard.xml.in b/interface-definitions/interfaces-wireguard.xml.in index dd4a73efd..9db608afb 100644 --- a/interface-definitions/interfaces-wireguard.xml.in +++ b/interface-definitions/interfaces-wireguard.xml.in @@ -7,7 +7,7 @@ <help>WireGuard Interface</help> <priority>459</priority> <constraint> - <regex>wg[0-9]+$</regex> + <regex>^wg[0-9]+$</regex> </constraint> <constraintErrorMessage>WireGuard interface must be named wgN</constraintErrorMessage> <valueHelp> @@ -19,22 +19,9 @@ #include <include/address-ipv4-ipv6.xml.i> #include <include/interface-description.xml.i> #include <include/interface-disable.xml.i> - <leafNode name="port"> - <properties> - <help>Local port number to accept connections</help> - <constraint> - <validator name="numeric" argument="--range 1024-65535"/> - </constraint> - </properties> - </leafNode> - <leafNode name="mtu"> - <properties> - <help>interface mtu size(default: 1420)</help> - <constraint> - <validator name="numeric" argument="--range 68-9000"/> - </constraint> - </properties> - </leafNode> + #include <include/interface-vrf.xml.i> + #include <include/port-number.xml.i> + #include <include/interface-mtu-68-9000.xml.i> <leafNode name="fwmark"> <properties> <help>A 32-bit fwmark value set on all outgoing packets</help> @@ -97,12 +84,19 @@ <multi/> </properties> </leafNode> - <!-- eventually check format IP:port --> - <leafNode name="endpoint"> + <leafNode name="address"> <properties> - <help>Remote endpoint (IP:port)</help> + <help>IP address of tunnel remote end</help> + <valueHelp> + <format>ipv4</format> + <description>IP address to listen for incoming connections</description> + </valueHelp> + <constraint> + <validator name="ipv4-address"/> + </constraint> </properties> </leafNode> + #include <include/port-number.xml.i> <leafNode name="persistent-keepalive"> <properties> <help>how often send keep alives in seconds</help> diff --git a/interface-definitions/interfaces-wireless.xml.in b/interface-definitions/interfaces-wireless.xml.in index d6b257978..194669f77 100644 --- a/interface-definitions/interfaces-wireless.xml.in +++ b/interface-definitions/interfaces-wireless.xml.in @@ -7,7 +7,7 @@ <help>Wireless (WiFi/WLAN) Network Interface</help> <priority>400</priority> <constraint> - <regex>wlan[0-9]+$</regex> + <regex>^wlan[0-9]+$</regex> </constraint> <constraintErrorMessage>Wireless interface must be named wlanN</constraintErrorMessage> <valueHelp> @@ -208,11 +208,11 @@ <properties> <help>Number of antennas on this card</help> <valueHelp> - <format>1-9</format> + <format>1-8</format> <description>Number of antennas for this card</description> </valueHelp> <constraint> - <validator name="numeric" argument="--range 1-9"/> + <validator name="numeric" argument="--range 1-8"/> </constraint> </properties> </leafNode> @@ -320,7 +320,7 @@ <properties> <help>VHT link adaptation capabilities</help> <completionHelp> - <list>single-user-beamformer single-user-beamformee multi-user-beamformer multi-user-beamformee</list> + <list>unsolicited both</list> </completionHelp> <valueHelp> <format>unsolicited</format> @@ -454,6 +454,7 @@ </leafNode> #include <include/interface-disable-link-detect.xml.i> #include <include/interface-disable.xml.i> + #include <include/interface-vrf.xml.i> <leafNode name="expunge-failing-stations"> <properties> <help>Disassociate stations based on excessive transmission failures</help> @@ -468,6 +469,13 @@ #include <include/interface-enable-arp-ignore.xml.i> </children> </node> + <node name="ipv6"> + <children> + #include <include/ipv6-address.xml.i> + #include <include/ipv6-disable-forwarding.xml.i> + #include <include/ipv6-dup-addr-detect-transmits.xml.i> + </children> + </node> <leafNode name="hw-id"> <properties> <help>Media Access Control (MAC) address</help> @@ -666,28 +674,10 @@ <constraintErrorMessage>Invalid WPA pass phrase, must be 8 to 63 printable characters!</constraintErrorMessage> </properties> </leafNode> + #include <include/radius-server.xml.i> <node name="radius"> - <properties> - <help>RADIUS specific configuration</help> - </properties> <children> - <leafNode name="source-address"> - <properties> - <help>RADIUS client forced local IP address</help> - <valueHelp> - <format>ipv4</format> - <description>IPv4 address of RADIUS server</description> - </valueHelp> - </properties> - </leafNode> <tagNode name="server"> - <properties> - <help>IP address of RADIUS server</help> - <valueHelp> - <format>ipv4</format> - <description>IPv4 address of RADIUS server</description> - </valueHelp> - </properties> <children> <leafNode name="accounting"> <properties> @@ -695,23 +685,6 @@ <valueless/> </properties> </leafNode> - <leafNode name="port"> - <properties> - <help>RADIUS server port (default: 1812)</help> - <valueHelp> - <format>1-65535</format> - <description>RADIUS server port</description> - </valueHelp> - <constraint> - <validator name="numeric" argument="--range 1-65535"/> - </constraint> - </properties> - </leafNode> - <leafNode name="key"> - <properties> - <help>RADIUS shared secret key</help> - </properties> - </leafNode> </children> </tagNode> </children> diff --git a/interface-definitions/interfaces-wirelessmodem.xml.in b/interface-definitions/interfaces-wirelessmodem.xml.in new file mode 100644 index 000000000..6bec34b56 --- /dev/null +++ b/interface-definitions/interfaces-wirelessmodem.xml.in @@ -0,0 +1,81 @@ +<?xml version="1.0"?> +<interfaceDefinition> + <node name="interfaces"> + <children> + <tagNode name="wirelessmodem" owner="${vyos_conf_scripts_dir}/interfaces-wirelessmodem.py"> + <properties> + <help>Wireless Modem (WWAN) Interface</help> + <priority>350</priority> + <constraint> + <regex>^wlm[0-9]+$</regex> + </constraint> + <constraintErrorMessage>Wireless Modem interface must be named wlmN</constraintErrorMessage> + <valueHelp> + <format>wlmN</format> + <description>Wireless modem interface name</description> + </valueHelp> + </properties> + <children> + <leafNode name="apn"> + <properties> + <help>Access Point Name (APN)</help> + </properties> + </leafNode> + <node name="backup"> + <properties> + <help>Insert backup default route</help> + </properties> + <children> + <leafNode name="distance"> + <properties> + <help>Distance backup default route</help> + <valueHelp> + <format>1-255</format> + <description>Distance of the backup route (default: 10)</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 1-255"/> + </constraint> + <constraintErrorMessage>Must be between (1-255)</constraintErrorMessage> + </properties> + </leafNode> + </children> + </node> + #include <include/interface-description.xml.i> + #include <include/interface-disable.xml.i> + #include <include/interface-vrf.xml.i> + <leafNode name="device"> + <properties> + <help>System device name (default: ttyUSB0)</help> + <valueHelp> + <format>ttyXXX</format> + <description>System TTY device name</description> + </valueHelp> + </properties> + </leafNode> + #include <include/interface-disable-link-detect.xml.i> + #include <include/interface-mtu-68-9000.xml.i> + <node name="ipv6"> + <children> + #include <include/ipv6-address.xml.i> + #include <include/ipv6-disable-forwarding.xml.i> + #include <include/ipv6-dup-addr-detect-transmits.xml.i> + </children> + </node> + <leafNode name="no-peer-dns"> + <properties> + <help>Do not use peer supplied DNS server information</help> + <valueless/> + </properties> + </leafNode> + <leafNode name="ondemand"> + <properties> + <help>Only dial when traffic is available</help> + <valueless/> + </properties> + </leafNode> + </children> + </tagNode> + </children> + </node> +</interfaceDefinition> diff --git a/interface-definitions/protocols-igmp.xml.in b/interface-definitions/protocols-igmp.xml.in new file mode 100644 index 000000000..a9b11e1a3 --- /dev/null +++ b/interface-definitions/protocols-igmp.xml.in @@ -0,0 +1,88 @@ +<?xml version="1.0"?> +<!-- Internet Group Management Protocol (IGMP) configuration --> +<interfaceDefinition> + <node name="protocols"> + <children> + <node name="igmp" owner="${vyos_conf_scripts_dir}/protocols_igmp.py"> + <properties> + <help>Internet Group Management Protocol (IGMP)</help> + </properties> + <children> + <tagNode name="interface"> + <properties> + <help>IGMP interface</help> + <completionHelp> + <script>${vyos_completion_dir}/list_interfaces.py</script> + </completionHelp> + </properties> + <children> + <tagNode name="join"> + <properties> + <help>IGMP join multicast group</help> + <valueHelp> + <format>ipv4</format> + <description>Multicast group address</description> + </valueHelp> + <constraint> + <validator name="ipv4-address"/> + </constraint> + </properties> + <children> + <leafNode name="source"> + <properties> + <help>Source address</help> + <valueHelp> + <format>ipv4</format> + <description>Source address</description> + </valueHelp> + <constraint> + <validator name="ipv4-address"/> + </constraint> + <multi/> + </properties> + </leafNode> + </children> + </tagNode> + <leafNode name="version"> + <properties> + <help>IGMP version</help> + <valueHelp> + <format>2-3</format> + <description>IGMP version</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 2-3"/> + </constraint> + </properties> + </leafNode> + <leafNode name="query-interval"> + <properties> + <help>IGMP host query interval</help> + <valueHelp> + <format>1-1800</format> + <description>Query interval in seconds</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 1-1800"/> + </constraint> + </properties> + </leafNode> + <leafNode name="query-max-response-time"> + <properties> + <help>IGMP max query response time</help> + <valueHelp> + <format>10-250</format> + <description>Query response value in deci-seconds</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 10-250"/> + </constraint> + </properties> + </leafNode> + </children> + </tagNode> + </children> + </node> + </children> + </node> +</interfaceDefinition> diff --git a/interface-definitions/protocols-mpls.xml.in b/interface-definitions/protocols-mpls.xml.in new file mode 100644 index 000000000..376323855 --- /dev/null +++ b/interface-definitions/protocols-mpls.xml.in @@ -0,0 +1,98 @@ +<?xml version="1.0"?> +<!-- Multiprotocol Label Switching (MPLS) configuration --> +<interfaceDefinition> + <node name="protocols"> + <children> + <node name="mpls" owner="${vyos_conf_scripts_dir}/protocols_mpls.py"> + <properties> + <help>Multiprotocol Label Switching (MPLS)</help> + <priority>299</priority> + </properties> + <children> + <node name="ldp"> + <properties> + <help>LDP options</help> + </properties> + <children> + <leafNode name="router-id"> + <properties> + <help>x.x.x.x Label Switch Router (LSR) id</help> + <valueHelp> + <format>ipv4</format> + <description>LSR ipv4 id</description> + </valueHelp> + <constraint> + <validator name="ipv4-address"/> + </constraint> + </properties> + </leafNode> + <tagNode name="neighbor"> + <properties> + <help>LDP Id of neighbor</help> + <valueHelp> + <format>ipv4</format> + <description>neighbor IPv4 id</description> + </valueHelp> + <constraint> + <validator name="ipv4-address"/> + </constraint> + </properties> + <children> + <leafNode name="password"> + <properties> + <help>Peer password</help> + </properties> + </leafNode> + </children> + </tagNode> + <node name="discovery"> + <properties> + <help>Discovery parameters</help> + <valueHelp> + <format>ipv4</format> + <description>Discovery parameters</description> + </valueHelp> + </properties> + <children> + <leafNode name="transport-ipv4-address"> + <properties> + <help>Transport ipv4 address</help> + <valueHelp> + <format>ipv4</format> + <description>IPv4 bind as transport</description> + </valueHelp> + <constraint> + <validator name="ipv4-address"/> + </constraint> + </properties> + </leafNode> + <leafNode name="transport-ipv6-address"> + <properties> + <help>Transport ipv6 address</help> + <valueHelp> + <format>ipv6</format> + <description>IPv6 bind as transport</description> + </valueHelp> + <constraint> + <validator name="ipv6-address"/> + </constraint> + </properties> + </leafNode> + </children> + </node> + <leafNode name="interface"> + <properties> + <help>Listen interface for LDP</help> + <completionHelp> + <script>${vyos_completion_dir}/list_interfaces.py</script> + </completionHelp> + <multi/> + </properties> + </leafNode> + </children> + </node> + </children> + </node> + </children> + </node> +</interfaceDefinition>
\ No newline at end of file diff --git a/interface-definitions/protocols-pim.xml.in b/interface-definitions/protocols-pim.xml.in new file mode 100644 index 000000000..6152045a7 --- /dev/null +++ b/interface-definitions/protocols-pim.xml.in @@ -0,0 +1,96 @@ +<?xml version="1.0"?> +<!-- Protocol Independent Multicast (PIM) configuration --> +<interfaceDefinition> + <node name="protocols"> + <children> + <node name="pim" owner="${vyos_conf_scripts_dir}/protocols_pim.py"> + <properties> + <help>Protocol Independent Multicast (PIM)</help> + <priority>400</priority> + </properties> + <children> + <tagNode name="interface"> + <properties> + <help>PIM interface</help> + <completionHelp> + <script>${vyos_completion_dir}/list_interfaces.py</script> + </completionHelp> + </properties> + <children> + <leafNode name="dr-priority"> + <properties> + <help>Designated Router Election Priority</help> + <valueHelp> + <format>1-4294967295</format> + <description>Value of the new DR Priority</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 1-4294967295"/> + </constraint> + </properties> + </leafNode> + <leafNode name="hello"> + <properties> + <help>Hello Interval</help> + <valueHelp> + <format>1-180</format> + <description>Hello Interval in seconds</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 1-180"/> + </constraint> + </properties> + </leafNode> + </children> + </tagNode> + <node name="rp"> + <properties> + <help>Rendezvous Point</help> + </properties> + <children> + <tagNode name="address"> + <properties> + <help>Rendezvous Point address</help> + <valueHelp> + <format>ipv4</format> + <description>Rendezvous Point address</description> + </valueHelp> + <constraint> + <validator name="ipv4-address"/> + </constraint> + </properties> + <children> + <leafNode name="group"> + <properties> + <help>Group Address range</help> + <valueHelp> + <format>ipv4net</format> + <description>Group Address range RFC 3171</description> + </valueHelp> + <constraint> + <validator name="ip-prefix"/> + </constraint> + <multi/> + </properties> + </leafNode> + </children> + </tagNode> + <leafNode name="keep-alive-timer"> + <properties> + <help>Keep alive Timer</help> + <valueHelp> + <format>31-60000</format> + <description>Keep alive Timer in seconds</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 31-60000"/> + </constraint> + </properties> + </leafNode> + </children> + </node> + </children> + </node> + </children> + </node> +</interfaceDefinition> diff --git a/interface-definitions/salt-minion.xml.in b/interface-definitions/salt-minion.xml.in new file mode 100644 index 000000000..9aa60249a --- /dev/null +++ b/interface-definitions/salt-minion.xml.in @@ -0,0 +1,85 @@ +<?xml version="1.0"?> +<!--Salt-minion configuration --> +<interfaceDefinition> + <node name="service"> + <children> + <node name="salt-minion" owner="${vyos_conf_scripts_dir}/salt-minion.py"> + <properties> + <help>Salt Minion</help> + <priority>500</priority> + </properties> + <children> + <leafNode name="hash_type"> + <properties> + <help>The hash_type is the hash to use when discovering the hash of a file on the master server.</help> + </properties> + </leafNode> + <leafNode name="log_file"> + <properties> + <help>The location of the minion log file.</help> + </properties> + </leafNode> + <leafNode name="log_level"> + <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> + <valueHelp> + <format>error</format> + <description>log error info</description> + </valueHelp> + <valueHelp> + <format>critical</format> + <description>log critical info</description> + </valueHelp> + </properties> + </leafNode> + <leafNode name="master"> + <properties> + <help>The hostname or IP address of the master.</help> + <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> + </properties> + </leafNode> + <leafNode name="mine_interval"> + <properties> + <help>The number of minutes between mine updates.</help> + </properties> + </leafNode> + <leafNode name="master-key"> + <properties> + <help>Enables verification of the master-public-signature returned by the master in auth-replies.</help> + </properties> + </leafNode> + </children> + </node> + </children> + </node> +</interfaceDefinition> diff --git a/interface-definitions/service-router-advert.xml.in b/interface-definitions/service-router-advert.xml.in new file mode 100644 index 000000000..bd63b15a3 --- /dev/null +++ b/interface-definitions/service-router-advert.xml.in @@ -0,0 +1,266 @@ +<?xml version="1.0"?> +<interfaceDefinition> + <node name="service"> + <children> + <node name="router-advert" owner="${vyos_conf_scripts_dir}/service-router-advert.py"> + <properties> + <help>IPv6 Router Advertisements (RAs) service</help> + <priority>900</priority> + </properties> + <children> + <tagNode name="interface"> + <properties> + <help>Interface to send DDNS updates for [REQUIRED]</help> + <completionHelp> + <script>${vyos_completion_dir}/list_interfaces.py</script> + </completionHelp> + </properties> + <children> + <leafNode name="hop-limit"> + <properties> + <help>Set Hop Count field of the IP header for outgoing packets (default: 64)</help> + <valueHelp> + <format>1-255</format> + <description>Value should represent current diameter of the Internet</description> + </valueHelp> + <valueHelp> + <format>0</format> + <description>Unspecified (by this router)</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 0-255"/> + </constraint> + <constraintErrorMessage>Hop count must be between 0 and 255</constraintErrorMessage> + </properties> + </leafNode> + <leafNode name="default-lifetime"> + <properties> + <help>Lifetime associated with the default router in units of seconds</help> + <valueHelp> + <format>4-9000</format> + <description>Router Lifetime in seconds</description> + </valueHelp> + <valueHelp> + <format>0</format> + <description>Not a default router</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 0-0 --range 4-9000"/> + </constraint> + <constraintErrorMessage>Default router livetime bust be 0 or between 4 and 9000</constraintErrorMessage> + </properties> + </leafNode> + <leafNode name="default-preference"> + <properties> + <help>Preference associated with the default router,</help> + <completionHelp> + <list>low medium high</list> + </completionHelp> + <valueHelp> + <format>low</format> + <description>Default router has low preference</description> + </valueHelp> + <valueHelp> + <format>medium</format> + <description>Default router has medium preference (default)</description> + </valueHelp> + <valueHelp> + <format>high</format> + <description>Default router has high preference</description> + </valueHelp> + <constraint> + <regex>(low|medium|high)</regex> + </constraint> + <constraintErrorMessage>Default preference must be low, medium or high</constraintErrorMessage> + </properties> + </leafNode> + <leafNode name="dnssl"> + <properties> + <help>DNS search list</help> + <multi/> + </properties> + </leafNode> + <leafNode name="link-mtu"> + <properties> + <help>Link MTU value placed in RAs, exluded in RAs if unset</help> + <valueHelp> + <format>1280-9000</format> + <description>Link MTU value in RAs</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 1280-9000"/> + </constraint> + <constraintErrorMessage>Link MTU must be between 1280 and 9000</constraintErrorMessage> + </properties> + </leafNode> + <leafNode name="managed-flag"> + <properties> + <help>Hosts use the administered (stateful) protocol for address autoconfiguration in addition to any addresses autoconfigured using SLAAC</help> + <valueless/> + </properties> + </leafNode> + <node name="interval"> + <properties> + <help>Set interval between unsolicited multicast RAs</help> + </properties> + <children> + <leafNode name="max"> + <properties> + <help>Maximum interval between unsolicited multicast RAs (default: 600)</help> + <valueHelp> + <format>4-1800</format> + <description>Maximum interval in seconds</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 4-1800"/> + </constraint> + <constraintErrorMessage>Maximum interval must be between 4 and 1800 seconds</constraintErrorMessage> + </properties> + </leafNode> + <leafNode name="min"> + <properties> + <help>Minimum interval between unsolicited multicast RAs</help> + <valueHelp> + <format>3-1350</format> + <description>Minimum interval in seconds</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 3-1350"/> + </constraint> + <constraintErrorMessage>Minimum interval must be between 3 and 1350 seconds</constraintErrorMessage> + </properties> + </leafNode> + </children> + </node> + <leafNode name="name-server"> + <properties> + <help>IPv6 address of recursive DNS server</help> + <valueHelp> + <format>ipv6</format> + <description>IPv6 address of DNS name server</description> + </valueHelp> + <constraint> + <validator name="ipv6-address"/> + </constraint> + <multi/> + </properties> + </leafNode> + <leafNode name="other-config-flag"> + <properties> + <help>Hosts use the administered (stateful) protocol for autoconfiguration of other (non-address) information</help> + <valueless/> + </properties> + </leafNode> + <tagNode name="prefix"> + <properties> + <help>IPv6 prefix to be advertised in Router Advertisements (RAs)</help> + <valueHelp> + <format>ipv6net</format> + <description>IPv6 prefix to be advertized</description> + </valueHelp> + <constraint> + <validator name="ipv6-prefix"/> + </constraint> + </properties> + <children> + <leafNode name="no-autonomous-flag"> + <properties> + <help>Prefix can not be used for stateless address auto-configuration</help> + <valueless/> + </properties> + </leafNode> + <leafNode name="no-on-link-flag"> + <properties> + <help>Prefix can not be used for on-link determination</help> + <valueless/> + </properties> + </leafNode> + <leafNode name="preferred-lifetime"> + <properties> + <help>Time in seconds that the prefix will remain preferred (default 4 hours)</help> + <completionHelp> + <list>infinity</list> + </completionHelp> + <valueHelp> + <format>0-4294967295</format> + <description>Time in seconds that the prefix will remain preferred</description> + </valueHelp> + <valueHelp> + <format>infinity</format> + <description>Prefix will remain preferred forever</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 0-4294967295"/> + <regex>(infinity)</regex> + </constraint> + </properties> + </leafNode> + <leafNode name="valid-lifetime"> + <properties> + <help>Time in seconds that the prefix will remain valid (default: 30 days)</help> + <completionHelp> + <list>infinity</list> + </completionHelp> + <valueHelp> + <format>1-4294967295</format> + <description>Time in seconds that the prefix will remain valid</description> + </valueHelp> + <valueHelp> + <format>infinity</format> + <description>Prefix will remain preferred forever</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 0-4294967295"/> + <regex>(infinity)</regex> + </constraint> + </properties> + </leafNode> + </children> + </tagNode> + <leafNode name="reachable-time"> + <properties> + <help>Time, in milliseconds, that a node assumes a neighbor is reachable after having received a reachability confirmation</help> + <valueHelp> + <format>1-3600000</format> + <description>Reachable Time value in RAs (in milliseconds)</description> + </valueHelp> + <valueHelp> + <format>0</format> + <description>Reachable Time unspecified by this router</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 0-0 --range 1-3600000"/> + </constraint> + <constraintErrorMessage>Reachable time must be 0 or between 1 and 3600000 milliseconds</constraintErrorMessage> + </properties> + </leafNode> + <leafNode name="retrans-timer"> + <properties> + <help>Time in milliseconds between retransmitted Neighbor Solicitation messages</help> + <valueHelp> + <format>1-4294967295</format> + <description>Minimum interval in milliseconds</description> + </valueHelp> + <valueHelp> + <format>0</format> + <description>Time, in milliseconds, between retransmitted Neighbor Solicitation messages</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 0-0 --range 1-4294967295"/> + </constraint> + <constraintErrorMessage>Retransmit interval must be 0 or between 1 and 4294967295 milliseconds</constraintErrorMessage> + </properties> + </leafNode> + <leafNode name="no-send-advert"> + <properties> + <help>Do not send router adverts</help> + <valueless/> + </properties> + </leafNode> + </children> + </tagNode> + </children> + </node> + </children> + </node> +</interfaceDefinition> diff --git a/interface-definitions/system-login.xml.in b/interface-definitions/system-login.xml.in index 3ed85b8d3..2499a192c 100644 --- a/interface-definitions/system-login.xml.in +++ b/interface-definitions/system-login.xml.in @@ -110,58 +110,11 @@ </leafNode> </children> </tagNode> + #include <include/radius-server.xml.i> <node name="radius"> - <properties> - <help>RADIUS based user authentication</help> - </properties> <children> - <leafNode name="source-address"> - <properties> - <help>RADIUS client source address</help> - <valueHelp> - <format>ipv4</format> - <description>TFTP IPv4 listen address</description> - </valueHelp> - <constraint> - <validator name="ipv4-address"/> - </constraint> - </properties> - </leafNode> <tagNode name="server"> - <properties> - <help>RADIUS server configuration</help> - <valueHelp> - <format>ipv4</format> - <description>RADIUS server IPv4 address</description> - </valueHelp> - <constraint> - <validator name="ipv4-address"/> - </constraint> - </properties> <children> - <leafNode name="disable"> - <properties> - <help>Temporary disable this server</help> - <valueless/> - </properties> - </leafNode> - <leafNode name="key"> - <properties> - <help>Shared secret key</help> - </properties> - </leafNode> - <leafNode name="port"> - <properties> - <help>Authentication port</help> - <valueHelp> - <format>1-65535</format> - <description>Numeric IP port (default: 1812)</description> - </valueHelp> - <constraint> - <validator name="numeric" argument="--range 1-65535"/> - </constraint> - </properties> - </leafNode> <leafNode name="timeout"> <properties> <help>Session timeout</help> diff --git a/interface-definitions/system-syslog.xml.in b/interface-definitions/system-syslog.xml.in index 2079ec0ea..194cdb851 100644 --- a/interface-definitions/system-syslog.xml.in +++ b/interface-definitions/system-syslog.xml.in @@ -181,13 +181,17 @@ <properties> <help>Logging to a remote host</help> <constraint> - <validator name="ip-address" /> - <regex>(?=^.{4,253}$)(^((?!-)[a-zA-Z0-9-]{0,62}[a-zA-Z0-9]\.)+[a-zA-Z]{2,63}$)</regex> + <validator name="ip-address"/> + <validator name="fqdn"/> </constraint> - <constraintErrorMessage>Invalid host FQDN or IP address</constraintErrorMessage> + <constraintErrorMessage>Invalid host (FQDN or IP address)</constraintErrorMessage> <valueHelp> - <format>x.x.x.x or host.domain.tld</format> - <description>Remote host name or IP address</description> + <format>ipv4</format> + <description>Remote syslog server IPv4 address</description> + </valueHelp> + <valueHelp> + <format>hostname</format> + <description>Remote syslog server FQDN</description> </valueHelp> </properties> <children> diff --git a/interface-definitions/l2tp-server.xml.in b/interface-definitions/vpn-l2tp.xml.in index 7fc844054..7fc844054 100644 --- a/interface-definitions/l2tp-server.xml.in +++ b/interface-definitions/vpn-l2tp.xml.in diff --git a/interface-definitions/sstp.xml.in b/interface-definitions/vpn-sstp.xml.in index 10b97b833..b026417b3 100644 --- a/interface-definitions/sstp.xml.in +++ b/interface-definitions/vpn-sstp.xml.in @@ -1,11 +1,11 @@ <?xml version="1.0"?> <interfaceDefinition> - <node name="service"> + <node name="vpn"> <children> - <node name="sstp-server" owner="${vyos_conf_scripts_dir}/accel_sstp.py"> + <node name="sstp" owner="${vyos_conf_scripts_dir}/vpn_sstp.py"> <properties> - <help>Secure Socket Tunneling Protocol (SSTP) Server</help> - <priority>900</priority> + <help>Secure Socket Tunneling Protocol (SSTP) server</help> + <priority>901</priority> </properties> <children> <node name="authentication"> @@ -113,85 +113,96 @@ <multi /> </properties> </leafNode> - <tagNode name="radius-server"> - <properties> - <help>IP address of RADIUS server</help> - <valueHelp> - <format>ipv4</format> - <description>IP address of RADIUS server</description> - </valueHelp> - </properties> - <children> - <leafNode name="secret"> - <properties> - <help>Key for accessing the specified server</help> - </properties> - </leafNode> - <leafNode name="req-limit"> - <properties> - <help>Maximum number of simultaneous requests to server (default: unlimited)</help> - </properties> - </leafNode> - <leafNode name="fail-time"> - <properties> - <help>If server does not responds mark it as unavailable for this time (seconds)</help> - </properties> - </leafNode> - </children> - </tagNode> - <node name="radius-settings"> - <properties> - <help>RADIUS settings</help> - </properties> + #include <include/radius-server.xml.i> + <node name="radius"> <children> + <tagNode name="server"> + <children> + <leafNode name="fail-time"> + <properties> + <help>Mark server unavailable for <n> seconds on failure</help> + <valueHelp> + <format>0-600</format> + <description>Fail time penalty</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 0-600"/> + </constraint> + <constraintErrorMessage>Fail time must be between 0 and 600 seconds</constraintErrorMessage> + </properties> + </leafNode> + </children> + </tagNode> <leafNode name="timeout"> <properties> - <help>Timeout to wait response from server (seconds)</help> + <help>Timeout in seconds to wait response from RADIUS server</help> + <valueHelp> + <format>1-60</format> + <description>Timeout in seconds</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 1-60"/> + </constraint> + <constraintErrorMessage>Timeout must be between 1 and 60 seconds</constraintErrorMessage> </properties> </leafNode> <leafNode name="acct-timeout"> <properties> - <help>Timeout to wait reply for Interim-Update packets. (default 3 seconds)</help> + <help>Timeout for Interim-Update packets, terminate session afterwards (default 3 seconds)</help> + <valueHelp> + <format>0-60</format> + <description>Timeout in seconds, 0 to keep active</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 0-60"/> + </constraint> + <constraintErrorMessage>Timeout must be between 0 and 60 seconds</constraintErrorMessage> </properties> </leafNode> <leafNode name="max-try"> <properties> - <help>Maximum number of tries to send Access-Request/Accounting-Request queries</help> + <help>Number of tries to send Access-Request/Accounting-Request queries</help> + <valueHelp> + <format>1-20</format> + <description>Maximum tries</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 1-20"/> + </constraint> + <constraintErrorMessage>Maximum tries must be between 1 and 20</constraintErrorMessage> </properties> </leafNode> <leafNode name="nas-identifier"> <properties> - <help>Value to send to RADIUS server in NAS-Identifier attribute and to be matched in DM/CoA requests.</help> + <help>NAS-Identifier attribute sent to RADIUS</help> </properties> </leafNode> <leafNode name="nas-ip-address"> <properties> - <help>Value to send to RADIUS server in NAS-IP-Address attribute and to be matched in DM/CoA requests. Also DM/CoA server will bind to that address.</help> + <help>NAS-IP-Address attribute sent to RADIUS</help> <constraint> <validator name="ipv4-address"/> </constraint> - <constraintErrorMessage>invalid IPv4 address</constraintErrorMessage> <valueHelp> <format>ipv4</format> - <description>NAS-IP-Address Attribute Value</description> + <description>NAS-IP-Address attribute</description> </valueHelp> - </properties> - </leafNode> - <node name="dae-server"> + </properties> + </leafNode> + <node name="dynamic-author"> <properties> - <help>IPv4 address and port to bind Dynamic Authorization Extension server (DM/CoA)</help> + <help>Dynamic Authorization Extension/Change of Authorization server</help> </properties> <children> - <leafNode name="ip-address"> + <leafNode name="server"> <properties> <help>IP address for Dynamic Authorization Extension server (DM/CoA)</help> <constraint> <validator name="ipv4-address"/> </constraint> - <constraintErrorMessage>invalid IPv4 address</constraintErrorMessage> <valueHelp> <format>ipv4</format> - <description>Specifies IP address for Dynamic Authorization Extension server (DM/CoA)</description> + <description>IPv4 address for aynamic authorization server</description> </valueHelp> </properties> </leafNode> @@ -207,9 +218,9 @@ </constraint> </properties> </leafNode> - <leafNode name="secret"> + <leafNode name="key"> <properties> - <help>Secret for Dynamic Authorization Extension server (DM/CoA)</help> + <help>Shared secret for Dynamic Authorization Extension server</help> </properties> </leafNode> </children> @@ -221,17 +232,17 @@ <children> <leafNode name="attribute"> <properties> - <help>Specifies which radius attribute contains rate information. (default is Filter-Id)</help> + <help>Specifies RADIUS attribute containing rate information (default 'Filter-Id')</help> </properties> </leafNode> <leafNode name="vendor"> <properties> - <help>Specifies the vendor dictionary. (dictionary needs to be in /usr/share/accel-ppp/radius)</help> + <help>Specifies vendor dictionary (needs to be in /usr/share/accel-ppp/radius)</help> </properties> </leafNode> <leafNode name="enable"> <properties> - <help>Enables Bandwidth shaping via RADIUS</help> + <help>Enable RADIUS bandwidth shaping</help> <valueless /> </properties> </leafNode> @@ -241,42 +252,46 @@ </node> </children> </node> - <node name="sstp-settings"> + <node name="ssl"> <properties> - <help>SSTP settings</help> + <help>SSL Certificate, SSL Key and CA (/config/user-data/sstp)</help> </properties> <children> - <node name="ssl-certs"> + <leafNode name="ca-cert-file"> <properties> - <help>SSL Certificate, SSL Key and CA (/config/user-data/sstp)</help> + <help>Certificate Authority certificate</help> + <valueHelp> + <format>file</format> + <description>File in /config/auth directory</description> + </valueHelp> + <constraint> + <validator name="file-exists" argument="--directory /config/auth"/> + </constraint> </properties> - <children> - <leafNode name="ca"> - <properties> - <help>Certificate Authority certificate</help> - <completionHelp> - <script>if [ -e /config/user-data/sstp ]; then ls /config/user-data/sstp; fi</script> - </completionHelp> - </properties> - </leafNode> - <leafNode name="server-cert"> - <properties> - <help>Server Certificate</help> - <completionHelp> - <script>if [ -e /config/user-data/sstp ]; then ls /config/user-data/sstp; fi</script> - </completionHelp> - </properties> - </leafNode> - <leafNode name="server-key"> - <properties> - <help>Privat Key of the Server Certificate</help> - <completionHelp> - <script>if [ -e /config/user-data/sstp ]; then ls /config/user-data/sstp; fi</script> - </completionHelp> - </properties> - </leafNode> - </children> - </node> + </leafNode> + <leafNode name="cert-file"> + <properties> + <help>Server Certificate</help> + <completionHelp> + <script>ls /config</script> + </completionHelp> + <constraint> + <validator name="file-exists" argument="--directory /config/auth"/> + </constraint> + </properties> + </leafNode> + <leafNode name="key-file"> + <properties> + <help>Privat Key of the Server Certificate</help> + <valueHelp> + <format>file</format> + <description>File in /config/auth directory</description> + </valueHelp> + <constraint> + <validator name="file-exists" argument="--directory /config/auth"/> + </constraint> + </properties> + </leafNode> </children> </node> <node name="network-settings"> @@ -318,14 +333,9 @@ </leafNode> </children> </node> - <node name="dns-server"> + <leafNode name="name-server"> <properties> <help>DNS servers propagated to clients</help> - </properties> - <children> - <leafNode name="primary-dns"> - <properties> - <help>Primary DNS Server</help> <valueHelp> <format>ipv4</format> <description>IPv4 address</description> @@ -333,30 +343,10 @@ <constraint> <validator name="ipv4-address"/> </constraint> - </properties> - </leafNode> - <leafNode name="secondary-dns"> - <properties> - <help>Secondary DNS Server</help> - <valueHelp> - <format>ipv4</format> - <description>IPv4 address</description> - </valueHelp> - <constraint> - <validator name="ipv4-address"/> - </constraint> - </properties> - </leafNode> - </children> - </node> - <leafNode name="mtu"> - <properties> - <help>Maximum Transmission Unit (MTU)</help> - <constraint> - <validator name="numeric" argument="--range 128-16384"/> - </constraint> + <multi/> </properties> </leafNode> + #include <include/interface-mtu-68-1500.xml.i> </children> </node> <node name="ppp-settings"> diff --git a/interface-definitions/vrf.xml.in b/interface-definitions/vrf.xml.in new file mode 100644 index 000000000..7c75bf824 --- /dev/null +++ b/interface-definitions/vrf.xml.in @@ -0,0 +1,47 @@ +<?xml version="1.0"?> +<interfaceDefinition> + <node name="vrf" owner="${vyos_conf_scripts_dir}/vrf.py"> + <properties> + <help>Virtual Routing and Forwarding</help> + <!-- must be before any interface creation --> + <priority>210</priority> + </properties> + <children> + <leafNode name="bind-to-all"> + <properties> + <help>Enable binding services to all VRFs</help> + <valueless/> + </properties> + </leafNode> + <tagNode name="name"> + <properties> + <help>VRF instance name</help> + <constraint> + <validator name="vrf-name"/> + </constraint> + <constraintErrorMessage>VRF instance name must be 16 characters or less and can not\nbe named as regular network interfaces</constraintErrorMessage> + <valueHelp> + <format>name</format> + <description>Instance name</description> + </valueHelp> + </properties> + <children> + <leafNode name="table"> + <properties> + <help>Routing table associated with this instance</help> + <valueHelp> + <format>100-2147483647</format> + <description>Routing table ID</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 100-2147483647"/> + </constraint> + <constraintErrorMessage>VRF routing table must be in range from 100 to 2147483647</constraintErrorMessage> + </properties> + </leafNode> + #include <include/interface-description.xml.i> + </children> + </tagNode> + </children> + </node> +</interfaceDefinition> diff --git a/interface-definitions/vrrp.xml.in b/interface-definitions/vrrp.xml.in index 89d22f79f..120c7d218 100644 --- a/interface-definitions/vrrp.xml.in +++ b/interface-definitions/vrrp.xml.in @@ -254,6 +254,45 @@ </completionHelp> </properties> </leafNode> + <node name="transition-script"> + <properties> + <help>VRRP transition scripts</help> + </properties> + <children> + <leafNode name="master"> + <properties> + <help>Script to run on VRRP state transition to master</help> + <constraint> + <validator name="script"/> + </constraint> + </properties> + </leafNode> + <leafNode name="backup"> + <properties> + <help>Script to run on VRRP state transition to backup</help> + <constraint> + <validator name="script"/> + </constraint> + </properties> + </leafNode> + <leafNode name="fault"> + <properties> + <help>Script to run on VRRP state transition to fault</help> + <constraint> + <validator name="script"/> + </constraint> + </properties> + </leafNode> + <leafNode name="stop"> + <properties> + <help>Script to run on VRRP state transition to stop</help> + <constraint> + <validator name="script"/> + </constraint> + </properties> + </leafNode> + </children> + </node> </children> </tagNode> </children> diff --git a/op-mode-definitions/reset-ip-igmp.xml b/op-mode-definitions/reset-ip-igmp.xml new file mode 100644 index 000000000..143553d33 --- /dev/null +++ b/op-mode-definitions/reset-ip-igmp.xml @@ -0,0 +1,24 @@ +<?xml version="1.0"?> +<interfaceDefinition> + <node name="reset"> + <children> + <node name="ip"> + <children> + <node name="igmp"> + <properties> + <help>IGMP clear commands</help> + </properties> + <children> + <leafNode name="interfaces"> + <properties> + <help>Reset IGMP interfaces</help> + </properties> + <command>/usr/bin/vtysh -c "clear ip igmp interfaces"</command> + </leafNode> + </children> + </node> + </children> + </node> + </children> + </node> +</interfaceDefinition> diff --git a/op-mode-definitions/reset-ip-multicast.xml b/op-mode-definitions/reset-ip-multicast.xml new file mode 100644 index 000000000..d610add16 --- /dev/null +++ b/op-mode-definitions/reset-ip-multicast.xml @@ -0,0 +1,24 @@ +<?xml version="1.0"?> +<interfaceDefinition> + <node name="reset"> + <children> + <node name="ip"> + <children> + <node name="multicast"> + <properties> + <help>IP multicast routing table</help> + </properties> + <children> + <leafNode name="route"> + <properties> + <help>Clear multicast routing table</help> + </properties> + <command>/usr/bin/vtysh -c "clear ip mroute"</command> + </leafNode> + </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 3acb14486..591ec8f5b 100644 --- a/op-mode-definitions/show-interfaces-pppoe.xml +++ b/op-mode-definitions/show-interfaces-pppoe.xml @@ -27,6 +27,15 @@ </leafNode> </children> </node> + <leafNode name="statistics"> + <properties> + <help>Show specified wirelessmodem interface statistics</help> + <completionHelp> + <script>${vyos_completion_dir}/list_pppoe_peers.sh</script> + </completionHelp> + </properties> + <command>/usr/sbin/pppstats $4</command> + </leafNode> </children> </tagNode> </children> diff --git a/op-mode-definitions/show-interfaces-wirelessmodem.xml b/op-mode-definitions/show-interfaces-wirelessmodem.xml new file mode 100644 index 000000000..681f54f3d --- /dev/null +++ b/op-mode-definitions/show-interfaces-wirelessmodem.xml @@ -0,0 +1,45 @@ +<?xml version="1.0"?> +<interfaceDefinition> + <node name="show"> + <children> + <node name="interfaces"> + <children> + <tagNode name="wirelessmodem"> + <properties> + <help>Show Wireless Modem (WWAN) interface information</help> + <completionHelp> + <script>${vyos_completion_dir}/list_wlm_peers.sh</script> + </completionHelp> + </properties> + <command>${vyatta_bindir}/vyatta-show-interfaces.pl --intf="$4"</command> + <children> + <node name="log"> + <properties> + <help>Show PPPoE logs</help> + </properties> + <command>cat /var/log/vyatta/ppp_$4.log</command> + <children> + <leafNode name="tail"> + <properties> + <help>Watch PPPoE logs</help> + </properties> + <command>tail --follow=name /var/log/vyatta/ppp_$4.log</command> + </leafNode> + </children> + </node> + <leafNode name="statistics"> + <properties> + <help>Show specified wirelessmodem interface statistics</help> + <completionHelp> + <script>${vyos_completion_dir}/list_wlm_peers.sh</script> + </completionHelp> + </properties> + <command>/usr/sbin/pppstats $4</command> + </leafNode> + </children> + </tagNode> + </children> + </node> + </children> + </node> +</interfaceDefinition> diff --git a/op-mode-definitions/show-ip-igmp.xml b/op-mode-definitions/show-ip-igmp.xml new file mode 100644 index 000000000..b8f2f9107 --- /dev/null +++ b/op-mode-definitions/show-ip-igmp.xml @@ -0,0 +1,48 @@ +<?xml version="1.0"?> +<interfaceDefinition> + <node name="show"> + <children> + <node name="ip"> + <children> + <node name="igmp"> + <properties> + <help>Show IGMP (Internet Group Management Protocol) information</help> + </properties> + <children> + <leafNode name="groups"> + <properties> + <help>IGMP groups information</help> + </properties> + <command>/usr/bin/vtysh -c "show ip igmp groups"</command> + </leafNode> + <leafNode name="interfaces"> + <properties> + <help>IGMP interfaces information</help> + </properties> + <command>/usr/bin/vtysh -c "show ip igmp interface"</command> + </leafNode> + <leafNode name="join"> + <properties> + <help>IGMP static join information</help> + </properties> + <command>/usr/bin/vtysh -c "show ip igmp join"</command> + </leafNode> + <leafNode name="sources"> + <properties> + <help>IGMP sources information</help> + </properties> + <command>/usr/bin/vtysh -c "show ip igmp sources"</command> + </leafNode> + <leafNode name="statistics"> + <properties> + <help>IGMP statistics</help> + </properties> + <command>/usr/bin/vtysh -c "show ip igmp statistics"</command> + </leafNode> + </children> + </node> + </children> + </node> + </children> + </node> +</interfaceDefinition> diff --git a/op-mode-definitions/show-ip-multicast.xml b/op-mode-definitions/show-ip-multicast.xml index 6ffe40436..5331d2e35 100644 --- a/op-mode-definitions/show-ip-multicast.xml +++ b/op-mode-definitions/show-ip-multicast.xml @@ -21,6 +21,18 @@ </properties> <command>if ps -C igmpproxy &>/dev/null; then ${vyos_op_scripts_dir}/show_igmpproxy.py --mfc; else echo IGMP proxy not configured; fi</command> </leafNode> + <leafNode name="summary"> + <properties> + <help>IP multicast information</help> + </properties> + <command>/usr/bin/vtysh -c "show ip multicast"</command> + </leafNode> + <leafNode name="route"> + <properties> + <help>IP multicast routing table</help> + </properties> + <command>/usr/bin/vtysh -c "show ip mroute"</command> + </leafNode> </children> </node> </children> diff --git a/op-mode-definitions/show-ip-pim.xml b/op-mode-definitions/show-ip-pim.xml new file mode 100644 index 000000000..3f4edc779 --- /dev/null +++ b/op-mode-definitions/show-ip-pim.xml @@ -0,0 +1,72 @@ +<?xml version="1.0"?> +<interfaceDefinition> + <node name="show"> + <children> + <node name="ip"> + <children> + <node name="pim"> + <properties> + <help>Show PIM (Protocol Independent Multicast) information</help> + </properties> + <children> + <leafNode name="interfaces"> + <properties> + <help>PIM interfaces information</help> + </properties> + <command>/usr/bin/vtysh -c "show ip pim interface"</command> + </leafNode> + <leafNode name="join"> + <properties> + <help>PIM join information</help> + </properties> + <command>/usr/bin/vtysh -c "show ip pim join"</command> + </leafNode> + <leafNode name="neighbor"> + <properties> + <help>PIM neighbor information</help> + </properties> + <command>/usr/bin/vtysh -c "show ip pim neighbor"</command> + </leafNode> + <leafNode name="nexthop"> + <properties> + <help>PIM cached nexthop rpf information</help> + </properties> + <command>/usr/bin/vtysh -c "show ip pim nexthop"</command> + </leafNode> + <leafNode name="state"> + <properties> + <help>PIM state information</help> + </properties> + <command>/usr/bin/vtysh -c "show ip pim state"</command> + </leafNode> + <leafNode name="statistics"> + <properties> + <help>PIM statistics</help> + </properties> + <command>/usr/bin/vtysh -c "show ip pim statistics"</command> + </leafNode> + <leafNode name="rp"> + <properties> + <help>PIM RP (Rendevous Point) information</help> + </properties> + <command>/usr/bin/vtysh -c "show ip pim rp-info"</command> + </leafNode> + <leafNode name="rpf"> + <properties> + <help>PIM cached source rpf information</help> + </properties> + <command>/usr/bin/vtysh -c "show ip pim rpf"</command> + </leafNode> + <leafNode name="upstream"> + <properties> + <help>PIM upstream information</help> + </properties> + <command>/usr/bin/vtysh -c "show ip pim upstream"</command> + </leafNode> + </children> + </node> + </children> + </node> + </children> + </node> +</interfaceDefinition> diff --git a/op-mode-definitions/show-mpls.xml b/op-mode-definitions/show-mpls.xml new file mode 100644 index 000000000..6610788c7 --- /dev/null +++ b/op-mode-definitions/show-mpls.xml @@ -0,0 +1,62 @@ +<?xml version="1.0"?> +<interfaceDefinition> + <node name="show"> + <children> + <node name="mpls"> + <children> + <node name="ldp"> + <properties> + <help>Label Distribution Protocol (LDP)</help> + </properties> + <children> + <node name="binding"> + <properties> + <help>Label Information Base</help> + </properties> + <command>/usr/bin/vtysh -c "show mpls ldp binding"</command> + </node> + <node name="discovery"> + <properties> + <help>Discovery hello information</help> + </properties> + <command>/usr/bin/vtysh -c "show mpls ldp discovery"</command> + </node> + <node name="interface"> + <properties> + <help>LDP interface information</help> + </properties> + <command>/usr/bin/vtysh -c "show mpls ldp interface"</command> + </node> + <node name="neighbor"> + <properties> + <help>LDP neighbor information</help> + </properties> + <command>/usr/bin/vtysh -c "show mpls ldp neighbor"</command> + <children> + <node name="detail"> + <properties> + <help>Show neighbor detail</help> + </properties> + <command>/usr/bin/vtysh -c "show mpls ldp neighbor detail"</command> + </node> + </children> + </node> + </children> + </node> + <node name="pseudowire"> + <properties> + <help>Show MPLS pseudowire interfaces</help> + </properties> + <command>/usr/bin/vtysh -c "show mpls pseudowires"</command> + </node> + <node name="table"> + <properties> + <help>Show MPLS table</help> + </properties> + <command>/usr/bin/vtysh -c "show mpls table"</command> + </node> + </children> + </node> + </children> + </node> +</interfaceDefinition> diff --git a/op-mode-definitions/show-ntp.xml b/op-mode-definitions/show-ntp.xml index 48cee5bee..b7f0acdf8 100644 --- a/op-mode-definitions/show-ntp.xml +++ b/op-mode-definitions/show-ntp.xml @@ -6,7 +6,7 @@ <properties> <help>Show peer status of NTP daemon</help> </properties> - <command>if ps -C ntpd &>/dev/null; then ntpdc -n -c peers; else echo NTP daemon disabled; fi</command> + <command>if ps -C ntpd &>/dev/null; then ntpq -n -c peers; else echo NTP daemon disabled; fi</command> <children> <tagNode name="server"> <properties> @@ -21,7 +21,7 @@ <properties> <help>Show NTP operational summary</help> </properties> - <command>if ps -C ntpd &>/dev/null; then ntpdc -n -c sysinfo; ntpdc -n -c kerninfo; else echo NTP daemon disabled; fi</command> + <command>if ps -C ntpd &>/dev/null; then ntpq -n -c sysinfo; ntpq -n -c kerninfo; else echo NTP daemon disabled; fi</command> </node> </children> diff --git a/op-mode-definitions/show-raid.xml b/op-mode-definitions/show-raid.xml index 000fd4610..8bf394552 100644 --- a/op-mode-definitions/show-raid.xml +++ b/op-mode-definitions/show-raid.xml @@ -4,7 +4,7 @@ <children> <tagNode name="raid"> <properties> - <help>Show statis of RAID set</help> + <help>Show status of RAID set</help> <completionHelp> <script>${vyos_completion_dir}/list_raidset.sh</script> </completionHelp> diff --git a/op-mode-definitions/show-system-info.xml b/op-mode-definitions/show-system-info.xml index ade3829f2..61c947bbe 100644 --- a/op-mode-definitions/show-system-info.xml +++ b/op-mode-definitions/show-system-info.xml @@ -51,6 +51,13 @@ </children> </node> + <leafNode name= "integrity"> + <properties> + <help>Checks overall system integrity</help> + </properties> + <command>sudo ${vyos_op_scripts_dir}/system_integrity.py</command> + </leafNode> + <leafNode name="kernel-messages"> <properties> <help>Show messages in kernel ring buffer</help> diff --git a/op-mode-definitions/show-systemintegrity.xml b/op-mode-definitions/show-systemintegrity.xml deleted file mode 100644 index 44b5faf68..000000000 --- a/op-mode-definitions/show-systemintegrity.xml +++ /dev/null @@ -1,14 +0,0 @@ -<?xml version="1.0"?> - -<interfaceDefinition> - <node name="show"> - <children> - <leafNode name= "system-integrity"> - <properties> - <help>checks the integrity of the system</help> - </properties> - <command>sudo ${vyos_op_scripts_dir}/system_integrity.py</command> - </leafNode> - </children> - </node> -</interfaceDefinition> diff --git a/op-mode-definitions/show-vrf.xml b/op-mode-definitions/show-vrf.xml new file mode 100644 index 000000000..360153d8e --- /dev/null +++ b/op-mode-definitions/show-vrf.xml @@ -0,0 +1,22 @@ +<?xml version="1.0"?> +<interfaceDefinition> + <node name="show"> + <children> + <node name="vrf"> + <properties> + <help>Show VRF information</help> + </properties> + <command>${vyos_op_scripts_dir}/show_vrf.py -e</command> + </node> + <tagNode name="vrf"> + <properties> + <help>Show information on specific VRF instance</help> + <completionHelp> + <path>vrf name</path> + </completionHelp> + </properties> + <command>${vyos_op_scripts_dir}/show_vrf.py -e "$3"</command> + </tagNode> + </children> + </node> +</interfaceDefinition> diff --git a/op-mode-definitions/traceroute.xml b/op-mode-definitions/traceroute.xml index 85f6047c1..d623fe103 100644 --- a/op-mode-definitions/traceroute.xml +++ b/op-mode-definitions/traceroute.xml @@ -14,7 +14,6 @@ </properties> <command>/usr/bin/traceroute $2</command> </tagNode> - <tagNode name="ipv4"> <properties> <help>Track network path to <hostname|IPv4 address></help> @@ -24,7 +23,6 @@ </properties> <command>/usr/bin/traceroute -4 $3</command> </tagNode> - <tagNode name="ipv6"> <properties> <help>Track network path to <hostname|IPv6 address></help> @@ -34,9 +32,45 @@ </properties> <command>/usr/bin/traceroute -6 $3</command> </tagNode> + <tagNode name="vrf"> + <properties> + <help>Track network path to specified node via given VRF instance</help> + <completionHelp> + <path>vrf name</path> + </completionHelp> + </properties> + <children> + <tagNode name=""> + <properties> + <help>Track network path to specified node</help> + <completionHelp> + <list><hostname> <x.x.x.x> <h:h:h:h:h:h:h:h></list> + </completionHelp> + </properties> + <command>sudo ip vrf exec "$3" traceroute "$4"</command> + </tagNode> + <tagNode name="ipv4"> + <properties> + <help>Track network path to <hostname|IPv4 address></help> + <completionHelp> + <list><hostname> <x.x.x.x></list> + </completionHelp> + </properties> + <command>sudo ip vrf exec "$3" traceroute -4 "$5"</command> + </tagNode> + <tagNode name="ipv6"> + <properties> + <help>Track network path to <hostname|IPv6 address></help> + <completionHelp> + <list><hostname> <h:h:h:h:h:h:h:h></list> + </completionHelp> + </properties> + <command>sudo ip vrf exec "$3" traceroute -6 "$5"</command> + </tagNode> + </children> + </tagNode> </children> </node> - <node name="monitor"> <children> <tagNode name="traceroute"> diff --git a/python/setup.py b/python/setup.py index 304ea5cb7..ac7d0b573 100644 --- a/python/setup.py +++ b/python/setup.py @@ -10,7 +10,7 @@ setup( license = "LGPLv2+", keywords = "vyos", url = "http://www.vyos.io", - packages=['vyos'], + packages=["vyos","vyos.ifconfig"], long_description="VyOS configuration libraries", classifiers=[ "Development Status :: 4 - Beta", diff --git a/python/vyos/__init__.py b/python/vyos/__init__.py index 9b5ed21c9..e3e14fdd8 100644 --- a/python/vyos/__init__.py +++ b/python/vyos/__init__.py @@ -1 +1 @@ -from .base import * +from .base import ConfigError diff --git a/python/vyos/airbag.py b/python/vyos/airbag.py new file mode 100644 index 000000000..664974d5f --- /dev/null +++ b/python/vyos/airbag.py @@ -0,0 +1,169 @@ +# 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 +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library. If not, see <http://www.gnu.org/licenses/>. + +import os +import sys +import logging +import logging.handlers +from datetime import datetime + +from vyos.config import Config +from vyos.version import get_version +from vyos.util import run +from vyos.util import debug + + +# we allow to disable the extra logging +DISABLE = False + + +# emulate a file object +class _IO(object): + def __init__(self, std, log): + self.std = std + self.log = log + + def write(self, message): + self.std.write(message) + if DISABLE: + return + for line in message.split('\n'): + s = line.rstrip() + if s: + self.log(s) + + def flush(self): + self.std.flush() + + def close(self): + pass + + +# The function which will be used to report information +# to users when an exception is unhandled +def bug_report(dtype, value, trace): + from traceback import format_exception + + sys.stdout.flush() + sys.stderr.flush() + + information = { + 'date': datetime.now().strftime('%Y-%m-%d %H:%M:%S'), + 'version': get_version(), + 'trace': format_exception(dtype, value, trace), + 'instructions': COMMUNITY if 'rolling' in get_version() else SUPPORTED, + } + + sys.stdout.write(INTRO.format(**information)) + sys.stdout.flush() + + sys.stderr.write(FAULT.format(**information)) + sys.stderr.flush() + + +# define an exception handler to be run when an exception +# reach the end of __main__ and was not intercepted +def intercepter(dtype, value, trace): + bug_report(dtype, value, trace) + # debug returns either '' or 'developer' if debuging is enabled + if debug('developer'): + import pdb + pdb.pm() + + +def InterceptingLogger(address, _singleton=[False]): + skip = _singleton.pop() + _singleton.append(True) + if skip: + return + + logger = logging.getLogger('VyOS') + logger.setLevel(logging.DEBUG) + handler = logging.handlers.SysLogHandler(address='/dev/log', facility='syslog') + logger.addHandler(handler) + + # log to syslog any message sent to stderr + sys.stderr = _IO(sys.stderr, logger.critical) + + +# lists as default arguments in function is normally dangerous +# as they will keep any modification performed, unless this is +# what you want to do (in that case to only run the code once) +def InterceptingException(excepthook,_singleton=[False]): + skip = _singleton.pop() + _singleton.append(True) + if skip: + return + + # install the handler to replace the default behaviour + # which just prints the exception trace on screen + sys.excepthook = excepthook + + +# Do not attempt the extra logging for operational commands +try: + # This fails during boot + insession = Config().in_session() +except: + # we save info on boot to help debugging + insession = True + + +# Installing the interception, it currently does not work when +# running testing so we are checking that we are on the router +# as otherwise it prevents dpkg-buildpackage to work +if get_version() and insession: + InterceptingLogger('/run/systemd/journal/dev-log') + InterceptingException(intercepter) + + +# Messages to print + +FAULT = """\ +Date: {date} +VyOS image: {version} + +{trace} +""" + +INTRO = """\ +VyOS had an issue completing a command. + +We are sorry that you encountered a problem with VyOS. +There are a few things you can do to help us (and yourself): +{instructions} + +PLEASE, when reporting, do include as much information as you can: +- do not obfuscate any data (feel free to send us a private communication with + the extra information if your business policy is strict on information sharing) +- and include all the information presented below + +""" + +COMMUNITY = """\ +- Make sure you are running the latest version of the code available at + https://downloads.vyos.io/rolling/current/amd64/vyos-rolling-latest.iso +- Consult the forum to see how to handle this issue + https://forum.vyos.io +- Join our community on slack where our users exchange help and advice + https://vyos.slack.com +""".strip() + +SUPPORTED = """\ +- Make sure you are running the latest stable version of VyOS + the code is available at https://downloads.vyos.io/?dir=release/current +- Contact us on our online help desk + https://support.vyos.io/ +""".strip() diff --git a/python/vyos/authutils.py b/python/vyos/authutils.py index 234294649..90a46ffb4 100644 --- a/python/vyos/authutils.py +++ b/python/vyos/authutils.py @@ -15,16 +15,14 @@ import re -from subprocess import Popen, PIPE, STDOUT +from vyos.util import cmd def make_password_hash(password): """ Makes a password hash for /etc/shadow using mkpasswd """ - mkpasswd = Popen(['mkpasswd', '--method=sha-512', '--stdin'], stdout=PIPE, stdin=PIPE, stderr=PIPE) - hash = mkpasswd.communicate(input=password.encode(), timeout=5)[0].decode().strip() - - return hash + mkpassword = 'mkpasswd --method=sha-512 --stdin' + return cmd(mkpassword, input=password.encode(), timeout=5) def split_ssh_public_key(key_string, defaultname=""): """ Splits an SSH public key into its components """ diff --git a/python/vyos/config.py b/python/vyos/config.py index 2342f7021..75055a603 100644 --- a/python/vyos/config.py +++ b/python/vyos/config.py @@ -238,6 +238,19 @@ class Config(object): str: working configuration """ + # show_config should be independent of CLI edit level. + # Set the CLI edit environment to the top level, and + # restore original on exit. + save_env = self.__session_env + + env_str = self._run(self._make_command('getEditResetEnv', '')) + env_list = re.findall(r'([A-Z_]+)=\'([^;\s]+)\'', env_str) + root_env = os.environ + for k, v in env_list: + root_env[k] = v + + self.__session_env = root_env + # FIXUP: by default, showConfig will give you a diff # if there are uncommitted changes. # The config parser obviously cannot work with diffs, @@ -253,8 +266,10 @@ class Config(object): path = " ".join(path) try: out = self._run(self._make_command('showConfig', path)) + self.__session_env = save_env return out except VyOSError: + self.__session_env = save_env return(default) def get_config_dict(self, path=[], effective=False): diff --git a/python/vyos/configdict.py b/python/vyos/configdict.py index 80e199907..24fe174d2 100644 --- a/python/vyos/configdict.py +++ b/python/vyos/configdict.py @@ -123,10 +123,15 @@ def vlan_to_dict(conf): '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 + 'mtu': 1500, + 'vrf': '' } # retrieve configured interface addresses if conf.exists('address'): @@ -186,6 +191,22 @@ def vlan_to_dict(conf): if conf.exists('ip enable-arp-ignore'): vlan['ip_enable_arp_ignore'] = 1 + # Enable Proxy ARP + if conf.exists('ip enable-proxy-arp'): + vlan['ip_proxy_arp'] = 1 + + # Enable acquisition of IPv6 address using stateless autoconfig (SLAAC) + if conf.exists('ipv6 address autoconf'): + vlan['ipv6_autoconf'] = 1 + + # Disable IPv6 forwarding on this interface + if conf.exists('ipv6 disable-forwarding'): + vlan['ipv6_forwarding'] = 0 + + # 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')) + # Media Access Control (MAC) address if conf.exists('mac'): vlan['mac'] = conf.return_value('mac') @@ -194,6 +215,10 @@ def vlan_to_dict(conf): if conf.exists('mtu'): vlan['mtu'] = int(conf.return_value('mtu')) + # retrieve VRF instance + if conf.exists('vrf'): + vlan['vrf'] = conf.return_value('vrf') + # VLAN egress QoS if conf.exists('egress-qos'): vlan['egress_qos'] = conf.return_value('egress-qos') diff --git a/python/vyos/configsession.py b/python/vyos/configsession.py index d326b3b11..aaf08e726 100644 --- a/python/vyos/configsession.py +++ b/python/vyos/configsession.py @@ -29,6 +29,7 @@ SAVE_CONFIG = ['/opt/vyatta/sbin/vyatta-save-config.pl'] INSTALL_IMAGE = ['/opt/vyatta/sbin/install-image'] REMOVE_IMAGE = ['/opt/vyatta/bin/vyatta-boot-image.pl', '--del'] GENERATE = ['/opt/vyatta/bin/vyatta-op-cmd-wrapper', 'generate'] +SHOW = ['/opt/vyatta/bin/vyatta-op-cmd-wrapper', 'show'] # Default "commit via" string APP = "vyos-http-api" @@ -181,5 +182,10 @@ class ConfigSession(object): return out def generate(self, cmd): - out = self.__run_command(GENERATE + cmd) + out = self.__run_command(GENERATE + cmd.split()) return out + + def show(self, cmd): + out = self.__run_command(SHOW + cmd.split()) + return out + diff --git a/python/vyos/configtree.py b/python/vyos/configtree.py index 0274f3573..a0b0eb3c1 100644 --- a/python/vyos/configtree.py +++ b/python/vyos/configtree.py @@ -24,58 +24,10 @@ def escape_backslash(string: str) -> str: result = p.sub(r'\\\\', string) return result -def strip_comments(s): - """ Split a config string into the config section and the trailing comments """ - INITIAL = 0 - IN_COMMENT = 1 - - i = len(s) - 1 - - state = INITIAL - - config_end = 0 - - # Find the first character of the comments section at the end, - # if it exists - while (i >= 0): - c = s[i] - - if (state == INITIAL) and re.match(r'\s', c): - # Ignore whitespace - if (i != 0): - i -= 1 - else: - config_end = 0 - break - elif (state == INITIAL) and not re.match(r'(\s|\/)', c): - # Assume there are no (more) trailing comments, - # this is an end of a node: either a brace of the last character - # of a leaf node value - config_end = i + 1 - break - elif (state == INITIAL) and (c == '/'): - # A comment begins, or it's a stray slash - if (s[i-1] == '*'): - state = IN_COMMENT - i -= 2 - else: - raise ValueError("Invalid syntax: stray slash at character {0}".format(i + 1)) - elif (state == IN_COMMENT) and (c == '*'): - # A comment ends here - try: - if (s[i-1] == '/'): - state = INITIAL - i -= 2 - except: - raise ValueError("Invalid syntax: malformed commend end at character {0}".format(i + 1)) - elif (state == IN_COMMENT) and (c != '*'): - # Ignore everything inside comments, including braces - i -= 1 - else: - # Shouldn't happen - raise ValueError("Invalid syntax at character {0}: invalid character {1}".format(i + 1, c)) - - return (s[0:config_end], s[config_end+1:]) +def extract_version(s): + """ Extract the version string from the config string """ + t = re.split('(^//)', s, maxsplit=1, flags=re.MULTILINE) + return (s, ''.join(t[1:])) def check_path(path): # Necessary type checking @@ -174,7 +126,7 @@ class ConfigTree(object): self.__destroy = self.__lib.destroy self.__destroy.argtypes = [c_void_p] - config_section, comments_section = strip_comments(config_string) + config_section, version_section = extract_version(config_string) config_section = escape_backslash(config_section) config = self.__from_string(config_section.encode()) if config is None: @@ -182,7 +134,7 @@ class ConfigTree(object): raise ValueError("Failed to parse config: {0}".format(msg)) else: self.__config = config - self.__comments = comments_section + self.__version = version_section def __del__(self): if self.__config is not None: @@ -193,7 +145,7 @@ class ConfigTree(object): def to_string(self): config_string = self.__to_string(self.__config).decode() - config_string = "{0}\n{1}".format(config_string, self.__comments) + config_string = "{0}\n{1}".format(config_string, self.__version) return config_string def to_commands(self): diff --git a/python/vyos/ifconfig.py b/python/vyos/ifconfig.py deleted file mode 100644 index 81867d086..000000000 --- a/python/vyos/ifconfig.py +++ /dev/null @@ -1,1920 +0,0 @@ -# Copyright 2019 VyOS maintainers and contributors <maintainers@vyos.io> -# -# This library is free software; you can redistribute it and/or -# modify it under the terms of the GNU Lesser General Public -# License as published by the Free Software Foundation; either -# version 2.1 of the License, or (at your option) any later version. -# -# This library is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU -# Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public -# License along with this library. If not, see <http://www.gnu.org/licenses/>. - -import os -import re -import jinja2 -import json -import glob -import time - -import vyos.interfaces - -from vyos.validate import * -from vyos.config import Config -from vyos import ConfigError - -from ipaddress import IPv4Network, IPv6Address -from netifaces import ifaddresses, AF_INET, AF_INET6 -from subprocess import Popen, PIPE, STDOUT -from time import sleep -from os.path import isfile -from tabulate import tabulate -from hurry.filesize import size,alternative -from datetime import timedelta - -dhclient_base = r'/var/lib/dhcp/dhclient_' -dhcp_cfg = """ -# generated by ifconfig.py -option rfc3442-classless-static-routes code 121 = array of unsigned integer 8; -timeout 60; -retry 300; - -interface "{{ intf }}" { - send host-name "{{ hostname }}"; - {% if client_id -%} - send dhcp-client-identifier "{{ client_id }}"; - {% endif -%} - {% if vendor_class_id -%} - send vendor-class-identifier "{{ vendor_class_id }}"; - {% endif -%} - request subnet-mask, broadcast-address, routers, domain-name-servers, - rfc3442-classless-static-routes, domain-name, interface-mtu; - require subnet-mask; -} - -""" - -dhcpv6_cfg = """ -# generated by ifconfig.py -interface "{{ intf }}" { - request routers, domain-name-servers, domain-name; -} - -""" - -class Interface: - def __init__(self, ifname, type=None): - """ - This is the base interface class which supports basic IP/MAC address - operations as well as DHCP(v6). Other interface which represent e.g. - and ethernet bridge are implemented as derived classes adding all - additional functionality. - - DEBUG: - This class has embedded debugging (print) which can be enabled by - creating the following file: - vyos@vyos# touch /tmp/vyos.ifconfig.debug - - Example: - >>> from vyos.ifconfig import Interface - >>> i = Interface('eth0') - """ - self._ifname = str(ifname) - - if not os.path.exists('/sys/class/net/{}'.format(ifname)) and not type: - raise Exception('interface "{}" not found'.format(self._ifname)) - - if not os.path.exists('/sys/class/net/{}'.format(self._ifname)): - cmd = 'ip link add dev {} type {}'.format(self._ifname, type) - self._cmd(cmd) - - # per interface DHCP config files - self._dhcp_cfg_file = dhclient_base + self._ifname + '.conf' - self._dhcp_pid_file = dhclient_base + self._ifname + '.pid' - self._dhcp_lease_file = dhclient_base + self._ifname + '.leases' - - # per interface DHCPv6 config files - self._dhcpv6_cfg_file = dhclient_base + self._ifname + '.v6conf' - self._dhcpv6_pid_file = dhclient_base + self._ifname + '.v6pid' - self._dhcpv6_lease_file = dhclient_base + self._ifname + '.v6leases' - - # DHCP options - self._dhcp_options = { - 'intf' : self._ifname, - 'hostname' : '', - 'client_id' : '', - 'vendor_class_id' : '' - } - - # DHCPv6 options - self._dhcpv6_options = { - 'intf' : self._ifname, - 'dhcpv6_prm_only' : False, - 'dhcpv6_temporary' : False - } - - # list of assigned IP addresses - self._addr = [] - - def _debug_msg(self, msg): - if os.path.isfile('/tmp/vyos.ifconfig.debug'): - print('DEBUG/{:<6} {}'.format(self._ifname, msg)) - - def _cmd(self, command): - p = Popen(command, stdout=PIPE, stderr=STDOUT, shell=True) - tmp = p.communicate()[0].strip() - self._debug_msg("cmd '{}'".format(command)) - if tmp.decode(): - self._debug_msg("returned:\n{}".format(tmp.decode())) - - # do we need some error checking code here? - return tmp.decode() - - def _read_sysfs(self, filename): - """ - Provide a single primitive w/ error checking for reading from sysfs. - """ - value = None - with open(filename, 'r') as f: - value = f.read().rstrip('\n') - - self._debug_msg("read '{}' < '{}'".format(value, filename)) - return value - - def _write_sysfs(self, filename, value): - """ - Provide a single primitive w/ error checking for writing to sysfs. - """ - self._debug_msg("write '{}' > '{}'".format(value, filename)) - with open(filename, 'w') as f: - f.write(str(value)) - - return None - - def remove(self): - """ - Remove interface from operating system. Removing the interface - deconfigures all assigned IP addresses and clear possible DHCP(v6) - client processes. - - Example: - >>> from vyos.ifconfig import Interface - >>> i = Interface('eth0') - >>> i.remove() - """ - # stop DHCP(v6) if running - self._del_dhcp() - self._del_dhcpv6() - - # 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) - - # Ethernet interfaces can not be removed - if type(self) == type(EthernetIf(self._ifname)): - return - - # NOTE (Improvement): - # after interface removal no other commands should be allowed - # to be called and instead should raise an Exception: - cmd = 'ip link del dev {}'.format(self._ifname) - return self._cmd(cmd) - - def get_mtu(self): - """ - Get/set interface mtu in bytes. - - Example: - >>> from vyos.ifconfig import Interface - >>> Interface('eth0').get_mtu() - '1500' - """ - return self._read_sysfs('/sys/class/net/{}/mtu' - .format(self._ifname)) - - def set_mtu(self, mtu): - """ - Get/set interface mtu in bytes. - - Example: - >>> from vyos.ifconfig import Interface - >>> Interface('eth0').set_mtu(1400) - >>> Interface('eth0').get_mtu() - '1400' - """ - if mtu < 68 or mtu > 9000: - raise ValueError('Invalid MTU size: "{}"'.format(mru)) - - return self._write_sysfs('/sys/class/net/{}/mtu' - .format(self._ifname), mtu) - - def set_mac(self, mac): - """ - Set interface MAC (Media Access Contrl) address to given value. - - Example: - >>> from vyos.ifconfig import Interface - >>> Interface('eth0').set_mac('00:50:ab:cd:ef:01') - """ - # on interface removal (ethernet) an empty string is passed - ignore it - if not mac: - return None - - # a mac address consits out of 6 octets - octets = len(mac.split(':')) - if octets != 6: - raise ValueError('wrong number of MAC octets: {} '.format(octets)) - - # validate against the first mac address byte if it's a multicast - # address - if int(mac.split(':')[0], 16) & 1: - raise ValueError('{} is a multicast MAC address'.format(mac)) - - # overall mac address is not allowed to be 00:00:00:00:00:00 - if sum(int(i, 16) for i in mac.split(':')) == 0: - raise ValueError('00:00:00:00:00:00 is not a valid MAC address') - - # check for VRRP mac address - if mac.split(':')[0] == '0' and addr.split(':')[1] == '0' and mac.split(':')[2] == '94' and mac.split(':')[3] == '0' and mac.split(':')[4] == '1': - raise ValueError('{} is a VRRP MAC address'.format(mac)) - - # Assemble command executed on system. Unfortunately there is no way - # of altering the MAC address via sysfs - cmd = 'ip link set dev {} address {}'.format(self._ifname, mac) - return self._cmd(cmd) - - - def set_arp_cache_tmo(self, tmo): - """ - Set ARP cache timeout value in seconds. Internal Kernel representation - is in milliseconds. - - Example: - >>> from vyos.ifconfig import Interface - >>> Interface('eth0').set_arp_cache_tmo(40) - """ - return self._write_sysfs('/proc/sys/net/ipv4/neigh/{0}/base_reachable_time_ms' - .format(self._ifname), (int(tmo) * 1000)) - - def set_arp_filter(self, arp_filter): - """ - Filter ARP requests - - 1 - Allows you to have multiple network interfaces on the same - subnet, and have the ARPs for each interface be answered - based on whether or not the kernel would route a packet from - the ARP'd IP out that interface (therefore you must use source - based routing for this to work). In other words it allows control - of which cards (usually 1) will respond to an arp request. - - 0 - (default) The kernel can respond to arp requests with addresses - from other interfaces. This may seem wrong but it usually makes - sense, because it increases the chance of successful communication. - IP addresses are owned by the complete host on Linux, not by - particular interfaces. Only for more complex setups like load- - balancing, does this behaviour cause problems. - """ - if int(arp_filter) >= 0 and int(arp_filter) <= 1: - return self._write_sysfs('/proc/sys/net/ipv4/conf/{0}/arp_filter' - .format(self._ifname), arp_filter) - else: - raise ValueError("Value out of range") - - def set_arp_accept(self, arp_accept): - """ - Define behavior for gratuitous ARP frames who's IP is not - already present in the ARP table: - 0 - don't create new entries in the ARP table - 1 - create new entries in the ARP table - - Both replies and requests type gratuitous arp will trigger the - ARP table to be updated, if this setting is on. - - If the ARP table already contains the IP address of the - gratuitous arp frame, the arp table will be updated regardless - if this setting is on or off. - """ - if int(arp_accept) >= 0 and int(arp_accept) <= 1: - return self._write_sysfs('/proc/sys/net/ipv4/conf/{0}/arp_accept' - .format(self._ifname), arp_accept) - else: - raise ValueError("Value out of range") - - def set_arp_announce(self, arp_announce): - """ - Define different restriction levels for announcing the local - source IP address from IP packets in ARP requests sent on - interface: - 0 - (default) Use any local address, configured on any interface - 1 - Try to avoid local addresses that are not in the target's - subnet for this interface. This mode is useful when target - hosts reachable via this interface require the source IP - address in ARP requests to be part of their logical network - configured on the receiving interface. When we generate the - request we will check all our subnets that include the - target IP and will preserve the source address if it is from - such subnet. - - Increasing the restriction level gives more chance for - receiving answer from the resolved target while decreasing - the level announces more valid sender's information. - """ - if int(arp_announce) >= 0 and int(arp_announce) <= 1: - return self._write_sysfs('/proc/sys/net/ipv4/conf/{0}/arp_announce' - .format(self._ifname), arp_announce) - else: - raise ValueError("Value out of range") - - def set_arp_ignore(self, arp_ignore): - """ - Define different modes for sending replies in response to received ARP - requests that resolve local target IP addresses: - - 0 - (default): reply for any local target IP address, configured - on any interface - 1 - reply only if the target IP address is local address - configured on the incoming interface - """ - if int(arp_ignore) >= 0 and int(arp_ignore) <= 1: - return self._write_sysfs('/proc/sys/net/ipv4/conf/{0}/arp_ignore' - .format(self._ifname), arp_ignore) - else: - raise ValueError("Value out of range") - - def set_link_detect(self, link_filter): - """ - Configure kernel response in packets received on interfaces that are 'down' - - 0 - Allow packets to be received for the address on this interface - even if interface is disabled or no carrier. - - 1 - Ignore packets received if interface associated with the incoming - address is down. - - 2 - Ignore packets received if interface associated with the incoming - address is down or has no carrier. - - Default value is 0. Note that some distributions enable it in startup - scripts. - - Example: - >>> from vyos.ifconfig import Interface - >>> Interface('eth0').set_link_detect(1) - """ - if int(link_filter) >= 0 and int(link_filter) <= 2: - return self._write_sysfs('/proc/sys/net/ipv4/conf/{0}/link_filter' - .format(self._ifname), link_filter) - else: - raise ValueError("Value out of range") - - def set_alias(self, ifalias=None): - """ - Set interface alias name used by e.g. SNMP - - Example: - >>> from vyos.ifconfig import Interface - >>> Interface('eth0').set_alias('VyOS upstream interface') - - to clear alias e.g. delete it use: - - >>> Interface('eth0').set_ifalias('') - """ - if not ifalias: - # clear interface alias - ifalias = '\0' - - self._write_sysfs('/sys/class/net/{}/ifalias' - .format(self._ifname), ifalias) - - def get_state(self): - """ - Enable (up) / Disable (down) an interface - - Example: - >>> from vyos.ifconfig import Interface - >>> Interface('eth0').get_state() - 'up' - """ - cmd = 'ip -json link show dev {}'.format(self._ifname) - tmp = self._cmd(cmd) - out = json.loads(tmp) - return out[0]['operstate'].lower() - - def set_state(self, state): - """ - Enable (up) / Disable (down) an interface - - Example: - >>> from vyos.ifconfig import Interface - >>> Interface('eth0').set_state('down') - >>> Interface('eth0').get_state() - 'down' - """ - if state not in ['up', 'down']: - raise ValueError('state must be "up" or "down"') - - # Assemble command executed on system. Unfortunately there is no way - # to up/down an interface via sysfs - cmd = 'ip link set dev {} {}'.format(self._ifname, state) - return self._cmd(cmd) - - def set_proxy_arp(self, enable): - """ - Set per interface proxy ARP configuration - - Example: - >>> from vyos.ifconfig import Interface - >>> Interface('eth0').set_proxy_arp(1) - """ - if int(enable) >= 0 and int(enable) <= 1: - return self._write_sysfs('/proc/sys/net/ipv4/conf/{}/proxy_arp' - .format(self._ifname), enable) - else: - raise ValueError("Value out of range") - - def set_proxy_arp_pvlan(self, enable): - """ - Private VLAN proxy arp. - Basically allow proxy arp replies back to the same interface - (from which the ARP request/solicitation was received). - - This is done to support (ethernet) switch features, like RFC - 3069, where the individual ports are NOT allowed to - communicate with each other, but they are allowed to talk to - the upstream router. As described in RFC 3069, it is possible - to allow these hosts to communicate through the upstream - router by proxy_arp'ing. Don't need to be used together with - proxy_arp. - - This technology is known by different names: - In RFC 3069 it is called VLAN Aggregation. - Cisco and Allied Telesyn call it Private VLAN. - Hewlett-Packard call it Source-Port filtering or port-isolation. - Ericsson call it MAC-Forced Forwarding (RFC Draft). - - Example: - >>> from vyos.ifconfig import Interface - >>> Interface('eth0').set_proxy_arp_pvlan(1) - """ - if int(enable) >= 0 and int(enable) <= 1: - return self._write_sysfs('/proc/sys/net/ipv4/conf/{}/proxy_arp_pvlan' - .format(self._ifname), enable) - else: - raise ValueError("Value out of range") - - def get_addr(self): - """ - Retrieve assigned IPv4 and IPv6 addresses from given interface. - This is done using the netifaces and ipaddress python modules. - - Example: - >>> from vyos.ifconfig import Interface - >>> Interface('eth0').get_addrs() - ['172.16.33.30/24', 'fe80::20c:29ff:fe11:a174/64'] - """ - - ipv4 = [] - ipv6 = [] - - if AF_INET in ifaddresses(self._ifname).keys(): - for v4_addr in ifaddresses(self._ifname)[AF_INET]: - # we need to manually assemble a list of IPv4 address/prefix - prefix = '/' + \ - str(IPv4Network('0.0.0.0/' + v4_addr['netmask']).prefixlen) - ipv4.append(v4_addr['addr'] + prefix) - - if AF_INET6 in ifaddresses(self._ifname).keys(): - for v6_addr in ifaddresses(self._ifname)[AF_INET6]: - # Note that currently expanded netmasks are not supported. That means - # 2001:db00::0/24 is a valid argument while 2001:db00::0/ffff:ff00:: not. - # see https://docs.python.org/3/library/ipaddress.html - bits = bin( - int(v6_addr['netmask'].replace(':', ''), 16)).count('1') - prefix = '/' + str(bits) - - # we alsoneed to remove the interface suffix on link local - # addresses - v6_addr['addr'] = v6_addr['addr'].split('%')[0] - ipv6.append(v6_addr['addr'] + prefix) - - return ipv4 + ipv6 - - def add_addr(self, addr): - """ - Add IP(v6) address to interface. Address is only added if it is not - already assigned to that interface. - - addr: can be an IPv4 address, IPv6 address, dhcp or dhcpv6! - IPv4: add IPv4 address to interface - IPv6: add IPv6 address to interface - dhcp: start dhclient (IPv4) on interface - dhcpv6: start dhclient (IPv6) on interface - - Example: - >>> from vyos.ifconfig import Interface - >>> j = Interface('eth0') - >>> j.add_addr('192.0.2.1/24') - >>> j.add_addr('2001:db8::ffff/64') - >>> j.get_addr() - ['192.0.2.1/24', '2001:db8::ffff/64'] - """ - - # cache new IP address which is assigned to interface - self._addr.append(addr) - - # 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") - - if addr == 'dhcp': - self._set_dhcp() - elif addr == 'dhcpv6': - self._set_dhcpv6() - else: - if not is_intf_addr_assigned(self._ifname, addr): - cmd = 'ip addr add "{}" dev "{}"'.format(addr, self._ifname) - return self._cmd(cmd) - - def del_addr(self, addr): - """ - Delete IP(v6) address to interface. Address is only added if it is - assigned to that interface. - - addr: can be an IPv4 address, IPv6 address, dhcp or dhcpv6! - IPv4: delete IPv4 address from interface - IPv6: delete IPv6 address from interface - dhcp: stop dhclient (IPv4) on interface - dhcpv6: stop dhclient (IPv6) on interface - - Example: - >>> from vyos.ifconfig import Interface - >>> j = Interface('eth0') - >>> j.add_addr('2001:db8::ffff/64') - >>> j.add_addr('192.0.2.1/24') - >>> j.get_addr() - ['192.0.2.1/24', '2001:db8::ffff/64'] - >>> j.del_addr('192.0.2.1/24') - >>> j.get_addr() - ['2001:db8::ffff/64'] - """ - if addr == 'dhcp': - self._del_dhcp() - elif addr == 'dhcpv6': - self._del_dhcpv6() - else: - if is_intf_addr_assigned(self._ifname, addr): - cmd = 'ip addr del "{}" dev "{}"'.format(addr, self._ifname) - return self._cmd(cmd) - - - def get_dhcp_options(self): - """ - Return dictionary with supported DHCP options. - - Dictionary should be altered and send back via set_dhcp_options() - so those options are applied when DHCP is run. - """ - return self._dhcp_options - - def set_dhcp_options(self, options): - """ - Store new DHCP options used by next run of DHCP client. - """ - self._dhcp_options = options - - def get_dhcpv6_options(self): - """ - Return dictionary with supported DHCPv6 options. - - Dictionary should be altered and send back via set_dhcp_options() - so those options are applied when DHCP is run. - """ - return self._dhcpv6_options - - def set_dhcpv6_options(self, options): - """ - Store new DHCP options used by next run of DHCP client. - """ - self._dhcpv6_options = options - - # replace dhcpv4/v6 with systemd.networkd? - def _set_dhcp(self): - """ - Configure interface as DHCP client. The dhclient binary is automatically - started in background! - - Example: - - >>> from vyos.ifconfig import Interface - >>> j = Interface('eth0') - >>> j.set_dhcp() - """ - - dhcp = self.get_dhcp_options() - if not dhcp['hostname']: - # read configured system hostname. - # maybe change to vyos hostd client ??? - with open('/etc/hostname', 'r') as f: - dhcp['hostname'] = f.read().rstrip('\n') - - # render DHCP configuration - tmpl = jinja2.Template(dhcp_cfg) - dhcp_text = tmpl.render(dhcp) - with open(self._dhcp_cfg_file, 'w') as f: - f.write(dhcp_text) - - cmd = 'start-stop-daemon --start --quiet --pidfile ' + \ - self._dhcp_pid_file - cmd += ' --exec /sbin/dhclient --' - # now pass arguments to dhclient binary - cmd += ' -4 -nw -cf {} -pf {} -lf {} {}'.format( - self._dhcp_cfg_file, self._dhcp_pid_file, self._dhcp_lease_file, self._ifname) - return self._cmd(cmd) - - - def _del_dhcp(self): - """ - De-configure interface as DHCP clinet. All auto generated files like - pid, config and lease will be removed. - - Example: - - >>> from vyos.ifconfig import Interface - >>> j = Interface('eth0') - >>> j.del_dhcp() - """ - pid = 0 - if os.path.isfile(self._dhcp_pid_file): - with open(self._dhcp_pid_file, 'r') as f: - pid = int(f.read()) - else: - self._debug_msg('No DHCP client PID found') - return None - - # 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 {} -pf {} -lf {} -r {}'.format( - self._dhcp_cfg_file, self._dhcp_pid_file, self._dhcp_lease_file, self._ifname) - self._cmd(cmd) - - # cleanup old config file - if os.path.isfile(self._dhcp_cfg_file): - os.remove(self._dhcp_cfg_file) - - # cleanup old pid file - if os.path.isfile(self._dhcp_pid_file): - os.remove(self._dhcp_pid_file) - - # cleanup old lease file - if os.path.isfile(self._dhcp_lease_file): - os.remove(self._dhcp_lease_file) - - - def _set_dhcpv6(self): - """ - Configure interface as DHCPv6 client. The dhclient binary is automatically - started in background! - - Example: - - >>> from vyos.ifconfig import Interface - >>> j = Interface('eth0') - >>> j.set_dhcpv6() - """ - dhcpv6 = self.get_dhcpv6_options() - - # better save then sorry .. should be checked in interface script - # but if you missed it we are safe! - if dhcpv6['dhcpv6_prm_only'] and dhcpv6['dhcpv6_temporary']: - raise Exception('DHCPv6 temporary and parameters-only options are mutually exclusive!') - - # render DHCP configuration - tmpl = jinja2.Template(dhcpv6_cfg) - dhcpv6_text = tmpl.render(dhcpv6) - with open(self._dhcpv6_cfg_file, 'w') as f: - f.write(dhcpv6_text) - - # https://bugs.launchpad.net/ubuntu/+source/ifupdown/+bug/1447715 - # - # wee need to wait for IPv6 DAD to finish once and interface is added - # this suxx :-( - sleep(5) - - # no longer accept router announcements on this interface - self._write_sysfs('/proc/sys/net/ipv6/conf/{}/accept_ra' - .format(self._ifname), 0) - - # assemble command-line to start DHCPv6 client (dhclient) - cmd = 'start-stop-daemon --start --quiet --pidfile ' + \ - self._dhcpv6_pid_file - cmd += ' --exec /sbin/dhclient --' - # now pass arguments to dhclient binary - cmd += ' -6 -nw -cf {} -pf {} -lf {}'.format( - self._dhcpv6_cfg_file, self._dhcpv6_pid_file, self._dhcpv6_lease_file) - - # add optional arguments - if dhcpv6['dhcpv6_prm_only']: - cmd += ' -S' - if dhcpv6['dhcpv6_temporary']: - cmd += ' -T' - - cmd += ' {}'.format(self._ifname) - return self._cmd(cmd) - - - def _del_dhcpv6(self): - """ - De-configure interface as DHCPv6 clinet. All auto generated files like - pid, config and lease will be removed. - - Example: - - >>> from vyos.ifconfig import Interface - >>> j = Interface('eth0') - >>> j.del_dhcpv6() - """ - pid = 0 - if os.path.isfile(self._dhcpv6_pid_file): - with open(self._dhcpv6_pid_file, 'r') as f: - pid = int(f.read()) - else: - self._debug_msg('No DHCPv6 client PID found') - return None - - # stop dhclient - cmd = 'start-stop-daemon --stop --quiet --pidfile {}'.format(self._dhcpv6_pid_file) - self._cmd(cmd) - - # accept router announcements on this interface - self._write_sysfs('/proc/sys/net/ipv6/conf/{}/accept_ra' - .format(self._ifname), 1) - - # cleanup old config file - if os.path.isfile(self._dhcpv6_cfg_file): - os.remove(self._dhcpv6_cfg_file) - - # cleanup old pid file - if os.path.isfile(self._dhcpv6_pid_file): - os.remove(self._dhcpv6_pid_file) - - # cleanup old lease file - if os.path.isfile(self._dhcpv6_lease_file): - os.remove(self._dhcpv6_lease_file) - - def op_show_interface_stats(self): - stats = self.get_interface_stats() - rx = [['bytes','packets','errors','dropped','overrun','mcast'],[stats['rx_bytes'],stats['rx_packets'],stats['rx_errors'],stats['rx_dropped'],stats['rx_over_errors'],stats['multicast']]] - tx = [['bytes','packets','errors','dropped','carrier','collisions'],[stats['tx_bytes'],stats['tx_packets'],stats['tx_errors'],stats['tx_dropped'],stats['tx_carrier_errors'],stats['collisions']]] - output = "RX: \n" - output += tabulate(rx,headers="firstrow",numalign="right",tablefmt="plain") - output += "\n\nTX: \n" - output += tabulate(tx,headers="firstrow",numalign="right",tablefmt="plain") - print(' '.join(('\n'+output.lstrip()).splitlines(True))) - - def get_interface_stats(self): - interface_stats = dict() - devices = [f for f in glob.glob("/sys/class/net/**/statistics")] - for dev_path in devices: - metrics = [f for f in glob.glob(dev_path +"/**")] - dev = re.findall(r"/sys/class/net/(.*)/statistics",dev_path)[0] - dev_dict = dict() - for metric_path in metrics: - metric = metric_path.replace(dev_path+"/","") - if isfile(metric_path): - data = open(metric_path, 'r').read()[:-1] - dev_dict[metric] = int(data) - interface_stats[dev] = dev_dict - - return interface_stats[self._ifname] - -class LoopbackIf(Interface): - - """ - The loopback device is a special, virtual network interface that your router - uses to communicate with itself. - """ - - def __init__(self, ifname): - super().__init__(ifname, type='loopback') - - def remove(self): - """ - Loopback interface can not be deleted from operating system. We can - only remove all assigned IP addresses. - - Example: - >>> from vyos.ifconfig import Interface - >>> i = LoopbackIf('lo').remove() - """ - # remove all assigned IP addresses from interface - for addr in self.get_addr(): - if addr in ["127.0.0.1/8", "::1/128"]: - # Do not allow deletion of the default loopback addresses as - # this will cause weird system behavior like snmp/ssh no longer - # operating as expected, see https://phabricator.vyos.net/T2034. - continue - - self.del_addr(addr) - -class DummyIf(Interface): - - """ - A dummy interface is entirely virtual like, for example, the loopback - interface. The purpose of a dummy interface is to provide a device to route - packets through without actually transmitting them. - """ - - def __init__(self, ifname): - super().__init__(ifname, type='dummy') - - -class STPIf(Interface): - """ - A spanning-tree capable interface. This applies only to bridge port member - interfaces! - """ - def __init__(self, ifname): - super().__init__(ifname) - - def set_path_cost(self, cost): - """ - Set interface path cost, only relevant for STP enabled interfaces - - Example: - - >>> from vyos.ifconfig import Interface - >>> Interface('eth0').set_path_cost(4) - """ - if not os.path.isfile('/sys/class/net/{}/brport/path_cost' - .format(self._ifname)): - raise TypeError('{} is not a bridge port member'.format(self._ifname)) - - return self._write_sysfs('/sys/class/net/{}/brport/path_cost' - .format(self._ifname), cost) - - def set_path_priority(self, priority): - """ - Set interface path priority, only relevant for STP enabled interfaces - - Example: - - >>> from vyos.ifconfig import Interface - >>> Interface('eth0').set_path_priority(4) - """ - if not os.path.isfile('/sys/class/net/{}/brport/priority' - .format(self._ifname)): - raise TypeError('{} is not a bridge port member'.format(self._ifname)) - - return self._write_sysfs('/sys/class/net/{}/brport/priority' - .format(self._ifname), priority) - - -class BridgeIf(Interface): - - """ - A bridge is a way to connect two Ethernet segments together in a protocol - independent way. Packets are forwarded based on Ethernet address, rather - than IP address (like a router). Since forwarding is done at Layer 2, all - protocols can go transparently through a bridge. - - The Linux bridge code implements a subset of the ANSI/IEEE 802.1d standard. - """ - - def __init__(self, ifname): - super().__init__(ifname, type='bridge') - - def set_ageing_time(self, time): - """ - Set bridge interface MAC address aging time in seconds. Internal kernel - representation is in centiseconds. Kernel default is 300 seconds. - - Example: - >>> from vyos.ifconfig import BridgeIf - >>> BridgeIf('br0').ageing_time(2) - """ - time = int(time) * 100 - return self._write_sysfs('/sys/class/net/{}/bridge/ageing_time' - .format(self._ifname), time) - - def set_forward_delay(self, time): - """ - Set bridge forwarding delay in seconds. Internal Kernel representation - is in centiseconds. - - Example: - >>> from vyos.ifconfig import BridgeIf - >>> BridgeIf('br0').forward_delay(15) - """ - return self._write_sysfs('/sys/class/net/{}/bridge/forward_delay' - .format(self._ifname), (int(time) * 100)) - - def set_hello_time(self, time): - """ - Set bridge hello time in seconds. Internal Kernel representation - is in centiseconds. - - Example: - >>> from vyos.ifconfig import BridgeIf - >>> BridgeIf('br0').set_hello_time(2) - """ - return self._write_sysfs('/sys/class/net/{}/bridge/hello_time' - .format(self._ifname), (int(time) * 100)) - - def set_max_age(self, time): - """ - Set bridge max message age in seconds. Internal Kernel representation - is in centiseconds. - - Example: - >>> from vyos.ifconfig import Interface - >>> BridgeIf('br0').set_max_age(30) - """ - return self._write_sysfs('/sys/class/net/{}/bridge/max_age' - .format(self._ifname), (int(time) * 100)) - - def set_priority(self, priority): - """ - Set bridge max aging time in seconds. - - Example: - >>> from vyos.ifconfig import BridgeIf - >>> BridgeIf('br0').set_priority(8192) - """ - return self._write_sysfs('/sys/class/net/{}/bridge/priority' - .format(self._ifname), priority) - - def set_stp(self, state): - """ - Set bridge STP (Spanning Tree) state. 0 -> STP disabled, 1 -> STP enabled - - Example: - >>> from vyos.ifconfig import BridgeIf - >>> BridgeIf('br0').set_stp(1) - """ - - if int(state) >= 0 and int(state) <= 1: - return self._write_sysfs('/sys/class/net/{}/bridge/stp_state' - .format(self._ifname), state) - else: - raise ValueError("Value out of range") - - - def set_multicast_querier(self, enable): - """ - Sets whether the bridge actively runs a multicast querier or not. When a - bridge receives a 'multicast host membership' query from another network - host, that host is tracked based on the time that the query was received - plus the multicast query interval time. - - Use enable=1 to enable or enable=0 to disable - - Example: - >>> from vyos.ifconfig import Interface - >>> BridgeIf('br0').set_multicast_querier(1) - """ - if int(enable) >= 0 and int(enable) <= 1: - return self._write_sysfs('/sys/class/net/{}/bridge/multicast_querier' - .format(self._ifname), enable) - else: - raise ValueError("Value out of range") - - - def add_port(self, interface): - """ - Add physical interface to bridge (member port) - - Example: - >>> from vyos.ifconfig import Interface - >>> BridgeIf('br0').add_port('eth0') - >>> BridgeIf('br0').add_port('eth1') - """ - cmd = 'ip link set dev {} master {}'.format(interface, self._ifname) - return self._cmd(cmd) - - def del_port(self, interface): - """ - Remove member port from bridge instance. - - Example: - >>> from vyos.ifconfig import Interface - >>> BridgeIf('br0').del_port('eth1') - """ - cmd = 'ip link set dev {} nomaster'.format(interface) - return self._cmd(cmd) - -class VLANIf(Interface): - """ - This class handels the creation and removal of a VLAN interface. It serves - as base class for BondIf and EthernetIf. - """ - def __init__(self, ifname, type=None): - super().__init__(ifname, type) - - def remove(self): - """ - Remove interface from operating system. Removing the interface - deconfigures all assigned IP addresses and clear possible DHCP(v6) - client processes. - - Example: - >>> from vyos.ifconfig import Interface - >>> i = Interface('eth0') - >>> i.remove() - """ - # Do we have sub interfaces (VLANs)? We apply a regex matching - # subinterfaces (indicated by a .) of a parent interface. - # - # As interfaces need to be deleted "in order" starting from Q-in-Q - # we delete them first. - vlan_ifs = [f for f in os.listdir(r'/sys/class/net') \ - if re.match(self._ifname + r'(?:\.\d+)(?:\.\d+)', f)] - - for vlan in vlan_ifs: - Interface(vlan).remove() - - # After deleting all Q-in-Q interfaces delete other VLAN interfaces - # which probably acted as parent to Q-in-Q or have been regular 802.1q - # interface. - vlan_ifs = [f for f in os.listdir(r'/sys/class/net') \ - if re.match(self._ifname + r'(?:\.\d+)', f)] - - for vlan in vlan_ifs: - Interface(vlan).remove() - - # All subinterfaces are now removed, continue on the physical interface - super().remove() - - - def add_vlan(self, vlan_id, ethertype='', ingress_qos='', egress_qos=''): - """ - A virtual LAN (VLAN) is any broadcast domain that is partitioned and - isolated in a computer network at the data link layer (OSI layer 2). - Use this function to create a new VLAN interface on a given physical - interface. - - This function creates both 802.1q and 802.1ad (Q-in-Q) interfaces. Proto - parameter is used to indicate VLAN type. - - A new object of type VLANIf is returned once the interface has been - created. - - @param ethertype: If specified, create 802.1ad or 802.1q Q-in-Q VLAN - interface - @param ingress_qos: Defines a mapping of VLAN header prio field to the - Linux internal packet priority on incoming frames. - @param ingress_qos: Defines a mapping of Linux internal packet priority - to VLAN header prio field but for outgoing frames. - - Example: - >>> from vyos.ifconfig import VLANIf - >>> i = VLANIf('eth0') - >>> i.add_vlan(10) - """ - vlan_ifname = self._ifname + '.' + str(vlan_id) - if not os.path.exists('/sys/class/net/{}'.format(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 {intf} name {intf}.{vlan} type vlan {proto} id {vlan} {opt_e} {opt_i}' \ - .format(intf=self._ifname, vlan=self._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 - # or interface description and so on - return VLANIf(vlan_ifname) - - - def del_vlan(self, vlan_id): - """ - Remove VLAN interface from operating system. Removing the interface - deconfigures all assigned IP addresses and clear possible DHCP(v6) - client processes. - - Example: - >>> from vyos.ifconfig import VLANIf - >>> i = VLANIf('eth0.10') - >>> i.del_vlan() - """ - vlan_ifname = self._ifname + '.' + str(vlan_id) - VLANIf(vlan_ifname).remove() - - -class EthernetIf(VLANIf): - """ - Abstraction of a Linux Ethernet Interface - """ - def __init__(self, ifname): - super().__init__(ifname) - - def get_driver_name(self): - """ - Return the driver name used by NIC. Some NICs don't support all - features e.g. changing link-speed, duplex - - Example: - >>> from vyos.ifconfig import EthernetIf - >>> i = EthernetIf('eth0') - >>> i.get_driver_name() - 'vmxnet3' - """ - link = os.readlink('/sys/class/net/{}/device/driver/module'.format(self._ifname)) - return os.path.basename(link) - - def set_flow_control(self, enable): - """ - Changes the pause parameters of the specified Ethernet device. - - @param enable: true -> enable pause frames, false -> disable pause frames - - Example: - >>> from vyos.ifconfig import EthernetIf - >>> i = EthernetIf('eth0') - >>> i.set_flow_control(True) - """ - if enable not in ['on', 'off']: - raise ValueError("Value out of range") - - if self.get_driver_name() in ['vmxnet3', 'virtio_net']: - self._debug_msg('{} driver does not support changing flow control settings!' - .format(self.get_driver_name())) - return - - # Get current flow control settings: - cmd = '/sbin/ethtool --show-pause {0}'.format(self._ifname) - tmp = self._cmd(cmd) - - # The above command returns - with tabs: - # - # Pause parameters for eth0: - # Autonegotiate: on - # RX: off - # TX: off - if re.search("Autonegotiate:\ton", tmp): - if enable == "on": - # flowcontrol is already enabled - no need to re-enable it again - # this will prevent the interface from flapping as applying the - # flow-control settings will take the interface down and bring - # it back up every time. - return - - # Assemble command executed on system. Unfortunately there is no way - # to change this setting via sysfs - cmd = '/sbin/ethtool --pause {0} autoneg {1} tx {1} rx {1}'.format( - self._ifname, enable) - try: - # An exception will be thrown if the settings are not changed - return self._cmd(cmd) - except CalledProcessError: - pass - - - def set_speed_duplex(self, speed, duplex): - """ - Set link speed in Mbit/s and duplex. - - @speed can be any link speed in MBit/s, e.g. 10, 100, 1000 auto - @duplex can be half, full, auto - - Example: - >>> from vyos.ifconfig import EthernetIf - >>> i = EthernetIf('eth0') - >>> i.set_speed_duplex('auto', 'auto') - """ - - if speed not in ['auto', '10', '100', '1000', '2500', '5000', '10000', '25000', '40000', '50000', '100000', '400000']: - raise ValueError("Value out of range (speed)") - - if duplex not in ['auto', 'full', 'half']: - raise ValueError("Value out of range (duplex)") - - if self.get_driver_name() in ['vmxnet3', 'virtio_net']: - self._debug_msg('{} driver does not support changing speed/duplex settings!' - .format(self.get_driver_name())) - return - - # Get current speed and duplex settings: - cmd = '/sbin/ethtool {0}'.format(self._ifname) - tmp = self._cmd(cmd) - - if re.search("\tAuto-negotiation: on", tmp): - if speed == 'auto' and duplex == 'auto': - # bail out early as nothing is to change - return - else: - # read in current speed and duplex settings - cur_speed = 0 - cur_duplex = '' - for line in tmp.splitlines(): - if line.lstrip().startswith("Speed:"): - non_decimal = re.compile(r'[^\d.]+') - cur_speed = non_decimal.sub('', line) - continue - - if line.lstrip().startswith("Duplex:"): - cur_duplex = line.split()[-1].lower() - break - - if (cur_speed == speed) and (cur_duplex == duplex): - # bail out early as nothing is to change - return - - cmd = '/sbin/ethtool -s {}'.format(self._ifname) - if speed == 'auto' or duplex == 'auto': - cmd += ' autoneg on' - else: - cmd += ' speed {} duplex {} autoneg off'.format(speed, duplex) - - return self._cmd(cmd) - - - def set_gro(self, state): - """ - Example: - >>> from vyos.ifconfig import EthernetIf - >>> i = EthernetIf('eth0') - >>> i.set_gro('on') - """ - if state not in ['on', 'off']: - raise ValueError('state must be "on" or "off"') - - cmd = '/sbin/ethtool -K {} gro {}'.format(self._ifname, state) - return self._cmd(cmd) - - - def set_gso(self, state): - """ - Example: - >>> from vyos.ifconfig import EthernetIf - >>> i = EthernetIf('eth0') - >>> i.set_gso('on') - """ - if state not in ['on', 'off']: - raise ValueError('state must be "on" or "off"') - - cmd = '/sbin/ethtool -K {} gso {}'.format(self._ifname, state) - return self._cmd(cmd) - - - def set_sg(self, state): - """ - Example: - >>> from vyos.ifconfig import EthernetIf - >>> i = EthernetIf('eth0') - >>> i.set_sg('on') - """ - if state not in ['on', 'off']: - raise ValueError('state must be "on" or "off"') - - cmd = '/sbin/ethtool -K {} sg {}'.format(self._ifname, state) - return self._cmd(cmd) - - - def set_tso(self, state): - """ - Example: - >>> from vyos.ifconfig import EthernetIf - >>> i = EthernetIf('eth0') - >>> i.set_tso('on') - """ - if state not in ['on', 'off']: - raise ValueError('state must be "on" or "off"') - - cmd = '/sbin/ethtool -K {} tso {}'.format(self._ifname, state) - return self._cmd(cmd) - - - def set_ufo(self, state): - """ - Example: - >>> from vyos.ifconfig import EthernetIf - >>> i = EthernetIf('eth0') - >>> i.set_udp_offload('on') - """ - if state not in ['on', 'off']: - raise ValueError('state must be "on" or "off"') - - cmd = '/sbin/ethtool -K {} ufo {}'.format(self._ifname, state) - return self._cmd(cmd) - -class MACVLANIf(VLANIf): - """ - Abstraction of a Linux MACvlan interface - """ - def __init__(self, ifname, config=''): - self._ifname = ifname - - if not os.path.exists('/sys/class/net/{}'.format(self._ifname)) and config: - cmd = 'ip link add {intf} link {link} type macvlan mode {mode}' \ - .format(intf=self._ifname, link=config['link'], mode=config['mode']) - self._cmd(cmd) - - super().__init__(ifname, type='macvlan') - - @staticmethod - def get_config(): - """ - VXLAN interfaces require a configuration when they are added using - iproute2. This static method will provide the configuration dictionary - used by this class. - - Example: - >> dict = MACVLANIf().get_config() - """ - config = { - 'address': '', - 'link': 0, - 'mode': '' - } - return config - - def set_mode(self, mode): - """ - """ - - cmd = 'ip link set dev {} type macvlan mode {}'.format(self._ifname, mode) - return self._cmd(cmd) - - -class BondIf(VLANIf): - """ - The Linux bonding driver provides a method for aggregating multiple network - interfaces into a single logical "bonded" interface. The behavior of the - bonded interfaces depends upon the mode; generally speaking, modes provide - either hot standby or load balancing services. Additionally, link integrity - monitoring may be performed. - """ - def __init__(self, ifname): - super().__init__(ifname, type='bond') - - def remove(self): - """ - Remove interface from operating system. Removing the interface - deconfigures all assigned IP addresses and clear possible DHCP(v6) - client processes. - Example: - >>> from vyos.ifconfig import Interface - >>> i = Interface('eth0') - >>> i.remove() - """ - # when a bond member gets deleted, all members are placed in A/D state - # even when they are enabled inside CLI. This will make the config - # and system look async. - slave_list = [] - for s in self.get_slaves(): - slave = { - 'ifname' : s, - 'state': Interface(s).get_state() - } - slave_list.append(slave) - - # remove bond master which places members in disabled state - super().remove() - - # replicate previous interface state before bond destruction back to - # physical interface - for slave in slave_list: - i = Interface(slave['ifname']) - i.set_state(slave['state']) - - - def set_hash_policy(self, mode): - """ - Selects the transmit hash policy to use for slave selection in - balance-xor, 802.3ad, and tlb modes. Possible values are: layer2, - layer2+3, layer3+4, encap2+3, encap3+4. - - The default value is layer2 - - Example: - >>> from vyos.ifconfig import BondIf - >>> BondIf('bond0').set_hash_policy('layer2+3') - """ - if not mode in ['layer2', 'layer2+3', 'layer3+4', 'encap2+3', 'encap3+4']: - raise ValueError("Value out of range") - return self._write_sysfs('/sys/class/net/{}/bonding/xmit_hash_policy' - .format(self._ifname), mode) - - def set_arp_interval(self, interval): - """ - Specifies the ARP link monitoring frequency in milliseconds. - - The ARP monitor works by periodically checking the slave devices - to determine whether they have sent or received traffic recently - (the precise criteria depends upon the bonding mode, and the - state of the slave). Regular traffic is generated via ARP probes - issued for the addresses specified by the arp_ip_target option. - - If ARP monitoring is used in an etherchannel compatible mode - (modes 0 and 2), the switch should be configured in a mode that - evenly distributes packets across all links. If the switch is - configured to distribute the packets in an XOR fashion, all - replies from the ARP targets will be received on the same link - which could cause the other team members to fail. - - value of 0 disables ARP monitoring. The default value is 0. - - Example: - >>> from vyos.ifconfig import BondIf - >>> BondIf('bond0').set_arp_interval('100') - """ - if int(interval) == 0: - """ - Specifies the MII link monitoring frequency in milliseconds. - This determines how often the link state of each slave is - inspected for link failures. A value of zero disables MII - link monitoring. A value of 100 is a good starting point. - """ - return self._write_sysfs('/sys/class/net/{}/bonding/miimon' - .format(self._ifname), interval) - else: - return self._write_sysfs('/sys/class/net/{}/bonding/arp_interval' - .format(self._ifname), interval) - - def get_arp_ip_target(self): - """ - Specifies the IP addresses to use as ARP monitoring peers when - arp_interval is > 0. These are the targets of the ARP request sent to - determine the health of the link to the targets. Specify these values - in ddd.ddd.ddd.ddd format. Multiple IP addresses must be separated by - a comma. At least one IP address must be given for ARP monitoring to - function. The maximum number of targets that can be specified is 16. - - The default value is no IP addresses. - - Example: - >>> from vyos.ifconfig import BondIf - >>> BondIf('bond0').get_arp_ip_target() - '192.0.2.1' - """ - return self._read_sysfs('/sys/class/net/{}/bonding/arp_ip_target' - .format(self._ifname)) - - def set_arp_ip_target(self, target): - """ - Specifies the IP addresses to use as ARP monitoring peers when - arp_interval is > 0. These are the targets of the ARP request sent to - determine the health of the link to the targets. Specify these values - in ddd.ddd.ddd.ddd format. Multiple IP addresses must be separated by - a comma. At least one IP address must be given for ARP monitoring to - function. The maximum number of targets that can be specified is 16. - - The default value is no IP addresses. - - Example: - >>> from vyos.ifconfig import BondIf - >>> BondIf('bond0').set_arp_ip_target('192.0.2.1') - >>> BondIf('bond0').get_arp_ip_target() - '192.0.2.1' - """ - return self._write_sysfs('/sys/class/net/{}/bonding/arp_ip_target' - .format(self._ifname), target) - - def add_port(self, interface): - """ - Enslave physical interface to bond. - - Example: - >>> from vyos.ifconfig import BondIf - >>> BondIf('bond0').add_port('eth0') - >>> BondIf('bond0').add_port('eth1') - """ - # An interface can only be added to a bond if it is in 'down' state. If - # interface is in 'up' state, the following Kernel error will be thrown: - # bond0: eth1 is up - this may be due to an out of date ifenslave. - Interface(interface).set_state('down') - - return self._write_sysfs('/sys/class/net/{}/bonding/slaves' - .format(self._ifname), '+' + interface) - - def del_port(self, interface): - """ - Remove physical port from bond - - Example: - >>> from vyos.ifconfig import BondIf - >>> BondIf('bond0').del_port('eth1') - """ - return self._write_sysfs('/sys/class/net/{}/bonding/slaves' - .format(self._ifname), '-' + interface) - - def get_slaves(self): - """ - Return a list with all configured slave interfaces on this bond. - - Example: - >>> from vyos.ifconfig import BondIf - >>> BondIf('bond0').get_slaves() - ['eth1', 'eth2'] - """ - enslaved_ifs = [] - # retrieve real enslaved interfaces from OS kernel - sysfs_bond = '/sys/class/net/{}'.format(self._ifname) - if os.path.isdir(sysfs_bond): - for directory in os.listdir(sysfs_bond): - if 'lower_' in directory: - enslaved_ifs.append(directory.replace('lower_','')) - - return enslaved_ifs - - - def set_primary(self, interface): - """ - A string (eth0, eth2, etc) specifying which slave is the primary - device. The specified device will always be the active slave while it - is available. Only when the primary is off-line will alternate devices - be used. This is useful when one slave is preferred over another, e.g., - when one slave has higher throughput than another. - - The primary option is only valid for active-backup, balance-tlb and - balance-alb mode. - - Example: - >>> from vyos.ifconfig import BondIf - >>> BondIf('bond0').set_primary('eth2') - """ - if not interface: - # reset primary interface - interface = '\0' - - return self._write_sysfs('/sys/class/net/{}/bonding/primary' - .format(self._ifname), interface) - - def set_mode(self, mode): - """ - Specifies one of the bonding policies. The default is balance-rr - (round robin). - - Possible values are: balance-rr, active-backup, balance-xor, - broadcast, 802.3ad, balance-tlb, balance-alb - - NOTE: the bonding mode can not be changed when the bond itself has - slaves - - Example: - >>> from vyos.ifconfig import BondIf - >>> BondIf('bond0').set_mode('802.3ad') - """ - if not mode in [ - 'balance-rr', 'active-backup', 'balance-xor', 'broadcast', - '802.3ad', 'balance-tlb', 'balance-alb']: - raise ValueError("Value out of range") - - return self._write_sysfs('/sys/class/net/{}/bonding/mode' - .format(self._ifname), mode) - -class WireGuardIf(Interface): - """ - Wireguard interface class, contains a comnfig dictionary since - wireguard VPN is being comnfigured via the wg command rather than - writing the config into a file. Otherwise if a pre-shared key is used - (symetric enryption key), it would we exposed within multiple files. - Currently it's only within the config.boot if the config was saved. - - Example: - >>> from vyos.ifconfig import WireGuardIf as wg_if - >>> wg_intfc = wg_if("wg01") - >>> print (wg_intfc.wg_config) - {'private-key': None, 'keepalive': 0, 'endpoint': None, 'port': 0, - 'allowed-ips': [], 'pubkey': None, 'fwmark': 0, 'psk': '/dev/null'} - >>> wg_intfc.wg_config['keepalive'] = 100 - >>> print (wg_intfc.wg_config) - {'private-key': None, 'keepalive': 100, 'endpoint': None, 'port': 0, - 'allowed-ips': [], 'pubkey': None, 'fwmark': 0, 'psk': '/dev/null'} - """ - - def __init__(self, ifname): - super().__init__(ifname, type='wireguard') - - self.config = { - 'port': 0, - 'private-key': None, - 'pubkey': None, - 'psk': '/dev/null', - 'allowed-ips': [], - 'fwmark': 0x00, - 'endpoint': None, - 'keepalive': 0 - } - - def update(self): - if not self.config['private-key']: - raise ValueError("private key required") - else: - # fmask permission check? - pass - - cmd = "wg set {} ".format(self._ifname) - cmd += "listen-port {} ".format(self.config['port']) - cmd += "fwmark {} ".format(str(self.config['fwmark'])) - cmd += "private-key {} ".format(self.config['private-key']) - cmd += "peer {} ".format(self.config['pubkey']) - cmd += " preshared-key {} ".format(self.config['psk']) - cmd += " allowed-ips " - for aip in self.config['allowed-ips']: - if aip != self.config['allowed-ips'][-1]: - cmd += aip + "," - else: - cmd += aip - if self.config['endpoint']: - cmd += " endpoint {}".format(self.config['endpoint']) - cmd += " persistent-keepalive {}".format(self.config['keepalive']) - - self._cmd(cmd) - - # remove psk since it isn't required anymore and is saved in the cli - # config only !! - if self.config['psk'] != '/dev/null': - if os.path.exists(self.config['psk']): - os.remove(self.config['psk']) - - - def remove_peer(self, peerkey): - """ - Remove a peer of an interface, peers are identified by their public key. - Giving it a readable name is a vyos feature, to remove a peer the pubkey - and the interface is needed, to remove the entry. - """ - cmd = "wg set {0} peer {1} remove".format( - self._ifname, str(peerkey)) - return self._cmd(cmd) - - def op_show_interface(self): - wgdump = vyos.interfaces.wireguard_dump().get(self._ifname,None) - - c = Config() - c.set_level(["interfaces","wireguard",self._ifname]) - description = c.return_effective_value(["description"]) - ips = c.return_effective_values(["address"]) - - print ("interface: {}".format(self._ifname)) - if (description): - print (" description: {}".format(description)) - - if (ips): - print (" address: {}".format(", ".join(ips))) - print (" public key: {}".format(wgdump['public_key'])) - print (" private key: (hidden)") - print (" listening port: {}".format(wgdump['listen_port'])) - print () - - for peer in c.list_effective_nodes(["peer"]): - if wgdump['peers']: - pubkey = c.return_effective_value(["peer",peer,"pubkey"]) - if pubkey in wgdump['peers']: - wgpeer = wgdump['peers'][pubkey] - - print (" peer: {}".format(peer)) - print (" public key: {}".format(pubkey)) - - """ figure out if the tunnel is recently active or not """ - status = "inactive" - if (wgpeer['latest_handshake'] is None): - """ no handshake ever """ - status = "inactive" - else: - if int(wgpeer['latest_handshake']) > 0: - delta = timedelta(seconds=int(time.time() - wgpeer['latest_handshake'])) - print (" latest handshake: {}".format(delta)) - if (time.time() - int(wgpeer['latest_handshake']) < (60*5)): - """ Five minutes and the tunnel is still active """ - status = "active" - else: - """ it's been longer than 5 minutes """ - status = "inactive" - elif int(wgpeer['latest_handshake']) == 0: - """ no handshake ever """ - status = "inactive" - print (" status: {}".format(status)) - - if wgpeer['endpoint'] is not None: - print (" endpoint: {}".format(wgpeer['endpoint'])) - - if wgpeer['allowed_ips'] is not None: - print (" allowed ips: {}".format(",".join(wgpeer['allowed_ips']).replace(",",", "))) - - if wgpeer['transfer_rx'] > 0 or wgpeer['transfer_tx'] > 0: - rx_size =size(wgpeer['transfer_rx'],system=alternative) - tx_size =size(wgpeer['transfer_tx'],system=alternative) - print (" transfer: {} received, {} sent".format(rx_size,tx_size)) - - if wgpeer['persistent_keepalive'] is not None: - print (" persistent keepalive: every {} seconds".format(wgpeer['persistent_keepalive'])) - print() - super().op_show_interface_stats() - - -class VXLANIf(Interface): - """ - The VXLAN protocol is a tunnelling protocol designed to solve the - problem of limited VLAN IDs (4096) in IEEE 802.1q. With VXLAN the - size of the identifier is expanded to 24 bits (16777216). - - VXLAN is described by IETF RFC 7348, and has been implemented by a - number of vendors. The protocol runs over UDP using a single - destination port. This document describes the Linux kernel tunnel - device, there is also a separate implementation of VXLAN for - Openvswitch. - - Unlike most tunnels, a VXLAN is a 1 to N network, not just point to - point. A VXLAN device can learn the IP address of the other endpoint - either dynamically in a manner similar to a learning bridge, or make - use of statically-configured forwarding entries. - - For more information please refer to: - https://www.kernel.org/doc/Documentation/networking/vxlan.txt - """ - def __init__(self, ifname, config=''): - if config: - self._ifname = ifname - - if not os.path.exists('/sys/class/net/{}'.format(self._ifname)): - # we assume that by default a multicast interface is created - group = 'group {}'.format(config['group']) - - # if remote host is specified we ignore the multicast address - if config['remote']: - group = 'remote {}'.format(config['remote']) - - # an underlay device is not always specified - dev = '' - if config['dev']: - dev = 'dev {}'.format(config['dev']) - - cmd = 'ip link add {intf} type vxlan id {vni} {grp_rem} {dev} dstport {port}' \ - .format(intf=self._ifname, vni=config['vni'], grp_rem=group, dev=dev, port=config['port']) - self._cmd(cmd) - - super().__init__(ifname, type='vxlan') - - @staticmethod - def get_config(): - """ - VXLAN interfaces require a configuration when they are added using - iproute2. This static method will provide the configuration dictionary - used by this class. - - Example: - >> dict = VXLANIf().get_config() - """ - config = { - 'vni': 0, - 'dev': '', - 'group': '', - 'port': 8472, # The Linux implementation of VXLAN pre-dates - # the IANA's selection of a standard destination port - 'remote': '' - } - return config - -class GeneveIf(Interface): - """ - Geneve: Generic Network Virtualization Encapsulation - - For more information please refer to: - https://tools.ietf.org/html/draft-gross-geneve-00 - https://www.redhat.com/en/blog/what-geneve - https://developers.redhat.com/blog/2019/05/17/an-introduction-to-linux-virtual-interfaces-tunnels/#geneve - https://lwn.net/Articles/644938/ - """ - def __init__(self, ifname, config=''): - if config: - self._ifname = ifname - if not os.path.exists('/sys/class/net/{}'.format(self._ifname)): - cmd = 'ip link add name {} type geneve id {} remote {}' \ - .format(self._ifname, config['vni'], config['remote']) - self._cmd(cmd) - - # interface is always A/D down. It needs to be enabled explicitly - self.set_state('down') - - super().__init__(ifname, type='geneve') - - @staticmethod - def get_config(): - """ - GENEVE interfaces require a configuration when they are added using - iproute2. This static method will provide the configuration dictionary - used by this class. - - Example: - >> dict = GeneveIf().get_config() - """ - config = { - 'vni': 0, - 'remote': '' - } - return config - -class L2TPv3If(Interface): - """ - The Linux bonding driver provides a method for aggregating multiple network - interfaces into a single logical "bonded" interface. The behavior of the - bonded interfaces depends upon the mode; generally speaking, modes provide - either hot standby or load balancing services. Additionally, link integrity - monitoring may be performed. - """ - def __init__(self, ifname, config=''): - self._config = {} - if config: - self._ifname = ifname - self._config = config - if not os.path.exists('/sys/class/net/{}'.format(self._ifname)): - # create tunnel interface - cmd = 'ip l2tp add tunnel tunnel_id {} '.format(config['tunnel_id']) - cmd += 'peer_tunnel_id {} '.format(config['peer_tunnel_id']) - cmd += 'udp_sport {} '.format(config['local_port']) - cmd += 'udp_dport {} '.format(config['remote_port']) - cmd += 'encap {} '.format(config['encapsulation']) - cmd += 'local {} '.format(config['local_address']) - cmd += 'remote {} '.format(config['remote_address']) - self._cmd(cmd) - - # setup session - cmd = 'ip l2tp add session name {} '.format(self._ifname) - cmd += 'tunnel_id {} '.format(config['tunnel_id']) - cmd += 'session_id {} '.format(config['session_id']) - cmd += 'peer_session_id {} '.format(config['peer_session_id']) - self._cmd(cmd) - - # interface is always A/D down. It needs to be enabled explicitly - self.set_state('down') - - super().__init__(ifname, type='l2tp') - - def remove(self): - """ - Remove interface from operating system. Removing the interface - deconfigures all assigned IP addresses. - Example: - >>> from vyos.ifconfig import L2TPv3If - >>> i = L2TPv3If('l2tpeth0') - >>> i.remove() - """ - - if os.path.exists('/sys/class/net/{}'.format(self._ifname)): - # interface is always A/D down. It needs to be enabled explicitly - self.set_state('down') - - if self._config['tunnel_id'] and self._config['session_id']: - cmd = 'ip l2tp del session tunnel_id {} '.format(self._config['tunnel_id']) - cmd += 'session_id {} '.format(self._config['session_id']) - self._cmd(cmd) - - if self._config['tunnel_id']: - cmd = 'ip l2tp del tunnel tunnel_id {} '.format(self._config['tunnel_id']) - self._cmd(cmd) - - @staticmethod - def get_config(): - """ - L2TPv3 interfaces require a configuration when they are added using - iproute2. This static method will provide the configuration dictionary - used by this class. - - Example: - >> dict = L2TPv3If().get_config() - """ - config = { - 'peer_tunnel_id': '', - 'local_port': 0, - 'remote_port': 0, - 'encapsulation': 'udp', - 'local_address': '', - 'remote_address': '', - 'session_id': '', - 'tunnel_id': '', - 'peer_session_id': '' - } - return config diff --git a/python/vyos/ifconfig/__init__.py b/python/vyos/ifconfig/__init__.py new file mode 100644 index 000000000..1f9956af0 --- /dev/null +++ b/python/vyos/ifconfig/__init__.py @@ -0,0 +1,39 @@ +# Copyright 2019 VyOS maintainers and contributors <maintainers@vyos.io> +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library. If not, see <http://www.gnu.org/licenses/>. + + +from vyos.ifconfig.interface import Interface + +from vyos.ifconfig.bond import BondIf +from vyos.ifconfig.bridge import BridgeIf +from vyos.ifconfig.dummy import DummyIf +from vyos.ifconfig.ethernet import EthernetIf +from vyos.ifconfig.geneve import GeneveIf +from vyos.ifconfig.loopback import LoopbackIf +from vyos.ifconfig.macvlan import MACVLANIf +from vyos.ifconfig.vxlan import VXLANIf +from vyos.ifconfig.wireguard import WireGuardIf +from vyos.ifconfig.vtun import VTunIf +from vyos.ifconfig.pppoe import PPPoEIf +from vyos.ifconfig.tunnel import GREIf +from vyos.ifconfig.tunnel import GRETapIf +from vyos.ifconfig.tunnel import IP6GREIf +from vyos.ifconfig.tunnel import IPIPIf +from vyos.ifconfig.tunnel import IPIP6If +from vyos.ifconfig.tunnel import IP6IP6If +from vyos.ifconfig.tunnel import SitIf +from vyos.ifconfig.tunnel import Sit6RDIf +from vyos.ifconfig.wireless import WiFiIf +from vyos.ifconfig.l2tpv3 import L2TPv3If diff --git a/python/vyos/ifconfig/afi.py b/python/vyos/ifconfig/afi.py new file mode 100644 index 000000000..fd263d220 --- /dev/null +++ b/python/vyos/ifconfig/afi.py @@ -0,0 +1,19 @@ +# Copyright 2019 VyOS maintainers and contributors <maintainers@vyos.io> +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library. If not, see <http://www.gnu.org/licenses/>. + +# https://www.iana.org/assignments/address-family-numbers/address-family-numbers.xhtml + +IP4 = 1 +IP6 = 2 diff --git a/python/vyos/ifconfig/bond.py b/python/vyos/ifconfig/bond.py new file mode 100644 index 000000000..47dd4ff34 --- /dev/null +++ b/python/vyos/ifconfig/bond.py @@ -0,0 +1,279 @@ +# Copyright 2019 VyOS maintainers and contributors <maintainers@vyos.io> +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library. If not, see <http://www.gnu.org/licenses/>. + +import os + +from vyos.ifconfig.interface import Interface +from vyos.ifconfig.vlan import VLAN + +from vyos.validate import assert_list +from vyos.validate import assert_positive + + +@Interface.register +@VLAN.enable +class BondIf(Interface): + """ + The Linux bonding driver provides a method for aggregating multiple network + interfaces into a single logical "bonded" interface. The behavior of the + bonded interfaces depends upon the mode; generally speaking, modes provide + either hot standby or load balancing services. Additionally, link integrity + monitoring may be performed. + """ + + default = { + 'type': 'bond', + } + definition = { + **Interface.definition, + ** { + 'section': 'bonding', + 'prefixes': ['bond', ], + 'broadcast': True, + 'bridgeable': True, + }, + } + + _sysfs_set = {**Interface._sysfs_set, **{ + 'bond_hash_policy': { + 'validate': lambda v: assert_list(v, ['layer2', 'layer2+3', 'layer3+4', 'encap2+3', 'encap3+4']), + 'location': '/sys/class/net/{ifname}/bonding/xmit_hash_policy', + }, + 'bond_miimon': { + 'validate': assert_positive, + 'location': '/sys/class/net/{ifname}/bonding/miimon' + }, + 'bond_arp_interval': { + 'validate': assert_positive, + 'location': '/sys/class/net/{ifname}/bonding/arp_interval' + }, + 'bond_arp_ip_target': { + # XXX: no validation of the IP + 'location': '/sys/class/net/{ifname}/bonding/arp_ip_target', + }, + 'bond_add_port': { + 'location': '/sys/class/net/{ifname}/bonding/slaves', + }, + 'bond_del_port': { + 'location': '/sys/class/net/{ifname}/bonding/slaves', + }, + 'bond_primary': { + 'convert': lambda name: name if name else '\0', + 'location': '/sys/class/net/{ifname}/bonding/primary', + }, + 'bond_mode': { + 'validate': lambda v: assert_list(v, ['balance-rr', 'active-backup', 'balance-xor', 'broadcast', '802.3ad', 'balance-tlb', 'balance-alb']), + 'location': '/sys/class/net/{ifname}/bonding/mode', + }, + }} + + _sysfs_get = {**Interface._sysfs_get, **{ + 'bond_arp_ip_target': { + 'location': '/sys/class/net/{ifname}/bonding/arp_ip_target', + } + }} + + def remove(self): + """ + Remove interface from operating system. Removing the interface + deconfigures all assigned IP addresses and clear possible DHCP(v6) + client processes. + Example: + >>> from vyos.ifconfig import Interface + >>> i = Interface('eth0') + >>> i.remove() + """ + # when a bond member gets deleted, all members are placed in A/D state + # even when they are enabled inside CLI. This will make the config + # and system look async. + slave_list = [] + for s in self.get_slaves(): + slave = { + 'ifname': s, + 'state': Interface(s).get_admin_state() + } + slave_list.append(slave) + + # remove bond master which places members in disabled state + super().remove() + + # replicate previous interface state before bond destruction back to + # physical interface + for slave in slave_list: + i = Interface(slave['ifname']) + i.set_admin_state(slave['state']) + + def set_hash_policy(self, mode): + """ + Selects the transmit hash policy to use for slave selection in + balance-xor, 802.3ad, and tlb modes. Possible values are: layer2, + layer2+3, layer3+4, encap2+3, encap3+4. + + The default value is layer2 + + Example: + >>> from vyos.ifconfig import BondIf + >>> BondIf('bond0').set_hash_policy('layer2+3') + """ + self.set_interface('bond_hash_policy', mode) + + def set_arp_interval(self, interval): + """ + Specifies the ARP link monitoring frequency in milliseconds. + + The ARP monitor works by periodically checking the slave devices + to determine whether they have sent or received traffic recently + (the precise criteria depends upon the bonding mode, and the + state of the slave). Regular traffic is generated via ARP probes + issued for the addresses specified by the arp_ip_target option. + + If ARP monitoring is used in an etherchannel compatible mode + (modes 0 and 2), the switch should be configured in a mode that + evenly distributes packets across all links. If the switch is + configured to distribute the packets in an XOR fashion, all + replies from the ARP targets will be received on the same link + which could cause the other team members to fail. + + value of 0 disables ARP monitoring. The default value is 0. + + Example: + >>> from vyos.ifconfig import BondIf + >>> BondIf('bond0').set_arp_interval('100') + """ + if int(interval) == 0: + """ + Specifies the MII link monitoring frequency in milliseconds. + This determines how often the link state of each slave is + inspected for link failures. A value of zero disables MII + link monitoring. A value of 100 is a good starting point. + """ + return self.set_interface('bond_miimon', interval) + else: + return self.set_interface('bond_arp_interval', interval) + + def get_arp_ip_target(self): + """ + Specifies the IP addresses to use as ARP monitoring peers when + arp_interval is > 0. These are the targets of the ARP request sent to + determine the health of the link to the targets. Specify these values + in ddd.ddd.ddd.ddd format. Multiple IP addresses must be separated by + a comma. At least one IP address must be given for ARP monitoring to + function. The maximum number of targets that can be specified is 16. + + The default value is no IP addresses. + + Example: + >>> from vyos.ifconfig import BondIf + >>> BondIf('bond0').get_arp_ip_target() + '192.0.2.1' + """ + return self.get_interface('bond_arp_ip_target') + + def set_arp_ip_target(self, target): + """ + Specifies the IP addresses to use as ARP monitoring peers when + arp_interval is > 0. These are the targets of the ARP request sent to + determine the health of the link to the targets. Specify these values + in ddd.ddd.ddd.ddd format. Multiple IP addresses must be separated by + a comma. At least one IP address must be given for ARP monitoring to + function. The maximum number of targets that can be specified is 16. + + The default value is no IP addresses. + + Example: + >>> from vyos.ifconfig import BondIf + >>> BondIf('bond0').set_arp_ip_target('192.0.2.1') + >>> BondIf('bond0').get_arp_ip_target() + '192.0.2.1' + """ + return self.set_interface('bond_arp_ip_target', target) + + def add_port(self, interface): + """ + Enslave physical interface to bond. + + Example: + >>> from vyos.ifconfig import BondIf + >>> BondIf('bond0').add_port('eth0') + >>> BondIf('bond0').add_port('eth1') + """ + # An interface can only be added to a bond if it is in 'down' state. If + # interface is in 'up' state, the following Kernel error will be thrown: + # bond0: eth1 is up - this may be due to an out of date ifenslave. + Interface(interface).set_admin_state('down') + return self.set_interface('bond_add_port', f'+{interface}') + + def del_port(self, interface): + """ + Remove physical port from bond + + Example: + >>> from vyos.ifconfig import BondIf + >>> BondIf('bond0').del_port('eth1') + """ + return self.set_interface('bond_del_port', f'-{interface}') + + def get_slaves(self): + """ + Return a list with all configured slave interfaces on this bond. + + Example: + >>> from vyos.ifconfig import BondIf + >>> BondIf('bond0').get_slaves() + ['eth1', 'eth2'] + """ + enslaved_ifs = [] + # retrieve real enslaved interfaces from OS kernel + sysfs_bond = '/sys/class/net/{}'.format(self.config['ifname']) + if os.path.isdir(sysfs_bond): + for directory in os.listdir(sysfs_bond): + if 'lower_' in directory: + enslaved_ifs.append(directory.replace('lower_', '')) + + return enslaved_ifs + + def set_primary(self, interface): + """ + A string (eth0, eth2, etc) specifying which slave is the primary + device. The specified device will always be the active slave while it + is available. Only when the primary is off-line will alternate devices + be used. This is useful when one slave is preferred over another, e.g., + when one slave has higher throughput than another. + + The primary option is only valid for active-backup, balance-tlb and + balance-alb mode. + + Example: + >>> from vyos.ifconfig import BondIf + >>> BondIf('bond0').set_primary('eth2') + """ + return self.set_interface('bond_primary', interface) + + def set_mode(self, mode): + """ + Specifies one of the bonding policies. The default is balance-rr + (round robin). + + Possible values are: balance-rr, active-backup, balance-xor, + broadcast, 802.3ad, balance-tlb, balance-alb + + NOTE: the bonding mode can not be changed when the bond itself has + slaves + + Example: + >>> from vyos.ifconfig import BondIf + >>> BondIf('bond0').set_mode('802.3ad') + """ + return self.set_interface('bond_mode', mode) diff --git a/python/vyos/ifconfig/bridge.py b/python/vyos/ifconfig/bridge.py new file mode 100644 index 000000000..44b92c1db --- /dev/null +++ b/python/vyos/ifconfig/bridge.py @@ -0,0 +1,189 @@ +# Copyright 2019 VyOS maintainers and contributors <maintainers@vyos.io> +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library. If not, see <http://www.gnu.org/licenses/>. + + +from vyos.ifconfig.interface import Interface + +from vyos.validate import assert_boolean +from vyos.validate import assert_positive + + +@Interface.register +class BridgeIf(Interface): + """ + A bridge is a way to connect two Ethernet segments together in a protocol + independent way. Packets are forwarded based on Ethernet address, rather + than IP address (like a router). Since forwarding is done at Layer 2, all + protocols can go transparently through a bridge. + + The Linux bridge code implements a subset of the ANSI/IEEE 802.1d standard. + """ + + default = { + 'type': 'bridge', + } + definition = { + **Interface.definition, + **{ + 'section': 'bridge', + 'prefixes': ['br', ], + 'broadcast': True, + }, + } + + _sysfs_set = {**Interface._sysfs_set, **{ + 'ageing_time': { + 'validate': assert_positive, + 'convert': lambda t: int(t) * 100, + 'location': '/sys/class/net/{ifname}/bridge/ageing_time', + }, + 'forward_delay': { + 'validate': assert_positive, + 'convert': lambda t: int(t) * 100, + 'location': '/sys/class/net/{ifname}/bridge/forward_delay', + }, + 'hello_time': { + 'validate': assert_positive, + 'convert': lambda t: int(t) * 100, + 'location': '/sys/class/net/{ifname}/bridge/hello_time', + }, + 'max_age': { + 'validate': assert_positive, + 'convert': lambda t: int(t) * 100, + 'location': '/sys/class/net/{ifname}/bridge/max_age', + }, + 'priority': { + 'validate': assert_positive, + 'location': '/sys/class/net/{ifname}/bridge/priority', + }, + 'stp': { + 'validate': assert_boolean, + 'location': '/sys/class/net/{ifname}/bridge/stp_state', + }, + 'multicast_querier': { + 'validate': assert_boolean, + 'location': '/sys/class/net/{ifname}/bridge/multicast_querier', + }, + }} + + _command_set = {**Interface._command_set, **{ + 'add_port': { + 'shellcmd': 'ip link set dev {value} master {ifname}', + }, + 'del_port': { + 'shellcmd': 'ip link set dev {value} nomaster', + }, + }} + + + def set_ageing_time(self, time): + """ + Set bridge interface MAC address aging time in seconds. Internal kernel + representation is in centiseconds. Kernel default is 300 seconds. + + Example: + >>> from vyos.ifconfig import BridgeIf + >>> BridgeIf('br0').ageing_time(2) + """ + self.set_interface('ageing_time', time) + + def set_forward_delay(self, time): + """ + Set bridge forwarding delay in seconds. Internal Kernel representation + is in centiseconds. + + Example: + >>> from vyos.ifconfig import BridgeIf + >>> BridgeIf('br0').forward_delay(15) + """ + self.set_interface('forward_delay', time) + + def set_hello_time(self, time): + """ + Set bridge hello time in seconds. Internal Kernel representation + is in centiseconds. + + Example: + >>> from vyos.ifconfig import BridgeIf + >>> BridgeIf('br0').set_hello_time(2) + """ + self.set_interface('hello_time', time) + + def set_max_age(self, time): + """ + Set bridge max message age in seconds. Internal Kernel representation + is in centiseconds. + + Example: + >>> from vyos.ifconfig import Interface + >>> BridgeIf('br0').set_max_age(30) + """ + self.set_interface('max_age', time) + + def set_priority(self, priority): + """ + Set bridge max aging time in seconds. + + Example: + >>> from vyos.ifconfig import BridgeIf + >>> BridgeIf('br0').set_priority(8192) + """ + self.set_interface('priority', priority) + + def set_stp(self, state): + """ + Set bridge STP (Spanning Tree) state. 0 -> STP disabled, 1 -> STP enabled + + Example: + >>> from vyos.ifconfig import BridgeIf + >>> BridgeIf('br0').set_stp(1) + """ + self.set_interface('stp', state) + + def set_multicast_querier(self, enable): + """ + Sets whether the bridge actively runs a multicast querier or not. When a + bridge receives a 'multicast host membership' query from another network + host, that host is tracked based on the time that the query was received + plus the multicast query interval time. + + Use enable=1 to enable or enable=0 to disable + + Example: + >>> from vyos.ifconfig import Interface + >>> BridgeIf('br0').set_multicast_querier(1) + """ + self.set_interface('multicast_querier', enable) + + def add_port(self, interface): + """ + Add physical interface to bridge (member port) + + Example: + >>> from vyos.ifconfig import Interface + >>> BridgeIf('br0').add_port('eth0') + >>> BridgeIf('br0').add_port('eth1') + """ + return self.set_interface('add_port', interface) + + def del_port(self, interface): + """ + Remove member port from bridge instance. + + Example: + >>> from vyos.ifconfig import Interface + >>> BridgeIf('br0').del_port('eth1') + """ + return self.set_interface('del_port', interface) diff --git a/python/vyos/ifconfig/control.py b/python/vyos/ifconfig/control.py new file mode 100644 index 000000000..c7a2fa2d6 --- /dev/null +++ b/python/vyos/ifconfig/control.py @@ -0,0 +1,154 @@ +# Copyright 2019 VyOS maintainers and contributors <maintainers@vyos.io> +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library. If not, see <http://www.gnu.org/licenses/>. + + +import os + +from vyos.util import debug, debug_msg +from vyos.util import popen, cmd +from vyos.ifconfig.register import Register + + +class Control(Register): + _command_get = {} + _command_set = {} + + def __init__(self, **kargs): + # some commands (such as operation comands - show interfaces, etc.) + # need to query the interface statistics. If the interface + # code is used and the debugging is enabled, the screen output + # will include both the command but also the debugging for that command + # to prevent this, debugging can be explicitely disabled + + # if debug is not explicitely disabled the the config, enable it + self.debug = '' + if kargs.get('debug', True): + self.debug = debug('ifconfig') + + def _debug_msg (self, message): + return debug_msg(message, self.debug) + + def _popen(self, command): + return popen(command, self.debug) + + def _cmd(self, command): + return cmd(command, self.debug) + + def _get_command(self, config, name): + """ + Using the defined names, set data write to sysfs. + """ + cmd = self._command_get[name]['shellcmd'].format(**config) + return self._command_get[name].get('format', lambda _: _)(self._cmd(cmd)) + + def _set_command(self, config, name, value): + """ + Using the defined names, set data write to sysfs. + """ + # the code can pass int as int + value = str(value) + + validate = self._command_set[name].get('validate', None) + if validate: + try: + validate(value) + except Exception as e: + raise e.__class__(f'Could not set {name}. {e}') + + convert = self._command_set[name].get('convert', None) + if convert: + value = convert(value) + + possible = self._command_set[name].get('possible', None) + if possible and not possible(config['ifname'], value): + return False + + config = {**config, **{'value': value}} + + cmd = self._command_set[name]['shellcmd'].format(**config) + return self._command_set[name].get('format', lambda _: _)(self._cmd(cmd)) + + _sysfs_get = {} + _sysfs_set = {} + + def _read_sysfs(self, filename): + """ + Provide a single primitive w/ error checking for reading from sysfs. + """ + value = None + with open(filename, 'r') as f: + value = f.read().rstrip('\n') + + self._debug_msg("read '{}' < '{}'".format(value, filename)) + return value + + def _write_sysfs(self, filename, value): + """ + Provide a single primitive w/ error checking for writing to sysfs. + """ + self._debug_msg("write '{}' > '{}'".format(value, filename)) + if os.path.isfile(filename): + with open(filename, 'w') as f: + f.write(str(value)) + return True + return False + + def _get_sysfs(self, config, name): + """ + Using the defined names, get data write from sysfs. + """ + filename = self._sysfs_get[name]['location'].format(**config) + if not filename: + return None + return self._read_sysfs(filename) + + def _set_sysfs(self, config, name, value): + """ + Using the defined names, set data write to sysfs. + """ + # the code can pass int as int + value = str(value) + + validate = self._sysfs_set[name].get('validate', None) + if validate: + validate(value) + + config = {**config, **{'value': value}} + + convert = self._sysfs_set[name].get('convert', None) + if convert: + value = convert(value) + + commited = self._write_sysfs( + self._sysfs_set[name]['location'].format(**config), value) + if not commited: + errmsg = self._sysfs_set.get('errormsg', '') + if errmsg: + raise TypeError(errmsg.format(**config)) + return commited + + def get_interface(self, name): + if name in self._sysfs_get: + return self._get_sysfs(self.config, name) + if name in self._command_get: + return self._get_command(self.config, name) + raise KeyError(f'{name} is not a attribute of the interface we can get') + + def set_interface(self, name, value): + if name in self._sysfs_set: + return self._set_sysfs(self.config, name, value) + if name in self._command_set: + return self._set_command(self.config, name, value) + raise KeyError(f'{name} is not a attribute of the interface we can set') diff --git a/python/vyos/ifconfig/dhcp.py b/python/vyos/ifconfig/dhcp.py new file mode 100644 index 000000000..8ec8263b5 --- /dev/null +++ b/python/vyos/ifconfig/dhcp.py @@ -0,0 +1,268 @@ +# Copyright 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 +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library. If not, see <http://www.gnu.org/licenses/>. + +import os +import jinja2 + +from vyos.ifconfig.control import Control + +template_v4 = """ +# generated by ifconfig.py +option rfc3442-classless-static-routes code 121 = array of unsigned integer 8; +timeout 60; +retry 300; + +interface "{{ intf }}" { + send host-name "{{ hostname }}"; + {% if client_id -%} + send dhcp-client-identifier "{{ client_id }}"; + {% endif -%} + {% if vendor_class_id -%} + send vendor-class-identifier "{{ vendor_class_id }}"; + {% endif -%} + request subnet-mask, broadcast-address, routers, domain-name-servers, + rfc3442-classless-static-routes, domain-name, interface-mtu; + require subnet-mask; +} + +""" + +template_v6 = """ +# generated by ifconfig.py +interface "{{ intf }}" { + request routers, domain-name-servers, domain-name; +} + +""" + +class DHCP (Control): + client_base = r'/var/lib/dhcp/dhclient_' + + def __init__ (self, ifname, **kargs): + super().__init__(**kargs) + + # per interface DHCP config files + self._dhcp = { + 4: { + 'ifname': ifname, + 'conf': self.client_base + ifname + '.conf', + 'pid': self.client_base + ifname + '.pid', + 'lease': self.client_base + ifname + '.leases', + 'options': { + 'intf': ifname, + 'hostname': '', + 'client_id': '', + 'vendor_class_id': '' + }, + }, + 6: { + 'ifname': ifname, + 'conf': self.client_base + ifname + '.v6conf', + 'pid': self.client_base + ifname + '.v6pid', + 'lease': self.client_base + ifname + '.v6leases', + 'accept_ra': f'/proc/sys/net/ipv6/conf/{ifname}/accept_ra', + 'options': { + 'intf': ifname, + 'dhcpv6_prm_only': False, + 'dhcpv6_temporary': False + }, + }, + } + + def get_dhcp_options(self): + """ + Return dictionary with supported DHCP options. + + Dictionary should be altered and send back via set_dhcp_options() + so those options are applied when DHCP is run. + """ + return self._dhcp[4]['options'] + + def set_dhcp_options(self, options): + """ + Store new DHCP options used by next run of DHCP client. + """ + self._dhcp[4]['options'] = options + + def get_dhcpv6_options(self): + """ + Return dictionary with supported DHCPv6 options. + + Dictionary should be altered and send back via set_dhcp_options() + so those options are applied when DHCP is run. + """ + return self._dhcp[6]['options'] + + def set_dhcpv6_options(self, options): + """ + Store new DHCP options used by next run of DHCP client. + """ + self._dhcp[6]['options'] = options + + # replace dhcpv4/v6 with systemd.networkd? + def _set_dhcp(self): + """ + Configure interface as DHCP client. The dhclient binary is automatically + started in background! + + Example: + + >>> from vyos.ifconfig import Interface + >>> j = Interface('eth0') + >>> j.set_dhcp() + """ + + dhcp = self.get_dhcp_options() + if not dhcp['hostname']: + # read configured system hostname. + # maybe change to vyos hostd client ??? + with open('/etc/hostname', 'r') as f: + dhcp['hostname'] = f.read().rstrip('\n') + + # render DHCP configuration + tmpl = jinja2.Template(template_v4) + dhcp_text = tmpl.render(dhcp) + with open(self._dhcp[4]['conf'], 'w') as f: + f.write(dhcp_text) + + 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._dhcp[4])) + + def _del_dhcp(self): + """ + De-configure interface as DHCP clinet. All auto generated files like + pid, config and lease will be removed. + + Example: + + >>> from vyos.ifconfig import Interface + >>> j = Interface('eth0') + >>> j.del_dhcp() + """ + if not os.path.isfile(self._dhcp[4]['pid']): + self._debug_msg('No DHCP client PID found') + return None + + # with open(self._dhcp[4]['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._dhcp[4])) + + # cleanup old config files + for name in ('conf', 'pid', 'lease'): + if os.path.isfile(self._dhcp[4][name]): + os.remove(self._dhcp[4][name]) + + def _set_dhcpv6(self): + """ + Configure interface as DHCPv6 client. The dhclient binary is automatically + started in background! + + Example: + + >>> from vyos.ifconfig import Interface + >>> j = Interface('eth0') + >>> j.set_dhcpv6() + """ + dhcpv6 = self.get_dhcpv6_options() + + # better save then sorry .. should be checked in interface script + # but if you missed it we are safe! + if dhcpv6['dhcpv6_prm_only'] and dhcpv6['dhcpv6_temporary']: + raise Exception( + 'DHCPv6 temporary and parameters-only options are mutually exclusive!') + + # render DHCP configuration + tmpl = jinja2.Template(template_v6) + dhcpv6_text = tmpl.render(dhcpv6) + with open(self._dhcp[6]['conf'], 'w') as f: + f.write(dhcpv6_text) + + # no longer accept router announcements on this interface + self._write_sysfs(self._dhcp[6]['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 dhcpv6['dhcpv6_prm_only']: + cmd += ' -S' + if dhcpv6['dhcpv6_temporary']: + cmd += ' -T' + cmd += ' {ifname}' + + return self._cmd(cmd.format(**self._dhcp[6])) + + def _del_dhcpv6(self): + """ + De-configure interface as DHCPv6 clinet. All auto generated files like + pid, config and lease will be removed. + + Example: + + >>> from vyos.ifconfig import Interface + >>> j = Interface('eth0') + >>> j.del_dhcpv6() + """ + if not os.path.isfile(self._dhcp[6]['pid']): + self._debug_msg('No DHCPv6 client PID found') + return None + + # with open(self._dhcp[6]['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._dhcp[6])) + + # accept router announcements on this interface + self._write_sysfs(self._dhcp[6]['accept_ra'], 1) + + # cleanup old config files + for name in ('conf', 'pid', 'lease'): + if os.path.isfile(self._dhcp[6][name]): + os.remove(self._dhcp[6][name]) + diff --git a/python/vyos/ifconfig/dummy.py b/python/vyos/ifconfig/dummy.py new file mode 100644 index 000000000..404c490c7 --- /dev/null +++ b/python/vyos/ifconfig/dummy.py @@ -0,0 +1,37 @@ +# Copyright 2019 VyOS maintainers and contributors <maintainers@vyos.io> +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library. If not, see <http://www.gnu.org/licenses/>. + + +from vyos.ifconfig.interface import Interface + + +@Interface.register +class DummyIf(Interface): + """ + A dummy interface is entirely virtual like, for example, the loopback + interface. The purpose of a dummy interface is to provide a device to route + packets through without actually transmitting them. + """ + + default = { + 'type': 'dummy', + } + definition = { + **Interface.definition, + **{ + 'section': 'dummy', + 'prefixes': ['dum', ], + }, + } diff --git a/python/vyos/ifconfig/ethernet.py b/python/vyos/ifconfig/ethernet.py new file mode 100644 index 000000000..542de4f59 --- /dev/null +++ b/python/vyos/ifconfig/ethernet.py @@ -0,0 +1,257 @@ +# Copyright 2019 VyOS maintainers and contributors <maintainers@vyos.io> +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library. If not, see <http://www.gnu.org/licenses/>. + +import os +import re + +from vyos.ifconfig.interface import Interface +from vyos.ifconfig.vlan import VLAN +from vyos.validate import assert_list +from vyos.util import run + + +@Interface.register +@VLAN.enable +class EthernetIf(Interface): + """ + Abstraction of a Linux Ethernet Interface + """ + + default = { + 'type': 'ethernet', + } + definition = { + **Interface.definition, + **{ + 'section': 'ethernet', + 'prefixes': ['lan', 'eth', 'eno', 'ens', 'enp', 'enx'], + 'bondable': True, + 'broadcast': True, + 'bridgeable': True, + } + } + + @staticmethod + def feature(ifname, option, value): + run(f'/sbin/ethtool -K {ifname} {option} {value}','ifconfig') + return False + + _command_set = {**Interface._command_set, **{ + 'gro': { + 'validate': lambda v: assert_list(v, ['on', 'off']), + 'possible': lambda i, v: EthernetIf.feature(i, 'gro', v), + # 'shellcmd': '/sbin/ethtool -K {ifname} gro {value}', + }, + 'gso': { + 'validate': lambda v: assert_list(v, ['on', 'off']), + 'possible': lambda i, v: EthernetIf.feature(i, 'gso', v), + # 'shellcmd': '/sbin/ethtool -K {ifname} gso {value}', + }, + 'sg': { + 'validate': lambda v: assert_list(v, ['on', 'off']), + 'possible': lambda i, v: EthernetIf.feature(i, 'sg', v), + # 'shellcmd': '/sbin/ethtool -K {ifname} sg {value}', + }, + 'tso': { + 'validate': lambda v: assert_list(v, ['on', 'off']), + 'possible': lambda i, v: EthernetIf.feature(i, 'tso', v), + # 'shellcmd': '/sbin/ethtool -K {ifname} tso {value}', + }, + 'ufo': { + 'validate': lambda v: assert_list(v, ['on', 'off']), + 'possible': lambda i, v: EthernetIf.feature(i, 'ufo', v), + # 'shellcmd': '/sbin/ethtool -K {ifname} ufo {value}', + }, + }} + + def _delete(self): + # Ethernet interfaces can not be removed + pass + + def get_driver_name(self): + """ + Return the driver name used by NIC. Some NICs don't support all + features e.g. changing link-speed, duplex + + Example: + >>> from vyos.ifconfig import EthernetIf + >>> i = EthernetIf('eth0') + >>> i.get_driver_name() + 'vmxnet3' + """ + sysfs_file = '/sys/class/net/{}/device/driver/module'.format( + self.config['ifname']) + if os.path.exists(sysfs_file): + link = os.readlink(sysfs_file) + return os.path.basename(link) + else: + return None + + def set_flow_control(self, enable): + """ + Changes the pause parameters of the specified Ethernet device. + + @param enable: true -> enable pause frames, false -> disable pause frames + + Example: + >>> from vyos.ifconfig import EthernetIf + >>> i = EthernetIf('eth0') + >>> i.set_flow_control(True) + """ + ifname = self.config['ifname'] + + if enable not in ['on', 'off']: + raise ValueError("Value out of range") + + if self.get_driver_name() in ['vmxnet3', 'virtio_net', 'xen_netfront']: + self._debug_msg('{} driver does not support changing flow control settings!' + .format(self.get_driver_name())) + return + + # Get current flow control settings: + cmd = f'/sbin/ethtool --show-pause {ifname}' + output, code = self._popen(cmd) + if code == 76: + # the interface does not support it + return '' + if code: + # never fail here as it prevent vyos to boot + print(f'unexpected return code {code} from {cmd}') + return '' + + # The above command returns - with tabs: + # + # Pause parameters for eth0: + # Autonegotiate: on + # RX: off + # TX: off + if re.search("Autonegotiate:\ton", output): + if enable == "on": + # flowcontrol is already enabled - no need to re-enable it again + # this will prevent the interface from flapping as applying the + # flow-control settings will take the interface down and bring + # it back up every time. + return '' + + # Assemble command executed on system. Unfortunately there is no way + # to change this setting via sysfs + cmd = f'/sbin/ethtool --pause {ifname} autoneg {enable} tx {enable} rx {enable}' + output, code = self._popen(cmd) + if code: + print(f'could not set flowcontrol for {ifname}') + return output + + def set_speed_duplex(self, speed, duplex): + """ + Set link speed in Mbit/s and duplex. + + @speed can be any link speed in MBit/s, e.g. 10, 100, 1000 auto + @duplex can be half, full, auto + + Example: + >>> from vyos.ifconfig import EthernetIf + >>> i = EthernetIf('eth0') + >>> i.set_speed_duplex('auto', 'auto') + """ + + if speed not in ['auto', '10', '100', '1000', '2500', '5000', '10000', '25000', '40000', '50000', '100000', '400000']: + raise ValueError("Value out of range (speed)") + + if duplex not in ['auto', 'full', 'half']: + raise ValueError("Value out of range (duplex)") + + if self.get_driver_name() in ['vmxnet3', 'virtio_net', 'xen_netfront']: + self._debug_msg('{} driver does not support changing speed/duplex settings!' + .format(self.get_driver_name())) + return + + # Get current speed and duplex settings: + cmd = '/sbin/ethtool {0}'.format(self.config['ifname']) + tmp = self._cmd(cmd) + + if re.search("\tAuto-negotiation: on", tmp): + if speed == 'auto' and duplex == 'auto': + # bail out early as nothing is to change + return + else: + # read in current speed and duplex settings + cur_speed = 0 + cur_duplex = '' + for line in tmp.splitlines(): + if line.lstrip().startswith("Speed:"): + non_decimal = re.compile(r'[^\d.]+') + cur_speed = non_decimal.sub('', line) + continue + + if line.lstrip().startswith("Duplex:"): + cur_duplex = line.split()[-1].lower() + break + + if (cur_speed == speed) and (cur_duplex == duplex): + # bail out early as nothing is to change + return + + cmd = '/sbin/ethtool -s {}'.format(self.config['ifname']) + if speed == 'auto' or duplex == 'auto': + cmd += ' autoneg on' + else: + cmd += ' speed {} duplex {} autoneg off'.format(speed, duplex) + + return self._cmd(cmd) + + def set_gro(self, state): + """ + Example: + >>> from vyos.ifconfig import EthernetIf + >>> i = EthernetIf('eth0') + >>> i.set_gro('on') + """ + return self.set_interface('gro', state) + + def set_gso(self, state): + """ + Example: + >>> from vyos.ifconfig import EthernetIf + >>> i = EthernetIf('eth0') + >>> i.set_gso('on') + """ + return self.set_interface('gso', state) + + def set_sg(self, state): + """ + Example: + >>> from vyos.ifconfig import EthernetIf + >>> i = EthernetIf('eth0') + >>> i.set_sg('on') + """ + return self.set_interface('sg', state) + + def set_tso(self, state): + """ + Example: + >>> from vyos.ifconfig import EthernetIf + >>> i = EthernetIf('eth0') + >>> i.set_tso('on') + """ + return self.set_interface('tso', state) + + def set_ufo(self, state): + """ + Example: + >>> from vyos.ifconfig import EthernetIf + >>> i = EthernetIf('eth0') + >>> i.set_udp_offload('on') + """ + return self.set_interface('ufo', state) diff --git a/python/vyos/ifconfig/geneve.py b/python/vyos/ifconfig/geneve.py new file mode 100644 index 000000000..0c1cdade9 --- /dev/null +++ b/python/vyos/ifconfig/geneve.py @@ -0,0 +1,64 @@ +# Copyright 2019 VyOS maintainers and contributors <maintainers@vyos.io> +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library. If not, see <http://www.gnu.org/licenses/>. + +from copy import deepcopy + +from vyos.ifconfig.interface import Interface + + +@Interface.register +class GeneveIf(Interface): + """ + Geneve: Generic Network Virtualization Encapsulation + + For more information please refer to: + https://tools.ietf.org/html/draft-gross-geneve-00 + https://www.redhat.com/en/blog/what-geneve + https://developers.redhat.com/blog/2019/05/17/an-introduction-to-linux-virtual-interfaces-tunnels/#geneve + https://lwn.net/Articles/644938/ + """ + + default = { + 'type': 'geneve', + 'vni': 0, + 'remote': '', + } + definition = { + **Interface.definition, + **{ + 'section': 'geneve', + 'prefixes': ['gnv', ], + 'bridgeable': True, + } + } + + def _create(self): + cmd = 'ip link add name {ifname} type geneve id {vni} remote {remote}'.format(**self.config) + self._cmd(cmd) + + # interface is always A/D down. It needs to be enabled explicitly + self.set_admin_state('down') + + @classmethod + def get_config(cls): + """ + GENEVE interfaces require a configuration when they are added using + iproute2. This static method will provide the configuration dictionary + used by this class. + + Example: + >> dict = GeneveIf().get_config() + """ + return deepcopy(cls.default) diff --git a/python/vyos/ifconfig/interface.py b/python/vyos/ifconfig/interface.py new file mode 100644 index 000000000..22c71a464 --- /dev/null +++ b/python/vyos/ifconfig/interface.py @@ -0,0 +1,738 @@ +# Copyright 2019 VyOS maintainers and contributors <maintainers@vyos.io> +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library. If not, see <http://www.gnu.org/licenses/>. + +import os +import re +import json +import glob +import time +from time import sleep +from os.path import isfile +from copy import deepcopy +from datetime import timedelta + +from hurry.filesize import size, alternative +from ipaddress import IPv4Network, IPv6Address, IPv6Network +from netifaces import ifaddresses, AF_INET, AF_INET6 +from tabulate import tabulate + +from vyos.util import mac2eui64 +from vyos import ConfigError +from vyos.ifconfig.dhcp import DHCP +from vyos.validate import is_ipv4 +from vyos.validate import is_ipv6 +from vyos.validate import is_intf_addr_assigned +from vyos.validate import assert_boolean +from vyos.validate import assert_list +from vyos.validate import assert_mac +from vyos.validate import assert_mtu +from vyos.validate import assert_positive +from vyos.validate import assert_range + + +class Interface(DHCP): + options = [] + required = [] + default = { + 'type': '', + 'debug': True, + 'create': True, + } + definition = { + 'section': '', + 'prefixes': [], + 'vlan': False, + 'bondable': False, + 'broadcast': False, + 'bridgeable': False, + } + + _command_get = { + 'admin_state': { + 'shellcmd': 'ip -json link show dev {ifname}', + 'format': lambda j: 'up' if 'UP' in json.loads(j)[0]['flags'] else 'down', + } + } + + _command_set = { + 'admin_state': { + 'validate': lambda v: assert_list(v, ['up', 'down']), + 'shellcmd': 'ip link set dev {ifname} {value}', + }, + 'mac': { + 'validate': assert_mac, + 'shellcmd': 'ip link set dev {ifname} address {value}', + }, + 'vrf': { + 'convert': lambda v: f'master {v}' if v else 'nomaster', + 'shellcmd': 'ip link set dev {ifname} {value}', + }, + } + + _sysfs_get = { + 'alias': { + 'location': '/sys/class/net/{ifname}/ifalias', + }, + 'mac': { + 'location': '/sys/class/net/{ifname}/address', + }, + 'mtu': { + 'location': '/sys/class/net/{ifname}/mtu', + }, + 'oper_state':{ + 'location': '/sys/class/net/{ifname}/operstate', + }, + } + + _sysfs_set = { + 'alias': { + 'convert': lambda name: name if name else '\0', + 'location': '/sys/class/net/{ifname}/ifalias', + }, + 'mtu': { + 'validate': assert_mtu, + 'location': '/sys/class/net/{ifname}/mtu', + }, + 'arp_cache_tmo': { + 'convert': lambda tmo: (int(tmo) * 1000), + 'location': '/proc/sys/net/ipv4/neigh/{ifname}/base_reachable_time_ms', + }, + 'arp_filter': { + 'validate': assert_boolean, + 'location': '/proc/sys/net/ipv4/conf/{ifname}/arp_filter', + }, + 'arp_accept': { + 'validate': lambda arp: assert_range(arp,0,2), + 'location': '/proc/sys/net/ipv4/conf/{ifname}/arp_accept', + }, + 'arp_announce': { + 'validate': assert_boolean, + 'location': '/proc/sys/net/ipv4/conf/{ifname}/arp_announce', + }, + 'arp_ignore': { + 'validate': assert_boolean, + 'location': '/proc/sys/net/ipv4/conf/{ifname}/arp_ignore', + }, + 'ipv6_autoconf': { + 'validate': lambda fwd: assert_range(fwd,0,2), + 'location': '/proc/sys/net/ipv6/conf/{ifname}/autoconf', + }, + 'ipv6_forwarding': { + 'validate': lambda fwd: assert_range(fwd,0,2), + 'location': '/proc/sys/net/ipv6/conf/{ifname}/forwarding', + }, + 'ipv6_dad_transmits': { + 'validate': assert_positive, + 'location': '/proc/sys/net/ipv6/conf/{ifname}/dad_transmits', + }, + 'proxy_arp': { + 'validate': assert_boolean, + 'location': '/proc/sys/net/ipv4/conf/{ifname}/proxy_arp', + }, + 'proxy_arp_pvlan': { + 'validate': assert_boolean, + 'location': '/proc/sys/net/ipv4/conf/{ifname}/proxy_arp_pvlan', + }, + # link_detect vs link_filter name weirdness + 'link_detect': { + 'validate': lambda link: assert_range(link,0,3), + 'location': '/proc/sys/net/ipv4/conf/{ifname}/link_filter', + }, + } + + def __init__(self, ifname, **kargs): + """ + This is the base interface class which supports basic IP/MAC address + operations as well as DHCP(v6). Other interface which represent e.g. + and ethernet bridge are implemented as derived classes adding all + additional functionality. + + For creation you will need to provide the interface type, otherwise + the existing interface is used + + DEBUG: + This class has embedded debugging (print) which can be enabled by + creating the following file: + vyos@vyos# touch /tmp/vyos.ifconfig.debug + + Example: + >>> from vyos.ifconfig import Interface + >>> i = Interface('eth0') + """ + + self.config = deepcopy(self.default) + for k in self.options: + if k in kargs: + self.config[k] = kargs[k] + + # make sure the ifname is the first argument and not from the dict + self.config['ifname'] = ifname + + # we must have updated config before initialising the Interface + super().__init__(ifname, **kargs) + + if not os.path.exists('/sys/class/net/{}'.format(self.config['ifname'])): + # Any instance of Interface, such as Interface('eth0') + # can be used safely to access the generic function in this class + # as 'type' is unset, the class can not be created + if not self.config['type']: + raise Exception('interface "{}" not found'.format(self.config['ifname'])) + + # Should an Instance of a child class (EthernetIf, DummyIf, ..) + # be required, then create should be set to False to not accidentally create it. + # In case a subclass does not define it, we use get to set the default to True + if self.config.get('create',True): + for k in self.required: + if k not in kargs: + name = self.default['type'] + raise ConfigError(f'missing required option {k} for {name} {ifname} creation') + + self._create() + # If we can not connect to the interface then let the caller know + # as the class could not be correctly initialised + else: + raise Exception('interface "{}" not found'.format(self.config['ifname'])) + + # list of assigned IP addresses + self._addr = [] + + def _create(self): + cmd = 'ip link add dev {ifname} type {type}'.format(**self.config) + self._cmd(cmd) + + def remove(self): + """ + Remove interface from operating system. Removing the interface + deconfigures all assigned IP addresses and clear possible DHCP(v6) + client processes. + + Example: + >>> from vyos.ifconfig import Interface + >>> i = Interface('eth0') + >>> i.remove() + """ + # stop DHCP(v6) if running + self._del_dhcp() + self._del_dhcpv6() + + # 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) + + # --------------------------------------------------------------------- + # A code refactoring is required as this type check is present as + # Interface implement behaviour for one of it's sub-class. + + # It is required as the current pattern for vlan is: + # Interface('name').remove() to delete an interface + # The code should be modified to have a class method called connect and + # have Interface.connect('name').remove() + + # each subclass should register within Interface the pattern for that + # interface ie: (ethX, etc.) and use this to create an instance of + # the right class (EthernetIf, ...) + + # Ethernet interfaces can not be removed + + # Commented out as nowhere in the code do we call Interface() + # This would also cause an import loop + # if self.__class__ == EthernetIf: + # return + + # --------------------------------------------------------------------- + + self._delete() + + def _delete(self): + # NOTE (Improvement): + # after interface removal no other commands should be allowed + # to be called and instead should raise an Exception: + cmd = 'ip link del dev {}'.format(self.config['ifname']) + return self._cmd(cmd) + + def get_mtu(self): + """ + Get/set interface mtu in bytes. + + Example: + >>> from vyos.ifconfig import Interface + >>> Interface('eth0').get_mtu() + '1500' + """ + return self.get_interface('mtu') + + def set_mtu(self, mtu): + """ + Get/set interface mtu in bytes. + + Example: + >>> from vyos.ifconfig import Interface + >>> Interface('eth0').set_mtu(1400) + >>> Interface('eth0').get_mtu() + '1400' + """ + return self.set_interface('mtu', mtu) + + def get_mac(self): + """ + Get current interface MAC (Media Access Contrl) address used. + + Example: + >>> from vyos.ifconfig import Interface + >>> Interface('eth0').get_mac() + '00:50:ab:cd:ef:00' + """ + return self.get_interface('mac') + + def set_mac(self, mac): + """ + Set interface MAC (Media Access Contrl) address to given value. + + Example: + >>> from vyos.ifconfig import Interface + >>> Interface('eth0').set_mac('00:50:ab:cd:ef:01') + """ + + # If MAC is unchanged, bail out early + if mac == self.get_mac(): + return None + + # MAC address can only be changed if interface is in 'down' state + prev_state = self.get_admin_state() + if prev_state == 'up': + self.set_admin_state('down') + + self.set_interface('mac', mac) + + def set_vrf(self, vrf=''): + """ + Add/Remove interface from given VRF instance. + + Example: + >>> from vyos.ifconfig import Interface + >>> Interface('eth0').set_vrf('foo') + >>> Interface('eth0').set_vrf() + """ + self.set_interface('vrf', vrf) + + def set_arp_cache_tmo(self, tmo): + """ + Set ARP cache timeout value in seconds. Internal Kernel representation + is in milliseconds. + + Example: + >>> from vyos.ifconfig import Interface + >>> Interface('eth0').set_arp_cache_tmo(40) + """ + return self.set_interface('arp_cache_tmo', tmo) + + def set_arp_filter(self, arp_filter): + """ + Filter ARP requests + + 1 - Allows you to have multiple network interfaces on the same + subnet, and have the ARPs for each interface be answered + based on whether or not the kernel would route a packet from + the ARP'd IP out that interface (therefore you must use source + based routing for this to work). In other words it allows control + of which cards (usually 1) will respond to an arp request. + + 0 - (default) The kernel can respond to arp requests with addresses + from other interfaces. This may seem wrong but it usually makes + sense, because it increases the chance of successful communication. + IP addresses are owned by the complete host on Linux, not by + particular interfaces. Only for more complex setups like load- + balancing, does this behaviour cause problems. + """ + return self.set_interface('arp_filter', arp_filter) + + def set_arp_accept(self, arp_accept): + """ + Define behavior for gratuitous ARP frames who's IP is not + already present in the ARP table: + 0 - don't create new entries in the ARP table + 1 - create new entries in the ARP table + + Both replies and requests type gratuitous arp will trigger the + ARP table to be updated, if this setting is on. + + If the ARP table already contains the IP address of the + gratuitous arp frame, the arp table will be updated regardless + if this setting is on or off. + """ + return self.set_interface('arp_accept', arp_accept) + + def set_arp_announce(self, arp_announce): + """ + Define different restriction levels for announcing the local + source IP address from IP packets in ARP requests sent on + interface: + 0 - (default) Use any local address, configured on any interface + 1 - Try to avoid local addresses that are not in the target's + subnet for this interface. This mode is useful when target + hosts reachable via this interface require the source IP + address in ARP requests to be part of their logical network + configured on the receiving interface. When we generate the + request we will check all our subnets that include the + target IP and will preserve the source address if it is from + such subnet. + + Increasing the restriction level gives more chance for + receiving answer from the resolved target while decreasing + the level announces more valid sender's information. + """ + return self.set_interface('arp_announce', arp_announce) + + def set_arp_ignore(self, arp_ignore): + """ + Define different modes for sending replies in response to received ARP + requests that resolve local target IP addresses: + + 0 - (default): reply for any local target IP address, configured + on any interface + 1 - reply only if the target IP address is local address + configured on the incoming interface + """ + return self.set_interface('arp_ignore', arp_ignore) + + def set_ipv6_autoconf(self, autoconf): + """ + Autoconfigure addresses using Prefix Information in Router + Advertisements. + """ + return self.set_interface('ipv6_autoconf', autoconf) + + def set_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. + + If prefix is passed address is assigned, if prefix is '' address is + removed from 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) + + 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 + + # 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): + """ + Configure IPv6 interface-specific Host/Router behaviour. + + False: + + By default, Host behaviour is assumed. This means: + + 1. IsRouter flag is not set in Neighbour Advertisements. + 2. If accept_ra is TRUE (default), transmit Router + Solicitations. + 3. If accept_ra is TRUE (default), accept Router + Advertisements (and do autoconfiguration). + 4. If accept_redirects is TRUE (default), accept Redirects. + + True: + + If local forwarding is enabled, Router behaviour is assumed. + This means exactly the reverse from the above: + + 1. IsRouter flag is set in Neighbour Advertisements. + 2. Router Solicitations are not sent unless accept_ra is 2. + 3. Router Advertisements are ignored unless accept_ra is 2. + 4. Redirects are ignored. + """ + return self.set_interface('ipv6_forwarding', forwarding) + + def set_ipv6_dad_messages(self, dad): + """ + The amount of Duplicate Address Detection probes to send. + Default: 1 + """ + return self.set_interface('ipv6_dad_transmits', dad) + + def set_link_detect(self, link_filter): + """ + Configure kernel response in packets received on interfaces that are 'down' + + 0 - Allow packets to be received for the address on this interface + even if interface is disabled or no carrier. + + 1 - Ignore packets received if interface associated with the incoming + address is down. + + 2 - Ignore packets received if interface associated with the incoming + address is down or has no carrier. + + Default value is 0. Note that some distributions enable it in startup + scripts. + + Example: + >>> from vyos.ifconfig import Interface + >>> Interface('eth0').set_link_detect(1) + """ + return self.set_interface('link_detect', link_filter) + + def get_alias(self): + """ + Get interface alias name used by e.g. SNMP + + Example: + >>> Interface('eth0').get_alias() + 'interface description as set by user' + """ + return self.get_interface('alias') + + def set_alias(self, ifalias=''): + """ + Set interface alias name used by e.g. SNMP + + Example: + >>> from vyos.ifconfig import Interface + >>> Interface('eth0').set_alias('VyOS upstream interface') + + to clear alias e.g. delete it use: + + >>> Interface('eth0').set_ifalias('') + """ + self.set_interface('alias', ifalias) + + def get_admin_state(self): + """ + Get interface administrative state. Function will return 'up' or 'down' + + Example: + >>> from vyos.ifconfig import Interface + >>> Interface('eth0').get_admin_state() + 'up' + """ + return self.get_interface('admin_state') + + def set_admin_state(self, state): + """ + Set interface administrative state to be 'up' or 'down' + + Example: + >>> from vyos.ifconfig import Interface + >>> Interface('eth0').set_admin_state('down') + >>> Interface('eth0').get_admin_state() + 'down' + """ + return self.set_interface('admin_state', state) + + def get_oper_state(self): + """ + Get interface operational state + + Example: + >>> from vyos.ifconfig import Interface + >>> Interface('eth0').get_oper_sate() + 'up' + """ + # https://www.kernel.org/doc/Documentation/ABI/testing/sysfs-class-net + # "unknown", "notpresent", "down", "lowerlayerdown", "testing", "dormant", "up" + return self.get_interface('oper_state') + + def set_proxy_arp(self, enable): + """ + Set per interface proxy ARP configuration + + Example: + >>> from vyos.ifconfig import Interface + >>> Interface('eth0').set_proxy_arp(1) + """ + self.set_interface('proxy_arp', enable) + + def set_proxy_arp_pvlan(self, enable): + """ + Private VLAN proxy arp. + Basically allow proxy arp replies back to the same interface + (from which the ARP request/solicitation was received). + + This is done to support (ethernet) switch features, like RFC + 3069, where the individual ports are NOT allowed to + communicate with each other, but they are allowed to talk to + the upstream router. As described in RFC 3069, it is possible + to allow these hosts to communicate through the upstream + router by proxy_arp'ing. Don't need to be used together with + proxy_arp. + + This technology is known by different names: + In RFC 3069 it is called VLAN Aggregation. + Cisco and Allied Telesyn call it Private VLAN. + Hewlett-Packard call it Source-Port filtering or port-isolation. + Ericsson call it MAC-Forced Forwarding (RFC Draft). + + Example: + >>> from vyos.ifconfig import Interface + >>> Interface('eth0').set_proxy_arp_pvlan(1) + """ + self.set_interface('proxy_arp_pvlan', enable) + + def get_addr(self): + """ + Retrieve assigned IPv4 and IPv6 addresses from given interface. + This is done using the netifaces and ipaddress python modules. + + Example: + >>> from vyos.ifconfig import Interface + >>> Interface('eth0').get_addrs() + ['172.16.33.30/24', 'fe80::20c:29ff:fe11:a174/64'] + """ + + ipv4 = [] + ipv6 = [] + + if AF_INET in ifaddresses(self.config['ifname']).keys(): + for v4_addr in ifaddresses(self.config['ifname'])[AF_INET]: + # we need to manually assemble a list of IPv4 address/prefix + prefix = '/' + \ + str(IPv4Network('0.0.0.0/' + v4_addr['netmask']).prefixlen) + ipv4.append(v4_addr['addr'] + prefix) + + if AF_INET6 in ifaddresses(self.config['ifname']).keys(): + for v6_addr in ifaddresses(self.config['ifname'])[AF_INET6]: + # Note that currently expanded netmasks are not supported. That means + # 2001:db00::0/24 is a valid argument while 2001:db00::0/ffff:ff00:: not. + # see https://docs.python.org/3/library/ipaddress.html + bits = bin( + int(v6_addr['netmask'].replace(':', ''), 16)).count('1') + prefix = '/' + str(bits) + + # we alsoneed to remove the interface suffix on link local + # addresses + v6_addr['addr'] = v6_addr['addr'].split('%')[0] + ipv6.append(v6_addr['addr'] + prefix) + + return ipv4 + ipv6 + + def add_addr(self, addr): + """ + Add IP(v6) address to interface. Address is only added if it is not + already assigned to that interface. + + addr: can be an IPv4 address, IPv6 address, dhcp or dhcpv6! + IPv4: add IPv4 address to interface + IPv6: add IPv6 address to interface + dhcp: start dhclient (IPv4) on interface + dhcpv6: start dhclient (IPv6) on interface + + Example: + >>> from vyos.ifconfig import Interface + >>> j = Interface('eth0') + >>> j.add_addr('192.0.2.1/24') + >>> j.add_addr('2001:db8::ffff/64') + >>> j.get_addr() + ['192.0.2.1/24', '2001:db8::ffff/64'] + """ + + # cache new IP address which is assigned to interface + self._addr.append(addr) + + # 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") + + if addr == 'dhcp': + self._set_dhcp() + elif addr == 'dhcpv6': + self._set_dhcpv6() + 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) + + def del_addr(self, addr): + """ + Delete IP(v6) address to interface. Address is only added if it is + assigned to that interface. + + addr: can be an IPv4 address, IPv6 address, dhcp or dhcpv6! + IPv4: delete IPv4 address from interface + IPv6: delete IPv6 address from interface + dhcp: stop dhclient (IPv4) on interface + dhcpv6: stop dhclient (IPv6) on interface + + Example: + >>> from vyos.ifconfig import Interface + >>> j = Interface('eth0') + >>> j.add_addr('2001:db8::ffff/64') + >>> j.add_addr('192.0.2.1/24') + >>> j.get_addr() + ['192.0.2.1/24', '2001:db8::ffff/64'] + >>> j.del_addr('192.0.2.1/24') + >>> j.get_addr() + ['2001:db8::ffff/64'] + """ + if addr == 'dhcp': + self._del_dhcp() + elif addr == 'dhcpv6': + self._del_dhcpv6() + else: + if is_intf_addr_assigned(self.config['ifname'], addr): + cmd = 'ip addr del "{}" dev "{}"'.format(addr, self.config['ifname']) + return self._cmd(cmd) + + def op_show_interface_stats(self): + stats = self.get_interface_stats() + rx = [['bytes','packets','errors','dropped','overrun','mcast'],[stats['rx_bytes'],stats['rx_packets'],stats['rx_errors'],stats['rx_dropped'],stats['rx_over_errors'],stats['multicast']]] + tx = [['bytes','packets','errors','dropped','carrier','collisions'],[stats['tx_bytes'],stats['tx_packets'],stats['tx_errors'],stats['tx_dropped'],stats['tx_carrier_errors'],stats['collisions']]] + output = "RX: \n" + output += tabulate(rx,headers="firstrow",numalign="right",tablefmt="plain") + output += "\n\nTX: \n" + output += tabulate(tx,headers="firstrow",numalign="right",tablefmt="plain") + print(' '.join(('\n'+output.lstrip()).splitlines(True))) + + def get_interface_stats(self): + interface_stats = dict() + devices = [f for f in glob.glob("/sys/class/net/**/statistics")] + for dev_path in devices: + metrics = [f for f in glob.glob(dev_path +"/**")] + dev = re.findall(r"/sys/class/net/(.*)/statistics",dev_path)[0] + dev_dict = dict() + for metric_path in metrics: + metric = metric_path.replace(dev_path+"/","") + if isfile(metric_path): + data = open(metric_path, 'r').read()[:-1] + dev_dict[metric] = int(data) + interface_stats[dev] = dev_dict + + return interface_stats[self.config['ifname']] + diff --git a/python/vyos/ifconfig/l2tpv3.py b/python/vyos/ifconfig/l2tpv3.py new file mode 100644 index 000000000..34147eb38 --- /dev/null +++ b/python/vyos/ifconfig/l2tpv3.py @@ -0,0 +1,113 @@ +# Copyright 2019 VyOS maintainers and contributors <maintainers@vyos.io> +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library. If not, see <http://www.gnu.org/licenses/>. + + +import os + +from vyos.ifconfig.interface import Interface + + +@Interface.register +class L2TPv3If(Interface): + """ + The Linux bonding driver provides a method for aggregating multiple network + interfaces into a single logical "bonded" interface. The behavior of the + bonded interfaces depends upon the mode; generally speaking, modes provide + either hot standby or load balancing services. Additionally, link integrity + monitoring may be performed. + """ + + default = { + 'type': 'l2tp', + } + definition = { + **Interface.definition, + **{ + 'section': 'l2tpeth', + 'prefixes': ['l2tpeth', ], + 'bridgeable': True, + } + } + options = Interface.options + \ + ['tunnel_id', 'peer_tunnel_id', 'local_port', 'remote_port', + 'encapsulation', 'local_address', 'remote_address', 'session_id', + 'peer_session_id'] + + def _create(self): + # create tunnel interface + cmd = 'ip l2tp add tunnel tunnel_id {tunnel_id}' + cmd += ' peer_tunnel_id {peer_tunnel_id}' + cmd += ' udp_sport {local_port}' + cmd += ' udp_dport {remote_port}' + cmd += ' encap {encapsulation}' + cmd += ' local {local_address}' + cmd += ' remote {remote_address}' + self._cmd(cmd.format(**self.config)) + + # setup session + cmd = 'ip l2tp add session name {ifname}' + cmd += ' tunnel_id {tunnel_id}' + cmd += ' session_id {session_id}' + cmd += ' peer_session_id {peer_session_id}' + self._cmd(cmd.format(**self.config)) + + # interface is always A/D down. It needs to be enabled explicitly + self.set_admin_state('down') + + def remove(self): + """ + Remove interface from operating system. Removing the interface + deconfigures all assigned IP addresses. + Example: + >>> from vyos.ifconfig import L2TPv3If + >>> i = L2TPv3If('l2tpeth0') + >>> i.remove() + """ + + if os.path.exists('/sys/class/net/{}'.format(self.config['ifname'])): + # interface is always A/D down. It needs to be enabled explicitly + self.set_admin_state('down') + + if self.config['tunnel_id'] and self.config['session_id']: + cmd = 'ip l2tp del session tunnel_id {tunnel_id}' + cmd += ' session_id {session_id}' + self._cmd(cmd.format(**self.config)) + + if self.config['tunnel_id']: + cmd = 'ip l2tp del tunnel tunnel_id {tunnel_id}' + self._cmd(cmd.format(**self.config)) + + @staticmethod + def get_config(): + """ + L2TPv3 interfaces require a configuration when they are added using + iproute2. This static method will provide the configuration dictionary + used by this class. + + Example: + >> dict = L2TPv3If().get_config() + """ + config = { + 'peer_tunnel_id': '', + 'local_port': 0, + 'remote_port': 0, + 'encapsulation': 'udp', + 'local_address': '', + 'remote_address': '', + 'session_id': '', + 'tunnel_id': '', + 'peer_session_id': '' + } + return config diff --git a/python/vyos/ifconfig/loopback.py b/python/vyos/ifconfig/loopback.py new file mode 100644 index 000000000..8e4438662 --- /dev/null +++ b/python/vyos/ifconfig/loopback.py @@ -0,0 +1,58 @@ +# Copyright 2019 VyOS maintainers and contributors <maintainers@vyos.io> +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library. If not, see <http://www.gnu.org/licenses/>. + + +from vyos.ifconfig.interface import Interface + + +@Interface.register +class LoopbackIf(Interface): + """ + The loopback device is a special, virtual network interface that your router + uses to communicate with itself. + """ + + default = { + 'type': 'loopback', + } + definition = { + **Interface.definition, + **{ + 'section': 'loopback', + 'prefixes': ['lo', ], + 'bridgeable': True, + } + } + + name = 'loopback' + + def remove(self): + """ + Loopback interface can not be deleted from operating system. We can + only remove all assigned IP addresses. + + Example: + >>> from vyos.ifconfig import Interface + >>> i = LoopbackIf('lo').remove() + """ + # remove all assigned IP addresses from interface + for addr in self.get_addr(): + if addr in ["127.0.0.1/8", "::1/128"]: + # Do not allow deletion of the default loopback addresses as + # this will cause weird system behavior like snmp/ssh no longer + # operating as expected, see https://phabricator.vyos.net/T2034. + continue + + self.del_addr(addr) diff --git a/python/vyos/ifconfig/macvlan.py b/python/vyos/ifconfig/macvlan.py new file mode 100644 index 000000000..55b1a3e91 --- /dev/null +++ b/python/vyos/ifconfig/macvlan.py @@ -0,0 +1,67 @@ +# Copyright 2019 VyOS maintainers and contributors <maintainers@vyos.io> +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library. If not, see <http://www.gnu.org/licenses/>. + + +from vyos.ifconfig.interface import Interface +from vyos.ifconfig.vlan import VLAN + + +@Interface.register +@VLAN.enable +class MACVLANIf(Interface): + """ + Abstraction of a Linux MACvlan interface + """ + + default = { + 'type': 'macvlan', + } + definition = { + **Interface.definition, + **{ + 'section': 'pseudo-ethernet', + 'prefixes': ['peth', ], + }, + } + options = Interface.options + ['source_interface', 'mode'] + + def _create(self): + cmd = 'ip link add {ifname} link {source_interface} type macvlan mode {mode}'.format( + **self.config) + self._cmd(cmd) + + @staticmethod + def get_config(): + """ + VXLAN interfaces require a configuration when they are added using + iproute2. This static method will provide the configuration dictionary + used by this class. + + Example: + >> dict = MACVLANIf().get_config() + """ + config = { + 'address': '', + 'source_interface': '', + 'mode': '' + } + return config + + def set_mode(self, mode): + """ + """ + ifname = self.config['ifname'] + cmd = f'ip link set dev {ifname} type macvlan mode {mode}' + return self._cmd(cmd) diff --git a/python/vyos/ifconfig/pppoe.py b/python/vyos/ifconfig/pppoe.py new file mode 100644 index 000000000..7504408cf --- /dev/null +++ b/python/vyos/ifconfig/pppoe.py @@ -0,0 +1,33 @@ +# Copyright 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 +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library. If not, see <http://www.gnu.org/licenses/>. + + +from vyos.ifconfig.interface import Interface + + +@Interface.register +class PPPoEIf(Interface): + default = { + 'type': 'pppoe', + } + definition = { + **Interface.definition, + **{ + 'section': 'pppoe', + 'prefixes': ['pppoe', ], + }, + } + + # The _create and _delete need to be moved from interface-ppoe to here diff --git a/python/vyos/ifconfig/register.py b/python/vyos/ifconfig/register.py new file mode 100644 index 000000000..c90782b70 --- /dev/null +++ b/python/vyos/ifconfig/register.py @@ -0,0 +1,95 @@ +# Copyright 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 +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library. If not, see <http://www.gnu.org/licenses/>. + +import netifaces + + +class Register: + # the known interface prefixes + _prefixes = {} + + # class need to define: definition['prefixes'] + # the interface prefixes declared by a class used to name interface with + # prefix[0-9]*(\.[0-9]+)?(\.[0-9]+)?, such as lo, eth0 or eth0.1.2 + + @classmethod + def register(cls, klass): + if not klass.definition.get('prefixes',[]): + raise RuntimeError(f'valid interface prefixes not defined for {klass.__name__}') + + for ifprefix in klass.definition['prefixes']: + if ifprefix in cls._prefixes: + raise RuntimeError(f'only one class can be registered for prefix "{ifprefix}" type') + cls._prefixes[ifprefix] = klass + + return klass + + @classmethod + def _basename (cls, name, vlan): + # remove number from interface name + name = name.rstrip('0123456789') + name = name.rstrip('.') + if vlan: + name = name.rstrip('0123456789') + return name + + @classmethod + def section(cls, name, vlan=True): + # return the name of a section an interface should be under + name = cls._basename(name, vlan) + + # XXX: To leave as long as vti and input are not moved to vyos + if name == 'vti': + return 'vti' + if name == 'ifb': + return 'input' + + if name in cls._prefixes: + return cls._prefixes[name].definition['section'] + return '' + + @classmethod + def klass(cls, name, vlan=True): + name = cls._basename(name, vlan) + if name in cls._prefixes: + return cls._prefixes[name] + raise ValueError(f'No type found for interface name: {name}') + + @classmethod + def _listing (cls,section=''): + interfaces = netifaces.interfaces() + + for ifname in interfaces: + # XXX: Temporary hack as vti and input are not yet moved from vyatta to vyos + if ifname.startswith('vti') or ifname.startswith('input'): + yield ifname + continue + + ifsection = cls.section(ifname) + if not ifsection: + continue + + if section and ifsection != section: + continue + + yield ifname + + @classmethod + def listing(cls, section=''): + return list(cls._listing(section)) + + +# XXX: TODO - limit name for VRF interfaces + diff --git a/python/vyos/ifconfig/stp.py b/python/vyos/ifconfig/stp.py new file mode 100644 index 000000000..5e83206c2 --- /dev/null +++ b/python/vyos/ifconfig/stp.py @@ -0,0 +1,70 @@ +# Copyright 2019 VyOS maintainers and contributors <maintainers@vyos.io> +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library. If not, see <http://www.gnu.org/licenses/>. + + +from vyos.ifconfig.interface import Interface + +from vyos.validate import assert_positive + + +class STP: + """ + A spanning-tree capable interface. This applies only to bridge port member + interfaces! + """ + + @classmethod + def enable (cls, adaptee): + adaptee._sysfs_set = {**adaptee._sysfs_set, **cls._sysfs_set} + adaptee.set_path_cost = cls.set_path_cost + adaptee.set_path_priority = cls.set_path_priority + return adaptee + + _sysfs_set = { + 'path_cost': { + # XXX: we should set a maximum + 'validate': assert_positive, + 'location': '/sys/class/net/{ifname}/brport/path_cost', + 'errormsg': '{ifname} is not a bridge port member' + }, + 'path_priority': { + # XXX: we should set a maximum + 'validate': assert_positive, + 'location': '/sys/class/net/{ifname}/brport/priority', + 'errormsg': '{ifname} is not a bridge port member' + }, + } + + def set_path_cost(self, cost): + """ + Set interface path cost, only relevant for STP enabled interfaces + + Example: + + >>> from vyos.ifconfig import Interface + >>> Interface('eth0').set_path_cost(4) + """ + self.set_interface('path_cost', cost) + + def set_path_priority(self, priority): + """ + Set interface path priority, only relevant for STP enabled interfaces + + Example: + + >>> from vyos.ifconfig import Interface + >>> Interface('eth0').set_path_priority(4) + """ + self.set_interface('path_priority', priority) diff --git a/python/vyos/ifconfig/tunnel.py b/python/vyos/ifconfig/tunnel.py new file mode 100644 index 000000000..05060669a --- /dev/null +++ b/python/vyos/ifconfig/tunnel.py @@ -0,0 +1,324 @@ +# Copyright 2019 VyOS maintainers and contributors <maintainers@vyos.io> +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library. If not, see <http://www.gnu.org/licenses/>. + +# https://developers.redhat.com/blog/2019/05/17/an-introduction-to-linux-virtual-interfaces-tunnels/ +# https://community.hetzner.com/tutorials/linux-setup-gre-tunnel + + +from copy import deepcopy + +from vyos.ifconfig.interface import Interface +from vyos.ifconfig.afi import IP4, IP6 +from vyos.validate import assert_list + +def enable_to_on(value): + if value == 'enable': + return 'on' + if value == 'disable': + return 'off' + raise ValueError(f'expect enable or disable but got "{value}"') + + +@Interface.register +class _Tunnel(Interface): + """ + _Tunnel: private base class for tunnels + https://git.kernel.org/pub/scm/network/iproute2/iproute2.git/tree/ip/tunnel.c + https://git.kernel.org/pub/scm/network/iproute2/iproute2.git/tree/ip/ip6tunnel.c + """ + definition = { + **Interface.definition, + **{ + 'section': 'tunnel', + 'prefixes': ['tun',], + 'bridgeable': True, + }, + } + + # TODO: This is surely used for more than tunnels + # TODO: could be refactored elsewhere + _command_set = {**Interface._command_set, **{ + 'multicast': { + 'validate': lambda v: assert_list(v, ['enable', 'disable']), + 'convert': enable_to_on, + 'shellcmd': 'ip link set dev {ifname} multicast {value}', + }, + 'allmulticast': { + 'validate': lambda v: assert_list(v, ['enable', 'disable']), + 'convert': enable_to_on, + 'shellcmd': 'ip link set dev {ifname} allmulticast {value}', + }, + }} + + # use for "options" and "updates" + # If an key is only in the options list, it can only be set at creation time + # the create comand will only be make using the key in options + + # If an option is in the updates list, it can be updated + # upon, the creation, all key not yet applied will be updated + + # multicast/allmulticast can not be part of the create command + + # options matrix: + # with ip = 4, we have multicast + # wiht ip = 6, nothing + # with tunnel = 4, we have tos, ttl, key + # with tunnel = 6, we have encaplimit, hoplimit, tclass, flowlabel + + # TODO: For multicast, it is allowed on IP6IP6 and Sit6RD + # TODO: to match vyatta but it should be checked for correctness + + updates = [] + + create = '' + change = '' + delete = '' + + ip = [] # AFI of the families which can be used in the tunnel + tunnel = 0 # invalid - need to be set by subclasses + + def __init__(self, ifname, **config): + self.config = deepcopy(config) if config else {} + super().__init__(ifname, **config) + + def _create(self): + # add " option-name option-name-value ..." for all options set + options = " ".join(["{} {}".format(k, self.config[k]) + for k in self.options if k in self.config and self.config[k]]) + self._cmd('{} {}'.format(self.create.format(**self.config), options)) + self.set_admin_state('down') + + def _delete(self): + self.set_admin_state('down') + cmd = self.delete.format(**self.config) + return self._cmd(cmd) + + def set_interface(self, option, value): + try: + return Interface.set_interface(self, option, value) + except Exception: + pass + + if value == '': + # remove the value so that it is not used + self.config.pop(option, '') + + if self.change: + self._cmd('{} {} {}'.format( + self.change.format(**self.config), option, value)) + return True + + @classmethod + def get_config(cls): + return dict(zip(cls.options, ['']*len(cls.options))) + + +class GREIf(_Tunnel): + """ + GRE: Generic Routing Encapsulation + + For more information please refer to: + RFC1701, RFC1702, RFC2784 + https://tools.ietf.org/html/rfc2784 + https://git.kernel.org/pub/scm/network/iproute2/iproute2.git/tree/ip/link_gre.c + """ + + ip = [IP4, IP6] + tunnel = IP4 + + default = {'type': 'gre'} + required = ['local', ] # mGRE is a GRE without remote endpoint + + options = ['local', 'remote', 'ttl', 'tos', 'key'] + updates = ['local', 'remote', 'ttl', 'tos', + 'multicast', 'allmulticast'] + + create = 'ip tunnel add {ifname} mode {type}' + change = 'ip tunnel cha {ifname}' + delete = 'ip tunnel del {ifname}' + + +# GreTap also called GRE Bridge +class GRETapIf(_Tunnel): + """ + GRETapIF: GreIF using TAP instead of TUN + + https://en.wikipedia.org/wiki/TUN/TAP + """ + + # no multicast, ttl or tos for gretap + + ip = [IP4, ] + tunnel = IP4 + + default = {'type': 'gretap'} + required = ['local', ] + + options = ['local', 'remote', ] + updates = [] + + create = 'ip link add {ifname} type {type}' + change = '' + delete = 'ip link del {ifname}' + + +class IP6GREIf(_Tunnel): + """ + IP6Gre: IPv6 Support for Generic Routing Encapsulation (GRE) + + For more information please refer to: + https://tools.ietf.org/html/rfc7676 + https://git.kernel.org/pub/scm/network/iproute2/iproute2.git/tree/ip/link_gre6.c + """ + + ip = [IP4, IP6] + tunnel = IP6 + + default = {'type': 'ip6gre'} + required = ['local', 'remote'] + + options = ['local', 'remote', 'encaplimit', + 'hoplimit', 'tclass', 'flowlabel'] + updates = ['local', 'remote', 'encaplimit', + 'hoplimit', 'tclass', 'flowlabel', + 'multicast', 'allmulticast'] + + create = 'ip tunnel add {ifname} mode {type}' + change = 'ip tunnel cha {ifname} mode {type}' + delete = 'ip tunnel del {ifname}' + + # using "ip tunnel change" without using "mode" causes errors + # sudo ip tunnel add tun100 mode ip6gre local ::1 remote 1::1 + # sudo ip tunnel cha tun100 hoplimit 100 + # *** stack smashing detected ** *: < unknown > terminated + # sudo ip tunnel cha tun100 local: : 2 + # Error: an IP address is expected rather than "::2" + # works if mode is explicit + + +class IPIPIf(_Tunnel): + """ + IPIP: IP Encapsulation within IP + + For more information please refer to: + https://tools.ietf.org/html/rfc2003 + """ + + # IPIP does not allow to pass multicast, unlike GRE + # but the interface itself can be set with multicast + + ip = [IP4,] + tunnel = IP4 + + default = {'type': 'ipip'} + required = ['local', 'remote'] + + options = ['local', 'remote', 'ttl', 'tos', 'key'] + updates = ['local', 'remote', 'ttl', 'tos', + 'multicast', 'allmulticast'] + + create = 'ip tunnel add {ifname} mode {type}' + change = 'ip tunnel cha {ifname}' + delete = 'ip tunnel del {ifname}' + + +class IPIP6If(_Tunnel): + """ + IPIP6: IPv4 over IPv6 tunnel + + For more information please refer to: + https://git.kernel.org/pub/scm/network/iproute2/iproute2.git/tree/ip/link_ip6tnl.c + """ + + ip = [IP4,] + tunnel = IP6 + + default = {'type': 'ipip6'} + required = ['local', 'remote'] + + options = ['local', 'remote', 'encaplimit', + 'hoplimit', 'tclass', 'flowlabel'] + updates = ['local', 'remote', 'encaplimit', + 'hoplimit', 'tclass', 'flowlabel', + 'multicast', 'allmulticast'] + + create = 'ip -6 tunnel add {ifname} mode {type}' + change = 'ip -6 tunnel cha {ifname}' + delete = 'ip -6 tunnel del {ifname}' + + +class IP6IP6If(IPIP6If): + """ + IP6IP6: IPv6 over IPv6 tunnel + + For more information please refer to: + https://tools.ietf.org/html/rfc2473 + """ + + ip = [IP6,] + + default = {'type': 'ip6ip6'} + + +class SitIf(_Tunnel): + """ + Sit: Simple Internet Transition + + For more information please refer to: + https://git.kernel.org/pub/scm/network/iproute2/iproute2.git/tree/ip/link_iptnl.c + """ + + ip = [IP6, IP4] + tunnel = IP4 + + default = {'type': 'sit'} + required = ['local', 'remote'] + + options = ['local', 'remote', 'ttl', 'tos', 'key'] + updates = ['local', 'remote', 'ttl', 'tos', + 'multicast', 'allmulticast'] + + create = 'ip tunnel add {ifname} mode {type}' + change = 'ip tunnel cha {ifname}' + delete = 'ip tunnel del {ifname}' + + +class Sit6RDIf(SitIf): + """ + Sit6RDIf: Simple Internet Transition with 6RD + + https://en.wikipedia.org/wiki/IPv6_rapid_deployment + """ + + ip = [IP6,] + + required = ['remote', '6rd-prefix'] + + # TODO: check if key can really be used with 6RD + options = ['remote', 'ttl', 'tos', 'key', '6rd-prefix', '6rd-relay-prefix'] + updates = ['remote', 'ttl', 'tos', + 'multicast', 'allmulticast'] + + def _create(self): + # do not call _Tunnel.create, building fully here + + create = 'ip tunnel add {ifname} mode {type} remote {remote}' + self._cmd(create.format(**self.config)) + self.set_interface('state','down') + + set6rd = 'ip tunnel 6rd dev {ifname} 6rd-prefix {6rd-prefix}' + if '6rd-relay-prefix' in self.config: + set6rd += ' 6rd-relay-prefix {6rd-relay-prefix}' + self._cmd(set6rd.format(**self.config)) diff --git a/python/vyos/ifconfig/vlan.py b/python/vyos/ifconfig/vlan.py new file mode 100644 index 000000000..7b1e00d87 --- /dev/null +++ b/python/vyos/ifconfig/vlan.py @@ -0,0 +1,142 @@ +# Copyright 2019 VyOS maintainers and contributors <maintainers@vyos.io> +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library. If not, see <http://www.gnu.org/licenses/>. + + +import os +import re + +from vyos.ifconfig.interface import Interface + + +# This is an internal implementation class +class VLAN: + """ + This class handels the creation and removal of a VLAN interface. It serves + as base class for BondIf and EthernetIf. + """ + + _novlan_remove = lambda : None + + @classmethod + def enable (cls,adaptee): + adaptee._novlan_remove = adaptee.remove + adaptee.remove = cls.remove + adaptee.add_vlan = cls.add_vlan + adaptee.del_vlan = cls.del_vlan + adaptee.definition['vlan'] = True + return adaptee + + def remove(self): + """ + Remove interface from operating system. Removing the interface + deconfigures all assigned IP addresses and clear possible DHCP(v6) + client processes. + + Example: + >>> from vyos.ifconfig import Interface + >>> i = Interface('eth0') + >>> i.remove() + """ + ifname = self.config['ifname'] + + # Do we have sub interfaces (VLANs)? We apply a regex matching + # subinterfaces (indicated by a .) of a parent interface. + # + # As interfaces need to be deleted "in order" starting from Q-in-Q + # we delete them first. + vlan_ifs = [f for f in os.listdir(r'/sys/class/net') + if re.match(ifname + r'(?:\.\d+)(?:\.\d+)', f)] + + for vlan in vlan_ifs: + Interface(vlan).remove() + + # After deleting all Q-in-Q interfaces delete other VLAN interfaces + # which probably acted as parent to Q-in-Q or have been regular 802.1q + # interface. + vlan_ifs = [f for f in os.listdir(r'/sys/class/net') + if re.match(ifname + r'(?:\.\d+)', f)] + + for vlan in vlan_ifs: + # self.__class__ is already VLAN.enabled + self.__class__(vlan)._novlan_remove() + + # All subinterfaces are now removed, continue on the physical interface + self._novlan_remove() + + def add_vlan(self, vlan_id, ethertype='', ingress_qos='', egress_qos=''): + """ + A virtual LAN (VLAN) is any broadcast domain that is partitioned and + isolated in a computer network at the data link layer (OSI layer 2). + Use this function to create a new VLAN interface on a given physical + interface. + + This function creates both 802.1q and 802.1ad (Q-in-Q) interfaces. Proto + parameter is used to indicate VLAN type. + + A new object of type VLANIf is returned once the interface has been + created. + + @param ethertype: If specified, create 802.1ad or 802.1q Q-in-Q VLAN + interface + @param ingress_qos: Defines a mapping of VLAN header prio field to the + Linux internal packet priority on incoming frames. + @param ingress_qos: Defines a mapping of Linux internal packet priority + to VLAN header prio field but for outgoing frames. + + Example: + >>> from vyos.ifconfig import MACVLANIf + >>> i = MACVLANIf('eth0') + >>> 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) + + # return new object mapping to the newly created interface + # we can now work on this object for e.g. IP address setting + # or interface description and so on + return self.__class__(vlan_ifname) + + def del_vlan(self, vlan_id): + """ + Remove VLAN interface from operating system. Removing the interface + deconfigures all assigned IP addresses and clear possible DHCP(v6) + client processes. + + Example: + >>> from vyos.ifconfig import MACVLANIf + >>> i = MACVLANIf('eth0.10') + >>> i.del_vlan() + """ + ifname = self.config['ifname'] + self.__class__(f'{ifname}.{vlan_id}')._novlan_remove() diff --git a/python/vyos/ifconfig/vtun.py b/python/vyos/ifconfig/vtun.py new file mode 100644 index 000000000..07d39fcbb --- /dev/null +++ b/python/vyos/ifconfig/vtun.py @@ -0,0 +1,34 @@ +# Copyright 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 +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library. If not, see <http://www.gnu.org/licenses/>. + + +from vyos.ifconfig.interface import Interface + + +@Interface.register +class VTunIf(Interface): + default = { + 'type': 'vtun', + } + definition = { + **Interface.definition, + **{ + 'section': 'openvpn', + 'prefixes': ['vtun', ], + 'bridgeable': True, + }, + } + + # The _create and _delete need to be moved from interface-ppoe to here diff --git a/python/vyos/ifconfig/vxlan.py b/python/vyos/ifconfig/vxlan.py new file mode 100644 index 000000000..f47ae17cc --- /dev/null +++ b/python/vyos/ifconfig/vxlan.py @@ -0,0 +1,106 @@ +# Copyright 2019 VyOS maintainers and contributors <maintainers@vyos.io> +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library. If not, see <http://www.gnu.org/licenses/>. + +from copy import deepcopy + +from vyos import ConfigError +from vyos.ifconfig.interface import Interface + + +@Interface.register +class VXLANIf(Interface): + """ + The VXLAN protocol is a tunnelling protocol designed to solve the + problem of limited VLAN IDs (4096) in IEEE 802.1q. With VXLAN the + size of the identifier is expanded to 24 bits (16777216). + + VXLAN is described by IETF RFC 7348, and has been implemented by a + number of vendors. The protocol runs over UDP using a single + destination port. This document describes the Linux kernel tunnel + device, there is also a separate implementation of VXLAN for + Openvswitch. + + Unlike most tunnels, a VXLAN is a 1 to N network, not just point to + point. A VXLAN device can learn the IP address of the other endpoint + either dynamically in a manner similar to a learning bridge, or make + use of statically-configured forwarding entries. + + For more information please refer to: + https://www.kernel.org/doc/Documentation/networking/vxlan.txt + """ + + default = { + 'type': 'vxlan', + 'group': '', + 'port': 8472, # The Linux implementation of VXLAN pre-dates + # the IANA's selection of a standard destination port + 'remote': '', + 'src_address': '', + 'src_interface': '', + 'vni': 0 + } + definition = { + **Interface.definition, + **{ + 'section': 'vxlan', + 'prefixes': ['vxlan', ], + 'bridgeable': True, + } + } + options = ['group', 'remote', 'src_interface', 'port', 'vni', 'src_address'] + + mapping = { + 'ifname': 'add', + 'vni': 'id', + 'port': 'dstport', + 'src_address': 'nolearning local', + } + + def _create(self): + cmdline = set() + if self.config['remote']: + cmdline = ('ifname', 'type', 'remote', 'src_interface', 'vni', 'port') + + elif self.config['src_address']: + cmdline = ('ifname', 'type', 'src_address', 'vni', 'port') + + elif self.config['group'] and self.config['src_interface']: + cmdline = ('ifname', 'type', 'group', 'src_interface', 'vni', 'port') + + else: + ifname = self.config['ifname'] + raise ConfigError( + f'VXLAN "{ifname}" is missing mandatory underlay interface for a multicast network.') + + cmd = 'ip link' + for key in cmdline: + value = self.config.get(key, '') + if not value: + continue + cmd += ' {} {}'.format(self.mapping.get(key, key), value) + + self._cmd(cmd) + + @classmethod + def get_config(cls): + """ + VXLAN interfaces require a configuration when they are added using + iproute2. This static method will provide the configuration dictionary + used by this class. + + Example: + >> dict = VXLANIf().get_config() + """ + return deepcopy(cls.default) diff --git a/python/vyos/ifconfig/wireguard.py b/python/vyos/ifconfig/wireguard.py new file mode 100644 index 000000000..e2b8a5924 --- /dev/null +++ b/python/vyos/ifconfig/wireguard.py @@ -0,0 +1,222 @@ +# Copyright 2019 VyOS maintainers and contributors <maintainers@vyos.io> +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library. If not, see <http://www.gnu.org/licenses/>. + + +import os +import time +from datetime import timedelta + +from vyos.config import Config +from vyos.ifconfig.interface import Interface +from hurry.filesize import size,alternative + + +@Interface.register +class WireGuardIf(Interface): + default = { + 'type': 'wireguard', + 'port': 0, + 'private-key': None, + 'pubkey': None, + 'psk': '/dev/null', + 'allowed-ips': [], + 'fwmark': 0x00, + 'endpoint': None, + 'keepalive': 0 + } + definition = { + **Interface.definition, + **{ + 'section': 'wireguard', + 'prefixes': ['wg', ], + 'bridgeable': True, + } + } + options = ['port', 'private-key', 'pubkey', 'psk', + 'allowed-ips', 'fwmark', 'endpoint', 'keepalive'] + + """ + Wireguard interface class, contains a comnfig dictionary since + wireguard VPN is being comnfigured via the wg command rather than + writing the config into a file. Otherwise if a pre-shared key is used + (symetric enryption key), it would we exposed within multiple files. + Currently it's only within the config.boot if the config was saved. + + Example: + >>> from vyos.ifconfig import WireGuardIf as wg_if + >>> wg_intfc = wg_if("wg01") + >>> print (wg_intfc.wg_config) + {'private-key': None, 'keepalive': 0, 'endpoint': None, 'port': 0, + 'allowed-ips': [], 'pubkey': None, 'fwmark': 0, 'psk': '/dev/null'} + >>> wg_intfc.wg_config['keepalive'] = 100 + >>> print (wg_intfc.wg_config) + {'private-key': None, 'keepalive': 100, 'endpoint': None, 'port': 0, + 'allowed-ips': [], 'pubkey': None, 'fwmark': 0, 'psk': '/dev/null'} + """ + + def update(self): + if not self.config['private-key']: + raise ValueError("private key required") + else: + # fmask permission check? + pass + + cmd = "wg set {} ".format(self.config['ifname']) + cmd += "listen-port {} ".format(self.config['port']) + cmd += "fwmark {} ".format(str(self.config['fwmark'])) + cmd += "private-key {} ".format(self.config['private-key']) + cmd += "peer {} ".format(self.config['pubkey']) + cmd += " preshared-key {} ".format(self.config['psk']) + cmd += " allowed-ips " + for aip in self.config['allowed-ips']: + if aip != self.config['allowed-ips'][-1]: + cmd += aip + "," + else: + cmd += aip + if self.config['endpoint']: + cmd += " endpoint {}".format(self.config['endpoint']) + cmd += " persistent-keepalive {}".format(self.config['keepalive']) + + self._cmd(cmd) + + # remove psk since it isn't required anymore and is saved in the cli + # config only !! + if self.config['psk'] != '/dev/null': + if os.path.exists(self.config['psk']): + os.remove(self.config['psk']) + + def remove_peer(self, peerkey): + """ + Remove a peer of an interface, peers are identified by their public key. + Giving it a readable name is a vyos feature, to remove a peer the pubkey + and the interface is needed, to remove the entry. + """ + cmd = "wg set {0} peer {1} remove".format( + self.config['ifname'], str(peerkey)) + return self._cmd(cmd) + + def op_show_interface(self): + wgdump = self._dump().get( + self.config['ifname'], None) + + c = Config() + c.set_level(["interfaces", "wireguard", self.config['ifname']]) + description = c.return_effective_value(["description"]) + ips = c.return_effective_values(["address"]) + + print ("interface: {}".format(self.config['ifname'])) + if (description): + print (" description: {}".format(description)) + + if (ips): + print (" address: {}".format(", ".join(ips))) + print (" public key: {}".format(wgdump['public_key'])) + print (" private key: (hidden)") + print (" listening port: {}".format(wgdump['listen_port'])) + print () + + for peer in c.list_effective_nodes(["peer"]): + if wgdump['peers']: + pubkey = c.return_effective_value(["peer", peer, "pubkey"]) + if pubkey in wgdump['peers']: + wgpeer = wgdump['peers'][pubkey] + + print (" peer: {}".format(peer)) + print (" public key: {}".format(pubkey)) + + """ figure out if the tunnel is recently active or not """ + status = "inactive" + if (wgpeer['latest_handshake'] is None): + """ no handshake ever """ + status = "inactive" + else: + if int(wgpeer['latest_handshake']) > 0: + delta = timedelta(seconds=int( + time.time() - wgpeer['latest_handshake'])) + print (" latest handshake: {}".format(delta)) + if (time.time() - int(wgpeer['latest_handshake']) < (60*5)): + """ Five minutes and the tunnel is still active """ + status = "active" + else: + """ it's been longer than 5 minutes """ + status = "inactive" + elif int(wgpeer['latest_handshake']) == 0: + """ no handshake ever """ + status = "inactive" + print (" status: {}".format(status)) + + if wgpeer['endpoint'] is not None: + print (" endpoint: {}".format(wgpeer['endpoint'])) + + if wgpeer['allowed_ips'] is not None: + print (" allowed ips: {}".format( + ",".join(wgpeer['allowed_ips']).replace(",", ", "))) + + if wgpeer['transfer_rx'] > 0 or wgpeer['transfer_tx'] > 0: + rx_size = size( + wgpeer['transfer_rx'], system=alternative) + tx_size = size( + wgpeer['transfer_tx'], system=alternative) + print (" transfer: {} received, {} sent".format( + rx_size, tx_size)) + + if wgpeer['persistent_keepalive'] is not None: + print (" persistent keepalive: every {} seconds".format( + wgpeer['persistent_keepalive'])) + print() + super().op_show_interface_stats() + + def _dump(self): + """Dump wireguard data in a python friendly way.""" + last_device = None + output = {} + + # Dump wireguard connection data + _f = self._cmd('wg show all dump') + for line in _f.split('\n'): + if not line: + # Skip empty lines and last line + continue + items = line.split('\t') + + if last_device != items[0]: + # We are currently entering a new node + device, private_key, public_key, listen_port, fw_mark = items + last_device = device + + output[device] = { + 'private_key': None if private_key == '(none)' else private_key, + 'public_key': None if public_key == '(none)' else public_key, + 'listen_port': int(listen_port), + 'fw_mark': None if fw_mark == 'off' else int(fw_mark), + 'peers': {}, + } + else: + # We are entering a peer + device, public_key, preshared_key, endpoint, allowed_ips, latest_handshake, transfer_rx, transfer_tx, persistent_keepalive = items + if allowed_ips == '(none)': + allowed_ips = [] + else: + allowed_ips = allowed_ips.split('\t') + output[device]['peers'][public_key] = { + 'preshared_key': None if preshared_key == '(none)' else preshared_key, + 'endpoint': None if endpoint == '(none)' else endpoint, + 'allowed_ips': allowed_ips, + 'latest_handshake': None if latest_handshake == '0' else int(latest_handshake), + 'transfer_rx': int(transfer_rx), + 'transfer_tx': int(transfer_tx), + 'persistent_keepalive': None if persistent_keepalive == 'off' else int(persistent_keepalive), + } + return output diff --git a/python/vyos/ifconfig/wireless.py b/python/vyos/ifconfig/wireless.py new file mode 100644 index 000000000..946ae1642 --- /dev/null +++ b/python/vyos/ifconfig/wireless.py @@ -0,0 +1,82 @@ +# Copyright 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 +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library. If not, see <http://www.gnu.org/licenses/>. + +import os + +from vyos.ifconfig.interface import Interface +from vyos.ifconfig.vlan import VLAN + + +@Interface.register +@VLAN.enable +class WiFiIf(Interface): + """ + Handle WIFI/WLAN interfaces. + """ + + default = { + 'type': 'wifi', + 'phy': 'phy0' + } + definition = { + **Interface.definition, + **{ + 'section': 'wireless', + 'prefixes': ['wlan', ], + 'bridgeable': True, + } + } + options = ['phy', 'op_mode'] + + def _create(self): + # all interfaces will be added in monitor mode + cmd = 'iw phy {phy} interface add {ifname} type monitor' \ + .format(**self.config) + self._cmd(cmd) + + # wireless interface is administratively down by default + self.set_admin_state('down') + + def _delete(self): + cmd = 'iw dev {ifname} del' \ + .format(**self.config) + self._cmd(cmd) + + @staticmethod + def get_config(): + """ + WiFi interfaces require a configuration when they are added using + iw (type/phy). This static method will provide the configuration + ictionary used by this class. + + Example: + >> conf = WiFiIf().get_config() + """ + config = { + 'phy': 'phy0' + } + return config + + + +@Interface.register +class WiFiModemIf(WiFiIf): + definition = { + **WiFiIf.definition, + **{ + 'section': 'wirelessmodem', + 'prefixes': ['wlm', ], + } + } diff --git a/python/vyos/ifconfig_vlan.py b/python/vyos/ifconfig_vlan.py index 8e09db95a..ed22646c1 100644 --- a/python/vyos/ifconfig_vlan.py +++ b/python/vyos/ifconfig_vlan.py @@ -13,7 +13,8 @@ # You should have received a copy of the GNU Lesser General Public # License along with this library. If not, see <http://www.gnu.org/licenses/>. -from vyos.ifconfig import VLANIf +from netifaces import interfaces +from vyos import ConfigError def apply_vlan_config(vlan, config): """ @@ -21,7 +22,7 @@ def apply_vlan_config(vlan, config): to a VLAN interface """ - if type(vlan) != type(VLANIf("lo")): + if not vlan.definition['vlan']: raise TypeError() # get DHCP config dictionary and update values @@ -63,17 +64,29 @@ def apply_vlan_config(vlan, config): vlan.set_arp_announce(config['ip_enable_arp_announce']) # configure ARP ignore vlan.set_arp_ignore(config['ip_enable_arp_ignore']) + # configure Proxy ARP + vlan.set_proxy_arp(config['ip_proxy_arp']) + # IPv6 address autoconfiguration + vlan.set_ipv6_autoconf(config['ipv6_autoconf']) + # IPv6 forwarding + vlan.set_ipv6_forwarding(config['ipv6_forwarding']) + # IPv6 Duplicate Address Detection (DAD) tries + vlan.set_ipv6_dad_messages(config['ipv6_dup_addr_detect']) # Maximum Transmission Unit (MTU) vlan.set_mtu(config['mtu']) + + # assign/remove VRF + vlan.set_vrf(config['vrf']) + # Change VLAN interface MAC address if config['mac']: vlan.set_mac(config['mac']) # enable/disable VLAN interface if config['disable']: - vlan.set_state('down') + vlan.set_admin_state('down') else: - vlan.set_state('up') + vlan.set_admin_state('up') # Configure interface address(es) # - not longer required addresses get removed first @@ -83,3 +96,46 @@ def apply_vlan_config(vlan, config): for addr in config['address']: vlan.add_addr(addr) +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']: + # 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') + + # 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') + + # 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']: + # 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') + + diff --git a/python/vyos/interfaces.py b/python/vyos/interfaces.py deleted file mode 100644 index 37c093aca..000000000 --- a/python/vyos/interfaces.py +++ /dev/null @@ -1,99 +0,0 @@ -# Copyright 2018 VyOS maintainers and contributors <maintainers@vyos.io> -# -# This library is free software; you can redistribute it and/or -# modify it under the terms of the GNU Lesser General Public -# License as published by the Free Software Foundation; either -# version 2.1 of the License, or (at your option) any later version. -# -# This library is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU -# Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public -# License along with this library. If not, see <http://www.gnu.org/licenses/>. - -import re -import json - -import subprocess -import netifaces - -intf_type_data_file = '/usr/share/vyos/interface-types.json' - -def list_interfaces(): - interfaces = netifaces.interfaces() - - # Remove "fake" interfaces associated with drivers - for i in ["dummy0", "ip6tnl0", "tunl0", "ip_vti0", "ip6_vti0"]: - try: - interfaces.remove(i) - except ValueError: - pass - - return interfaces - -def list_interfaces_of_type(typ): - with open(intf_type_data_file, 'r') as f: - types_data = json.load(f) - - all_intfs = list_interfaces() - if not (typ in types_data.keys()): - raise ValueError("Unknown interface type: {0}".format(typ)) - else: - r = re.compile('^{0}\d+'.format(types_data[typ])) - return list(filter(lambda i: re.match(r, i), all_intfs)) - -def get_type_of_interface(intf): - with open(intf_type_data_file, 'r') as f: - types_data = json.load(f) - - for key,val in types_data.items(): - r = re.compile('^{0}\d+'.format(val)) - if re.match(r, intf): - return key - - raise ValueError("No type found for interface name: {0}".format(intf)) - -def wireguard_dump(): - """Dump wireguard data in a python friendly way.""" - last_device=None - output = {} - - # Dump wireguard connection data - _f = subprocess.check_output(["wg", "show", "all", "dump"]).decode() - for line in _f.split('\n'): - if not line: - # Skip empty lines and last line - continue - items = line.split('\t') - - if last_device != items[0]: - # We are currently entering a new node - device, private_key, public_key, listen_port, fw_mark = items - last_device = device - - output[device] = { - 'private_key': None if private_key == '(none)' else private_key, - 'public_key': None if public_key == '(none)' else public_key, - 'listen_port': int(listen_port), - 'fw_mark': None if fw_mark == 'off' else int(fw_mark), - 'peers': {}, - } - else: - # We are entering a peer - device, public_key, preshared_key, endpoint, allowed_ips, latest_handshake, transfer_rx, transfer_tx, persistent_keepalive = items - if allowed_ips == '(none)': - allowed_ips = [] - else: - allowed_ips = allowed_ips.split('\t') - output[device]['peers'][public_key] = { - 'preshared_key': None if preshared_key == '(none)' else preshared_key, - 'endpoint': None if endpoint == '(none)' else endpoint, - 'allowed_ips': allowed_ips, - 'latest_handshake': None if latest_handshake == '0' else int(latest_handshake), - 'transfer_rx': int(transfer_rx), - 'transfer_tx': int(transfer_tx), - 'persistent_keepalive': None if persistent_keepalive == 'off' else int(persistent_keepalive), - } - return output diff --git a/python/vyos/ioctl.py b/python/vyos/ioctl.py index e57d261e4..cfa75aac6 100644 --- a/python/vyos/ioctl.py +++ b/python/vyos/ioctl.py @@ -13,9 +13,11 @@ # You should have received a copy of the GNU Lesser General Public # License along with this library. If not, see <http://www.gnu.org/licenses/>. +import sys import os -import fcntl, struct, sys -from socket import * +import socket +import fcntl +import struct SIOCGIFFLAGS = 0x8913 @@ -28,7 +30,7 @@ def get_terminal_size(): def get_interface_flags(intf): """ Pull the SIOCGIFFLAGS """ nullif = '\0'*256 - sock = socket(AF_INET, SOCK_DGRAM) + sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) raw = fcntl.ioctl(sock.fileno(), SIOCGIFFLAGS, intf + nullif) flags, = struct.unpack('H', raw[16:18]) return flags diff --git a/python/vyos/migrator.py b/python/vyos/migrator.py index f05228041..9a5fdef2f 100644 --- a/python/vyos/migrator.py +++ b/python/vyos/migrator.py @@ -25,7 +25,7 @@ class MigratorError(Exception): pass class Migrator(object): - def __init__(self, config_file, force=False, set_vintage=None): + def __init__(self, config_file, force=False, set_vintage='vyos'): self._config_file = config_file self._force = force self._set_vintage = set_vintage @@ -61,9 +61,6 @@ class Migrator(object): if self._set_vintage: self._config_file_vintage = self._set_vintage - if not self._config_file_vintage: - self._config_file_vintage = vyos.defaults.cfg_vintage - if self._config_file_vintage not in ['vyatta', 'vyos']: raise MigratorError("Unknown vintage.") @@ -204,16 +201,12 @@ class Migrator(object): return self._changed class VirtualMigrator(Migrator): - def __init__(self, config_file, vintage='vyos'): - super().__init__(config_file, set_vintage = vintage) - def run(self): cfg_file = self._config_file cfg_versions = self.read_config_file_versions() if not cfg_versions: - raise MigratorError("Config file has no version information;" - " virtual migration not possible.") + return if self.update_vintage(): self._changed = True diff --git a/python/vyos/remote.py b/python/vyos/remote.py index f8a21f068..f918461d1 100644 --- a/python/vyos/remote.py +++ b/python/vyos/remote.py @@ -17,7 +17,8 @@ import sys import os import re import fileinput -import subprocess + +from vyos.util import cmd, DEVNULL def check_and_add_host_key(host_name): @@ -33,10 +34,8 @@ def check_and_add_host_key(host_name): keyscan_cmd = 'ssh-keyscan -t rsa {} 2>/dev/null'.format(host_name) try: - host_key = subprocess.check_output(keyscan_cmd, shell=True, - stderr=subprocess.DEVNULL, - universal_newlines=True) - except subprocess.CalledProcessError as err: + host_key = cmd(keyscan_cmd, shell=True, stderr=DEVNULL) + except OSError: sys.exit("Can not get RSA host key") # libssh2 (jessie; stretch) does not recognize ec host keys, and curl @@ -64,10 +63,8 @@ def check_and_add_host_key(host_name): fingerprint_cmd = 'ssh-keygen -lf /dev/stdin <<< "{}"'.format(host_key) try: - fingerprint = subprocess.check_output(fingerprint_cmd, shell=True, - stderr=subprocess.DEVNULL, - universal_newlines=True) - except subprocess.CalledProcessError as err: + fingerprint = cmd(fingerprint_cmd, shell=True, stderr=DEVNULL) + except OSError: sys.exit("Can not get RSA host key fingerprint.") print("RSA host key fingerprint is {}".format(fingerprint.split()[1])) @@ -128,9 +125,8 @@ def get_remote_config(remote_file): # Try header first, and look for 'OK' or 'Moved' codes: curl_cmd = 'curl {0} -q -I {1}'.format(redirect_opt, remote_file) try: - curl_output = subprocess.check_output(curl_cmd, shell=True, - universal_newlines=True) - except subprocess.CalledProcessError: + curl_output = cmd(curl_cmd, shell=True) + except OSError: sys.exit(1) return_vals = re.findall(r'^HTTP\/\d+\.?\d\s+(\d+)\s+(.*)$', @@ -146,9 +142,6 @@ def get_remote_config(remote_file): curl_cmd = 'curl {0} -# {1}'.format(redirect_opt, remote_file) try: - config_file = subprocess.check_output(curl_cmd, shell=True, - universal_newlines=True) - except subprocess.CalledProcessError: - config_file = None - - return config_file + return cmd(curl_cmd, shell=True, stderr=None) + except OSError: + return None diff --git a/python/vyos/util.py b/python/vyos/util.py index 67a602f7a..291ce64ea 100644 --- a/python/vyos/util.py +++ b/python/vyos/util.py @@ -15,15 +15,96 @@ import os import re -import getpass -import grp -import time -import subprocess import sys +from subprocess import Popen, PIPE, STDOUT, DEVNULL + +def debug(flag): + # this is to force all new flags to be registered here so that + # they can be documented: + # - developer: the code will drop into PBD on un-handled exception + # - ifconfig: prints command and sysfs access on stdout for interface + if flag not in ['developer', 'ifconfig']: + return '' + return flag if os.path.isfile(f'/tmp/vyos.{flag}.debug') else '' + + +def debug_msg(message, section=''): + if debug(section): + print(f'DEBUG/{section:<6} {message}') + + +def popen(command, section='', shell=None, input=None, timeout=None, env=None, + universal_newlines=None, stdout=PIPE, stderr=STDOUT, decode=None): + """ popen does not raise, returns the output and error code of command """ + use_shell = shell + if shell is None: + use_shell = True if ' ' in command else False + p = Popen( + command, + stdout=stdout, stderr=stderr, + env=env, shell=use_shell, + universal_newlines=universal_newlines, + ) + tmp = p.communicate(input, timeout)[0].strip() + debug_msg(f"cmd '{command}'", section) + decoded = tmp.decode(decode) if decode else tmp.decode() + if decoded: + debug_msg(f"returned:\n{decoded}", section) + return decoded, p.returncode + + +def run(command, section='', shell=None, input=None, timeout=None, env=None, + universal_newlines=None, stdout=PIPE, stderr=STDOUT, decode=None): + """ does not raise exception on error, returns error code """ + _, code = popen( + command, section, + stdout=stdout, stderr=stderr, + input=input, timeout=timeout, + env=env, shell=shell, + universal_newlines=universal_newlines, + decode=decode, + ) + return code + + +def cmd(command, section='', shell=None, input=None, timeout=None, env=None, + universal_newlines=None, stdout=PIPE, stderr=STDOUT, decode=None, + raising=None, message=''): + """ does raise exception, returns output of command """ + decoded, code = popen( + command, section, + stdout=stdout, stderr=stderr, + input=input, timeout=timeout, + env=env, shell=shell, + universal_newlines=universal_newlines, + decode=decode, + ) + if code != 0: + feedback = message + '\n' if message else '' + feedback += f'failed to run command: {command}\n' + feedback += f'returned: {decoded}\n' + feedback += f'exit code: {code}' + if raising is None: + # error code can be recovered with .errno + raise OSError(code, feedback) + else: + raise raising(feedback) + return decoded -import psutil -import vyos.defaults +def call(command, section='', shell=None, input=None, timeout=None, env=None, + universal_newlines=None, stdout=PIPE, stderr=STDOUT, decode=None): + """ does not raise exception on error, returns error code, print output """ + out, code = popen( + command, section, + stdout=stdout, stderr=stderr, + input=input, timeout=timeout, + env=env, shell=shell, + universal_newlines=universal_newlines, + decode=decode, + ) + print(out) + return code def read_file(path): @@ -32,6 +113,36 @@ def read_file(path): data = f.read().strip() return data + +def chown(path, user, group): + """ change file/directory owner """ + 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) + +def chmod_750(path): + """ make file/directory only executable to user and group """ + from stat import S_IRUSR, S_IWUSR, S_IXUSR, S_IRGRP, S_IXGRP + + if os.path.exists(path): + bitmask = S_IRUSR | S_IWUSR | S_IXUSR | S_IRGRP | S_IXGRP + os.chmod(path, bitmask) + + +def chmod_x(path): + """ make file executable """ + from stat import S_IRUSR, S_IWUSR, S_IXUSR, S_IRGRP, S_IXGRP, S_IROTH, S_IXOTH + + if os.path.exists(path): + bitmask = S_IRUSR | S_IWUSR | S_IXUSR | S_IRGRP | S_IXGRP | \ + S_IROTH | S_IXOTH + os.chmod(path, bitmask) + + 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. @@ -80,11 +191,16 @@ def colon_separated_to_dict(data_string, uniquekeys=False): return data + def process_running(pid_file): """ Checks if a process with PID in pid_file is running """ + from psutil import pid_exists + if not os.path.isfile(pid_file): + return False with open(pid_file, 'r') as f: pid = f.read().strip() - return psutil.pid_exists(int(pid)) + return pid_exists(int(pid)) + def seconds_to_human(s, separator=""): """ Converts number of seconds passed to a human-readable @@ -125,10 +241,15 @@ def seconds_to_human(s, separator=""): return result + def get_cfg_group_id(): - group_data = grp.getgrnam(vyos.defaults.cfg_group) + from grp import getgrnam + from vyos.defaults import cfg_group + + group_data = getgrnam(cfg_group) return group_data.gr_gid + def file_is_persistent(path): 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) @@ -137,6 +258,7 @@ def file_is_persistent(path): else: return (True, None) + def commit_in_progress(): """ Not to be used in normal op mode scripts! """ @@ -154,29 +276,34 @@ def commit_in_progress(): # Since this will be used in scripts that modify the config outside of the CLI # framework, those knowingly have root permissions. # For everything else, we add a safeguard. - id = subprocess.check_output(['/usr/bin/id', '-u']).decode().strip() - if id != '0': + from psutil import process_iter, NoSuchProcess + from vyos.defaults import commit_lock + + idu = cmd('/usr/bin/id -u') + if idu != '0': raise OSError("This functions needs root permissions to return correct results") - for proc in psutil.process_iter(): + for proc in process_iter(): try: files = proc.open_files() if files: for f in files: - if f.path == vyos.defaults.commit_lock: + if f.path == commit_lock: return True - except psutil.NoSuchProcess as err: + except NoSuchProcess as err: # Process died before we could examine it pass # Default case return False + def wait_for_commit_lock(): """ Not to be used in normal op mode scripts! """ - + from time import sleep # Very synchronous approach to multiprocessing while commit_in_progress(): - time.sleep(1) + sleep(1) + def ask_yes_no(question, default=False) -> bool: """Ask a yes/no question via input() and return their answer.""" @@ -196,6 +323,50 @@ def ask_yes_no(question, default=False) -> bool: def is_admin() -> bool: """Look if current user is in sudo group""" - current_user = getpass.getuser() - (_, _, _, admin_group_members) = grp.getgrnam('sudo') + from getpass import getuser + from grp import getgrnam + current_user = getuser() + (_, _, _, admin_group_members) = getgrnam('sudo') return current_user in admin_group_members + + +def mac2eui64(mac, prefix=None): + """ + Convert a MAC address to a EUI64 address or, with prefix provided, a full + IPv6 address. + Thankfully copied from https://gist.github.com/wido/f5e32576bb57b5cc6f934e177a37a0d3 + """ + from ipaddress import ip_network + # http://tools.ietf.org/html/rfc4291#section-2.5.1 + eui64 = re.sub(r'[.:-]', '', mac).lower() + eui64 = eui64[0:6] + 'fffe' + eui64[6:] + eui64 = hex(int(eui64[0:2], 16) ^ 2)[2:].zfill(2) + eui64[2:] + + if prefix is None: + return ':'.join(re.findall(r'.{4}', eui64)) + else: + try: + net = ip_network(prefix, strict=False) + euil = int('0x{0}'.format(eui64), 16) + return str(net[euil]) + except: # pylint: disable=bare-except + return + +def is_bridge_member(interface): + """ + Checks if passed interfaces is part of a bridge device or not. + + Returns a tuple: + False, None -> Not part of a bridge + True, bridge-name -> If it is assigned to a bridge + """ + from vyos.config import Config + c = Config() + base = ['interfaces', 'bridge'] + for bridge in c.list_nodes(base): + members = c.list_nodes(base + [bridge, 'member', 'interface']) + if interface in members: + return (True, bridge) + + return False, None + diff --git a/python/vyos/validate.py b/python/vyos/validate.py index 33c495d91..9d413ffab 100644 --- a/python/vyos/validate.py +++ b/python/vyos/validate.py @@ -13,6 +13,7 @@ # You should have received a copy of the GNU Lesser General Public # License along with this library. If not, see <http://www.gnu.org/licenses/>. +import socket import netifaces import ipaddress @@ -64,51 +65,61 @@ def is_ipv6_link_local(addr): return False +def _are_same_ip(one, two): + # compare the binary representation of the IP + f_one = socket.AF_INET if is_ipv4(one) else socket.AF_INET6 + s_two = socket.AF_INET if is_ipv4(two) else socket.AF_INET6 + return socket.inet_pton(f_one, one) == socket.inet_pton(f_one, two) + def is_intf_addr_assigned(intf, addr): + if '/' in addr: + ip,mask = addr.split('/') + return _is_intf_addr_assigned(intf, ip, mask) + return _is_intf_addr_assigned(intf, addr) + +def _is_intf_addr_assigned(intf, address, netmask=''): """ Verify if the given IPv4/IPv6 address is assigned to specific interface. It can check both a single IP address (e.g. 192.0.2.1 or a assigned CIDR address 192.0.2.1/24. """ - # determine IP version (AF_INET or AF_INET6) depending on passed address - addr_type = netifaces.AF_INET - if is_ipv6(addr): - addr_type = netifaces.AF_INET6 - # check if the requested address type is configured at all + # { + # 17: [{'addr': '08:00:27:d9:5b:04', 'broadcast': 'ff:ff:ff:ff:ff:ff'}], + # 2: [{'addr': '10.0.2.15', 'netmask': '255.255.255.0', 'broadcast': '10.0.2.255'}], + # 10: [{'addr': 'fe80::a00:27ff:fed9:5b04%eth0', 'netmask': 'ffff:ffff:ffff:ffff::'}] + # } try: - netifaces.ifaddresses(intf) + ifaces = netifaces.ifaddresses(intf) except ValueError as e: print(e) return False - if addr_type in netifaces.ifaddresses(intf).keys(): - # Check every IP address on this interface for a match - for ip in netifaces.ifaddresses(intf)[addr_type]: - # Check if it matches to the address requested - # If passed address contains a '/' indicating a normalized IP - # address we have to take this into account, too - if r'/' in addr: - prefixlen = '' - if is_ipv6(addr): - # Note that currently expanded netmasks are not supported. That means - # 2001:db00::0/24 is a valid argument while 2001:db00::0/ffff:ff00:: not. - # see https://docs.python.org/3/library/ipaddress.html - bits = bin( int(ip['netmask'].replace(':',''), 16) ).count('1') - prefixlen = '/' + str(bits) - - else: - prefixlen = '/' + str(ipaddress.IPv4Network('0.0.0.0/' + ip['netmask']).prefixlen) - - # construct temporary variable holding IPv6 address and netmask - # in CIDR notation - tmp = ip['addr'] + prefixlen - if addr == tmp: - return True + # determine IP version (AF_INET or AF_INET6) depending on passed address + addr_type = netifaces.AF_INET if is_ipv4(address) else netifaces.AF_INET6 - elif ip['addr'] == addr: - return True + # Check every IP address on this interface for a match + for ip in ifaces.get(addr_type,[]): + # ip can have the interface name in the 'addr' field, we need to remove it + # {'addr': 'fe80::a00:27ff:fec5:f821%eth2', 'netmask': 'ffff:ffff:ffff:ffff::'} + ip_addr = ip['addr'].split('%')[0] + + if not _are_same_ip(address, ip_addr): + continue + + # we do not have a netmask to compare against, they are the same + if netmask == '': + return True + + prefixlen = '' + if is_ipv4(ip_addr): + prefixlen = sum([bin(int(_)).count('1') for _ in ip['netmask'].split('.')]) + else: + prefixlen = sum([bin(int(_,16)).count('1') for _ in ip['netmask'].split(':') if _]) + + if str(prefixlen) == netmask: + return True return False @@ -168,3 +179,64 @@ def is_subnet_connected(subnet, primary=False): return True return False + + +def assert_boolean(b): + if int(b) not in (0, 1): + raise ValueError(f'Value {b} out of range') + + +def assert_range(value, lower=0, count=3): + if int(value) not in range(lower,lower+count): + raise ValueError("Value out of range") + + +def assert_list(s, l): + if s not in l: + o = ' or '.join([f'"{n}"' for n in l]) + raise ValueError(f'state must be {o}, got {s}') + + +def assert_number(n): + if not str(n).isnumeric(): + raise ValueError(f'{n} must be a number') + + +def assert_positive(n, smaller=0): + assert_number(n) + if int(n) < smaller: + raise ValueError(f'{n} is smaller than {limit}') + + +def assert_mtu(mtu, min=68, max=9000): + assert_number(mtu) + if int(mtu) < min or int(mtu) > max: + raise ValueError(f'Invalid MTU size: "{mtu}"') + + +def assert_mac(m): + split = m.split(':') + size = len(split) + + # a mac address consits out of 6 octets + if size != 6: + raise ValueError(f'wrong number of MAC octets ({size}): {m}') + + octets = [] + try: + for octet in split: + octets.append(int(octet, 16)) + except ValueError: + raise ValueError(f'invalid hex number "{octet}" in : {m}') + + # validate against the first mac address byte if it's a multicast + # address + if octets[0] & 1: + raise ValueError(f'{m} is a multicast MAC address') + + # overall mac address is not allowed to be 00:00:00:00:00:00 + if sum(octets) == 0: + raise ValueError('00:00:00:00:00:00 is not a valid MAC address') + + if octets[:5] == (0, 0, 94, 0, 1): + raise ValueError(f'{m} is a VRRP MAC address') diff --git a/python/vyos/version.py b/python/vyos/version.py index 383efbc1e..d51a940d6 100644 --- a/python/vyos/version.py +++ b/python/vyos/version.py @@ -44,7 +44,7 @@ def get_version_data(file=version_file): file (str): path to the version file Returns: - dict: version data + dict: version data, if it can not be found and empty dict The optional ``file`` argument comes in handy in upgrade scripts that need to retrieve information from images other than the running image. @@ -52,17 +52,20 @@ def get_version_data(file=version_file): is an implementation detail and may change in the future, while the interface of this module will stay the same. """ - with open(file, 'r') as f: - version_data = json.load(f) - return version_data + try: + with open(file, 'r') as f: + version_data = json.load(f) + return version_data + except FileNotFoundError: + return {} def get_version(file=None): """ - Get the version number + Get the version number, or an empty string if it could not be determined """ version_data = None if file: version_data = get_version_data(file=file) else: version_data = get_version_data() - return version_data["version"] + return version_data.get('version','') diff --git a/scripts/build-command-op-templates b/scripts/build-command-op-templates index 0383c8982..689d19ece 100755 --- a/scripts/build-command-op-templates +++ b/scripts/build-command-op-templates @@ -111,7 +111,7 @@ def get_properties(p): for i in lists: comp_exprs.append("echo \"{0}\"".format(i.text)) for i in paths: - comp_exprs.append("/bin/cli-shell-api listNodes {0}".format(i.text)) + comp_exprs.append("/bin/cli-shell-api listActiveNodes {0} | sed -e \"s/'//g\"".format(i.text)) for i in scripts: comp_exprs.append("{0}".format(i.text)) comp_help = " && ".join(comp_exprs) diff --git a/scripts/build-command-templates b/scripts/build-command-templates index dbf4ad9c5..c6534a6d8 100755 --- a/scripts/build-command-templates +++ b/scripts/build-command-templates @@ -264,6 +264,11 @@ def process_node(n, tmpl_dir): if node_type == "tagNode": props["tag"] = "True" + if node_type != "leafNode": + if "multi" in props: + raise ValueError("<multi/> tag is only allowed in <leafNode>") + if "valueless" in props: + raise ValueError("<valueless/> is only allowed in <leafNode>") nodedef_path = os.path.join(make_path(my_tmpl_dir), "node.def") if not os.path.exists(nodedef_path): diff --git a/src/completion/list_dumpable_interfaces.py b/src/completion/list_dumpable_interfaces.py index 53ee89633..101c92fbe 100755 --- a/src/completion/list_dumpable_interfaces.py +++ b/src/completion/list_dumpable_interfaces.py @@ -3,12 +3,10 @@ # Extract the list of interfaces available for traffic dumps from tcpdump -D import re -import subprocess -if __name__ == '__main__': - out = subprocess.check_output(['/usr/sbin/tcpdump', '-D']).decode().strip() - out = out.split("\n") +from vyos.util import cmd +if __name__ == '__main__': + out = cmd('/usr/sbin/tcpdump -D').split('\n') intfs = " ".join(map(lambda s: re.search(r'\d+\.(\S+)\s', s).group(1), out)) - print(intfs) diff --git a/src/completion/list_interfaces.py b/src/completion/list_interfaces.py index 8cd59917d..98b32797a 100755 --- a/src/completion/list_interfaces.py +++ b/src/completion/list_interfaces.py @@ -2,7 +2,7 @@ import sys import argparse -import vyos.interfaces +from vyos.ifconfig import Interface parser = argparse.ArgumentParser() group = parser.add_mutually_exclusive_group() @@ -13,35 +13,39 @@ group.add_argument("-bo", "--bondable", action="store_true", help="List all bond args = parser.parse_args() +# XXX: Need to be rewritten using the data in the class definition +# XXX: It can be done once vti and input are moved into vyos +# XXX: We store for each class what type they are (broadcast, bridgeabe, ...) + if args.type: try: - interfaces = vyos.interfaces.list_interfaces_of_type(args.type) + interfaces = Interface.listing(args.type) except ValueError as e: print(e, file=sys.stderr) print("") elif args.broadcast: - eth = vyos.interfaces.list_interfaces_of_type("ethernet") - bridge = vyos.interfaces.list_interfaces_of_type("bridge") - bond = vyos.interfaces.list_interfaces_of_type("bonding") + eth = Interface.listing("ethernet") + bridge = Interface.listing("bridge") + bond = Interface.listing("bonding") interfaces = eth + bridge + bond elif args.bridgeable: - eth = vyos.interfaces.list_interfaces_of_type("ethernet") - bond = vyos.interfaces.list_interfaces_of_type("bonding") - l2tpv3 = vyos.interfaces.list_interfaces_of_type("l2tpv3") - openvpn = vyos.interfaces.list_interfaces_of_type("openvpn") - wireless = vyos.interfaces.list_interfaces_of_type("wireless") - tunnel = vyos.interfaces.list_interfaces_of_type("tunnel") - vxlan = vyos.interfaces.list_interfaces_of_type("vxlan") - geneve = vyos.interfaces.list_interfaces_of_type("geneve") + eth = Interface.listing("ethernet") + bond = Interface.listing("bonding") + l2tpv3 = Interface.listing("l2tpv3") + openvpn = Interface.listing("openvpn") + wireless = Interface.listing("wireless") + tunnel = Interface.listing("tunnel") + vxlan = Interface.listing("vxlan") + geneve = Interface.listing("geneve") interfaces = eth + bond + l2tpv3 + openvpn + vxlan + tunnel + wireless + geneve elif args.bondable: interfaces = [] - eth = vyos.interfaces.list_interfaces_of_type("ethernet") + eth = Interface.listing("ethernet") # we need to filter out VLAN interfaces identified by a dot (.) in their name for intf in eth: @@ -49,6 +53,6 @@ elif args.bondable: interfaces.append(intf) else: - interfaces = vyos.interfaces.list_interfaces() + interfaces = Interface.listing() print(" ".join(interfaces)) diff --git a/src/completion/list_local.py b/src/completion/list_local.py new file mode 100755 index 000000000..40cc95f1e --- /dev/null +++ b/src/completion/list_local.py @@ -0,0 +1,24 @@ +#!/usr/bin/env python3 + +import json +import argparse + +from vyos.util import cmd + +# [{"ifindex":1,"ifname":"lo","flags":["LOOPBACK","UP","LOWER_UP"],"mtu":65536,"qdisc":"noqueue","operstate":"UNKNOWN","group":"default","txqlen":1000,"link_type":"loopback","address":"00:00:00:00:00:00","broadcast":"00:00:00:00:00:00","addr_info":[{"family":"inet","local":"127.0.0.1","prefixlen":8,"scope":"host","label":"lo","valid_life_time":4294967295,"preferred_life_time":4294967295},{"family":"inet6","local":"::1","prefixlen":128,"scope":"host","valid_life_time":4294967295,"preferred_life_time":4294967295}]},{"ifindex":2,"ifname":"eth0","flags":["BROADCAST","MULTICAST","UP","LOWER_UP"],"mtu":1500,"qdisc":"pfifo_fast","operstate":"UP","group":"default","txqlen":1000,"link_type":"ether","address":"08:00:27:fa:12:53","broadcast":"ff:ff:ff:ff:ff:ff","addr_info":[{"family":"inet","local":"10.0.2.15","prefixlen":24,"broadcast":"10.0.2.255","scope":"global","label":"eth0","valid_life_time":4294967295,"preferred_life_time":4294967295},{"family":"inet6","local":"fe80::a00:27ff:fefa:1253","prefixlen":64,"scope":"link","valid_life_time":4294967295,"preferred_life_time":4294967295}]},{"ifindex":3,"ifname":"eth1","flags":["BROADCAST","MULTICAST","UP","LOWER_UP"],"mtu":1500,"qdisc":"pfifo_fast","operstate":"UP","group":"default","txqlen":1000,"link_type":"ether","address":"08:00:27:0d:25:dc","broadcast":"ff:ff:ff:ff:ff:ff","addr_info":[{"family":"inet6","local":"fe80::a00:27ff:fe0d:25dc","prefixlen":64,"scope":"link","valid_life_time":4294967295,"preferred_life_time":4294967295}]},{"ifindex":4,"ifname":"eth2","flags":["BROADCAST","MULTICAST","UP","LOWER_UP"],"mtu":1500,"qdisc":"pfifo_fast","operstate":"UP","group":"default","txqlen":1000,"link_type":"ether","address":"08:00:27:68:d0:b1","broadcast":"ff:ff:ff:ff:ff:ff","addr_info":[{"family":"inet6","local":"fe80::a00:27ff:fe68:d0b1","prefixlen":64,"scope":"link","valid_life_time":4294967295,"preferred_life_time":4294967295}]},{"ifindex":5,"ifname":"eth3","flags":["BROADCAST","MULTICAST","UP","LOWER_UP"],"mtu":1500,"qdisc":"pfifo_fast","operstate":"UP","group":"default","txqlen":1000,"link_type":"ether","address":"08:00:27:f0:17:c5","broadcast":"ff:ff:ff:ff:ff:ff","addr_info":[{"family":"inet6","local":"fe80::a00:27ff:fef0:17c5","prefixlen":64,"scope":"link","valid_life_time":4294967295,"preferred_life_time":4294967295}]}] + +if __name__ == '__main__': + parser = argparse.ArgumentParser() + group = parser.add_mutually_exclusive_group() + + out = cmd('ip -j address show') + data = json.loads(out) + + + interfaces = [] + for interface in data: + if not 'addr_info' in interface: + continue + interfaces.extend(interface['addr_info']) + + print(' '.join([interface['local'] for interface in interfaces if 'local' in interface])) diff --git a/src/completion/list_openvpn_clients.py b/src/completion/list_openvpn_clients.py index 828ce6b5e..17b0c7008 100755 --- a/src/completion/list_openvpn_clients.py +++ b/src/completion/list_openvpn_clients.py @@ -18,7 +18,7 @@ import os import sys import argparse -from vyos.interfaces import list_interfaces_of_type +from vyos.ifconfig import Interface def get_client_from_interface(interface): clients = [] @@ -50,7 +50,7 @@ if __name__ == "__main__": if args.interface: clients = get_client_from_interface(args.interface) elif args.all: - for interface in list_interfaces_of_type("openvpn"): + for interface in Interface.listing("openvpn"): clients += get_client_from_interface(interface) print(" ".join(clients)) diff --git a/src/completion/list_wlm_peers.sh b/src/completion/list_wlm_peers.sh new file mode 100755 index 000000000..12dd00650 --- /dev/null +++ b/src/completion/list_wlm_peers.sh @@ -0,0 +1,6 @@ +#!/bin/sh + +if [ -d /etc/ppp/peers ]; then + cd /etc/ppp/peers + ls wlm* +fi diff --git a/src/conf_mode/accel_l2tp.py b/src/conf_mode/accel_l2tp.py index a7af9cc68..4ca5a858a 100755 --- a/src/conf_mode/accel_l2tp.py +++ b/src/conf_mode/accel_l2tp.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 @@ -13,20 +13,21 @@ # # You should have received a copy of the GNU General Public License # along with this program. If not, see <http://www.gnu.org/licenses/>. -# -# import sys import os import re -import subprocess import jinja2 import socket import time -import syslog as sl + +from jinja2 import FileSystemLoader, Environment from vyos.config import Config +from vyos.defaults import directories as vyos_data_dir from vyos import ConfigError +from vyos.util import run + pidfile = r'/var/run/accel_l2tp.pid' l2tp_cnf_dir = r'/etc/accel-ppp/l2tp' @@ -34,526 +35,363 @@ chap_secrets = l2tp_cnf_dir + '/chap-secrets' l2tp_conf = l2tp_cnf_dir + '/l2tp.config' # accel-pppd -d -c /etc/accel-ppp/l2tp/l2tp.config -p /var/run/accel_l2tp.pid -### config path creation +# config path creation if not os.path.exists(l2tp_cnf_dir): - os.makedirs(l2tp_cnf_dir) - sl.syslog(sl.LOG_NOTICE, l2tp_cnf_dir + " created") - -l2tp_config = ''' -### generated by accel_l2tp.py ### -[modules] -log_syslog -l2tp -chap-secrets -{% for proto in authentication['auth_proto']: %} -{{proto}} -{% endfor%} -{% if authentication['mode'] == 'radius' %} -radius -{% endif -%} -ippool -shaper -ipv6pool -ipv6_nd -ipv6_dhcp - -[core] -thread-count={{thread_cnt}} - -[log] -syslog=accel-l2tp,daemon -copy=1 -level=5 - -{% if dns %} -[dns] -{% if dns[0] %} -dns1={{dns[0]}} -{% endif %} -{% if dns[1] %} -dns2={{dns[1]}} -{% endif %} -{% endif -%} - -{% if dnsv6 %} -[ipv6-dns] -{% for srv in dnsv6: %} -{{srv}} -{% endfor %} -{% endif %} - -{% if wins %} -[wins] -{% if wins[0] %} -wins1={{wins[0]}} -{% endif %} -{% if wins[1] %} -wins2={{wins[1]}} -{% endif %} -{% endif -%} - -[l2tp] -verbose=1 -ifname=l2tp%d -ppp-max-mtu={{mtu}} -mppe={{authentication['mppe']}} -{% if outside_addr %} -bind={{outside_addr}} -{% endif %} -{% if lns_shared_secret %} -secret={{lns_shared_secret}} -{% endif %} - -[client-ip-range] -0.0.0.0/0 - -{% if (client_ip_pool) or (client_ip_subnets) %} -[ip-pool] -{% if client_ip_pool %} -{{client_ip_pool}} -{% endif -%} -{% if client_ip_subnets %} -{% for sn in client_ip_subnets %} -{{sn}} -{% endfor -%} -{% endif %} -{% endif %} -{% if gateway_address %} -gw-ip-address={{gateway_address}} -{% endif %} - -{% if authentication['mode'] == 'local' %} -[chap-secrets] -chap-secrets=/etc/accel-ppp/l2tp/chap-secrets -{% if gateway_address %} -gw-ip-address={{gateway_address}} -{% endif %} -{% endif %} - -[ppp] -verbose=1 -check-ip=1 -single-session=replace -{% if idle_timeout %} -lcp-echo-timeout={{idle_timeout}} -{% endif %} -{% if ppp_options['lcp-echo-interval'] %} -lcp-echo-interval={{ppp_options['lcp-echo-interval']}} -{% else %} -lcp-echo-interval=30 -{% endif %} -{% if ppp_options['lcp-echo-failure'] %} -lcp-echo-failure={{ppp_options['lcp-echo-failure']}} -{% else %} -lcp-echo-failure=3 -{% endif %} -{% if ccp_disable %} -ccp=0 -{% endif %} -{% if client_ipv6_pool %} -ipv6=allow -{% endif %} - -{% if authentication['mode'] == 'radius' %} -[radius] -{% for rsrv in authentication['radiussrv']: %} -server={{rsrv}},{{authentication['radiussrv'][rsrv]['secret']}},\ -req-limit={{authentication['radiussrv'][rsrv]['req-limit']}},\ -fail-time={{authentication['radiussrv'][rsrv]['fail-time']}} -{% endfor %} -{% if authentication['radiusopt']['timeout'] %} -timeout={{authentication['radiusopt']['timeout']}} -{% endif %} -{% if authentication['radiusopt']['acct-timeout'] %} -acct-timeout={{authentication['radiusopt']['acct-timeout']}} -{% endif %} -{% if authentication['radiusopt']['max-try'] %} -max-try={{authentication['radiusopt']['max-try']}} -{% endif %} -{% if authentication['radiusopt']['nas-id'] %} -nas-identifier={{authentication['radiusopt']['nas-id']}} -{% endif %} -{% if authentication['radius_source_address'] %} -nas-ip-address={{authentication['radius_source_address']}} -{% endif -%} -{% if authentication['radiusopt']['dae-srv'] %} -dae-server={{authentication['radiusopt']['dae-srv']['ip-addr']}}:\ -{{authentication['radiusopt']['dae-srv']['port']}},\ -{{authentication['radiusopt']['dae-srv']['secret']}} -{% endif -%} -gw-ip-address={{gateway_address}} -verbose=1 -{% endif -%} - -{% if client_ipv6_pool %} -[ipv6-pool] -{% for prfx in client_ipv6_pool.prefix: %} -{{prfx}} -{% endfor %} -{% for prfx in client_ipv6_pool.delegate_prefix: %} -delegate={{prfx}} -{% endfor %} -{% endif %} - -{% if client_ipv6_pool['delegate_prefix'] %} -[ipv6-dhcp] -verbose=1 -{% endif %} - -{% if authentication['radiusopt']['shaper'] %} -[shaper] -verbose=1 -attr={{authentication['radiusopt']['shaper']['attr']}} -{% if authentication['radiusopt']['shaper']['vendor'] %} -vendor={{authentication['radiusopt']['shaper']['vendor']}} -{% endif -%} -{% endif %} - -[cli] -tcp=127.0.0.1:2004 -sessions-columns=ifname,username,calling-sid,ip,{{ip6_column}}{{ip6_dp_column}}rate-limit,type,comp,state,rx-bytes,tx-bytes,uptime - -''' - -### l2tp chap secrets -chap_secrets_conf = ''' -# username server password acceptable local IP addresses shaper -{% for user in authentication['local-users'] %} -{% if authentication['local-users'][user]['state'] == 'enabled' %} -{% if (authentication['local-users'][user]['upload']) and (authentication['local-users'][user]['download']) %} -{{user}}\t*\t{{authentication['local-users'][user]['passwd']}}\t{{authentication['local-users'][user]['ip']}}\t\ -{{authentication['local-users'][user]['download']}}/{{authentication['local-users'][user]['upload']}} -{% else %} -{{user}}\t*\t{{authentication['local-users'][user]['passwd']}}\t{{authentication['local-users'][user]['ip']}} -{% endif %} -{% endif %} -{% endfor %} -''' + os.makedirs(l2tp_cnf_dir) ### # inline helper functions ### # depending on hw and threads, daemon needs a little to start # if it takes longer than 100 * 0.5 secs, exception is being raised -# not sure if that's the best way to check it, but it worked so far quite well +# not sure if that's the best way to check it, but it worked so far quite well ### + + def chk_con(): - cnt = 0 - s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - while True: - try: - s.connect(("127.0.0.1", 2004)) - break - except ConnectionRefusedError: - time.sleep(0.5) - cnt +=1 - if cnt == 100: - raise("failed to start l2tp server") - break - -### chap_secrets file if auth mode local -def write_chap_secrets(c): - tmpl = jinja2.Template(chap_secrets_conf, trim_blocks=True) - chap_secrets_txt = tmpl.render(c) - old_umask = os.umask(0o077) - open(chap_secrets,'w').write(chap_secrets_txt) - os.umask(old_umask) - sl.syslog(sl.LOG_NOTICE, chap_secrets + ' written') - -def accel_cmd(cmd=''): - if not cmd: - return None - try: - ret = subprocess.check_output(['/usr/bin/accel-cmd','-p','2004',cmd]).decode().strip() - return ret - except: - return 1 + cnt = 0 + s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + while True: + try: + s.connect(("127.0.0.1", 2004)) + break + except ConnectionRefusedError: + time.sleep(0.5) + cnt += 1 + if cnt == 100: + raise("failed to start l2tp server") + break + + +def _accel_cmd(command): + return run(f'/usr/bin/accel-cmd -p 2004 {command}') ### # inline helper functions end ### + def get_config(): - c = Config() - if not c.exists('vpn l2tp remote-access '): - return None - - c.set_level('vpn l2tp remote-access') - config_data = { - 'authentication' : { - 'mode' : 'local', - 'local-users' : { + c = Config() + if not c.exists('vpn l2tp remote-access '): + return None + + c.set_level('vpn l2tp remote-access') + config_data = { + 'authentication': { + 'mode': 'local', + 'local-users': { + }, + 'radiussrv': {}, + 'radiusopt': {}, + 'auth_proto': [], + 'mppe': 'prefer' }, - 'radiussrv' : {}, - 'radiusopt' : {}, - 'auth_proto' : [], - 'mppe' : 'prefer' - }, - 'outside_addr' : '', - 'gateway_address' : '10.255.255.0', - 'dns' : [], - 'dnsv6' : [], - 'wins' : [], - 'client_ip_pool' : None, - 'client_ip_subnets' : [], - 'client_ipv6_pool' : {}, - 'mtu' : '1436', - 'ip6_column' : '', - 'ip6_dp_column' : '', - 'ppp_options' : {}, - } - - ### general options ### - - if c.exists('dns-servers server-1'): - config_data['dns'].append( c.return_value('dns-servers server-1')) - if c.exists('dns-servers server-2'): - config_data['dns'].append( c.return_value('dns-servers server-2')) - if c.exists('dnsv6-servers'): - for dns6_server in c.return_values('dnsv6-servers'): - config_data['dnsv6'].append(dns6_server) - if c.exists('wins-servers server-1'): - config_data['wins'].append( c.return_value('wins-servers server-1')) - if c.exists('wins-servers server-2'): - config_data['wins'].append( c.return_value('wins-servers server-2')) - if c.exists('outside-address'): - config_data['outside_addr'] = c.return_value('outside-address') - - ### auth local - if c.exists('authentication mode local'): - if c.exists('authentication local-users username'): - for usr in c.list_nodes('authentication local-users username'): - config_data['authentication']['local-users'].update( - { - usr : { - 'passwd' : '', - 'state' : 'enabled', - 'ip' : '*', - 'upload' : None, - 'download' : None - } - } - ) - - if c.exists('authentication local-users username ' + usr + ' password'): - config_data['authentication']['local-users'][usr]['passwd'] = c.return_value('authentication local-users username ' + usr + ' password') - if c.exists('authentication local-users username ' + usr + ' disable'): - config_data['authentication']['local-users'][usr]['state'] = 'disable' - if c.exists('authentication local-users username ' + usr + ' static-ip'): - config_data['authentication']['local-users'][usr]['ip'] = c.return_value('authentication local-users username ' + usr + ' static-ip') - if c.exists('authentication local-users username ' + usr + ' rate-limit download'): - config_data['authentication']['local-users'][usr]['download'] = c.return_value('authentication local-users username ' + usr + ' rate-limit download') - if c.exists('authentication local-users username ' + usr + ' rate-limit upload'): - config_data['authentication']['local-users'][usr]['upload'] = c.return_value('authentication local-users username ' + usr + ' rate-limit upload') - - ### authentication mode radius servers and settings - - if c.exists('authentication mode radius'): - config_data['authentication']['mode'] = 'radius' - rsrvs = c.list_nodes('authentication radius server') - for rsrv in rsrvs: - if c.return_value('authentication radius server ' + rsrv + ' fail-time') == None: - ftime = '0' - else: - ftime = str(c.return_value('authentication radius server ' + rsrv + ' fail-time')) - if c.return_value('authentication radius-server ' + rsrv + ' req-limit') == None: - reql = '0' - else: - reql = str(c.return_value('authentication radius server ' + rsrv + ' req-limit')) - - config_data['authentication']['radiussrv'].update( - { - rsrv : { - 'secret' : c.return_value('authentication radius server ' + rsrv + ' key'), - 'fail-time' : ftime, - 'req-limit' : reql - } - } - ) - ### Source ip address feature - if c.exists('authentication radius source-address'): - config_data['authentication']['radius_source_address'] = c.return_value('authentication radius source-address') - - #### advanced radius-setting - if c.exists('authentication radius acct-timeout'): - config_data['authentication']['radiusopt']['acct-timeout'] = c.return_value('authentication radius acct-timeout') - if c.exists('authentication radius max-try'): - config_data['authentication']['radiusopt']['max-try'] = c.return_value('authentication radius max-try') - if c.exists('authentication radius timeout'): - config_data['authentication']['radiusopt']['timeout'] = c.return_value('authentication radius timeout') - if c.exists('authentication radius nas-identifier'): - config_data['authentication']['radiusopt']['nas-id'] = c.return_value('authentication radius nas-identifier') - if c.exists('authentication radius dae-server'): - # Set default dae-server port if not defined - if c.exists('authentication radius dae-server port'): - dae_server_port = c.return_value('authentication radius dae-server port') - else: - dae_server_port = "3799" - config_data['authentication']['radiusopt'].update( - { - 'dae-srv' : { - 'ip-addr' : c.return_value('authentication radius dae-server ip-address'), - 'port' : dae_server_port, - 'secret' : str(c.return_value('authentication radius dae-server secret')) - } - } - ) - #### filter-id is the internal accel default if attribute is empty - #### set here as default for visibility which may change in the future - if c.exists('authentication radius rate-limit enable'): - if not c.exists('authentication radius rate-limit attribute'): - config_data['authentication']['radiusopt']['shaper'] = { - 'attr' : 'Filter-Id' - } - else: - config_data['authentication']['radiusopt']['shaper'] = { - 'attr' : c.return_value('authentication radius rate-limit attribute') - } - if c.exists('authentication radius rate-limit vendor'): - config_data['authentication']['radiusopt']['shaper']['vendor'] = c.return_value('authentication radius rate-limit vendor') - - if c.exists('client-ip-pool'): - if c.exists('client-ip-pool start') and c.exists('client-ip-pool stop'): - config_data['client_ip_pool'] = c.return_value('client-ip-pool start') + '-' + re.search('[0-9]+$', c.return_value('client-ip-pool stop')).group(0) - - if c.exists('client-ip-pool subnet'): - config_data['client_ip_subnets'] = c.return_values('client-ip-pool subnet') - - if c.exists('client-ipv6-pool prefix'): - config_data['client_ipv6_pool']['prefix'] = c.return_values('client-ipv6-pool prefix') - config_data['ip6_column'] = 'ip6,' - if c.exists('client-ipv6-pool delegate-prefix'): - config_data['client_ipv6_pool']['delegate_prefix'] = c.return_values('client-ipv6-pool delegate-prefix') - config_data['ip6_dp_column'] = 'ip6-dp,' - - if c.exists('mtu'): - config_data['mtu'] = c.return_value('mtu') - - ### gateway address - if c.exists('gateway-address'): - config_data['gateway_address'] = c.return_value('gateway-address') - else: - ### calculate gw-ip-address - if c.exists('client-ip-pool start'): - ### use start ip as gw-ip-address - config_data['gateway_address'] = c.return_value('client-ip-pool start') - elif c.exists('client-ip-pool subnet'): - ### use first ip address from first defined pool - lst_ip = re.findall("\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}", c.return_values('client-ip-pool subnet')[0]) - config_data['gateway_address'] = lst_ip[0] - - if c.exists('authentication require'): - auth_mods = {'pap' : 'pap','chap' : 'auth_chap_md5', 'mschap' : 'auth_mschap_v1', 'mschap-v2' : 'auth_mschap_v2'} - for proto in c.return_values('authentication require'): - config_data['authentication']['auth_proto'].append(auth_mods[proto]) - else: - config_data['authentication']['auth_proto'] = ['auth_mschap_v2'] - - if c.exists('authentication mppe'): - config_data['authentication']['mppe'] = c.return_value('authentication mppe') - - if c.exists('idle'): - config_data['idle_timeout'] = c.return_value('idle') - - ### LNS secret - if c.exists('lns shared-secret'): - config_data['lns_shared_secret'] = c.return_value('lns shared-secret') - - if c.exists('ccp-disable'): - config_data['ccp_disable'] = True - - ### ppp_options - ppp_options = {} - if c.exists('ppp-options'): - if c.exists('ppp-options lcp-echo-failure'): - ppp_options['lcp-echo-failure'] = c.return_value('ppp-options lcp-echo-failure') - if c.exists('ppp-options lcp-echo-interval'): - ppp_options['lcp-echo-interval'] = c.return_value('ppp-options lcp-echo-interval') - - if len(ppp_options) !=0: - config_data['ppp_options'] = ppp_options - - return config_data + 'outside_addr': '', + 'gateway_address': '10.255.255.0', + 'dns': [], + 'dnsv6': [], + 'wins': [], + 'client_ip_pool': None, + 'client_ip_subnets': [], + 'client_ipv6_pool': {}, + 'mtu': '1436', + 'ip6_column': '', + 'ip6_dp_column': '', + 'ppp_options': {}, + } + + ### general options ### + + if c.exists('dns-servers server-1'): + config_data['dns'].append(c.return_value('dns-servers server-1')) + if c.exists('dns-servers server-2'): + config_data['dns'].append(c.return_value('dns-servers server-2')) + if c.exists('dnsv6-servers'): + for dns6_server in c.return_values('dnsv6-servers'): + config_data['dnsv6'].append(dns6_server) + if c.exists('wins-servers server-1'): + config_data['wins'].append(c.return_value('wins-servers server-1')) + if c.exists('wins-servers server-2'): + config_data['wins'].append(c.return_value('wins-servers server-2')) + if c.exists('outside-address'): + config_data['outside_addr'] = c.return_value('outside-address') + + # auth local + if c.exists('authentication mode local'): + if c.exists('authentication local-users username'): + for usr in c.list_nodes('authentication local-users username'): + config_data['authentication']['local-users'].update( + { + usr: { + 'passwd': '', + 'state': 'enabled', + 'ip': '*', + 'upload': None, + 'download': None + } + } + ) + + if c.exists('authentication local-users username ' + usr + ' password'): + config_data['authentication']['local-users'][usr]['passwd'] = c.return_value( + 'authentication local-users username ' + usr + ' password') + if c.exists('authentication local-users username ' + usr + ' disable'): + config_data['authentication']['local-users'][usr]['state'] = 'disable' + if c.exists('authentication local-users username ' + usr + ' static-ip'): + config_data['authentication']['local-users'][usr]['ip'] = c.return_value( + 'authentication local-users username ' + usr + ' static-ip') + if c.exists('authentication local-users username ' + usr + ' rate-limit download'): + config_data['authentication']['local-users'][usr]['download'] = c.return_value( + 'authentication local-users username ' + usr + ' rate-limit download') + if c.exists('authentication local-users username ' + usr + ' rate-limit upload'): + config_data['authentication']['local-users'][usr]['upload'] = c.return_value( + 'authentication local-users username ' + usr + ' rate-limit upload') + + # authentication mode radius servers and settings + + if c.exists('authentication mode radius'): + config_data['authentication']['mode'] = 'radius' + rsrvs = c.list_nodes('authentication radius server') + for rsrv in rsrvs: + if c.return_value('authentication radius server ' + rsrv + ' fail-time') == None: + ftime = '0' + else: + ftime = str(c.return_value( + 'authentication radius server ' + rsrv + ' fail-time')) + if c.return_value('authentication radius-server ' + rsrv + ' req-limit') == None: + reql = '0' + else: + reql = str(c.return_value( + 'authentication radius server ' + rsrv + ' req-limit')) + + config_data['authentication']['radiussrv'].update( + { + rsrv: { + 'secret': c.return_value('authentication radius server ' + rsrv + ' key'), + 'fail-time': ftime, + 'req-limit': reql + } + } + ) + # Source ip address feature + if c.exists('authentication radius source-address'): + config_data['authentication']['radius_source_address'] = c.return_value( + 'authentication radius source-address') + + # advanced radius-setting + if c.exists('authentication radius acct-timeout'): + config_data['authentication']['radiusopt']['acct-timeout'] = c.return_value( + 'authentication radius acct-timeout') + if c.exists('authentication radius max-try'): + config_data['authentication']['radiusopt']['max-try'] = c.return_value( + 'authentication radius max-try') + if c.exists('authentication radius timeout'): + config_data['authentication']['radiusopt']['timeout'] = c.return_value( + 'authentication radius timeout') + if c.exists('authentication radius nas-identifier'): + config_data['authentication']['radiusopt']['nas-id'] = c.return_value( + 'authentication radius nas-identifier') + if c.exists('authentication radius dae-server'): + # Set default dae-server port if not defined + if c.exists('authentication radius dae-server port'): + dae_server_port = c.return_value( + 'authentication radius dae-server port') + else: + dae_server_port = "3799" + config_data['authentication']['radiusopt'].update( + { + 'dae-srv': { + 'ip-addr': c.return_value('authentication radius dae-server ip-address'), + 'port': dae_server_port, + 'secret': str(c.return_value('authentication radius dae-server secret')) + } + } + ) + # filter-id is the internal accel default if attribute is empty + # set here as default for visibility which may change in the future + if c.exists('authentication radius rate-limit enable'): + if not c.exists('authentication radius rate-limit attribute'): + config_data['authentication']['radiusopt']['shaper'] = { + 'attr': 'Filter-Id' + } + else: + config_data['authentication']['radiusopt']['shaper'] = { + 'attr': c.return_value('authentication radius rate-limit attribute') + } + if c.exists('authentication radius rate-limit vendor'): + config_data['authentication']['radiusopt']['shaper']['vendor'] = c.return_value( + 'authentication radius rate-limit vendor') + + if c.exists('client-ip-pool'): + if c.exists('client-ip-pool start') and c.exists('client-ip-pool stop'): + config_data['client_ip_pool'] = c.return_value( + 'client-ip-pool start') + '-' + re.search('[0-9]+$', c.return_value('client-ip-pool stop')).group(0) + + if c.exists('client-ip-pool subnet'): + config_data['client_ip_subnets'] = c.return_values( + 'client-ip-pool subnet') + + if c.exists('client-ipv6-pool prefix'): + config_data['client_ipv6_pool']['prefix'] = c.return_values( + 'client-ipv6-pool prefix') + config_data['ip6_column'] = 'ip6,' + if c.exists('client-ipv6-pool delegate-prefix'): + config_data['client_ipv6_pool']['delegate_prefix'] = c.return_values( + 'client-ipv6-pool delegate-prefix') + config_data['ip6_dp_column'] = 'ip6-dp,' + + if c.exists('mtu'): + config_data['mtu'] = c.return_value('mtu') + + # gateway address + if c.exists('gateway-address'): + config_data['gateway_address'] = c.return_value('gateway-address') + else: + # calculate gw-ip-address + if c.exists('client-ip-pool start'): + # use start ip as gw-ip-address + config_data['gateway_address'] = c.return_value( + 'client-ip-pool start') + elif c.exists('client-ip-pool subnet'): + # use first ip address from first defined pool + lst_ip = re.findall("\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}", c.return_values( + 'client-ip-pool subnet')[0]) + config_data['gateway_address'] = lst_ip[0] + + if c.exists('authentication require'): + auth_mods = {'pap': 'pap', 'chap': 'auth_chap_md5', + 'mschap': 'auth_mschap_v1', 'mschap-v2': 'auth_mschap_v2'} + for proto in c.return_values('authentication require'): + config_data['authentication']['auth_proto'].append( + auth_mods[proto]) + else: + config_data['authentication']['auth_proto'] = ['auth_mschap_v2'] + + if c.exists('authentication mppe'): + config_data['authentication']['mppe'] = c.return_value( + 'authentication mppe') + + if c.exists('idle'): + config_data['idle_timeout'] = c.return_value('idle') + + # LNS secret + if c.exists('lns shared-secret'): + config_data['lns_shared_secret'] = c.return_value('lns shared-secret') + + if c.exists('ccp-disable'): + config_data['ccp_disable'] = True + + # ppp_options + ppp_options = {} + if c.exists('ppp-options'): + if c.exists('ppp-options lcp-echo-failure'): + ppp_options['lcp-echo-failure'] = c.return_value( + 'ppp-options lcp-echo-failure') + if c.exists('ppp-options lcp-echo-interval'): + ppp_options['lcp-echo-interval'] = c.return_value( + 'ppp-options lcp-echo-interval') + + if len(ppp_options) != 0: + config_data['ppp_options'] = ppp_options + + return config_data + def verify(c): - if c == None: - return None - - if c['authentication']['mode'] == 'local': - if not c['authentication']['local-users']: - raise ConfigError('l2tp-server authentication local-users required') - for usr in c['authentication']['local-users']: - if not c['authentication']['local-users'][usr]['passwd']: - raise ConfigError('user ' + usr + ' requires a password') - - if c['authentication']['mode'] == 'radius': - if len(c['authentication']['radiussrv']) == 0: - raise ConfigError('radius server required') - for rsrv in c['authentication']['radiussrv']: - if c['authentication']['radiussrv'][rsrv]['secret'] == None: - raise ConfigError('radius server ' + rsrv + ' needs a secret configured') - - ### check for the existence of a client ip pool - if not c['client_ip_pool'] and not c['client_ip_subnets']: - raise ConfigError("set vpn l2tp remote-access client-ip-pool requires subnet or start/stop IP pool") - - ## check ipv6 - if 'delegate_prefix' in c['client_ipv6_pool'] and not 'prefix' in c['client_ipv6_pool']: - raise ConfigError("\"set vpn l2tp remote-access client-ipv6-pool prefix\" required for delegate-prefix ") - - if len(c['dnsv6']) > 3: - raise ConfigError("Maximum allowed dnsv6-servers addresses is 3") + if c == None: + return None + + if c['authentication']['mode'] == 'local': + if not c['authentication']['local-users']: + raise ConfigError( + 'l2tp-server authentication local-users required') + for usr in c['authentication']['local-users']: + if not c['authentication']['local-users'][usr]['passwd']: + raise ConfigError('user ' + usr + ' requires a password') + + if c['authentication']['mode'] == 'radius': + if len(c['authentication']['radiussrv']) == 0: + raise ConfigError('radius server required') + for rsrv in c['authentication']['radiussrv']: + if c['authentication']['radiussrv'][rsrv]['secret'] == None: + raise ConfigError('radius server ' + rsrv + + ' needs a secret configured') + + # check for the existence of a client ip pool + if not c['client_ip_pool'] and not c['client_ip_subnets']: + raise ConfigError( + "set vpn l2tp remote-access client-ip-pool requires subnet or start/stop IP pool") + + # check ipv6 + if 'delegate_prefix' in c['client_ipv6_pool'] and not 'prefix' in c['client_ipv6_pool']: + raise ConfigError( + "\"set vpn l2tp remote-access client-ipv6-pool prefix\" required for delegate-prefix ") + + if len(c['dnsv6']) > 3: + raise ConfigError("Maximum allowed dnsv6-servers addresses is 3") + def generate(c): - if c == None: - return None - - ### accel-cmd reload doesn't work so any change results in a restart of the daemon - try: - if os.cpu_count() == 1: - c['thread_cnt'] = 1 - else: - c['thread_cnt'] = int(os.cpu_count()/2) - except KeyError: - if os.cpu_count() == 1: - c['thread_cnt'] = 1 - else: - c['thread_cnt'] = int(os.cpu_count()/2) + if c == None: + return None - tmpl = jinja2.Template(l2tp_config, trim_blocks=True) - config_text = tmpl.render(c) - open(l2tp_conf,'w').write(config_text) + # Prepare Jinja2 template loader from files + tmpl_path = os.path.join(vyos_data_dir['data'], 'templates', 'l2tp') + fs_loader = FileSystemLoader(tmpl_path) + env = Environment(loader=fs_loader, trim_blocks=True) - if c['authentication']['local-users']: - write_chap_secrets(c) + # accel-cmd reload doesn't work so any change results in a restart of the daemon + try: + if os.cpu_count() == 1: + c['thread_cnt'] = 1 + else: + c['thread_cnt'] = int(os.cpu_count()/2) + except KeyError: + if os.cpu_count() == 1: + c['thread_cnt'] = 1 + else: + c['thread_cnt'] = int(os.cpu_count()/2) + + tmpl = env.get_template('l2tp.config.tmpl') + config_text = tmpl.render(c) + open(l2tp_conf, 'w').write(config_text) + + if c['authentication']['local-users']: + tmpl = env.get_template('chap-secrets.tmpl') + chap_secrets_txt = tmpl.render(c) + old_umask = os.umask(0o077) + open(chap_secrets, 'w').write(chap_secrets_txt) + os.umask(old_umask) + + return c - return c def apply(c): - if c == None: - if os.path.exists(pidfile): - accel_cmd('shutdown hard') - if os.path.exists(pidfile): - os.remove(pidfile) - return None - - if not os.path.exists(pidfile): - ret = subprocess.call(['/usr/sbin/accel-pppd','-c',l2tp_conf,'-p',pidfile,'-d']) - chk_con() - if ret !=0 and os.path.exists(pidfile): - os.remove(pidfile) - raise ConfigError('accel-pppd failed to start') - else: - ### if gw ip changes, only restart doesn't work - accel_cmd('restart') - sl.syslog(sl.LOG_NOTICE, "reloading config via daemon restart") + if c == None: + if os.path.exists(pidfile): + _accel_cmd('shutdown hard') + if os.path.exists(pidfile): + os.remove(pidfile) + return None + + if not os.path.exists(pidfile): + ret = run(f'/usr/sbin/accel-pppd -c {l2tp_conf} -p {pidfile} -d') + chk_con() + if ret != 0 and os.path.exists(pidfile): + os.remove(pidfile) + raise ConfigError('accel-pppd failed to start') + else: + # if gw ip changes, only restart doesn't work + _accel_cmd('restart') + if __name__ == '__main__': - try: - c = get_config() - verify(c) - generate(c) - apply(c) - except ConfigError as e: - print(e) - sys.exit(1) + try: + c = get_config() + verify(c) + generate(c) + apply(c) + except ConfigError as e: + print(e) + sys.exit(1) diff --git a/src/conf_mode/accel_sstp.py b/src/conf_mode/accel_sstp.py deleted file mode 100755 index 1317a32db..000000000 --- a/src/conf_mode/accel_sstp.py +++ /dev/null @@ -1,469 +0,0 @@ -#!/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 sys -import os -import re -import subprocess -import jinja2 -import socket -import time -import syslog as sl - -from vyos.config import Config -from vyos import ConfigError - -pidfile = r'/var/run/accel_sstp.pid' -sstp_cnf_dir = r'/etc/accel-ppp/sstp' -chap_secrets = sstp_cnf_dir + '/chap-secrets' -sstp_conf = sstp_cnf_dir + '/sstp.config' -ssl_cert_dir = r'/config/user-data/sstp' - -### config path creation -if not os.path.exists(sstp_cnf_dir): - os.makedirs(sstp_cnf_dir) - sl.syslog(sl.LOG_NOTICE, sstp_cnf_dir + " created") - -if not os.path.exists(ssl_cert_dir): - os.makedirs(ssl_cert_dir) - sl.syslog(sl.LOG_NOTICE, ssl_cert_dir + " created") - -sstp_config = ''' -### generated by accel_sstp.py ### -[modules] -log_syslog -sstp -ippool -shaper -{% if authentication['mode'] == 'local' %} -chap-secrets -{% endif -%} -{% for proto in authentication['auth_proto'] %} -{{proto}} -{% endfor %} -{% if authentication['mode'] == 'radius' %} -radius -{% endif %} - -[core] -thread-count={{thread_cnt}} - -[common] -single-session=replace - -[log] -syslog=accel-sstp,daemon -copy=1 -level=5 - -[client-ip-range] -disable - -[sstp] -verbose=1 -accept=ssl -{% if certs %} -ssl-ca-file=/config/user-data/sstp/{{certs['ca']}} -ssl-pemfile=/config/user-data/sstp/{{certs['server-cert']}} -ssl-keyfile=/config/user-data/sstp/{{certs['server-key']}} -{% endif %} - -{%if ip_pool %} -[ip-pool] -gw-ip-address={{gw}} -{% for sn in ip_pool %} -{{sn}} -{% endfor %} -{% endif %} - -{% if dnsv4 %} -[dns] -{% if dnsv4['primary'] %} -dns1={{dnsv4['primary']}} -{% endif -%} -{% if dnsv4['secondary'] %} -dns2={{dnsv4['secondary']}} -{% endif -%} -{% endif %} - -{% if authentication['mode'] == 'local' %} -[chap-secrets] -chap-secrets=/etc/accel-ppp/sstp/chap-secrets -{% endif %} - -{%- if authentication['mode'] == 'radius' %} -[radius] -verbose=1 -{% for rsrv in authentication['radius-srv']: %} -server={{rsrv}},{{authentication['radius-srv'][rsrv]['secret']}},\ -req-limit={{authentication['radius-srv'][rsrv]['req-limit']}},\ -fail-time={{authentication['radius-srv'][rsrv]['fail-time']}} -{% endfor -%} -{% if authentication['radiusopt']['acct-timeout'] %} -acct-timeout={{authentication['radiusopt']['acct-timeout']}} -{% endif -%} -{% if authentication['radiusopt']['timeout'] %} -timeout={{authentication['radiusopt']['timeout']}} -{% endif -%} -{% if authentication['radiusopt']['max-try'] %} -max-try={{authentication['radiusopt']['max-try']}} -{% endif -%} -{% if authentication['radiusopt']['nas-id'] %} -nas-identifier={{authentication['radiusopt']['nas-id']}} -{% endif -%} -{% if authentication['radiusopt']['nas-ip'] %} -nas-ip-address={{authentication['radiusopt']['nas-ip']}} -{% endif -%} -{% if authentication['radiusopt']['dae-srv'] %} -dae-server={{authentication['radiusopt']['dae-srv']['ip-addr']}}:\ -{{authentication['radiusopt']['dae-srv']['port']}},\ -{{authentication['radiusopt']['dae-srv']['secret']}} -{% endif -%} -{% endif %} - -[ppp] -verbose=1 -check-ip=1 -{% if mtu %} -mtu={{mtu}} -{% endif -%} -{% if ppp['mppe'] %} -mppe={{ppp['mppe']}} -{% endif -%} -{% if ppp['lcp-echo-interval'] %} -lcp-echo-interval={{ppp['lcp-echo-interval']}} -{% endif -%} -{% if ppp['lcp-echo-failure'] %} -lcp-echo-failure={{ppp['lcp-echo-failure']}} -{% endif -%} -{% if ppp['lcp-echo-timeout'] %} -lcp-echo-timeout={{ppp['lcp-echo-timeout']}} -{% endif %} - -{% if authentication['radiusopt']['shaper'] %} -[shaper] -verbose=1 -attr={{authentication['radiusopt']['shaper']['attr']}} -{% if authentication['radiusopt']['shaper']['vendor'] %} -vendor={{authentication['radiusopt']['shaper']['vendor']}} -{% endif -%} -{% endif %} - -[cli] -tcp=127.0.0.1:2005 -''' - -### sstp chap secrets -chap_secrets_conf = ''' -# username server password acceptable local IP addresses shaper -{% for user in authentication['local-users'] %} -{% if authentication['local-users'][user]['state'] == 'enabled' %} -{% if (authentication['local-users'][user]['upload']) and (authentication['local-users'][user]['download']) %} -{{user}}\t*\t{{authentication['local-users'][user]['passwd']}}\t{{authentication['local-users'][user]['ip']}}\t\ -{{authentication['local-users'][user]['download']}}/{{authentication['local-users'][user]['upload']}} -{% else %} -{{user}}\t*\t{{authentication['local-users'][user]['passwd']}}\t{{authentication['local-users'][user]['ip']}} -{% endif %} -{% endif %} -{% endfor %} -''' -### -# inline helper functions -### -# depending on hw and threads, daemon needs a little to start -# if it takes longer than 100 * 0.5 secs, exception is being raised -# not sure if that's the best way to check it, but it worked so far quite well -### -def chk_con(): - cnt = 0 - s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - while True: - try: - s.connect(("127.0.0.1", 2005)) - s.close() - break - except ConnectionRefusedError: - time.sleep(0.5) - cnt +=1 - if cnt == 100: - raise("failed to start sstp server") - break - -### chap_secrets file if auth mode local -def write_chap_secrets(c): - tmpl = jinja2.Template(chap_secrets_conf, trim_blocks=True) - chap_secrets_txt = tmpl.render(c) - old_umask = os.umask(0o077) - open(chap_secrets,'w').write(chap_secrets_txt) - os.umask(old_umask) - sl.syslog(sl.LOG_NOTICE, chap_secrets + ' written') - -def accel_cmd(cmd=''): - if not cmd: - return None - try: - ret = subprocess.check_output(['/usr/bin/accel-cmd','-p','2005',cmd]).decode().strip() - return ret - except: - return 1 - -#### check ig local-ip is in client pool subnet - - -### -# inline helper functions end -### - -def get_config(): - c = Config() - if not c.exists('service sstp-server'): - return None - - c.set_level('service sstp-server') - - config_data = { - 'authentication' : { - 'local-users' : { - }, - 'mode' : 'local', - 'auth_proto' : [], - 'radius-srv' : {}, - 'radiusopt' : {}, - 'dae-srv' : {} - }, - 'certs' : { - 'ca' : None, - 'server-key' : None, - 'server-cert' : None - }, - 'ip_pool' : [], - 'gw' : None, - 'dnsv4' : {}, - 'mtu' : None, - 'ppp' : {}, - } - - ### local auth - if c.exists('authentication mode local'): - if c.exists('authentication local-users'): - for usr in c.list_nodes('authentication local-users username'): - config_data['authentication']['local-users'].update( - { - usr : { - 'passwd' : None, - 'state' : 'enabled', - 'ip' : '*', - 'upload' : None, - 'download' : None - } - } - ) - if c.exists('authentication local-users username ' + usr + ' password'): - config_data['authentication']['local-users'][usr]['passwd'] = c.return_value('authentication local-users username ' + usr + ' password') - if c.exists('authentication local-users username ' + usr + ' disable'): - config_data['authentication']['local-users'][usr]['state'] = 'disable' - if c.exists('authentication local-users username ' + usr + ' static-ip'): - config_data['authentication']['local-users'][usr]['ip'] = c.return_value('authentication local-users username ' + usr + ' static-ip') - if c.exists('authentication local-users username ' + usr + ' rate-limit download'): - config_data['authentication']['local-users'][usr]['download'] = c.return_value('authentication local-users username ' + usr + ' rate-limit download') - if c.exists('authentication local-users username ' + usr + ' rate-limit upload'): - config_data['authentication']['local-users'][usr]['upload'] = c.return_value('authentication local-users username ' + usr + ' rate-limit upload') - - if c.exists('authentication protocols'): - auth_mods = {'pap' : 'pap','chap' : 'auth_chap_md5', 'mschap' : 'auth_mschap_v1', 'mschap-v2' : 'auth_mschap_v2'} - for proto in c.return_values('authentication protocols'): - config_data['authentication']['auth_proto'].append(auth_mods[proto]) - else: - config_data['authentication']['auth_proto'] = ['auth_mschap_v2'] - - #### RADIUS auth and settings - if c.exists('authentication mode radius'): - config_data['authentication']['mode'] = c.return_value('authentication mode') - if c.exists('authentication radius-server'): - for rsrv in c.list_nodes('authentication radius-server'): - config_data['authentication']['radius-srv'][rsrv] = {} - if c.exists('authentication radius-server ' + rsrv + ' secret'): - config_data['authentication']['radius-srv'][rsrv]['secret'] = c.return_value('authentication radius-server ' + rsrv + ' secret') - else: - config_data['authentication']['radius-srv'][rsrv]['secret'] = None - if c.exists('authentication radius-server ' + rsrv + ' fail-time'): - config_data['authentication']['radius-srv'][rsrv]['fail-time'] = c.return_value('authentication radius-server ' + rsrv + ' fail-time') - else: - config_data['authentication']['radius-srv'][rsrv]['fail-time'] = 0 - if c.exists('authentication radius-server ' + rsrv + ' req-limit'): - config_data['authentication']['radius-srv'][rsrv]['req-limit'] = c.return_value('authentication radius-server ' + rsrv + ' req-limit') - else: - config_data['authentication']['radius-srv'][rsrv]['req-limit'] = 0 - - #### advanced radius-setting - if c.exists('authentication radius-settings'): - if c.exists('authentication radius-settings acct-timeout'): - config_data['authentication']['radiusopt']['acct-timeout'] = c.return_value('authentication radius-settings acct-timeout') - if c.exists('authentication radius-settings max-try'): - config_data['authentication']['radiusopt']['max-try'] = c.return_value('authentication radius-settings max-try') - if c.exists('authentication radius-settings timeout'): - config_data['authentication']['radiusopt']['timeout'] = c.return_value('authentication radius-settings timeout') - if c.exists('authentication radius-settings nas-identifier'): - config_data['authentication']['radiusopt']['nas-id'] = c.return_value('authentication radius-settings nas-identifier') - if c.exists('authentication radius-settings nas-ip-address'): - config_data['authentication']['radiusopt']['nas-ip'] = c.return_value('authentication radius-settings nas-ip-address') - if c.exists('authentication radius-settings dae-server'): - config_data['authentication']['radiusopt'].update( - { - 'dae-srv' : { - 'ip-addr' : c.return_value('authentication radius-settings dae-server ip-address'), - 'port' : c.return_value('authentication radius-settings dae-server port'), - 'secret' : str(c.return_value('authentication radius-settings dae-server secret')) - } - } - ) - if c.exists('authentication radius-settings rate-limit enable'): - if not c.exists('authentication radius-settings rate-limit attribute'): - config_data['authentication']['radiusopt']['shaper'] = { 'attr' : 'Filter-Id' } - else: - config_data['authentication']['radiusopt']['shaper'] = { - 'attr' : c.return_value('authentication radius-settings rate-limit attribute') - } - if c.exists('authentication radius-settings rate-limit vendor'): - config_data['authentication']['radiusopt']['shaper']['vendor'] = c.return_value('authentication radius-settings rate-limit vendor') - - if c.exists('sstp-settings ssl-certs ca'): - config_data['certs']['ca'] = c.return_value('sstp-settings ssl-certs ca') - if c.exists('sstp-settings ssl-certs server-cert'): - config_data['certs']['server-cert'] = c.return_value('sstp-settings ssl-certs server-cert') - if c.exists('sstp-settings ssl-certs server-key'): - config_data['certs']['server-key'] = c.return_value('sstp-settings ssl-certs server-key') - - if c.exists('network-settings client-ip-settings subnet'): - config_data['ip_pool'] = c.return_values('network-settings client-ip-settings subnet') - if c.exists('network-settings client-ip-settings gateway-address'): - config_data['gw'] = c.return_value('network-settings client-ip-settings gateway-address') - if c.exists('network-settings dns-server primary-dns'): - config_data['dnsv4']['primary'] = c.return_value('network-settings dns-server primary-dns') - if c.exists('network-settings dns-server secondary-dns'): - config_data['dnsv4']['secondary'] = c.return_value('network-settings dns-server secondary-dns') - if c.exists('network-settings mtu'): - config_data['mtu'] = c.return_value('network-settings mtu') - - #### ppp - if c.exists('ppp-settings mppe'): - config_data['ppp']['mppe'] = c.return_value('ppp-settings mppe') - if c.exists('ppp-settings lcp-echo-failure'): - config_data['ppp']['lcp-echo-failure'] = c.return_value('ppp-settings lcp-echo-failure') - if c.exists('ppp-settings lcp-echo-interval'): - config_data['ppp']['lcp-echo-interval'] = c.return_value('ppp-settings lcp-echo-interval') - if c.exists('ppp-settings lcp-echo-timeout'): - config_data['ppp']['lcp-echo-timeout'] = c.return_value('ppp-settings lcp-echo-timeout') - - return config_data - -def verify(c): - if c == None: - return None - ### vertify auth settings - if c['authentication']['mode'] == 'local': - if not c['authentication']['local-users']: - raise ConfigError('sstp-server authentication local-users required') - - for usr in c['authentication']['local-users']: - if not c['authentication']['local-users'][usr]['passwd']: - raise ConfigError('user ' + usr + ' requires a password') - ### if up/download is set, check that both have a value - if c['authentication']['local-users'][usr]['upload']: - if not c['authentication']['local-users'][usr]['download']: - raise ConfigError('user ' + usr + ' requires download speed value') - if c['authentication']['local-users'][usr]['download']: - if not c['authentication']['local-users'][usr]['upload']: - raise ConfigError('user ' + usr + ' requires upload speed value') - - if not c['certs']['ca'] or not c['certs']['server-key'] or not c['certs']['server-cert']: - raise ConfigError('service sstp-server sstp-settings ssl-certs needs the ssl certificates set up') - else: - ssl_path = ssl_cert_dir + '/' - if not os.path.exists(ssl_path + c['certs']['ca']): - raise ConfigError('CA {0} doesn\'t exist'.format(ssl_path + c['certs']['ca'])) - if not os.path.exists(ssl_path + c['certs']['server-cert']): - raise ConfigError('SSL Cert {0} doesn\'t exist'.format(ssl_path + c['certs']['server-cert'])) - if not os.path.exists(ssl_path + c['certs']['server-cert']): - raise ConfigError('SSL Key {0} doesn\'t exist'.format(ssl_path + c['certs']['server-key'])) - - if c['authentication']['mode'] == 'radius': - if len(c['authentication']['radius-srv']) == 0: - raise ConfigError('service sstp-server authentication radius-server needs a value') - for rsrv in c['authentication']['radius-srv']: - if c['authentication']['radius-srv'][rsrv]['secret'] == None: - raise ConfigError('service sstp-server authentication radius-server {0} secret requires a value'.format(rsrv)) - - if c['authentication']['mode'] == 'local': - if not c['ip_pool']: - print ("WARNING: service sstp-server network-settings client-ip-settings subnet requires a value") - if not c['gw']: - print ("WARNING: service sstp-server network-settings client-ip-settings gateway-address requires a value") - -def generate(c): - if c == None: - return None - - ### accel-cmd reload doesn't work so any change results in a restart of the daemon - try: - if os.cpu_count() == 1: - c['thread_cnt'] = 1 - else: - c['thread_cnt'] = int(os.cpu_count()/2) - except KeyError: - if os.cpu_count() == 1: - c['thread_cnt'] = 1 - else: - c['thread_cnt'] = int(os.cpu_count()/2) - - tmpl = jinja2.Template(sstp_config, trim_blocks=True) - config_text = tmpl.render(c) - open(sstp_conf,'w').write(config_text) - - if c['authentication']['local-users']: - write_chap_secrets(c) - - return c - -def apply(c): - if c == None: - if os.path.exists(pidfile): - accel_cmd('shutdown hard') - if os.path.exists(pidfile): - os.remove(pidfile) - return None - - if not os.path.exists(pidfile): - ret = subprocess.call(['/usr/sbin/accel-pppd','-c',sstp_conf,'-p',pidfile,'-d']) - chk_con() - if ret !=0 and os.path.exists(pidfile): - os.remove(pidfile) - raise ConfigError('accel-pppd failed to start') - else: - accel_cmd('restart') - sl.syslog(sl.LOG_NOTICE, "reloading config via daemon restart") - -if __name__ == '__main__': - try: - c = get_config() - verify(c) - generate(c) - apply(c) - except ConfigError as e: - print(e) - sys.exit(1) diff --git a/src/conf_mode/arp.py b/src/conf_mode/arp.py index aeca08432..fde7dc521 100755 --- a/src/conf_mode/arp.py +++ b/src/conf_mode/arp.py @@ -20,9 +20,9 @@ import sys import os import re import syslog as sl -import subprocess from vyos.config import Config +from vyos.util import call from vyos import ConfigError arp_cmd = '/usr/sbin/arp' @@ -82,11 +82,12 @@ def generate(c): def apply(c): for ip_addr in c['remove']: sl.syslog(sl.LOG_NOTICE, "arp -d " + ip_addr) - subprocess.call([arp_cmd + ' -d ' + ip_addr + ' >/dev/null 2>&1'], shell=True) + call(f'{arp_cmd} -d {ip_addr} >/dev/null 2>&1') for ip_addr in c['update']: sl.syslog(sl.LOG_NOTICE, "arp -s " + ip_addr + " " + c['update'][ip_addr]) - subprocess.call([arp_cmd + ' -s ' + ip_addr + ' ' + c['update'][ip_addr] ], shell=True) + updated = c['update'][ip_addr] + call(f'{arp_cmd} -s {ip_addr} {updated}') if __name__ == '__main__': diff --git a/src/conf_mode/bcast_relay.py b/src/conf_mode/bcast_relay.py index 8889e701c..8d4c4a89a 100755 --- a/src/conf_mode/bcast_relay.py +++ b/src/conf_mode/bcast_relay.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2017 VyOS maintainers and contributors +# Copyright (C) 2017-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 @@ -13,42 +13,35 @@ # # You should have received a copy of the GNU General Public License # along with this program. If not, see <http://www.gnu.org/licenses/>. -# -# -import sys import os import fnmatch -import jinja2 + +from sys import exit +from copy import deepcopy +from jinja2 import FileSystemLoader, Environment from vyos.config import Config +from vyos.defaults import directories as vyos_data_dir from vyos import ConfigError +from vyos.util import call config_file = r'/etc/default/udp-broadcast-relay' -config_tmpl = """ -### Autogenerated by bcast_relay.py ### - -# UDP broadcast relay configuration for instance {{ id }} -{%- if description %} -# Comment: {{ description }} -{% endif %} -DAEMON_ARGS="{% if address %}-s {{ address }} {% endif %}{{ id }} {{ port }} {{ interfaces | join(' ') }}" - -""" - default_config_data = { 'disabled': False, 'instances': [] } def get_config(): - relay = default_config_data + relay = deepcopy(default_config_data) conf = Config() - if not conf.exists('service broadcast-relay'): + base = ['service', 'broadcast-relay'] + + if not conf.exists(base): return None else: - conf.set_level('service broadcast-relay') + conf.set_level(base) # Service can be disabled by user if conf.exists('disable'): @@ -58,7 +51,7 @@ def get_config(): # Parse configuration of each individual instance if conf.exists('id'): for id in conf.list_nodes('id'): - conf.set_level('service broadcast-relay id {0}'.format(id)) + conf.set_level(base + ['id', id]) config = { 'id': id, 'disabled': False, @@ -69,24 +62,24 @@ def get_config(): } # Check if individual broadcast relay service is disabled - if conf.exists('disable'): + if conf.exists(['disable']): config['disabled'] = True # Source IP of forwarded packets, if empty original senders address is used - if conf.exists('address'): - config['address'] = conf.return_value('address') + if conf.exists(['address']): + config['address'] = conf.return_value(['address']) # A description for each individual broadcast relay service - if conf.exists('description'): - config['description'] = conf.return_value('description') + if conf.exists(['description']): + config['description'] = conf.return_value(['description']) # UDP port to listen on for broadcast frames - if conf.exists('port'): - config['port'] = conf.return_value('port') + if conf.exists(['port']): + config['port'] = conf.return_value(['port']) # Network interfaces to listen on for broadcast frames to be relayed - if conf.exists('interface'): - config['interfaces'] = conf.return_values('interface') + if conf.exists(['interface']): + config['interfaces'] = conf.return_values(['interface']) relay['instances'].append(config) @@ -119,6 +112,11 @@ def generate(relay): if relay is None: return None + # Prepare Jinja2 template loader from files + tmpl_path = os.path.join(vyos_data_dir['data'], 'templates', 'bcast-relay') + fs_loader = FileSystemLoader(tmpl_path) + env = Environment(loader=fs_loader) + config_dir = os.path.dirname(config_file) config_filename = os.path.basename(config_file) active_configs = [] @@ -148,7 +146,7 @@ def generate(relay): # configuration filename contains instance id file = config_file + str(r['id']) - tmpl = jinja2.Template(config_tmpl) + tmpl = env.get_template('udp-broadcast-relay.tmpl') config_text = tmpl.render(r) with open(file, 'w') as f: f.write(config_text) @@ -157,7 +155,7 @@ def generate(relay): def apply(relay): # first stop all running services - os.system('sudo systemctl stop udp-broadcast-relay@{1..99}') + call('sudo systemctl stop udp-broadcast-relay@{1..99}') if (relay is None) or relay['disabled']: return None @@ -167,7 +165,7 @@ def apply(relay): # Don't start individual instance when it's disabled if r['disabled']: continue - os.system('sudo systemctl start udp-broadcast-relay@{0}'.format(r['id'])) + call('sudo systemctl start udp-broadcast-relay@{0}'.format(r['id'])) return None @@ -179,4 +177,4 @@ if __name__ == '__main__': apply(c) except ConfigError as e: print(e) - sys.exit(1) + exit(1) diff --git a/src/conf_mode/dhcp_relay.py b/src/conf_mode/dhcp_relay.py index a1af2575f..c92d6a4e1 100755 --- a/src/conf_mode/dhcp_relay.py +++ b/src/conf_mode/dhcp_relay.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2018 VyOS maintainers and contributors +# Copyright (C) 2018-2020 VyOS maintainers and contributors # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 or later as @@ -13,39 +13,19 @@ # # You should have received a copy of the GNU General Public License # along with this program. If not, see <http://www.gnu.org/licenses/>. -# -# -import sys import os -import jinja2 + +from jinja2 import FileSystemLoader, Environment +from sys import exit from vyos.config import Config +from vyos.defaults import directories as vyos_data_dir from vyos import ConfigError +from vyos.util import call config_file = r'/etc/default/isc-dhcp-relay' -# Please be careful if you edit the template. -config_tmpl = """ -### Autogenerated by dhcp_relay.py ### - -# Defaults for isc-dhcp-relay initscript -# sourced by /etc/init.d/isc-dhcp-relay - -# -# This is a POSIX shell fragment -# - -# What servers should the DHCP relay forward requests to? -SERVERS="{{ server | join(' ') }}" - -# On what interfaces should the DHCP relay (dhrelay) serve DHCP requests? -INTERFACES="{{ interface | join(' ') }}" - -# Additional options that are passed to the DHCP relay daemon? -OPTIONS="-4 {{ options | join(' ') }}" -""" - default_config_data = { 'interface': [], 'server': [], @@ -57,23 +37,23 @@ default_config_data = { def get_config(): relay = default_config_data conf = Config() - if not conf.exists('service dhcp-relay'): + if not conf.exists(['service', 'dhcp-relay']): return None else: - conf.set_level('service dhcp-relay') + conf.set_level(['service', 'dhcp-relay']) # Network interfaces to listen on - if conf.exists('interface'): - relay['interface'] = conf.return_values('interface') + if conf.exists(['interface']): + relay['interface'] = conf.return_values(['interface']) # Servers equal to the address of the DHCP server(s) - if conf.exists('server'): - relay['server'] = conf.return_values('server') + if conf.exists(['server']): + relay['server'] = conf.return_values(['server']) - conf.set_level('service dhcp-relay relay-options') + conf.set_level(['service', 'dhcp-relay', 'relay-options']) - if conf.exists('hop-count'): - count = '-c ' + conf.return_value('hop-count') + if conf.exists(['hop-count']): + count = '-c ' + conf.return_value(['hop-count']) relay['options'].append(count) # Specify the maximum packet size to send to a DHCPv4/BOOTP server. @@ -81,8 +61,8 @@ def get_config(): # options while still fitting into the Ethernet MTU size. # # Available in DHCPv4 mode only: - if conf.exists('max-size'): - size = '-A ' + conf.return_value('max-size') + if conf.exists(['max-size']): + size = '-A ' + conf.return_value(['max-size']) relay['options'].append(size) # Control the handling of incoming DHCPv4 packets which already contain @@ -94,8 +74,8 @@ def get_config(): # field; it may forward the packet unchanged; or, it may discard it. # # Available in DHCPv4 mode only: - if conf.exists('relay-agents-packets'): - pkt = '-a -m ' + conf.return_value('relay-agents-packets') + if conf.exists(['relay-agents-packets']): + pkt = '-a -m ' + conf.return_value(['relay-agents-packets']) relay['options'].append(pkt) return relay @@ -119,7 +99,12 @@ def generate(relay): if relay is None: return None - tmpl = jinja2.Template(config_tmpl) + # Prepare Jinja2 template loader from files + tmpl_path = os.path.join(vyos_data_dir['data'], 'templates', 'dhcp-relay') + fs_loader = FileSystemLoader(tmpl_path) + env = Environment(loader=fs_loader) + + tmpl = env.get_template('config.tmpl') config_text = tmpl.render(relay) with open(config_file, 'w') as f: f.write(config_text) @@ -128,10 +113,10 @@ def generate(relay): def apply(relay): if relay is not None: - os.system('sudo systemctl restart isc-dhcp-relay.service') + call('sudo systemctl restart isc-dhcp-relay.service') else: # DHCP relay support is removed in the commit - os.system('sudo systemctl stop isc-dhcp-relay.service') + call('sudo systemctl stop isc-dhcp-relay.service') os.unlink(config_file) return None @@ -144,4 +129,4 @@ if __name__ == '__main__': apply(c) except ConfigError as e: print(e) - sys.exit(1) + exit(1) diff --git a/src/conf_mode/dhcp_server.py b/src/conf_mode/dhcp_server.py index bf86e484b..553247b88 100755 --- a/src/conf_mode/dhcp_server.py +++ b/src/conf_mode/dhcp_server.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2018-2019 VyOS maintainers and contributors +# Copyright (C) 2018-2020 VyOS maintainers and contributors # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 or later as @@ -14,232 +14,26 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see <http://www.gnu.org/licenses/>. -import sys import os -import jinja2 -import socket -import struct - -import vyos.validate from ipaddress import ip_address, ip_network +from jinja2 import FileSystemLoader, Environment +from socket import inet_ntoa +from struct import pack +from sys import exit + from vyos.config import Config +from vyos.defaults import directories as vyos_data_dir +from vyos.validate import is_subnet_connected from vyos import ConfigError +from vyos.util import call + config_file = r'/etc/dhcp/dhcpd.conf' lease_file = r'/config/dhcpd.leases' pid_file = r'/var/run/dhcpd.pid' daemon_config_file = r'/etc/default/isc-dhcpv4-server' -# Please be careful if you edit the template. -config_tmpl = """ -### Autogenerated by dhcp_server.py ### - -# For options please consult the following website: -# https://www.isc.org/wp-content/uploads/2017/08/dhcp43options.html -# -# log-facility local7; - -{% if hostfile_update %} -on release { - set ClientName = pick-first-value(host-decl-name, option fqdn.hostname, option host-name); - set ClientIp = binary-to-ascii(10, 8, ".",leased-address); - set ClientMac = binary-to-ascii(16, 8, ":",substring(hardware, 1, 6)); - set ClientDomain = pick-first-value(config-option domain-name, "..YYZ!"); - execute("/usr/libexec/vyos/system/on-dhcp-event.sh", "release", ClientName, ClientIp, ClientMac, ClientDomain); -} - -on expiry { - set ClientName = pick-first-value(host-decl-name, option fqdn.hostname, option host-name); - set ClientIp = binary-to-ascii(10, 8, ".",leased-address); - set ClientMac = binary-to-ascii(16, 8, ":",substring(hardware, 1, 6)); - set ClientDomain = pick-first-value(config-option domain-name, "..YYZ!"); - execute("/usr/libexec/vyos/system/on-dhcp-event.sh", "release", ClientName, ClientIp, ClientMac, ClientDomain); -} -{% endif %} -{%- if host_decl_name %} -use-host-decl-names on; -{%- endif %} -ddns-update-style {% if ddns_enable -%} interim {%- else -%} none {%- endif %}; -{% if static_route -%} -option rfc3442-static-route code 121 = array of integer 8; -option windows-static-route code 249 = array of integer 8; -{%- endif %} -{% if wpad -%} -option wpad-url code 252 = text; -{% endif %} - -{%- if global_parameters %} -# The following {{ global_parameters | length }} line(s) were added as global-parameters in the CLI and have not been validated -{%- for param in global_parameters %} -{{ param }} -{%- endfor -%} -{%- endif %} - -# Failover configuration -{% for network in shared_network %} -{%- if not network.disabled -%} -{%- for subnet in network.subnet %} -{%- if subnet.failover_name -%} -failover peer "{{ subnet.failover_name }}" { -{%- if subnet.failover_status == 'primary' %} - primary; - mclt 1800; - split 128; -{%- elif subnet.failover_status == 'secondary' %} - secondary; -{%- endif %} - address {{ subnet.failover_local_addr }}; - port 520; - peer address {{ subnet.failover_peer_addr }}; - peer port 520; - max-response-delay 30; - max-unacked-updates 10; - load balance max seconds 3; -} -{% endif -%} -{% endfor -%} -{% endif -%} -{% endfor %} - -# Shared network configration(s) -{% for network in shared_network %} -{%- if not network.disabled -%} -shared-network {{ network.name }} { - {%- if network.authoritative %} - authoritative; - {%- endif %} - {%- if network.network_parameters %} - # The following {{ network.network_parameters | length }} line(s) were added as shared-network-parameters in the CLI and have not been validated - {%- for param in network.network_parameters %} - {{ param }} - {%- endfor %} - {%- endif %} - {%- for subnet in network.subnet %} - subnet {{ subnet.address }} netmask {{ subnet.netmask }} { - {%- if subnet.dns_server %} - option domain-name-servers {{ subnet.dns_server | join(', ') }}; - {%- endif %} - {%- if subnet.domain_search %} - option domain-search {{ subnet.domain_search | join(', ') }}; - {%- endif %} - {%- if subnet.ntp_server %} - option ntp-servers {{ subnet.ntp_server | join(', ') }}; - {%- endif %} - {%- if subnet.pop_server %} - option pop-server {{ subnet.pop_server | join(', ') }}; - {%- endif %} - {%- if subnet.smtp_server %} - option smtp-server {{ subnet.smtp_server | join(', ') }}; - {%- endif %} - {%- if subnet.time_server %} - option time-servers {{ subnet.time_server | join(', ') }}; - {%- endif %} - {%- if subnet.wins_server %} - option netbios-name-servers {{ subnet.wins_server | join(', ') }}; - {%- endif %} - {%- if subnet.static_route %} - option rfc3442-static-route {{ subnet.static_route }}; - option windows-static-route {{ subnet.static_route }}; - {%- endif %} - {%- if subnet.ip_forwarding %} - option ip-forwarding true; - {%- endif -%} - {%- if subnet.default_router %} - option routers {{ subnet.default_router }}; - {%- endif -%} - {%- if subnet.server_identifier %} - option dhcp-server-identifier {{ subnet.server_identifier }}; - {%- endif -%} - {%- if subnet.domain_name %} - option domain-name "{{ subnet.domain_name }}"; - {%- endif -%} - {%- if subnet.subnet_parameters %} - # The following {{ subnet.subnet_parameters | length }} line(s) were added as subnet-parameters in the CLI and have not been validated - {%- for param in subnet.subnet_parameters %} - {{ param }} - {%- endfor -%} - {%- endif %} - {%- if subnet.tftp_server %} - option tftp-server-name "{{ subnet.tftp_server }}"; - {%- endif -%} - {%- if subnet.bootfile_name %} - option bootfile-name "{{ subnet.bootfile_name }}"; - filename "{{ subnet.bootfile_name }}"; - {%- endif -%} - {%- if subnet.bootfile_server %} - next-server {{ subnet.bootfile_server }}; - {%- endif -%} - {%- if subnet.time_offset %} - option time-offset {{ subnet.time_offset }}; - {%- endif -%} - {%- if subnet.wpad_url %} - option wpad-url "{{ subnet.wpad_url }}"; - {%- endif -%} - {%- if subnet.client_prefix_length %} - option subnet-mask {{ subnet.client_prefix_length }}; - {%- endif -%} - {% if subnet.lease %} - default-lease-time {{ subnet.lease }}; - max-lease-time {{ subnet.lease }}; - {%- endif -%} - {%- for host in subnet.static_mapping %} - {% if not host.disabled -%} - host {% if host_decl_name -%} {{ host.name }} {%- else -%} {{ network.name }}_{{ host.name }} {%- endif %} { - {%- if host.ip_address %} - fixed-address {{ host.ip_address }}; - {%- endif %} - hardware ethernet {{ host.mac_address }}; - {%- if host.static_parameters %} - # The following {{ host.static_parameters | length }} line(s) were added as static-mapping-parameters in the CLI and have not been validated - {%- for param in host.static_parameters %} - {{ param }} - {%- endfor -%} - {%- endif %} - } - {%- endif %} - {%- endfor %} - {%- if subnet.failover_name %} - pool { - failover peer "{{ subnet.failover_name }}"; - deny dynamic bootp clients; - {%- for range in subnet.range %} - range {{ range.start }} {{ range.stop }}; - {%- endfor %} - } - {%- else %} - {%- for range in subnet.range %} - range {{ range.start }} {{ range.stop }}; - {%- endfor %} - {%- endif %} - } - {%- endfor %} - on commit { - set shared-networkname = "{{ network.name }}"; - {% if hostfile_update -%} - set ClientName = pick-first-value(host-decl-name, option fqdn.hostname, option host-name); - set ClientIp = binary-to-ascii(10, 8, ".", leased-address); - set ClientMac = binary-to-ascii(16, 8, ":", substring(hardware, 1, 6)); - set ClientDomain = pick-first-value(config-option domain-name, "..YYZ!"); - execute("/usr/libexec/vyos/system/on-dhcp-event.sh", "commit", ClientName, ClientIp, ClientMac, ClientDomain); - {%- endif %} - } -} -{%- endif %} -{% endfor %} -""" - -daemon_tmpl = """ -### Autogenerated by dhcp_server.py ### - -# sourced by /etc/init.d/isc-dhcpv4-server - -DHCPD_CONF={{ config_file }} -DHCPD_PID={{ pid_file }} -OPTIONS="-4 -lf {{ lease_file }}" -INTERFACES="" -""" - default_config_data = { 'lease_file': lease_file, 'disabled': False, @@ -315,6 +109,26 @@ def dhcp_slice_range(exclude_list, range_list): return output +def dhcp_static_route(static_subnet, static_router): + # https://ercpe.de/blog/pushing-static-routes-with-isc-dhcp-server + # Option format is: + # <netmask>, <network-byte1>, <network-byte2>, <network-byte3>, <router-byte1>, <router-byte2>, <router-byte3> + # where bytes with the value 0 are omitted. + net = ip_network(static_subnet) + # add netmask + string = str(net.prefixlen) + ',' + # add network bytes + if net.prefixlen: + width = net.prefixlen // 8 + if net.prefixlen % 8: + width += 1 + string += ','.join(map(str,tuple(net.network_address.packed)[:width])) + ',' + + # add router bytes + string += ','.join(static_router.split('.')) + + return string + def get_config(): dhcp = default_config_data conf = Config() @@ -395,6 +209,7 @@ def get_config(): 'bootfile_server': '', 'client_prefix_length': '', 'default_router': '', + 'rfc3442_default_router': '', 'dns_server': [], 'domain_name': '', 'domain_search': [], @@ -438,11 +253,12 @@ def get_config(): if conf.exists('client-prefix-length'): # snippet borrowed from https://stackoverflow.com/questions/33750233/convert-cidr-to-subnet-mask-in-python host_bits = 32 - int(conf.return_value('client-prefix-length')) - subnet['client_prefix_length'] = socket.inet_ntoa(struct.pack('!I', (1 << 32) - (1 << host_bits))) + subnet['client_prefix_length'] = inet_ntoa(pack('!I', (1 << 32) - (1 << host_bits))) # Default router IP address on the client's subnet if conf.exists('default-router'): subnet['default_router'] = conf.return_value('default-router') + subnet['rfc3442_default_router'] = dhcp_static_route("0.0.0.0/0", subnet['default_router']) # Specifies a list of Domain Name System (STD 13, RFC 1035) name servers available to # the client. Servers should be listed in order of preference. @@ -586,27 +402,7 @@ def get_config(): subnet['static_router'] = conf.return_value('static-route router') if subnet['static_router'] and subnet['static_subnet']: - # https://ercpe.de/blog/pushing-static-routes-with-isc-dhcp-server - # Option format is: - # <netmask>, <network-byte1>, <network-byte2>, <network-byte3>, <router-byte1>, <router-byte2>, <router-byte3> - # where bytes with the value 0 are omitted. - net = ip_network(subnet['static_subnet']) - # add netmask - string = str(net.prefixlen) + ',' - # add network bytes - bytes = str(net.network_address).split('.') - for b in bytes: - if b != '0': - string += b + ',' - - # add router bytes - bytes = subnet['static_router'].split('.') - for b in bytes: - string += b - if b is not bytes[-1]: - string += ',' - - subnet['static_route'] = string + subnet['static_route'] = dhcp_static_route(subnet['static_subnet'], subnet['static_router']) # HACKS AND TRICKS # @@ -776,7 +572,7 @@ def verify(dhcp): # There must be one subnet connected to a listen interface. # This only counts if the network itself is not disabled! if not network['disabled']: - if vyos.validate.is_subnet_connected(subnet['network'], primary=True): + if is_subnet_connected(subnet['network'], primary=True): listen_ok = True # Subnets must be non overlapping @@ -808,9 +604,13 @@ def generate(dhcp): print('Warning: DHCP server will be deactivated because it is disabled') return None - tmpl = jinja2.Template(config_tmpl) - config_text = tmpl.render(dhcp) + # Prepare Jinja2 template loader from files + tmpl_path = os.path.join(vyos_data_dir['data'], 'templates', 'dhcp-server') + fs_loader = FileSystemLoader(tmpl_path) + env = Environment(loader=fs_loader) + tmpl = env.get_template('dhcpd.conf.tmpl') + config_text = tmpl.render(dhcp) # Please see: https://phabricator.vyos.net/T1129 for quoting of the raw parameters # we can pass to ISC DHCPd config_text = config_text.replace(""",'"') @@ -818,7 +618,7 @@ def generate(dhcp): with open(config_file, 'w') as f: f.write(config_text) - tmpl = jinja2.Template(daemon_tmpl) + tmpl = env.get_template('daemon.tmpl') config_text = tmpl.render(dhcp) with open(daemon_config_file, 'w') as f: f.write(config_text) @@ -828,7 +628,7 @@ def generate(dhcp): def apply(dhcp): if (dhcp is None) or dhcp['disabled']: # DHCP server is removed in the commit - os.system('sudo systemctl stop isc-dhcpv4-server.service') + call('sudo systemctl stop isc-dhcpv4-server.service') if os.path.exists(config_file): os.unlink(config_file) if os.path.exists(daemon_config_file): @@ -838,7 +638,7 @@ def apply(dhcp): if not os.path.exists(lease_file): os.mknod(lease_file) - os.system('sudo systemctl restart isc-dhcpv4-server.service') + call('sudo systemctl restart isc-dhcpv4-server.service') return None @@ -850,4 +650,4 @@ if __name__ == '__main__': apply(c) except ConfigError as e: print(e) - sys.exit(1) + exit(1) diff --git a/src/conf_mode/dhcpv6_relay.py b/src/conf_mode/dhcpv6_relay.py index ccabc901d..9355d9794 100755 --- a/src/conf_mode/dhcpv6_relay.py +++ b/src/conf_mode/dhcpv6_relay.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2018 VyOS maintainers and contributors +# Copyright (C) 2018-2020 VyOS maintainers and contributors # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 or later as @@ -13,26 +13,20 @@ # # You should have received a copy of the GNU General Public License # along with this program. If not, see <http://www.gnu.org/licenses/>. -# -# -import sys import os -import jinja2 + +from sys import exit +from copy import deepcopy +from jinja2 import FileSystemLoader, Environment from vyos.config import Config +from vyos.defaults import directories as vyos_data_dir from vyos import ConfigError +from vyos.util import call -config_file = r'/etc/default/isc-dhcpv6-relay' - -# Please be careful if you edit the template. -config_tmpl = """ -### Autogenerated by dhcpv6_relay.py ### - -# Defaults for isc-dhcpv6-relay initscript sourced by /etc/init.d/isc-dhcpv6-relay -OPTIONS="-6 -l {{ listen_addr | join(' -l ') }} -u {{ upstream_addr | join(' -u ') }} {{ options | join(' ') }}" -""" +config_file = r'/etc/default/isc-dhcpv6-relay' default_config_data = { 'listen_addr': [], @@ -41,7 +35,7 @@ default_config_data = { } def get_config(): - relay = default_config_data + relay = deepcopy(default_config_data) conf = Config() if not conf.exists('service dhcpv6-relay'): return None @@ -92,7 +86,12 @@ def generate(relay): if relay is None: return None - tmpl = jinja2.Template(config_tmpl) + # Prepare Jinja2 template loader from files + tmpl_path = os.path.join(vyos_data_dir['data'], 'templates', 'dhcpv6-relay') + fs_loader = FileSystemLoader(tmpl_path) + env = Environment(loader=fs_loader) + + tmpl = env.get_template('config.tmpl') config_text = tmpl.render(relay) with open(config_file, 'w') as f: f.write(config_text) @@ -101,10 +100,10 @@ def generate(relay): def apply(relay): if relay is not None: - os.system('sudo systemctl restart isc-dhcpv6-relay.service') + call('sudo systemctl restart isc-dhcpv6-relay.service') else: # DHCPv6 relay support is removed in the commit - os.system('sudo systemctl stop isc-dhcpv6-relay.service') + call('sudo systemctl stop isc-dhcpv6-relay.service') os.unlink(config_file) return None @@ -117,4 +116,4 @@ if __name__ == '__main__': apply(c) except ConfigError as e: print(e) - sys.exit(1) + exit(1) diff --git a/src/conf_mode/dhcpv6_server.py b/src/conf_mode/dhcpv6_server.py index 44a927789..950ca1ce2 100755 --- a/src/conf_mode/dhcpv6_server.py +++ b/src/conf_mode/dhcpv6_server.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2018-2019 VyOS maintainers and contributors +# Copyright (C) 2018-2020 VyOS maintainers and contributors # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 or later as @@ -13,119 +13,26 @@ # # You should have received a copy of the GNU General Public License # along with this program. If not, see <http://www.gnu.org/licenses/>. -# -# -import sys import os import ipaddress -import jinja2 - -import vyos.validate +from sys import exit +from copy import deepcopy +from jinja2 import FileSystemLoader, Environment from vyos.config import Config +from vyos.defaults import directories as vyos_data_dir +from vyos.validate import is_subnet_connected from vyos import ConfigError +from vyos.util import call + config_file = r'/etc/dhcp/dhcpdv6.conf' lease_file = r'/config/dhcpdv6.leases' pid_file = r'/var/run/dhcpdv6.pid' daemon_config_file = r'/etc/default/isc-dhcpv6-server' -# Please be careful if you edit the template. -config_tmpl = """ -### Autogenerated by dhcpv6_server.py ### - -# For options please consult the following website: -# https://www.isc.org/wp-content/uploads/2017/08/dhcp43options.html - -log-facility local7; -{%- if preference %} -option dhcp6.preference {{ preference }}; -{%- endif %} - -# Shared network configration(s) -{% for network in shared_network %} -{%- if not network.disabled -%} -shared-network {{ network.name }} { - {%- for subnet in network.subnet %} - subnet6 {{ subnet.network }} { - {%- for range in subnet.range6_prefix %} - range6 {{ range.prefix }}{{ " temporary" if range.temporary }}; - {%- endfor %} - {%- for range in subnet.range6 %} - range6 {{ range.start }} {{ range.stop }}; - {%- endfor %} - {%- if subnet.domain_search %} - option dhcp6.domain-search {{ subnet.domain_search | join(', ') }}; - {%- endif %} - {%- if subnet.lease_def %} - default-lease-time {{ subnet.lease_def }}; - {%- endif %} - {%- if subnet.lease_max %} - max-lease-time {{ subnet.lease_max }}; - {%- endif %} - {%- if subnet.lease_min %} - min-lease-time {{ subnet.lease_min }}; - {%- endif %} - {%- if subnet.dns_server %} - option dhcp6.name-servers {{ subnet.dns_server | join(', ') }}; - {%- endif %} - {%- if subnet.nis_domain %} - option dhcp6.nis-domain-name "{{ subnet.nis_domain }}"; - {%- endif %} - {%- if subnet.nis_server %} - option dhcp6.nis-servers {{ subnet.nis_server | join(', ') }}; - {%- endif %} - {%- if subnet.nisp_domain %} - option dhcp6.nisp-domain-name "{{ subnet.nisp_domain }}"; - {%- endif %} - {%- if subnet.nisp_server %} - option dhcp6.nisp-servers {{ subnet.nisp_server | join(', ') }}; - {%- endif %} - {%- if subnet.sip_address %} - option dhcp6.sip-servers-addresses {{ subnet.sip_address | join(', ') }}; - {%- endif %} - {%- if subnet.sip_hostname %} - option dhcp6.sip-servers-names {{ subnet.sip_hostname | join(', ') }}; - {%- endif %} - {%- if subnet.sntp_server %} - option dhcp6.sntp-servers {{ subnet.sntp_server | join(', ') }}; - {%- endif %} - {%- for host in subnet.static_mapping %} - {% if not host.disabled -%} - host {{ network.name }}_{{ host.name }} { - {%- if host.client_identifier %} - host-identifier option dhcp6.client-id {{ host.client_identifier }}; - {%- endif %} - {%- if host.ipv6_address %} - fixed-address6 {{ host.ipv6_address }}; - {%- endif %} - } - {%- endif %} - {%- endfor %} - } - {%- endfor %} - on commit { - set shared-networkname = "{{ network.name }}"; - } -} -{%- endif %} -{% endfor %} - -""" - -daemon_tmpl = """ -### Autogenerated by dhcpv6_server.py ### - -# sourced by /etc/init.d/isc-dhcpv6-server - -DHCPD_CONF={{ config_file }} -DHCPD_PID={{ pid_file }} -OPTIONS="-6 -lf {{ lease_file }}" -INTERFACES="" -""" - default_config_data = { 'lease_file': lease_file, 'preference': '', @@ -134,7 +41,7 @@ default_config_data = { } def get_config(): - dhcpv6 = default_config_data + dhcpv6 = deepcopy(default_config_data) conf = Config() if not conf.exists('service dhcpv6-server'): return None @@ -409,7 +316,7 @@ def verify(dhcpv6): # There must be one subnet connected to a listen interface if network is not disabled. if not network['disabled']: - if vyos.validate.is_subnet_connected(subnet['network']): + if is_subnet_connected(subnet['network']): listen_ok = True # DHCPv6 subnet must not overlap. ISC DHCP also complains about overlapping @@ -437,12 +344,17 @@ def generate(dhcpv6): print('Warning: DHCPv6 server will be deactivated because it is disabled') return None - tmpl = jinja2.Template(config_tmpl) + # Prepare Jinja2 template loader from files + tmpl_path = os.path.join(vyos_data_dir['data'], 'templates', 'dhcpv6-server') + fs_loader = FileSystemLoader(tmpl_path) + env = Environment(loader=fs_loader) + + tmpl = env.get_template('dhcpdv6.conf.tmpl') config_text = tmpl.render(dhcpv6) with open(config_file, 'w') as f: f.write(config_text) - tmpl = jinja2.Template(daemon_tmpl) + tmpl = env.get_template('daemon.tmpl') config_text = tmpl.render(dhcpv6) with open(daemon_config_file, 'w') as f: f.write(config_text) @@ -452,7 +364,7 @@ def generate(dhcpv6): def apply(dhcpv6): if (dhcpv6 is None) or dhcpv6['disabled']: # DHCP server is removed in the commit - os.system('sudo systemctl stop isc-dhcpv6-server.service') + call('sudo systemctl stop isc-dhcpv6-server.service') if os.path.exists(config_file): os.unlink(config_file) if os.path.exists(daemon_config_file): @@ -462,7 +374,7 @@ def apply(dhcpv6): if not os.path.exists(lease_file): os.mknod(lease_file) - os.system('sudo systemctl restart isc-dhcpv6-server.service') + call('sudo systemctl restart isc-dhcpv6-server.service') return None @@ -474,4 +386,4 @@ if __name__ == '__main__': apply(c) except ConfigError as e: print(e) - sys.exit(1) + exit(1) diff --git a/src/conf_mode/dns_forwarding.py b/src/conf_mode/dns_forwarding.py index 38f3cb4de..4071c05c9 100755 --- a/src/conf_mode/dns_forwarding.py +++ b/src/conf_mode/dns_forwarding.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2018 VyOS maintainers and contributors +# Copyright (C) 2018-2020 VyOS maintainers and contributors # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 or later as @@ -13,22 +13,20 @@ # # You should have received a copy of the GNU General Public License # along with this program. If not, see <http://www.gnu.org/licenses/>. -# -# -import sys import os - import argparse -import jinja2 -import netifaces -import vyos.util -import vyos.hostsd_client +from sys import exit +from copy import deepcopy +from jinja2 import FileSystemLoader, Environment from vyos.config import Config +from vyos.defaults import directories as vyos_data_dir +from vyos.hostsd_client import Client as hostsd_client +from vyos.util import wait_for_commit_lock from vyos import ConfigError - +from vyos.util import call parser = argparse.ArgumentParser() parser.add_argument("--dhclient", action="store_true", @@ -36,53 +34,6 @@ parser.add_argument("--dhclient", action="store_true", config_file = r'/etc/powerdns/recursor.conf' -# XXX: pdns recursor doesn't like whitespace near entry separators, -# especially in the semicolon-separated lists of name servers. -# Please be careful if you edit the template. -config_tmpl = """ -### Autogenerated by dns_forwarding.py ### - -# Non-configurable defaults -daemon=yes -threads=1 -allow-from={{ allow_from | join(',') }} -log-common-errors=yes -non-local-bind=yes -query-local-address=0.0.0.0 -query-local-address6=:: - -# cache-size -max-cache-entries={{ cache_size }} - -# negative TTL for NXDOMAIN -max-negative-ttl={{ negative_ttl }} - -# ignore-hosts-file -export-etc-hosts={{ export_hosts_file }} - -# listen-on -local-address={{ listen_on | join(',') }} - -# dnssec -dnssec={{ dnssec }} - -# forward-zones / recursion -# -# statement is only inserted if either one forwarding domain or nameserver is configured -# if nothing is given at all, powerdns will act as a real recursor and resolve all requests by its own -# -{% if name_servers or domains %}forward-zones-recurse= -{%- for d in domains %} -{{ d.name }}={{ d.servers | join(";") }} -{{- ", " if not loop.last -}} -{%- endfor -%} -{%- if name_servers -%} -{%- if domains -%}, {% endif -%}.={{ name_servers | join(';') }} -{% endif %} -{% endif %} - -""" - default_config_data = { 'allow_from': [], 'cache_size': 10000, @@ -96,74 +47,74 @@ default_config_data = { def get_config(arguments): - dns = default_config_data + dns = deepcopy(default_config_data) conf = Config() + base = ['service', 'dns', 'forwarding'] if arguments.dhclient: conf.exists = conf.exists_effective conf.return_value = conf.return_effective_value conf.return_values = conf.return_effective_values - if not conf.exists('service dns forwarding'): + if not conf.exists(base): return None - conf.set_level('service dns forwarding') + conf.set_level(base) - if conf.exists('allow-from'): - dns['allow_from'] = conf.return_values('allow-from') + if conf.exists(['allow-from']): + dns['allow_from'] = conf.return_values(['allow-from']) - if conf.exists('cache-size'): - cache_size = conf.return_value('cache-size') + if conf.exists(['cache-size']): + cache_size = conf.return_value(['cache-size']) dns['cache_size'] = cache_size if conf.exists('negative-ttl'): - negative_ttl = conf.return_value('negative-ttl') + negative_ttl = conf.return_value(['negative-ttl']) dns['negative_ttl'] = negative_ttl - if conf.exists('domain'): - for node in conf.list_nodes('domain'): - servers = conf.return_values("domain {0} server".format(node)) + if conf.exists(['domain']): + for node in conf.list_nodes(['domain']): + servers = conf.return_values(['domain', node, 'server']) domain = { "name": node, "servers": bracketize_ipv6_addrs(servers) } dns['domains'].append(domain) - if conf.exists('ignore-hosts-file'): + if conf.exists(['ignore-hosts-file']): dns['export_hosts_file'] = "no" - if conf.exists('name-server'): - name_servers = conf.return_values('name-server') + if conf.exists(['name-server']): + name_servers = conf.return_values(['name-server']) dns['name_servers'] = dns['name_servers'] + name_servers - if conf.exists('system'): - conf.set_level('system') + if conf.exists(['system']): + conf.set_level(['system']) system_name_servers = [] - system_name_servers = conf.return_values('name-server') + system_name_servers = conf.return_values(['name-server']) if not system_name_servers: - print( - "DNS forwarding warning: No name-servers set under 'system name-server'\n") + print("DNS forwarding warning: No name-servers set under 'system name-server'\n") else: dns['name_servers'] = dns['name_servers'] + system_name_servers - conf.set_level('service dns forwarding') + conf.set_level(base) dns['name_servers'] = bracketize_ipv6_addrs(dns['name_servers']) - if conf.exists('listen-address'): - dns['listen_on'] = conf.return_values('listen-address') + if conf.exists(['listen-address']): + dns['listen_on'] = conf.return_values(['listen-address']) - if conf.exists('dnssec'): - dns['dnssec'] = conf.return_value('dnssec') + if conf.exists(['dnssec']): + dns['dnssec'] = conf.return_value(['dnssec']) # Add name servers received from DHCP - if conf.exists('dhcp'): + if conf.exists(['dhcp']): interfaces = [] - interfaces = conf.return_values('dhcp') - hc = vyos.hostsd_client.Client() + interfaces = conf.return_values(['dhcp']) + hc = hostsd_client() for interface in interfaces: - dhcp_resolvers = hc.get_name_servers("dhcp-{0}".format(interface)) - dhcpv6_resolvers = hc.get_name_servers("dhcpv6-{0}".format(interface)) + dhcp_resolvers = hc.get_name_servers(f'dhcp-{interface}') + dhcpv6_resolvers = hc.get_name_servers(f'dhcpv6-{interface}') if dhcp_resolvers: dns['name_servers'] = dns['name_servers'] + dhcp_resolvers @@ -202,7 +153,12 @@ def generate(dns): if dns is None: return None - tmpl = jinja2.Template(config_tmpl, trim_blocks=True) + # Prepare Jinja2 template loader from files + tmpl_path = os.path.join(vyos_data_dir['data'], 'templates', 'dns-forwarding') + fs_loader = FileSystemLoader(tmpl_path) + env = Environment(loader=fs_loader, trim_blocks=True) + + tmpl = env.get_template('recursor.conf.tmpl') config_text = tmpl.render(dns) with open(config_file, 'w') as f: f.write(config_text) @@ -211,11 +167,11 @@ def generate(dns): def apply(dns): if dns is None: # DNS forwarding is removed in the commit - os.system("systemctl stop pdns-recursor") + call("systemctl stop pdns-recursor") if os.path.isfile(config_file): os.unlink(config_file) else: - os.system("systemctl restart pdns-recursor") + call("systemctl restart pdns-recursor") if __name__ == '__main__': args = parser.parse_args() @@ -223,7 +179,7 @@ if __name__ == '__main__': if args.dhclient: # There's a big chance it was triggered by a commit still in progress # so we need to wait until the new values are in the running config - vyos.util.wait_for_commit_lock() + wait_for_commit_lock() try: c = get_config(args) @@ -232,4 +188,4 @@ if __name__ == '__main__': apply(c) except ConfigError as e: print(e) - sys.exit(1) + exit(1) diff --git a/src/conf_mode/dynamic_dns.py b/src/conf_mode/dynamic_dns.py index 4663987b4..b54d76b06 100755 --- a/src/conf_mode/dynamic_dns.py +++ b/src/conf_mode/dynamic_dns.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2018-2019 VyOS maintainers and contributors +# Copyright (C) 2018-2020 VyOS maintainers and contributors # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 or later as @@ -15,68 +15,22 @@ # along with this program. If not, see <http://www.gnu.org/licenses/>. import os -import sys -import jinja2 +from sys import exit +from copy import deepcopy +from jinja2 import FileSystemLoader, Environment from stat import S_IRUSR, S_IWUSR + from vyos.config import Config +from vyos.defaults import directories as vyos_data_dir from vyos import ConfigError +from vyos.util import call + config_file = r'/etc/ddclient/ddclient.conf' cache_file = r'/var/cache/ddclient/ddclient.cache' pid_file = r'/var/run/ddclient/ddclient.pid' -config_tmpl = """ -### Autogenerated by dynamic_dns.py ### -daemon=1m -syslog=yes -ssl=yes -pid={{ pid_file }} -cache={{ cache_file }} - -{% for interface in interfaces -%} - -# -# ddclient configuration for interface "{{ interface.interface }}": -# -{% if interface.web_url -%} -use=web, web='{{ interface.web_url}}' {%- if interface.web_skip %}, web-skip='{{ interface.web_skip }}'{% endif %} -{% else -%} -use=if, if={{ interface.interface }} -{% endif -%} - -{% for rfc in interface.rfc2136 -%} -{% for record in rfc.record %} -# RFC2136 dynamic DNS configuration for {{ record }}.{{ rfc.zone }} -server={{ rfc.server }} -protocol=nsupdate -password={{ rfc.keyfile }} -ttl={{ rfc.ttl }} -zone={{ rfc.zone }} -{{ record }} -{% endfor -%} -{% endfor -%} - -{% for srv in interface.service %} -{% for host in srv.host %} -# DynDNS provider configuration for {{ host }} -protocol={{ srv.protocol }}, -max-interval=28d, -login={{ srv.login }}, -password='{{ srv.password }}', -{% if srv.server -%} -server={{ srv.server }}, -{% endif -%} -{% if srv.zone -%} -zone={{ srv.zone }}, -{% endif -%} -{{ host }} -{% endfor %} -{% endfor %} - -{% endfor %} -""" - # Mapping of service name to service protocol default_service_protocol = { 'afraid': 'freedns', @@ -100,7 +54,7 @@ default_config_data = { } def get_config(): - dyndns = default_config_data + dyndns = deepcopy(default_config_data) conf = Config() base_level = ['service', 'dns', 'dynamic'] @@ -198,8 +152,8 @@ def get_config(): node['service'].append(srv) - # set config level back to top level - conf.set_level(base_level) + # Set config back to appropriate level for these options + conf.set_level(base_level + ['interface', interface]) # Additional settings in CLI if conf.exists(['use-web', 'skip']): @@ -208,6 +162,9 @@ def get_config(): if conf.exists(['use-web', 'url']): node['web_url'] = conf.return_value(['use-web', 'url']) + # set config level back to top level + conf.set_level(base_level) + dyndns['interfaces'].append(node) return dyndns @@ -269,6 +226,11 @@ def generate(dyndns): return None + # Prepare Jinja2 template loader from files + tmpl_path = os.path.join(vyos_data_dir['data'], 'templates', 'dynamic-dns') + fs_loader = FileSystemLoader(tmpl_path) + env = Environment(loader=fs_loader) + dirname = os.path.dirname(dyndns['pid_file']) if not os.path.exists(dirname): os.mkdir(dirname) @@ -277,7 +239,7 @@ def generate(dyndns): if not os.path.exists(dirname): os.mkdir(dirname) - tmpl = jinja2.Template(config_tmpl) + tmpl = env.get_template('ddclient.conf.tmpl') config_text = tmpl.render(dyndns) with open(config_file, 'w') as f: f.write(config_text) @@ -295,11 +257,11 @@ def apply(dyndns): os.unlink('/etc/ddclient.conf') if dyndns['deleted']: - os.system('/etc/init.d/ddclient stop') + call('/etc/init.d/ddclient stop') if os.path.exists(dyndns['pid_file']): os.unlink(dyndns['pid_file']) else: - os.system('/etc/init.d/ddclient restart') + call('/etc/init.d/ddclient restart') return None @@ -311,4 +273,4 @@ if __name__ == '__main__': apply(c) except ConfigError as e: print(e) - sys.exit(1) + exit(1) diff --git a/src/conf_mode/firewall_options.py b/src/conf_mode/firewall_options.py index 2be80cdbf..0b800f48f 100755 --- a/src/conf_mode/firewall_options.py +++ b/src/conf_mode/firewall_options.py @@ -21,6 +21,8 @@ import copy from vyos.config import Config from vyos import ConfigError +from vyos.util import call + default_config_data = { 'intf_opts': [], @@ -85,19 +87,19 @@ def apply(tcp): target = 'VYOS_FW_OPTIONS' # always cleanup iptables - os.system('iptables --table mangle --delete FORWARD --jump {} >&/dev/null'.format(target)) - os.system('iptables --table mangle --flush {} >&/dev/null'.format(target)) - os.system('iptables --table mangle --delete-chain {} >&/dev/null'.format(target)) + call('iptables --table mangle --delete FORWARD --jump {} >&/dev/null'.format(target)) + call('iptables --table mangle --flush {} >&/dev/null'.format(target)) + call('iptables --table mangle --delete-chain {} >&/dev/null'.format(target)) # always cleanup ip6tables - os.system('ip6tables --table mangle --delete FORWARD --jump {} >&/dev/null'.format(target)) - os.system('ip6tables --table mangle --flush {} >&/dev/null'.format(target)) - os.system('ip6tables --table mangle --delete-chain {} >&/dev/null'.format(target)) + call('ip6tables --table mangle --delete FORWARD --jump {} >&/dev/null'.format(target)) + call('ip6tables --table mangle --flush {} >&/dev/null'.format(target)) + call('ip6tables --table mangle --delete-chain {} >&/dev/null'.format(target)) # Setup new iptables rules if tcp['new_chain4']: - os.system('iptables --table mangle --new-chain {} >&/dev/null'.format(target)) - os.system('iptables --table mangle --append FORWARD --jump {} >&/dev/null'.format(target)) + call('iptables --table mangle --new-chain {} >&/dev/null'.format(target)) + call('iptables --table mangle --append FORWARD --jump {} >&/dev/null'.format(target)) for opts in tcp['intf_opts']: intf = opts['intf'] @@ -109,13 +111,13 @@ def apply(tcp): # adjust TCP MSS per interface if mss: - os.system('iptables --table mangle --append {} --out-interface {} --protocol tcp ' \ + call('iptables --table mangle --append {} --out-interface {} --protocol tcp ' '--tcp-flags SYN,RST SYN --jump TCPMSS --set-mss {} >&/dev/null'.format(target, intf, mss)) # Setup new ip6tables rules if tcp['new_chain6']: - os.system('ip6tables --table mangle --new-chain {} >&/dev/null'.format(target)) - os.system('ip6tables --table mangle --append FORWARD --jump {} >&/dev/null'.format(target)) + call('ip6tables --table mangle --new-chain {} >&/dev/null'.format(target)) + call('ip6tables --table mangle --append FORWARD --jump {} >&/dev/null'.format(target)) for opts in tcp['intf_opts']: intf = opts['intf'] @@ -127,8 +129,8 @@ def apply(tcp): # adjust TCP MSS per interface if mss: - os.system('ip6tables --table mangle --append {} --out-interface {} --protocol tcp ' \ - '--tcp-flags SYN,RST SYN --jump TCPMSS --set-mss {} >&/dev/null'.format(target, intf, mss)) + call('ip6tables --table mangle --append {} --out-interface {} --protocol tcp ' + '--tcp-flags SYN,RST SYN --jump TCPMSS --set-mss {} >&/dev/null'.format(target, intf, mss)) return None diff --git a/src/conf_mode/flow_accounting_conf.py b/src/conf_mode/flow_accounting_conf.py index 0bc50482c..1008f3fae 100755 --- a/src/conf_mode/flow_accounting_conf.py +++ b/src/conf_mode/flow_accounting_conf.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2018 VyOS maintainers and contributors +# Copyright (C) 2018-2020 VyOS maintainers and contributors # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 or later as @@ -13,18 +13,21 @@ # # You should have received a copy of the GNU General Public License # along with this program. If not, see <http://www.gnu.org/licenses/>. -# -import sys +import os import re import ipaddress -import subprocess +from ipaddress import ip_address +from jinja2 import FileSystemLoader, Environment +from sys import exit + +from vyos.ifconfig import Interface from vyos.config import Config +from vyos.defaults import directories as vyos_data_dir from vyos import ConfigError -import vyos.interfaces -from vyos.ifconfig import Interface -from jinja2 import Template +from vyos.util import cmd + # default values default_sflow_server_port = 6343 @@ -37,78 +40,6 @@ uacctd_conf_path = '/etc/pmacct/uacctd.conf' iptables_nflog_table = 'raw' iptables_nflog_chain = 'VYATTA_CT_PREROUTING_HOOK' -# pmacct config template -uacct_conf_jinja = '''# Genereated from VyOS configuration -daemonize: true -promisc: false -pidfile: /var/run/uacctd.pid -uacctd_group: 2 -uacctd_nl_size: 2097152 -snaplen: {{ snaplen }} -aggregate: in_iface,src_mac,dst_mac,vlan,src_host,dst_host,src_port,dst_port,proto,tos,flows -plugin_pipe_size: {{ templatecfg['plugin_pipe_size'] }} -plugin_buffer_size: {{ templatecfg['plugin_buffer_size'] }} -{%- if templatecfg['syslog-facility'] != none %} -syslog: {{ templatecfg['syslog-facility'] }} -{%- endif %} -{%- if templatecfg['disable-imt'] == none %} -imt_path: /tmp/uacctd.pipe -imt_mem_pools_number: 169 -{%- endif %} -plugins: -{%- if templatecfg['netflow']['servers'] != none -%} - {% for server in templatecfg['netflow']['servers'] %} - {%- if loop.last -%}nfprobe[nf_{{ server['address'] }}]{%- else %}nfprobe[nf_{{ server['address'] }}],{%- endif %} - {%- endfor -%} - {% set plugins_presented = true %} -{%- endif %} -{%- if templatecfg['sflow']['servers'] != none -%} - {% if plugins_presented -%} - {%- for server in templatecfg['sflow']['servers'] -%} - ,sfprobe[sf_{{ server['address'] }}] - {%- endfor %} - {%- else %} - {%- for server in templatecfg['sflow']['servers'] %} - {%- if loop.last -%}sfprobe[sf_{{ server['address'] }}]{%- else %}sfprobe[sf_{{ server['address'] }}],{%- endif %} - {%- endfor %} - {%- endif -%} - {% set plugins_presented = true %} -{%- endif %} -{%- if templatecfg['disable-imt'] == none %} - {%- if plugins_presented -%},memory{%- else %}memory{%- endif %} -{%- endif %} -{%- if templatecfg['netflow']['servers'] != none %} -{%- for server in templatecfg['netflow']['servers'] %} -nfprobe_receiver[nf_{{ server['address'] }}]: {{ server['address'] }}:{{ server['port'] }} -nfprobe_version[nf_{{ server['address'] }}]: {{ templatecfg['netflow']['version'] }} -{%- if templatecfg['netflow']['engine-id'] != none %} -nfprobe_engine[nf_{{ server['address'] }}]: {{ templatecfg['netflow']['engine-id'] }} -{%- endif %} -{%- if templatecfg['netflow']['max-flows'] != none %} -nfprobe_maxflows[nf_{{ server['address'] }}]: {{ templatecfg['netflow']['max-flows'] }} -{%- endif %} -{%- if templatecfg['netflow']['sampling-rate'] != none %} -sampling_rate[nf_{{ server['address'] }}]: {{ templatecfg['netflow']['sampling-rate'] }} -{%- endif %} -{%- if templatecfg['netflow']['source-ip'] != none %} -nfprobe_source_ip[nf_{{ server['address'] }}]: {{ templatecfg['netflow']['source-ip'] }} -{%- endif %} -{%- if templatecfg['netflow']['timeout_string'] != '' %} -nfprobe_timeouts[nf_{{ server['address'] }}]: {{ templatecfg['netflow']['timeout_string'] }} -{%- endif %} -{%- endfor %} -{%- endif %} -{%- if templatecfg['sflow']['servers'] != none %} -{%- for server in templatecfg['sflow']['servers'] %} -sfprobe_receiver[sf_{{ server['address'] }}]: {{ server['address'] }}:{{ server['port'] }} -sfprobe_agentip[sf_{{ server['address'] }}]: {{ templatecfg['sflow']['agent-address'] }} -{%- if templatecfg['sflow']['sampling-rate'] != none %} -sampling_rate[sf_{{ server['address'] }}]: {{ templatecfg['sflow']['sampling-rate'] }} -{%- endif %} -{%- endfor %} -{% endif %} -''' - # helper functions # check if node exists and return True if this is true def _node_exists(path): @@ -129,7 +60,7 @@ def _sflow_default_agentip(config): return config.return_value('protocols ospfv3 parameters router-id') # if router-id was not found, use first available ip of any interface - for iface in vyos.interfaces.list_interfaces(): + for iface in Interface.listing(): for address in Interface(iface).get_addr(): # return an IP, if this is not loopback regex_filter = re.compile('^(?!(127)|(::1)|(fe80))(?P<ipaddr>[a-f\d\.:]+)/\d+$') @@ -151,11 +82,7 @@ def _iptables_get_nflog(): for iptables_variant in ['iptables', 'ip6tables']: # run iptables, save output and split it by lines iptables_command = "sudo {0} -t {1} -S {2}".format(iptables_variant, iptables_nflog_table, iptables_nflog_chain) - process = subprocess.Popen(iptables_command, stdout=subprocess.PIPE, shell=True, universal_newlines=True) - stdout, stderr = process.communicate() - if not process.returncode == 0: - print("Failed to get flows list: command \"{}\" returned exit code: {}\nError: {}".format(command, process.returncode, stderr)) - sys.exit(1) + cmd(iptables_command, universal_newlines=True, message='Failed to get flows list') iptables_out = stdout.splitlines() # parse each line and add information to list @@ -180,14 +107,21 @@ def _iptables_config(configured_ifaces): # get currently configured interfaces with iptables rules active_nflog_rules = _iptables_get_nflog() - + # compare current active list with configured one and delete excessive interfaces, add missed active_nflog_ifaces = [] for rule in active_nflog_rules: - if rule['interface'] not in configured_ifaces: - iptable_commands.append("sudo {0} -t {1} -D {2}".format(rule['iptables_variant'], rule['table'], rule['rule_definition'])) + iptables = rule['iptables_variant'] + interface = rule['interface'] + if interface not in configured_ifaces: + table = rule['table'] + rule = rule['rule_definition'] + iptable_commands.append(f'sudo {iptables} -t {table} -D {rule}') else: - active_nflog_ifaces.append({ 'iface': rule['interface'], 'iptables_variant': rule['iptables_variant'] }) + active_nflog_ifaces.append({ + 'iface': interface, + 'iptables_variant': iptables, + }) # do not create new rules for already configured interfaces for iface in active_nflog_ifaces: @@ -196,14 +130,14 @@ def _iptables_config(configured_ifaces): # create missed rules for iface_extended in configured_ifaces_extended: - rule_definition = "{0} -i {1} -m comment --comment FLOW_ACCOUNTING_RULE -j NFLOG --nflog-group 2 --nflog-size {2} --nflog-threshold 100".format(iptables_nflog_chain, iface_extended['iface'], default_captured_packet_size) - iptable_commands.append("sudo {0} -t {1} -I {2}".format(iface_extended['iptables_variant'], iptables_nflog_table, rule_definition)) + iface = iface_extended['iface'] + iptables = iface_extended['iptables_variant'] + rule_definition = f'{iptables_nflog_chain} -i {iface} -m comment --comment FLOW_ACCOUNTING_RULE -j NFLOG --nflog-group 2 --nflog-size {default_captured_packet_size} --nflog-threshold 100' + iptable_commands.append(f'sudo {iptables} -t {iptables_nflog_table} -I {rule_definition}') # change iptables for command in iptable_commands: - return_code = subprocess.call(command.split(' ')) - if not return_code == 0: - raise ConfigError("Failed to run command: {}\nExit code {}".format(command, return_code)) + cmd(command, raising=ConfigError) def get_config(): @@ -300,7 +234,7 @@ def verify(config): # check that all configured interfaces exists in the system for iface in config['interfaces']: - if not iface in vyos.interfaces.list_interfaces(): + if not iface in Interface.listing(): # chnged from error to warning to allow adding dynamic interfaces and interface templates # raise ConfigError("The {} interface is not presented in the system".format(iface)) print("Warning: the {} interface is not presented in the system".format(iface)) @@ -315,20 +249,20 @@ def verify(config): sflow_collector_ipver = None for sflow_collector in config['sflow']['servers']: if sflow_collector_ipver: - if sflow_collector_ipver != ipaddress.ip_address(sflow_collector['address']).version: + if sflow_collector_ipver != ip_address(sflow_collector['address']).version: raise ConfigError("All sFlow servers must use the same IP protocol") else: - sflow_collector_ipver = ipaddress.ip_address(sflow_collector['address']).version + sflow_collector_ipver = ip_address(sflow_collector['address']).version # check agent-id for sFlow: we should avoid mixing IPv4 agent-id with IPv6 collectors and vice-versa for sflow_collector in config['sflow']['servers']: - if ipaddress.ip_address(sflow_collector['address']).version != ipaddress.ip_address(config['sflow']['agent-address']).version: + if ip_address(sflow_collector['address']).version != ip_address(config['sflow']['agent-address']).version: raise ConfigError("Different IP address versions cannot be mixed in \"sflow agent-address\" and \"sflow server\". You need to set manually the same IP version for \"agent-address\" as for all sFlow servers") # check if configured sFlow agent-id exist in the system agent_id_presented = None - for iface in vyos.interfaces.list_interfaces(): + for iface in Interface.listing(): for address in Interface(iface).get_addr(): # check an IP, if this is not loopback regex_filter = re.compile('^(?!(127)|(::1)|(fe80))(?P<ipaddr>[a-f\d\.:]+)/\d+$') @@ -348,7 +282,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 vyos.interfaces.list_interfaces(): + for iface in Interface.listing(): for address in Interface(iface).get_addr(): # check an IP regex_filter = re.compile('^(?!(127)|(::1)|(fe80))(?P<ipaddr>[a-f\d\.:]+)/\d+$') @@ -379,7 +313,7 @@ def generate(config): # skip all checks if flow-accounting was removed if not config['flow-accounting-configured']: return True - + # Calculate all necessary values if config['buffer-size']: # circular queue size @@ -400,13 +334,16 @@ def generate(config): timeout_string = "{}:{}={}".format(timeout_string, timeout_type, timeout_value) config['netflow']['timeout_string'] = timeout_string - # Generate daemon configs - uacct_conf_template = Template(uacct_conf_jinja) - uacct_conf_file_data = uacct_conf_template.render(templatecfg = config, snaplen = default_captured_packet_size) + # Prepare Jinja2 template loader from files + tmpl_path = os.path.join(vyos_data_dir['data'], 'templates', 'netflow') + fs_loader = FileSystemLoader(tmpl_path) + env = Environment(loader=fs_loader) - # save generated config to uacctd.conf + # Generate daemon configs + tmpl = env.get_template('uacctd.conf.tmpl') + config_text = tmpl.render(templatecfg = config, snaplen = default_captured_packet_size) with open(uacctd_conf_path, 'w') as file: - file.write(uacct_conf_file_data) + file.write(config_text) def apply(config): @@ -419,9 +356,7 @@ def apply(config): command = '/usr/bin/sudo /bin/systemctl restart uacctd' # run command to start or stop flow-accounting - return_code = subprocess.call(command.split(' ')) - if not return_code == 0: - raise ConfigError("Failed to start/stop flow-accounting: command {} returned exit code {}".format(command, return_code)) + cmd(command, raising=ConfigError, message='Failed to start/stop flow-accounting') # configure iptables rules for defined interfaces if config['interfaces']: @@ -437,4 +372,4 @@ if __name__ == '__main__': apply(config) except ConfigError as e: print(e) - sys.exit(1) + exit(1) diff --git a/src/conf_mode/host_name.py b/src/conf_mode/host_name.py index 47cf232e9..7c2f79abc 100755 --- a/src/conf_mode/host_name.py +++ b/src/conf_mode/host_name.py @@ -25,7 +25,6 @@ import re import sys import copy import glob -import subprocess import argparse import jinja2 @@ -34,6 +33,9 @@ import vyos.hostsd_client from vyos.config import Config from vyos import ConfigError +from vyos.util import cmd +from vyos.util import call +from vyos.util import run default_config_data = { @@ -156,21 +158,22 @@ def apply(config): # rsyslog runs into a race condition at boot time with systemd # restart rsyslog only if the hostname changed. - hostname_old = subprocess.check_output(['hostnamectl', '--static']).decode().strip() - - os.system("hostnamectl set-hostname --static {0}".format(hostname_new)) + hostname_old = cmd('hostnamectl --static') + call(f'hostnamectl set-hostname --static {hostname_new}') # Restart services that use the hostname if hostname_new != hostname_old: - os.system("systemctl restart rsyslog.service") + call("systemctl restart rsyslog.service") # If SNMP is running, restart it too - if os.system("pgrep snmpd > /dev/null") == 0: - os.system("systemctl restart snmpd.service") + ret = run("pgrep snmpd") + if ret == 0: + call("systemctl restart snmpd.service") # restart pdns if it is used - if os.system("/usr/bin/rec_control ping >/dev/null 2>&1") == 0: - os.system("/etc/init.d/pdns-recursor restart >/dev/null") + ret = run('/usr/bin/rec_control ping') + if ret == 0: + call('/etc/init.d/pdns-recursor restart >/dev/null') return None diff --git a/src/conf_mode/http-api.py b/src/conf_mode/http-api.py index 0c2e029e9..26f4aea7f 100755 --- a/src/conf_mode/http-api.py +++ b/src/conf_mode/http-api.py @@ -18,13 +18,14 @@ import sys import os -import subprocess import json from copy import deepcopy import vyos.defaults from vyos.config import Config from vyos import ConfigError +from vyos.util import cmd +from vyos.util import call config_file = '/etc/vyos/http-api.conf' @@ -91,16 +92,12 @@ def generate(http_api): def apply(http_api): if http_api is not None: - os.system('sudo systemctl restart vyos-http-api.service') + call('sudo systemctl restart vyos-http-api.service') else: - os.system('sudo systemctl stop vyos-http-api.service') + call('sudo systemctl stop vyos-http-api.service') for dep in dependencies: - cmd = '{0}/{1}'.format(vyos_conf_scripts_dir, dep) - try: - subprocess.check_call(cmd, shell=True) - except subprocess.CalledProcessError as err: - raise ConfigError("{}.".format(err)) + cmd(f'{vyos_conf_scripts_dir}/{dep}', raising=ConfigError) if __name__ == '__main__': try: diff --git a/src/conf_mode/https.py b/src/conf_mode/https.py index 84d1a7691..da7193c9b 100755 --- a/src/conf_mode/https.py +++ b/src/conf_mode/https.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 @@ -13,88 +13,26 @@ # # You should have received a copy of the GNU General Public License # along with this program. If not, see <http://www.gnu.org/licenses/>. -# -# -import sys import os -import jinja2 +from sys import exit +from copy import deepcopy +from jinja2 import FileSystemLoader, Environment import vyos.defaults import vyos.certbot_util + from vyos.config import Config +from vyos.defaults import directories as vyos_data_dir from vyos import ConfigError +from vyos.util import call -config_file = '/etc/nginx/sites-available/default' - -# Please be careful if you edit the template. -config_tmpl = """ - -### Autogenerated by https.py ### -# Default server configuration -# -server { - listen 80 default_server; - listen [::]:80 default_server; - server_name _; - return 301 https://$server_name$request_uri; -} - -{% for server in server_block_list %} -server { - - # SSL configuration - # -{% if server.address == '*' %} - listen {{ server.port }} ssl; - listen [::]:{{ server.port }} ssl; -{% else %} - listen {{ server.address }}:{{ server.port }} ssl; -{% endif %} - -{% for name in server.name %} - server_name {{ name }}; -{% endfor %} - -{% if server.certbot %} - ssl_certificate /etc/letsencrypt/live/{{ server.certbot_dir }}/fullchain.pem; - ssl_certificate_key /etc/letsencrypt/live/{{ server.certbot_dir }}/privkey.pem; - include /etc/letsencrypt/options-ssl-nginx.conf; - ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; -{% elif server.vyos_cert %} - include {{ server.vyos_cert.conf }}; -{% else %} - # - # Self signed certs generated by the ssl-cert package - # Don't use them in a production server! - # - include snippets/snakeoil.conf; -{% endif %} - - # proxy settings for HTTP API, if enabled; 503, if not - location ~ /(retrieve|configure|config-file|image) { -{% if server.api %} - proxy_pass http://localhost:{{ server.api.port }}; - proxy_buffering off; -{% else %} - return 503; -{% endif %} - } - - error_page 501 502 503 =200 @50*_json; - - location @50*_json { - default_type application/json; - return 200 '{"error": "Start service in configuration mode: set service https api"}'; - } - -} -{% endfor %} -""" +config_file = '/etc/nginx/sites-available/default' default_server_block = { + 'id' : '', 'address' : '*', 'port' : '443', 'name' : ['_'], @@ -111,22 +49,23 @@ def get_config(): else: conf.set_level('service https') - if conf.exists('listen-address'): - for addr in conf.list_nodes('listen-address'): - server_block = {'address' : addr} - server_block['port'] = '443' - server_block['name'] = ['_'] - if conf.exists('listen-address {0} listen-port'.format(addr)): - port = conf.return_value('listen-address {0} listen-port'.format(addr)) + if not conf.exists('virtual-host'): + server_block_list.append(default_server_block) + else: + for vhost in conf.list_nodes('virtual-host'): + server_block = deepcopy(default_server_block) + server_block['id'] = vhost + if conf.exists(f'virtual-host {vhost} listen-address'): + addr = conf.return_value(f'virtual-host {vhost} listen-address') + server_block['address'] = addr + if conf.exists(f'virtual-host {vhost} listen-port'): + port = conf.return_value(f'virtual-host {vhost} listen-port') server_block['port'] = port - if conf.exists('listen-address {0} server-name'.format(addr)): - names = conf.return_values('listen-address {0} server-name'.format(addr)) + if conf.exists(f'virtual-host {vhost} server-name'): + names = conf.return_values(f'virtual-host {vhost} server-name') server_block['name'] = names[:] server_block_list.append(server_block) - if not server_block_list: - server_block_list.append(default_server_block) - vyos_cert_data = {} if conf.exists('certificates system-generated-certificate'): vyos_cert_data = vyos.defaults.vyos_cert_data @@ -149,17 +88,33 @@ def get_config(): # certbot organizes certificates by first domain sb['certbot_dir'] = certbot_domains[0] + api_somewhere = False api_data = {} if conf.exists('api'): + api_somewhere = True api_data = vyos.defaults.api_data if conf.exists('api port'): port = conf.return_value('api port') api_data['port'] = port - if api_data: - for block in server_block_list: - block['api'] = api_data + if conf.exists('api-restrict virtual-host'): + vhosts = conf.return_values('api-restrict virtual-host') + api_data['vhost'] = vhosts[:] - https = {'server_block_list' : server_block_list, 'certbot': certbot} + if api_data: + # we do not want to include 'vhost' key as part of + # vyos.defaults.api_data, so check for key existence + vhost_list = api_data.get('vhost') + if vhost_list is None: + for block in server_block_list: + block['api'] = api_data + else: + for block in server_block_list: + if block['id'] in vhost_list: + block['api'] = api_data + + https = {'server_block_list' : server_block_list, + 'api_somewhere': api_somewhere, + 'certbot': certbot} return https def verify(https): @@ -170,7 +125,7 @@ def verify(https): for sb in https['server_block_list']: if sb['certbot']: return None - raise ConfigError("At least one 'listen-address x.x.x.x server-name' " + raise ConfigError("At least one 'virtual-host <id> server-name' " "matching the 'certbot domain-name' is required.") return None @@ -178,10 +133,15 @@ def generate(https): if https is None: return None + # Prepare Jinja2 template loader from files + tmpl_path = os.path.join(vyos_data_dir['data'], 'templates', 'https') + fs_loader = FileSystemLoader(tmpl_path) + env = Environment(loader=fs_loader, trim_blocks=True) + if 'server_block_list' not in https or not https['server_block_list']: https['server_block_list'] = [default_server_block] - tmpl = jinja2.Template(config_tmpl, trim_blocks=True) + tmpl = env.get_template('nginx.default.tmpl') config_text = tmpl.render(https) with open(config_file, 'w') as f: f.write(config_text) @@ -190,9 +150,9 @@ def generate(https): def apply(https): if https is not None: - os.system('sudo systemctl restart nginx.service') + call('sudo systemctl restart nginx.service') else: - os.system('sudo systemctl stop nginx.service') + call('sudo systemctl stop nginx.service') if __name__ == '__main__': try: @@ -202,4 +162,4 @@ if __name__ == '__main__': apply(c) except ConfigError as e: print(e) - sys.exit(1) + exit(1) diff --git a/src/conf_mode/igmp_proxy.py b/src/conf_mode/igmp_proxy.py index cd0704124..77e2bb150 100755 --- a/src/conf_mode/igmp_proxy.py +++ b/src/conf_mode/igmp_proxy.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2018 VyOS maintainers and contributors +# Copyright (C) 2018-2020 VyOS maintainers and contributors # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 or later as @@ -13,59 +13,21 @@ # # You should have received a copy of the GNU General Public License # along with this program. If not, see <http://www.gnu.org/licenses/>. -# -# -import sys import os -import jinja2 + +from sys import exit +from copy import deepcopy +from jinja2 import FileSystemLoader, Environment from netifaces import interfaces from vyos.config import Config +from vyos.defaults import directories as vyos_data_dir from vyos import ConfigError +from vyos.util import call -config_file = r'/etc/igmpproxy.conf' -# Please be careful if you edit the template. -config_tmpl = """ -######################################################## -# -# autogenerated by igmp_proxy.py -# -# The configuration file must define one upstream -# interface, and one or more downstream interfaces. -# -# If multicast traffic originates outside the -# upstream subnet, the "altnet" option can be -# used in order to define legal multicast sources. -# (Se example...) -# -# The "quickleave" should be used to avoid saturation -# of the upstream link. The option should only -# be used if it's absolutely nessecary to -# accurately imitate just one Client. -# -######################################################## - -{% if not disable_quickleave -%} -quickleave -{% endif -%} - -{% for interface in interfaces %} -# Configuration for {{ interface.name }} ({{ interface.role }} interface) -{% if interface.role == 'disabled' -%} -phyint {{ interface.name }} disabled -{%- else -%} -phyint {{ interface.name }} {{ interface.role }} ratelimit 0 threshold {{ interface.threshold }} -{%- endif -%} -{%- for subnet in interface.alt_subnet %} - altnet {{ subnet }} -{%- endfor %} -{%- for subnet in interface.whitelist %} - whitelist {{ subnet }} -{%- endfor %} -{% endfor %} -""" +config_file = r'/etc/igmpproxy.conf' default_config_data = { 'disable': False, @@ -74,23 +36,24 @@ default_config_data = { } def get_config(): - igmp_proxy = default_config_data + igmp_proxy = deepcopy(default_config_data) conf = Config() - if not conf.exists('protocols igmp-proxy'): + base = ['protocols', 'igmp-proxy'] + if not conf.exists(base): return None else: - conf.set_level('protocols igmp-proxy') + conf.set_level(base) # Network interfaces to listen on - if conf.exists('disable'): + if conf.exists(['disable']): igmp_proxy['disable'] = True # Option to disable "quickleave" - if conf.exists('disable-quickleave'): + if conf.exists(['disable-quickleave']): igmp_proxy['disable_quickleave'] = True - for intf in conf.list_nodes('interface'): - conf.set_level('protocols igmp-proxy interface {0}'.format(intf)) + for intf in conf.list_nodes(['interface']): + conf.set_level(base + ['interface', intf]) interface = { 'name': intf, 'alt_subnet': [], @@ -99,17 +62,17 @@ def get_config(): 'whitelist': [] } - if conf.exists('alt-subnet'): - interface['alt_subnet'] = conf.return_values('alt-subnet') + if conf.exists(['alt-subnet']): + interface['alt_subnet'] = conf.return_values(['alt-subnet']) - if conf.exists('role'): - interface['role'] = conf.return_value('role') + if conf.exists(['role']): + interface['role'] = conf.return_value(['role']) - if conf.exists('threshold'): - interface['threshold'] = conf.return_value('threshold') + if conf.exists(['threshold']): + interface['threshold'] = conf.return_value(['threshold']) - if conf.exists('whitelist'): - interface['whitelist'] = conf.return_values('whitelist') + if conf.exists(['whitelist']): + interface['whitelist'] = conf.return_values(['whitelist']) # Append interface configuration to global configuration list igmp_proxy['interfaces'].append(interface) @@ -153,7 +116,12 @@ def generate(igmp_proxy): print('Warning: IGMP Proxy will be deactivated because it is disabled') return None - tmpl = jinja2.Template(config_tmpl) + # Prepare Jinja2 template loader from files + tmpl_path = os.path.join(vyos_data_dir['data'], 'templates', 'igmp-proxy') + fs_loader = FileSystemLoader(tmpl_path) + env = Environment(loader=fs_loader) + + tmpl = env.get_template('igmpproxy.conf.tmpl') config_text = tmpl.render(igmp_proxy) with open(config_file, 'w') as f: f.write(config_text) @@ -163,11 +131,11 @@ def generate(igmp_proxy): def apply(igmp_proxy): if igmp_proxy is None or igmp_proxy['disable']: # IGMP Proxy support is removed in the commit - os.system('sudo systemctl stop igmpproxy.service') + call('sudo systemctl stop igmpproxy.service') if os.path.exists(config_file): os.unlink(config_file) else: - os.system('sudo systemctl restart igmpproxy.service') + call('systemctl restart igmpproxy.service') return None @@ -179,4 +147,4 @@ if __name__ == '__main__': apply(c) except ConfigError as e: print(e) - sys.exit(1) + exit(1) diff --git a/src/conf_mode/intel_qat.py b/src/conf_mode/intel_qat.py index a1abd5e81..cc7d4a915 100755 --- a/src/conf_mode/intel_qat.py +++ b/src/conf_mode/intel_qat.py @@ -19,10 +19,10 @@ import sys import os import re -import subprocess from vyos.config import Config from vyos import ConfigError +from vyos.util import popen, run # Define for recovering gl_ipsec_conf = None @@ -49,13 +49,10 @@ def get_config(): # Control configured VPN service which can use QAT def vpn_control(action): + # XXX: Should these commands report failure if action == 'restore' and gl_ipsec_conf: - ret = subprocess.Popen(['sudo', 'ipsec', 'start'], stdout=subprocess.PIPE, stderr=subprocess.STDOUT) - (output, err) = ret.communicate() - return - - ret = subprocess.Popen(['sudo', 'ipsec', action], stdout=subprocess.PIPE, stderr=subprocess.STDOUT) - (output, err) = ret.communicate() + return run('sudo ipsec start') + return run(f'sudo ipsec {action}') def verify(c): # Check if QAT service installed @@ -66,10 +63,9 @@ def verify(c): return # Check if QAT device exist - ret = subprocess.Popen(['sudo', 'lspci', '-nn'], stdout=subprocess.PIPE, stderr=subprocess.STDOUT) - (output, err) = ret.communicate() + output, err = popen('sudo lspci -nn', decode='utf-8') if not err: - data = re.findall('(8086:19e2)|(8086:37c8)|(8086:0435)|(8086:6f54)', output.decode("utf-8")) + data = re.findall('(8086:19e2)|(8086:37c8)|(8086:0435)|(8086:6f54)', output) #If QAT devices found if not data: print("\t No QAT acceleration device found") @@ -82,17 +78,13 @@ def apply(c): # Disable QAT service if c['qat_conf'] == None: - ret = subprocess.Popen(['sudo', '/etc/init.d/vyos-qat-utilities', 'stop'], stdout=subprocess.PIPE, stderr=subprocess.STDOUT) - (output, err) = ret.communicate() + run('sudo /etc/init.d/vyos-qat-utilities stop') if c['ipsec_conf']: vpn_control('start') - return # Run qat init.d script - ret = subprocess.Popen(['sudo', '/etc/init.d/vyos-qat-utilities', 'start'], stdout=subprocess.PIPE, stderr=subprocess.STDOUT) - (output, err) = ret.communicate() - + run('sudo /etc/init.d/vyos-qat-utilities start') if c['ipsec_conf']: # Recovery VPN service vpn_control('start') diff --git a/src/conf_mode/interfaces-bonding.py b/src/conf_mode/interfaces-bonding.py index 6cdfb764c..32aa2826b 100755 --- a/src/conf_mode/interfaces-bonding.py +++ b/src/conf_mode/interfaces-bonding.py @@ -21,9 +21,11 @@ from sys import exit from netifaces import interfaces from vyos.ifconfig import BondIf -from vyos.ifconfig_vlan import apply_vlan_config +from vyos.ifconfig_vlan import apply_vlan_config, verify_vlan_config from vyos.configdict import list_diff, vlan_to_dict from vyos.config import Config +from vyos.util import is_bridge_member +from vyos.util import call from vyos import ConfigError default_config_data = { @@ -48,6 +50,10 @@ default_config_data = { '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, 'intf': '', 'mac': '', 'mode': '802.3ad', @@ -58,7 +64,8 @@ default_config_data = { 'vif_s': [], 'vif_s_remove': [], 'vif': [], - 'vif_remove': [] + 'vif_remove': [], + 'vrf': '' } @@ -85,7 +92,7 @@ def get_config(): if not os.path.isfile('/sys/class/net/bonding_masters'): import syslog syslog.syslog(syslog.LOG_NOTICE, "loading bonding kernel module") - if os.system('modprobe bonding max_bonds=0 miimon=250') != 0: + if call('modprobe bonding max_bonds=0 miimon=250') != 0: syslog.syslog(syslog.LOG_NOTICE, "failed loading bonding kernel module") raise ConfigError("failed loading bonding kernel module") @@ -93,10 +100,10 @@ def get_config(): conf = Config() # determine tagNode instance - try: - bond['intf'] = os.environ['VYOS_TAGNODE_VALUE'] - except KeyError as E: - print("Interface not specified") + 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'] @@ -188,6 +195,22 @@ def get_config(): 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') @@ -221,8 +244,10 @@ def get_config(): if conf.exists('primary'): bond['primary'] = conf.return_value('primary') - # re-set configuration level to parse new nodes - conf.set_level(cfg_base) + # 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') @@ -253,38 +278,30 @@ def get_config(): def verify(bond): + if bond['deleted']: + interface = bond['intf'] + is_member, bridge = is_bridge_member(interface) + if is_member: + # can not use a f'' formatted-string here as bridge would not get + # expanded in the print statement + raise ConfigError('Can not delete interface "{0}" as it ' \ + 'is a member of bridge "{1}"!'.format(interface, bridge)) + return None + if len (bond['arp_mon_tgt']) > 16: raise ConfigError('The maximum number of targets that can be specified 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 this mode.'.format()) - - if bond['primary'] not in bond['member']: - raise ConfigError('Interface "{}" is not part of the bond' \ - .format(bond['primary'])) - - - # DHCPv6 parameters-only and temporary address are mutually exclusive - for vif_s in bond['vif_s']: - if vif_s['dhcpv6_prm_only'] and vif_s['dhcpv6_temporary']: - raise ConfigError('DHCPv6 temporary and parameters-only options are mutually exclusive!') + 'in mode "{}"!'.format(bond['mode'])) - for vif_c in vif_s['vif_c']: - if vif_c['dhcpv6_prm_only'] and vif_c['dhcpv6_temporary']: - raise ConfigError('DHCPv6 temporary and parameters-only options are mutually exclusive!') - - for vif in bond['vif']: - if vif['dhcpv6_prm_only'] and vif['dhcpv6_temporary']: - raise ConfigError('DHCPv6 temporary and parameters-only options are mutually exclusive!') - - - for vif_s in bond['vif_s']: - for vif in bond['vif']: - if vif['id'] == vif_s['id']: - raise ConfigError('Can not use identical ID on vif and vif-s interface') + vrf_name = bond['vrf'] + if vrf_name and vrf_name not in interfaces(): + raise ConfigError(f'VRF "{vrf_name}" does not exist') + # use common function to verify VLAN configuration + verify_vlan_config(bond) conf = Config() for intf in bond['member']: @@ -427,6 +444,14 @@ 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 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']) # Change interface MAC address if bond['mac']: @@ -442,7 +467,7 @@ def apply(bond): # Some parameters can not be changed when the bond is up. if bond['shutdown_required']: # Disable bond prior changing of certain properties - b.set_state('down') + b.set_admin_state('down') # The bonding mode can not be changed when there are interfaces enslaved # to this bond, thus we will free all interfaces from the bond first! @@ -460,9 +485,9 @@ def apply(bond): # parameters we will only re-enable the interface if it is not # administratively disabled if not bond['disable']: - b.set_state('up') + b.set_admin_state('up') else: - b.set_state('down') + b.set_admin_state('down') # Configure interface address(es) # - not longer required addresses get removed first @@ -472,6 +497,9 @@ 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) diff --git a/src/conf_mode/interfaces-bridge.py b/src/conf_mode/interfaces-bridge.py index a3213f309..79247ee51 100755 --- a/src/conf_mode/interfaces-bridge.py +++ b/src/conf_mode/interfaces-bridge.py @@ -20,7 +20,8 @@ from copy import deepcopy from sys import exit from netifaces import interfaces -from vyos.ifconfig import BridgeIf, STPIf +from vyos.ifconfig import BridgeIf +from vyos.ifconfig.stp import STP from vyos.configdict import list_diff from vyos.config import Config from vyos import ConfigError @@ -45,6 +46,10 @@ default_config_data = { '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' : '', @@ -52,7 +57,8 @@ default_config_data = { 'member': [], 'member_remove': [], 'priority': 32768, - 'stp': 0 + 'stp': 0, + 'vrf': '' } def get_config(): @@ -60,10 +66,10 @@ def get_config(): conf = Config() # determine tagNode instance - try: - bridge['intf'] = os.environ['VYOS_TAGNODE_VALUE'] - except KeyError as E: - print("Interface not specified") + if 'VYOS_TAGNODE_VALUE' not in os.environ: + raise ConfigError('Interface (VYOS_TAGNODE_VALUE) not specified') + + bridge['intf'] = os.environ['VYOS_TAGNODE_VALUE'] # Check if bridge has been removed if not conf.exists('interfaces bridge ' + bridge['intf']): @@ -150,6 +156,22 @@ def get_config(): if conf.exists('ip enable-arp-ignore'): bridge['ip_enable_arp_ignore'] = 1 + # Enable acquisition of IPv6 address using stateless autoconfig (SLAAC) + if conf.exists('ipv6 address autoconf'): + bridge['ipv6_autoconf'] = 1 + + # Get prefix 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') + + # Disable IPv6 forwarding on this interface + if conf.exists('ipv6 disable-forwarding'): + bridge['ipv6_forwarding'] = 0 + + # IPv6 Duplicate Address Detection (DAD) tries + if conf.exists('ipv6 dup-addr-detect-transmits'): + bridge['ipv6_dup_addr_detect'] = int(conf.return_value('ipv6 dup-addr-detect-transmits')) + # Media Access Control (MAC) address if conf.exists('mac'): bridge['mac'] = conf.return_value('mac') @@ -191,12 +213,20 @@ def get_config(): if conf.exists('stp'): bridge['stp'] = 1 + # retrieve VRF instance + if conf.exists('vrf'): + bridge['vrf'] = conf.return_value('vrf') + return bridge def verify(bridge): if bridge['dhcpv6_prm_only'] and bridge['dhcpv6_temporary']: raise ConfigError('DHCPv6 temporary and parameters-only options are mutually exclusive!') + vrf_name = bridge['vrf'] + if vrf_name and vrf_name not in interfaces(): + 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 @@ -213,6 +243,9 @@ def verify(bridge): if intf['name'] not in interfaces(): raise ConfigError('Can not add non existing interface "{}" to bridge "{}"'.format(intf['name'], 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'): @@ -233,7 +266,7 @@ def apply(bridge): br.remove() else: # enable interface - br.set_state('up') + br.set_admin_state('up') # set ageing time br.set_ageing_time(bridge['aging']) # set bridge forward delay @@ -248,6 +281,14 @@ 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 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 + br.set_ipv6_dad_messages(bridge['ipv6_dup_addr_detect']) # set max message age br.set_max_age(bridge['max_age']) # set bridge priority @@ -286,6 +327,9 @@ def apply(bridge): # store DHCPv6 config dictionary - used later on when addresses are aquired br.set_dhcpv6_options(opt) + # assign/remove VRF + br.set_vrf(bridge['vrf']) + # Change interface MAC address if bridge['mac']: br.set_mac(bridge['mac']) @@ -300,7 +344,7 @@ def apply(bridge): # up/down interface if bridge['disable']: - br.set_state('down') + br.set_admin_state('down') # Configure interface address(es) # - not longer required addresses get removed first @@ -310,9 +354,10 @@ def apply(bridge): for addr in bridge['address']: br.add_addr(addr) + STPBridgeIf = STP.enable(BridgeIf) # configure additional bridge member options for member in bridge['member']: - i = STPIf(member['name']) + i = STPBridgeIf(member['name']) # configure ARP cache timeout i.set_arp_cache_tmo(bridge['arp_cache_tmo']) # ignore link state changes diff --git a/src/conf_mode/interfaces-dummy.py b/src/conf_mode/interfaces-dummy.py index eb0145f65..a256103af 100755 --- a/src/conf_mode/interfaces-dummy.py +++ b/src/conf_mode/interfaces-dummy.py @@ -18,10 +18,12 @@ import os from copy import deepcopy from sys import exit +from netifaces import interfaces from vyos.ifconfig import DummyIf from vyos.configdict import list_diff from vyos.config import Config +from vyos.util import is_bridge_member from vyos import ConfigError default_config_data = { @@ -30,7 +32,8 @@ default_config_data = { 'deleted': False, 'description': '', 'disable': False, - 'intf': '' + 'intf': '', + 'vrf': '' } def get_config(): @@ -38,10 +41,10 @@ def get_config(): conf = Config() # determine tagNode instance - try: - dummy['intf'] = os.environ['VYOS_TAGNODE_VALUE'] - except KeyError as E: - print("Interface not specified") + if 'VYOS_TAGNODE_VALUE' not in os.environ: + raise ConfigError('Interface (VYOS_TAGNODE_VALUE) not specified') + + dummy['intf'] = os.environ['VYOS_TAGNODE_VALUE'] # Check if interface has been removed if not conf.exists('interfaces dummy ' + dummy['intf']): @@ -69,9 +72,27 @@ def get_config(): act_addr = conf.return_values('address') dummy['address_remove'] = list_diff(eff_addr, act_addr) + # retrieve VRF instance + if conf.exists('vrf'): + dummy['vrf'] = conf.return_value('vrf') + return dummy def verify(dummy): + if dummy['deleted']: + interface = dummy['intf'] + is_member, bridge = is_bridge_member(interface) + if is_member: + # can not use a f'' formatted-string here as bridge would not get + # expanded in the print statement + raise ConfigError('Can not delete interface "{0}" as it ' \ + 'is a member of bridge "{1}"!'.format(interface, bridge)) + return None + + vrf_name = dummy['vrf'] + if vrf_name and vrf_name not in interfaces(): + raise ConfigError(f'VRF "{vrf_name}" does not exist') + return None def generate(dummy): @@ -95,11 +116,14 @@ def apply(dummy): for addr in dummy['address']: d.add_addr(addr) + # assign/remove VRF + d.set_vrf(dummy['vrf']) + # disable interface on demand if dummy['disable']: - d.set_state('down') + d.set_admin_state('down') else: - d.set_state('up') + d.set_admin_state('up') return None diff --git a/src/conf_mode/interfaces-ethernet.py b/src/conf_mode/interfaces-ethernet.py index e4f6e5ff2..15e9b4185 100755 --- a/src/conf_mode/interfaces-ethernet.py +++ b/src/conf_mode/interfaces-ethernet.py @@ -16,11 +16,12 @@ import os -from copy import deepcopy from sys import exit +from copy import deepcopy +from netifaces import interfaces from vyos.ifconfig import EthernetIf -from vyos.ifconfig_vlan import apply_vlan_config +from vyos.ifconfig_vlan import apply_vlan_config, verify_vlan_config from vyos.configdict import list_diff, vlan_to_dict from vyos.config import Config from vyos import ConfigError @@ -47,6 +48,10 @@ default_config_data = { '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, 'intf': '', 'mac': '', 'mtu': 1500, @@ -59,7 +64,8 @@ default_config_data = { 'vif_s': [], 'vif_s_remove': [], 'vif': [], - 'vif_remove': [] + 'vif_remove': [], + 'vrf': '' } def get_config(): @@ -67,10 +73,10 @@ def get_config(): conf = Config() # determine tagNode instance - try: - eth['intf'] = os.environ['VYOS_TAGNODE_VALUE'] - except KeyError as E: - print("Interface not specified") + if 'VYOS_TAGNODE_VALUE' not in os.environ: + raise ConfigError('Interface (VYOS_TAGNODE_VALUE) not specified') + + eth['intf'] = os.environ['VYOS_TAGNODE_VALUE'] # check if ethernet interface has been removed cfg_base = ['interfaces', 'ethernet', eth['intf']] @@ -165,6 +171,22 @@ def get_config(): 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') @@ -197,6 +219,10 @@ 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') + # re-set configuration level to parse new nodes conf.set_level(cfg_base) # get vif-s interfaces (currently effective) - to determine which vif-s @@ -232,6 +258,9 @@ def verify(eth): if eth['deleted']: return None + if eth['intf'] not in interfaces(): + raise ConfigError(f"Interface ethernet {eth['intf']} does not exist") + if eth['speed'] == 'auto': if eth['duplex'] != 'auto': raise ConfigError('If speed is hardcoded, duplex must be hardcoded, too') @@ -243,6 +272,10 @@ 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') + conf = Config() # some options can not be changed when interface is enslaved to a bond for bond in conf.list_nodes('interfaces bonding'): @@ -250,21 +283,10 @@ def verify(eth): bond_member = conf.return_values('interfaces bonding ' + bond + ' member interface') if eth['intf'] in bond_member: if eth['address']: - raise ConfigError('Can not assign address to interface {} which is a member of {}').format(eth['intf'], bond) - - # DHCPv6 parameters-only and temporary address are mutually exclusive - for vif_s in eth['vif_s']: - if vif_s['dhcpv6_prm_only'] and vif_s['dhcpv6_temporary']: - raise ConfigError('DHCPv6 temporary and parameters-only options are mutually exclusive!') - - for vif_c in vif_s['vif_c']: - if vif_c['dhcpv6_prm_only'] and vif_c['dhcpv6_temporary']: - raise ConfigError('DHCPv6 temporary and parameters-only options are mutually exclusive!') - - for vif in eth['vif']: - if vif['dhcpv6_prm_only'] and vif['dhcpv6_temporary']: - raise ConfigError('DHCPv6 temporary and parameters-only options are mutually exclusive!') + raise ConfigError(f"Can not assign address to interface {eth['intf']} which is a member of {bond}") + # use common function to verify VLAN configuration + verify_vlan_config(eth) return None def generate(eth): @@ -324,12 +346,20 @@ 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 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']) # Change interface MAC address - re-set to real hardware address (hw-id) # if custom mac is removed if eth['mac']: e.set_mac(eth['mac']) - else: + elif eth['hw_id']: e.set_mac(eth['hw_id']) # Maximum Transmission Unit (MTU) @@ -355,9 +385,9 @@ def apply(eth): # Enable/Disable interface if eth['disable']: - e.set_state('down') + e.set_admin_state('down') else: - e.set_state('up') + e.set_admin_state('up') # Configure interface address(es) # - not longer required addresses get removed first @@ -367,6 +397,9 @@ 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) diff --git a/src/conf_mode/interfaces-geneve.py b/src/conf_mode/interfaces-geneve.py index b0c381656..e47473d76 100755 --- a/src/conf_mode/interfaces-geneve.py +++ b/src/conf_mode/interfaces-geneve.py @@ -18,11 +18,12 @@ import os from sys import exit from copy import deepcopy +from netifaces import interfaces from vyos.config import Config -from vyos.ifconfig import GeneveIf, Interface +from vyos.ifconfig import GeneveIf +from vyos.util import is_bridge_member from vyos import ConfigError -from netifaces import interfaces default_config_data = { 'address': [], @@ -42,10 +43,10 @@ def get_config(): conf = Config() # determine tagNode instance - try: - geneve['intf'] = os.environ['VYOS_TAGNODE_VALUE'] - except KeyError as E: - print("Interface not specified") + if 'VYOS_TAGNODE_VALUE' not in os.environ: + raise ConfigError('Interface (VYOS_TAGNODE_VALUE) not specified') + + geneve['intf'] = os.environ['VYOS_TAGNODE_VALUE'] # Check if interface has been removed if not conf.exists('interfaces geneve ' + geneve['intf']): @@ -92,7 +93,13 @@ def get_config(): def verify(geneve): if geneve['deleted']: - # bail out early + interface = geneve['intf'] + is_member, bridge = is_bridge_member(interface) + if is_member: + # can not use a f'' formatted-string here as bridge would not get + # expanded in the print statement + raise ConfigError('Can not delete interface "{0}" as it ' \ + 'is a member of bridge "{1}"!'.format(interface, bridge)) return None if not geneve['remote']: @@ -127,7 +134,7 @@ def apply(geneve): conf['remote'] = geneve['remote'] # Finally create the new interface - g = GeneveIf(geneve['intf'], config=conf) + g = GeneveIf(geneve['intf'], **conf) # update interface description used e.g. by SNMP g.set_alias(geneve['description']) # Maximum Transfer Unit (MTU) @@ -148,7 +155,7 @@ def apply(geneve): # parameters we will only re-enable the interface if it is not # administratively disabled if not geneve['disable']: - g.set_state('up') + g.set_admin_state('up') return None diff --git a/src/conf_mode/interfaces-l2tpv3.py b/src/conf_mode/interfaces-l2tpv3.py index ae49dadad..11ba9acdd 100755 --- a/src/conf_mode/interfaces-l2tpv3.py +++ b/src/conf_mode/interfaces-l2tpv3.py @@ -22,6 +22,8 @@ from copy import deepcopy from vyos.config import Config from vyos.ifconfig import L2TPv3If, Interface from vyos import ConfigError +from vyos.util import call +from vyos.util import is_bridge_member from netifaces import interfaces default_config_data = { @@ -33,6 +35,10 @@ default_config_data = { 'local_address': '', 'local_port': 5000, 'intf': '', + 'ipv6_autoconf': 0, + 'ipv6_eui64_prefix': '', + 'ipv6_forwarding': 1, + 'ipv6_dup_addr_detect': 1, 'mtu': 1488, 'peer_session_id': '', 'peer_tunnel_id': '', @@ -42,15 +48,22 @@ default_config_data = { 'tunnel_id': '' } +def check_kmod(): + modules = ['l2tp_eth', 'l2tp_netlink', 'l2tp_ip', 'l2tp_ip6'] + 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_config(): l2tpv3 = deepcopy(default_config_data) conf = Config() # determine tagNode instance - try: - l2tpv3['intf'] = os.environ['VYOS_TAGNODE_VALUE'] - except KeyError as E: - print("Interface not specified") + if 'VYOS_TAGNODE_VALUE' not in os.environ: + raise ConfigError('Interface (VYOS_TAGNODE_VALUE) not specified') + + l2tpv3['intf'] = os.environ['VYOS_TAGNODE_VALUE'] # Check if interface has been removed if not conf.exists('interfaces l2tpv3 ' + l2tpv3['intf']): @@ -94,6 +107,22 @@ def get_config(): if conf.exists('local-ip'): l2tpv3['local_address'] = conf.return_value('local-ip') + # Enable acquisition of IPv6 address using stateless autoconfig (SLAAC) + if conf.exists('ipv6 address autoconf'): + l2tpv3['ipv6_autoconf'] = 1 + + # Get prefix 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') + + # Disable IPv6 forwarding on this interface + if conf.exists('ipv6 disable-forwarding'): + l2tpv3['ipv6_forwarding'] = 0 + + # IPv6 Duplicate Address Detection (DAD) tries + if conf.exists('ipv6 dup-addr-detect-transmits'): + l2tpv3['ipv6_dup_addr_detect'] = int(conf.return_value('ipv6 dup-addr-detect-transmits')) + # Maximum Transmission Unit (MTU) if conf.exists('mtu'): l2tpv3['mtu'] = int(conf.return_value('mtu')) @@ -126,61 +155,41 @@ def get_config(): def verify(l2tpv3): + interface = l2tpv3['intf'] + if l2tpv3['deleted']: - # bail out early + is_member, bridge = is_bridge_member(interface) + if is_member: + # can not use a f'' formatted-string here as bridge would not get + # expanded in the print statement + raise ConfigError('Can not delete interface "{0}" as it ' \ + 'is a member of bridge "{1}"!'.format(interface, bridge)) return None if not l2tpv3['local_address']: - raise ConfigError('Must configure the l2tpv3 local-ip for {}'.format(l2tpv3['intf'])) + raise ConfigError(f'Must configure the l2tpv3 local-ip for {interface}') if not l2tpv3['remote_address']: - raise ConfigError('Must configure the l2tpv3 remote-ip for {}'.format(l2tpv3['intf'])) + raise ConfigError(f'Must configure the l2tpv3 remote-ip for {interface}') if not l2tpv3['tunnel_id']: - raise ConfigError('Must configure the l2tpv3 tunnel-id for {}'.format(l2tpv3['intf'])) + raise ConfigError(f'Must configure the l2tpv3 tunnel-id for {interface}') if not l2tpv3['peer_tunnel_id']: - raise ConfigError('Must configure the l2tpv3 peer-tunnel-id for {}'.format(l2tpv3['intf'])) + raise ConfigError(f'Must configure the l2tpv3 peer-tunnel-id for {interface}') if not l2tpv3['session_id']: - raise ConfigError('Must configure the l2tpv3 session-id for {}'.format(l2tpv3['intf'])) + raise ConfigError(f'Must configure the l2tpv3 session-id for {interface}') if not l2tpv3['peer_session_id']: - raise ConfigError('Must configure the l2tpv3 peer-session-id for {}'.format(l2tpv3['intf'])) + raise ConfigError(f'Must configure the l2tpv3 peer-session-id for {interface}') return None def generate(l2tpv3): - if l2tpv3['deleted']: - # bail out early - return None - - # initialize kernel module if not loaded - if not os.path.isdir('/sys/module/l2tp_eth'): - if os.system('modprobe l2tp_eth') != 0: - raise ConfigError("failed loading l2tp_eth kernel module") - - if not os.path.isdir('/sys/module/l2tp_netlink'): - if os.system('modprobe l2tp_netlink') != 0: - raise ConfigError("failed loading l2tp_netlink kernel module") - - if not os.path.isdir('/sys/module/l2tp_ip'): - if os.system('modprobe l2tp_ip') != 0: - raise ConfigError("failed loading l2tp_ip kernel module") - - if l2tpv3['encapsulation'] == 'ip': - if not os.path.isdir('/sys/module/l2tp_ip'): - if os.system('modprobe l2tp_ip') != 0: - raise ConfigError("failed loading l2tp_ip kernel module") - - if not os.path.isdir('/sys/module/l2tp_ip6 '): - if os.system('modprobe l2tp_ip6 ') != 0: - raise ConfigError("failed loading l2tp_ip6 kernel module") - return None - def apply(l2tpv3): # L2TPv3 interface needs to be created/deleted on-block, instead of # passing a ton of arguments, I just use a dict that is managed by @@ -193,7 +202,7 @@ def apply(l2tpv3): # always delete it first. conf['session_id'] = l2tpv3['session_id'] conf['tunnel_id'] = l2tpv3['tunnel_id'] - l = L2TPv3If(l2tpv3['intf'], config=conf) + l = L2TPv3If(l2tpv3['intf'], **conf) l.remove() if not l2tpv3['deleted']: @@ -208,11 +217,19 @@ def apply(l2tpv3): conf['peer_session_id'] = l2tpv3['peer_session_id'] # Finally create the new interface - l = L2TPv3If(l2tpv3['intf'], config=conf) + l = L2TPv3If(l2tpv3['intf'], **conf) # update interface description used e.g. by SNMP l.set_alias(l2tpv3['description']) # Maximum Transfer Unit (MTU) l.set_mtu(l2tpv3['mtu']) + # 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 + l.set_ipv6_dad_messages(l2tpv3['ipv6_dup_addr_detect']) # Configure interface address(es) - no need to implicitly delete the # old addresses as they have already been removed by deleting the @@ -224,12 +241,13 @@ def apply(l2tpv3): # we will only re-enable the interface if it is not administratively # disabled if not l2tpv3['disable']: - l.set_state('up') + l.set_admin_state('up') return None if __name__ == '__main__': try: + check_kmod() c = get_config() verify(c) generate(c) diff --git a/src/conf_mode/interfaces-loopback.py b/src/conf_mode/interfaces-loopback.py index 10722d137..ddd18ae24 100755 --- a/src/conf_mode/interfaces-loopback.py +++ b/src/conf_mode/interfaces-loopback.py @@ -37,10 +37,10 @@ def get_config(): conf = Config() # determine tagNode instance - try: - loopback['intf'] = os.environ['VYOS_TAGNODE_VALUE'] - except KeyError as E: - print("Interface not specified") + if 'VYOS_TAGNODE_VALUE' not in os.environ: + raise ConfigError('Interface (VYOS_TAGNODE_VALUE) not specified') + + loopback['intf'] = os.environ['VYOS_TAGNODE_VALUE'] # Check if interface has been removed if not conf.exists('interfaces loopback ' + loopback['intf']): diff --git a/src/conf_mode/interfaces-openvpn.py b/src/conf_mode/interfaces-openvpn.py index 6bd269e97..f34e4f7fe 100755 --- a/src/conf_mode/interfaces-openvpn.py +++ b/src/conf_mode/interfaces-openvpn.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2019 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 @@ -17,262 +17,27 @@ import os import re -from jinja2 import Template +from jinja2 import FileSystemLoader, Environment from copy import deepcopy from sys import exit from stat import S_IRUSR,S_IRWXU,S_IRGRP,S_IXGRP,S_IROTH,S_IXOTH from grp import getgrnam from ipaddress import ip_address,ip_network,IPv4Interface from netifaces import interfaces -from psutil import pid_exists from pwd import getpwnam -from subprocess import Popen, PIPE from time import sleep +from shutil import rmtree -from vyos import ConfigError from vyos.config import Config -from vyos.ifconfig import Interface +from vyos.defaults import directories as vyos_data_dir +from vyos.ifconfig import VTunIf +from vyos.util import process_running, cmd, is_bridge_member from vyos.validate import is_addr_assigned +from vyos import ConfigError user = 'openvpn' group = 'openvpn' -# Please be careful if you edit the template. -config_tmpl = """ -### Autogenerated by interfaces-openvpn.py ### -# -# See https://community.openvpn.net/openvpn/wiki/Openvpn24ManPage -# for individual keyword definition - -{% if description %} -# {{ description }} -{% endif %} - -verb 3 -status /opt/vyatta/etc/openvpn/status/{{ intf }}.status 30 -writepid /var/run/openvpn/{{ intf }}.pid - -dev-type {{ type }} -dev {{ intf }} -user {{ uid }} -group {{ gid }} -persist-key -iproute /usr/libexec/vyos/system/unpriv-ip - -proto {% if 'tcp-active' in protocol -%}tcp-client{% elif 'tcp-passive' in protocol -%}tcp-server{% else %}udp{% endif %} - -{%- if local_host %} -local {{ local_host }} -{% endif %} - -{%- if local_port %} -lport {{ local_port }} -{% endif %} - -{%- if remote_port %} -rport {{ remote_port }} -{% endif %} - -{%- if remote_host %} -{% for remote in remote_host -%} -remote {{ remote }} -{% endfor -%} -{% endif %} - -{%- if shared_secret_file %} -secret {{ shared_secret_file }} -{% endif %} - -{%- if persistent_tunnel %} -persist-tun -{% endif %} - -{%- if mode %} -{%- if 'client' in mode %} -# -# OpenVPN Client mode -# -client -nobind -{%- elif 'server' in mode %} -# -# OpenVPN Server mode -# -mode server -tls-server -keepalive {{ ping_interval }} {{ ping_restart }} -management /tmp/openvpn-mgmt-intf unix - -{%- if server_topology %} -topology {% if 'point-to-point' in server_topology %}p2p{% else %}subnet{% endif %} -{% endif %} - -{% for ns in server_dns_nameserver -%} -push "dhcp-option DNS {{ ns }}" -{% endfor -%} - -{% for route in server_push_route -%} -push "route {{ route }}" -{% endfor -%} - -{%- if server_domain %} -push "dhcp-option DOMAIN {{ server_domain }}" -{% endif %} - -{%- if server_max_conn %} -max-clients {{ server_max_conn }} -{% endif %} - -{%- if bridge_member %} -server-bridge nogw -{%- else %} -server {{ server_subnet }} -{% endif %} - -{%- if server_reject_unconfigured %} -ccd-exclusive -{% endif %} - -{%- else %} -# -# OpenVPN site-2-site mode -# -ping {{ ping_interval }} -ping-restart {{ ping_restart }} - -{%- if local_address_subnet %} -ifconfig {{ local_address }} {{ local_address_subnet }} -{% elif remote_address %} -ifconfig {{ local_address }} {{ remote_address }} -{% endif %} - -{% endif %} -{% endif %} - -{%- if tls_ca_cert %} -ca {{ tls_ca_cert }} -{% endif %} - -{%- if tls_cert %} -cert {{ tls_cert }} -{% endif %} - -{%- if tls_key %} -key {{ tls_key }} -{% endif %} - -{%- if tls_crl %} -crl-verify {{ tls_crl }} -{% endif %} - -{%- if tls_version_min %} -tls-version-min {{tls_version_min}} -{% endif %} - -{%- if tls_dh %} -dh {{ tls_dh }} -{% endif %} - -{%- if tls_auth %} -tls-auth {{tls_auth}} -{% endif %} - -{%- if 'active' in tls_role %} -tls-client -{%- elif 'passive' in tls_role %} -tls-server -{% endif %} - -{%- if redirect_gateway %} -push "redirect-gateway {{ redirect_gateway }}" -{% endif %} - -{%- if compress_lzo %} -compress lzo -{% endif %} - -{%- if hash %} -auth {{ hash }} -{% endif %} - -{%- if encryption %} -{%- if 'des' in encryption %} -cipher des-cbc -{%- elif '3des' in encryption %} -cipher des-ede3-cbc -{%- elif 'bf128' in encryption %} -cipher bf-cbc -keysize 128 -{%- elif 'bf256' in encryption %} -cipher bf-cbc -keysize 25 -{%- elif 'aes128gcm' in encryption %} -cipher aes-128-gcm -{%- elif 'aes128' in encryption %} -cipher aes-128-cbc -{%- elif 'aes192gcm' in encryption %} -cipher aes-192-gcm -{%- elif 'aes192' in encryption %} -cipher aes-192-cbc -{%- elif 'aes256gcm' in encryption %} -cipher aes-256-gcm -{%- elif 'aes256' in encryption %} -cipher aes-256-cbc -{% endif %} -{% endif %} - -{%- if ncp_ciphers %} -ncp-ciphers {{ncp_ciphers}} -{% endif %} -{%- if disable_ncp %} -ncp-disable -{% endif %} - -{%- if auth %} -auth-user-pass /tmp/openvpn-{{ intf }}-pw -auth-retry nointeract -{% endif %} - -{%- if client %} -client-config-dir /opt/vyatta/etc/openvpn/ccd/{{ intf }} -{% endif %} - -# DEPRECATED This option will be removed in OpenVPN 2.5 -# Until OpenVPN v2.3 the format of the X.509 Subject fields was formatted like this: -# /C=US/L=Somewhere/CN=John Doe/emailAddress=john@example.com In addition the old -# behaviour was to remap any character other than alphanumeric, underscore ('_'), -# dash ('-'), dot ('.'), and slash ('/') to underscore ('_'). The X.509 Subject -# string as returned by the tls_id environmental variable, could additionally -# contain colon (':') or equal ('='). When using the --compat-names option, this -# old formatting and remapping will be re-enabled again. This is purely implemented -# for compatibility reasons when using older plug-ins or scripts which does not -# handle the new formatting or UTF-8 characters. -# -# See https://phabricator.vyos.net/T1512 -compat-names - -{% for option in options -%} -{{ option }} -{% endfor -%} -""" - -client_tmpl = """ -### Autogenerated by interfaces-openvpn.py ### - -ifconfig-push {{ ip }} {{ remote_netmask }} -{% for route in push_route -%} -push "route {{ route }}" -{% endfor -%} - -{% for net in subnet -%} -iroute {{ net }} -{% endfor -%} - -{%- if disable %} -disable -{% endif %} -""" - default_config_data = { 'address': [], 'auth_user': '', @@ -287,6 +52,10 @@ default_config_data = { 'encryption': '', 'hash': '', 'intf': '', + 'ipv6_autoconf': 0, + 'ipv6_eui64_prefix': '', + 'ipv6_forwarding': 1, + 'ipv6_dup_addr_detect': 1, 'ping_restart': '60', 'ping_interval': '10', 'local_address': '', @@ -297,7 +66,7 @@ default_config_data = { 'ncp_ciphers': '', 'options': [], 'persistent_tunnel': False, - 'protocol': '', + 'protocol': 'udp', 'redirect_gateway': '', 'remote_address': '', 'remote_host': [], @@ -318,6 +87,7 @@ default_config_data = { 'tls_crl': '', 'tls_dh': '', 'tls_key': '', + 'tls_crypt': '', 'tls_role': '', 'tls_version_min': '', 'type': 'tun', @@ -325,9 +95,6 @@ default_config_data = { 'gid': group, } -def subprocess_cmd(command): - p = Popen(command, stdout=PIPE, shell=True) - p.communicate() def get_config_name(intf): cfg_file = r'/opt/vyatta/etc/openvpn/openvpn-{}.conf'.format(intf) @@ -360,7 +127,7 @@ def fixup_permission(filename, permission=S_IRUSR): def checkCertHeader(header, filename): """ Verify if filename contains specified header. - Returns True on success or on file not found to not trigger the exceptions + Returns True if match is found, False if no match or file is not found """ if not os.path.isfile(filename): return False @@ -370,17 +137,17 @@ def checkCertHeader(header, filename): if re.match(header, line): return True - return True + return False def get_config(): openvpn = deepcopy(default_config_data) conf = Config() # determine tagNode instance - try: - openvpn['intf'] = os.environ['VYOS_TAGNODE_VALUE'] - except KeyError as E: - print("Interface not specified") + if 'VYOS_TAGNODE_VALUE' not in os.environ: + raise ConfigError('Interface (VYOS_TAGNODE_VALUE) not specified') + + openvpn['intf'] = os.environ['VYOS_TAGNODE_VALUE'] # Check if interface instance has been removed if not conf.exists('interfaces openvpn ' + openvpn['intf']): @@ -482,10 +249,25 @@ def get_config(): if conf.exists('local-port'): openvpn['local_port'] = conf.return_value('local-port') + # Enable acquisition of IPv6 address using stateless autoconfig (SLAAC) + if conf.exists('ipv6 address autoconf'): + openvpn['ipv6_autoconf'] = 1 + + # Get prefix 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') + + # Disable IPv6 forwarding on this interface + if conf.exists('ipv6 disable-forwarding'): + openvpn['ipv6_forwarding'] = 0 + + # IPv6 Duplicate Address Detection (DAD) tries + if conf.exists('ipv6 dup-addr-detect-transmits'): + openvpn['ipv6_dup_addr_detect'] = int(conf.return_value('ipv6 dup-addr-detect-transmits')) + # OpenVPN operation mode if conf.exists('mode'): - mode = conf.return_value('mode') - openvpn['mode'] = mode + openvpn['mode'] = conf.return_value('mode') # Additional OpenVPN options if conf.exists('openvpn-option'): @@ -633,6 +415,11 @@ def get_config(): openvpn['tls_key'] = conf.return_value('tls key-file') openvpn['tls'] = True + # File containing key to encrypt control channel packets + if conf.exists('tls crypt-file'): + openvpn['tls_crypt'] = conf.return_value('tls crypt-file') + openvpn['tls'] = True + # Role in TLS negotiation if conf.exists('tls role'): openvpn['tls_role'] = conf.return_value('tls role') @@ -641,6 +428,7 @@ def get_config(): # Minimum required TLS version if conf.exists('tls tls-version-min'): openvpn['tls_version_min'] = conf.return_value('tls tls-version-min') + openvpn['tls'] = True if conf.exists('shared-secret-key-file'): openvpn['shared_secret_file'] = conf.return_value('shared-secret-key-file') @@ -648,12 +436,25 @@ def get_config(): if conf.exists('use-lzo-compression'): openvpn['compress_lzo'] = True + # Special case when using EC certificates: + # if key-file is EC and dh-file is unset, set tls_dh to 'none' + if not openvpn['tls_dh'] and openvpn['tls_key'] and checkCertHeader('-----BEGIN EC PRIVATE KEY-----', openvpn['tls_key']): + openvpn['tls_dh'] = 'none' + return openvpn def verify(openvpn): if openvpn['deleted']: + interface = openvpn['intf'] + is_member, bridge = is_bridge_member(interface) + if is_member: + # can not use a f'' formatted-string here as bridge would not get + # expanded in the print statement + raise ConfigError('Can not delete interface "{0}" as it ' \ + 'is a member of bridge "{1}"!'.format(interface, bridge)) return None + if not openvpn['mode']: raise ConfigError('Must specify OpenVPN operation mode') @@ -682,7 +483,7 @@ def verify(openvpn): if not openvpn['remote_host']: raise ConfigError('Must specify "remote-host" in client mode') - if openvpn['tls_dh']: + if openvpn['tls_dh'] and openvpn['tls_dh'] != 'none': raise ConfigError('Cannot specify "tls dh-file" in client mode') # @@ -732,8 +533,8 @@ def verify(openvpn): if openvpn['protocol'] == 'tcp-passive' and len(openvpn['remote_host']) > 1: raise ConfigError('Cannot specify more than 1 "remote-host" with "tcp-passive"') - if not openvpn['tls_dh']: - raise ConfigError('Must specify "tls dh-file" in server mode') + if not openvpn['tls_dh'] and not checkCertHeader('-----BEGIN EC PRIVATE KEY-----', openvpn['tls_key']): + raise ConfigError('Must specify "tls dh-file" when not using EC keys in server mode') if not openvpn['server_subnet']: if not openvpn['bridge_member']: @@ -800,6 +601,9 @@ def verify(openvpn): if not openvpn['tls_key']: raise ConfigError('Must specify "tls key-file"') + if openvpn['tls_auth'] and openvpn['tls_crypt']: + raise ConfigError('TLS auth and crypt are mutually exclusive') + if not checkCertHeader('-----BEGIN CERTIFICATE-----', openvpn['tls_ca_cert']): raise ConfigError('Specified ca-cert-file "{}" is invalid'.format(openvpn['tls_ca_cert'])) @@ -812,14 +616,18 @@ def verify(openvpn): raise ConfigError('Specified cert-file "{}" is invalid'.format(openvpn['tls_cert'])) if openvpn['tls_key']: - if not checkCertHeader('-----BEGIN (?:RSA )?PRIVATE KEY-----', openvpn['tls_key']): + if not checkCertHeader('-----BEGIN (?:RSA |EC )?PRIVATE KEY-----', openvpn['tls_key']): raise ConfigError('Specified key-file "{}" is not valid'.format(openvpn['tls_key'])) + if openvpn['tls_crypt']: + if not checkCertHeader('-----BEGIN OpenVPN Static key V1-----', openvpn['tls_crypt']): + raise ConfigError('Specified TLS crypt-file "{}" is invalid'.format(openvpn['tls_crypt'])) + if openvpn['tls_crl']: if not checkCertHeader('-----BEGIN X509 CRL-----', openvpn['tls_crl']): raise ConfigError('Specified crl-file "{} not valid'.format(openvpn['tls_crl'])) - if openvpn['tls_dh']: + if openvpn['tls_dh'] and openvpn['tls_dh'] != 'none': if not checkCertHeader('-----BEGIN DH PARAMETERS-----', openvpn['tls_dh']): raise ConfigError('Specified dh-file "{}" is not valid'.format(openvpn['tls_dh'])) @@ -832,7 +640,7 @@ def verify(openvpn): if openvpn['protocol'] == 'tcp-passive': raise ConfigError('Cannot specify "tcp-passive" when "tls role" is "active"') - if openvpn['tls_dh']: + if openvpn['tls_dh'] and openvpn['tls_dh'] != 'none': raise ConfigError('Cannot specify "tls dh-file" when "tls role" is "active"') elif openvpn['tls_role'] == 'passive': @@ -842,6 +650,12 @@ def verify(openvpn): if not openvpn['tls_dh']: raise ConfigError('Must specify "tls dh-file" when "tls role" is "passive"') + if openvpn['tls_key'] and checkCertHeader('-----BEGIN EC PRIVATE KEY-----', openvpn['tls_key']): + if openvpn['tls_dh'] and openvpn['tls_dh'] != 'none': + print('Warning: using dh-file and EC keys simultaneously will lead to DH ciphers being used instead of ECDH') + else: + print('Diffie-Hellman prime file is unspecified, assuming ECDH') + # # Auth user/pass # @@ -857,20 +671,27 @@ def verify(openvpn): # subnet = openvpn['server_subnet'].replace(' ', '/') for client in openvpn['client']: - if not ip_address(client['ip']) in ip_network(subnet): + if client['ip'] and not ip_address(client['ip']) in ip_network(subnet): raise ConfigError('Client IP "{}" not in server subnet "{}'.format(client['ip'], subnet)) - - return None def generate(openvpn): if openvpn['deleted'] or openvpn['disable']: return None + # Prepare Jinja2 template loader from files + tmpl_path = os.path.join(vyos_data_dir['data'], 'templates', 'openvpn') + fs_loader = FileSystemLoader(tmpl_path) + env = Environment(loader=fs_loader) + interface = openvpn['intf'] directory = os.path.dirname(get_config_name(interface)) + # we can't know which clients were deleted, remove all client configs + if os.path.isdir(os.path.join(directory, 'ccd', interface)): + rmtree(os.path.join(directory, 'ccd', interface), ignore_errors=True) + # create config directory on demand openvpn_mkdir(directory) # create status directory on demand @@ -892,6 +713,11 @@ def generate(openvpn): fixup_permission(auth_file) + else: + # delete old auth file if present + if os.path.isfile('/tmp/openvpn-{}-pw'.format(interface)): + os.remove('/tmp/openvpn-{}-pw'.format(interface)) + # get numeric uid/gid uid = getpwnam(user).pw_uid gid = getgrnam(group).gr_gid @@ -899,19 +725,17 @@ def generate(openvpn): # Generate client specific configuration for client in openvpn['client']: client_file = directory + '/ccd/' + interface + '/' + client['name'] - tmpl = Template(client_tmpl) + tmpl = env.get_template('client.conf.tmpl') client_text = tmpl.render(client) with open(client_file, 'w') as f: f.write(client_text) os.chown(client_file, uid, gid) - tmpl = Template(config_tmpl) + tmpl = env.get_template('server.conf.tmpl') config_text = tmpl.render(openvpn) - # we need to support quoting of raw parameters from OpenVPN CLI # see https://phabricator.vyos.net/T1632 config_text = config_text.replace(""",'"') - with open(get_config_name(interface), 'w') as f: f.write(config_text) os.chown(get_config_name(interface), uid, gid) @@ -919,20 +743,18 @@ def generate(openvpn): return None def apply(openvpn): - pid = 0 pidfile = '/var/run/openvpn/{}.pid'.format(openvpn['intf']) - if os.path.isfile(pidfile): - pid = 0 - with open(pidfile, 'r') as f: - pid = int(f.read()) # Always stop OpenVPN service. We can not send a SIGUSR1 for restart of the # service as the configuration is not re-read. Stop daemon only if it's # running - it could have died or killed by someone evil - if pid_exists(pid): - cmd = 'start-stop-daemon --stop --quiet' - cmd += ' --pidfile ' + pidfile - subprocess_cmd(cmd) + if process_running(pidfile): + command = 'start-stop-daemon' + command += ' --stop ' + command += ' --quiet' + command += ' --oknodo' + command += ' --pidfile ' + pidfile + cmd(command) # cleanup old PID file if os.path.isfile(pidfile): @@ -946,11 +768,12 @@ def apply(openvpn): # cleanup client config dir directory = os.path.dirname(get_config_name(openvpn['intf'])) - if os.path.isdir(directory + '/ccd/' + openvpn['intf']): - try: - os.remove(directory + '/ccd/' + openvpn['intf'] + '/*') - except: - pass + if os.path.isdir(os.path.join(directory, 'ccd', openvpn['intf'])): + rmtree(os.path.join(directory, 'ccd', openvpn['intf']), ignore_errors=True) + + # cleanup auth file + if os.path.isfile('/tmp/openvpn-{}-pw'.format(openvpn['intf'])): + os.remove('/tmp/openvpn-{}-pw'.format(openvpn['intf'])) return None @@ -962,16 +785,19 @@ def apply(openvpn): # No matching OpenVPN process running - maybe it got killed or none # existed - nevertheless, spawn new OpenVPN process - cmd = 'start-stop-daemon --start --quiet' - cmd += ' --pidfile ' + pidfile - cmd += ' --exec /usr/sbin/openvpn' + command = 'start-stop-daemon' + command += ' --start ' + command += ' --quiet' + command += ' --oknodo' + command += ' --pidfile ' + pidfile + command += ' --exec /usr/sbin/openvpn' # now pass arguments to openvpn binary - cmd += ' --' - cmd += ' --daemon openvpn-' + openvpn['intf'] - cmd += ' --config ' + get_config_name(openvpn['intf']) + command += ' --' + command += ' --daemon openvpn-' + openvpn['intf'] + command += ' --config ' + get_config_name(openvpn['intf']) # execute assembled command - subprocess_cmd(cmd) + cmd(command) # better late then sorry ... but we can only set interface alias after # OpenVPN has been launched and created the interface @@ -991,14 +817,25 @@ def apply(openvpn): try: # we need to catch the exception if the interface is not up due to # reason stated above - Interface(openvpn['intf']).set_alias(openvpn['description']) + o = VTunIf(openvpn['intf']) + # update interface description used e.g. within SNMP + o.set_alias(openvpn['description']) + # 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']) + except: pass # TAP interface needs to be brought up explicitly if openvpn['type'] == 'tap': if not openvpn['disable']: - Interface(openvpn['intf']).set_state('up') + VTunIf(openvpn['intf']).set_admin_state('up') return None diff --git a/src/conf_mode/interfaces-pppoe.py b/src/conf_mode/interfaces-pppoe.py index 6acb45d5e..353a5a12c 100755 --- a/src/conf_mode/interfaces-pppoe.py +++ b/src/conf_mode/interfaces-pppoe.py @@ -18,85 +18,14 @@ import os from sys import exit from copy import deepcopy -from jinja2 import Template -from subprocess import Popen, PIPE -from time import sleep -from pwd import getpwnam -from grp import getgrnam +from jinja2 import FileSystemLoader, Environment +from netifaces import interfaces from vyos.config import Config +from vyos.defaults import directories as vyos_data_dir from vyos.ifconfig import Interface +from vyos.util import chown, chmod_x, cmd from vyos import ConfigError -from netifaces import interfaces - -# Please be careful if you edit the template. -config_pppoe_tmpl = """ -### Autogenerated by interfaces-pppoe.py ### - -{% if description %} -# {{ description }} -{% endif %} - -# Require peer to provide the local IP address if it is not -# specified explicitly in the config file. -noipdefault - -# Don't show the password in logfiles: -hide-password - -# Standard Link Control Protocol (LCP) parameters: -lcp-echo-interval 20 -lcp-echo-failure 3 - -# RFC 2516, paragraph 7 mandates that the following options MUST NOT be -# requested and MUST be rejected if requested by the peer: -# Address-and-Control-Field-Compression (ACFC) -noaccomp - -# Asynchronous-Control-Character-Map (ACCM) -default-asyncmap - -# Override any connect script that may have been set in /etc/ppp/options. -connect /bin/true - -# Don't try to authenticate the remote node -noauth - -# Don't try to proxy ARP for the remote endpoint. User can set proxy -# arp entries up manually if they wish. More importantly, having -# the "proxyarp" parameter set disables the "defaultroute" option. -noproxyarp - -plugin rp-pppoe.so -{{ source_interface }} -persist -ifname {{ intf }} -ipparam {{ intf }} -debug -logfile {{ logfile }} -{% if 'auto' in default_route -%} -defaultroute -{% elif 'force' in default_route -%} -defaultroute -replacedefaultroute -{% endif %} -mtu {{ mtu }} -mru {{ mtu }} -user "{{ auth_username }}" -password "{{ auth_password }}" -{% if name_server -%} -usepeerdns -{% endif %} -{% if ipv6_enable -%} -+ipv6 -{% endif %} -{% if service_name -%} -rp_pppoe_service "{{ service_name }}" -{% endif %} - -""" - -PPP_LOGFILE = '/var/log/vyatta/ppp_{}.log' default_config_data = { 'access_concentrator': '', @@ -105,7 +34,7 @@ default_config_data = { 'on_demand': False, 'default_route': 'auto', 'deleted': False, - 'description': '', + 'description': '\0', 'disable': False, 'intf': '', 'idle_timeout': '', @@ -117,24 +46,21 @@ default_config_data = { 'name_server': True, 'remote_address': '', 'service_name': '', - 'source_interface': '' + 'source_interface': '', + 'vrf': '' } -def subprocess_cmd(command): - p = Popen(command, stdout=PIPE, shell=True) - p.communicate() - def get_config(): pppoe = deepcopy(default_config_data) conf = Config() base_path = ['interfaces', 'pppoe'] # determine tagNode instance - try: - pppoe['intf'] = os.environ['VYOS_TAGNODE_VALUE'] - pppoe['logfile'] = PPP_LOGFILE.format(pppoe['intf']) - except KeyError as E: - print("Interface not specified") + if 'VYOS_TAGNODE_VALUE' not in os.environ: + raise ConfigError('Interface (VYOS_TAGNODE_VALUE) not specified') + + pppoe['intf'] = os.environ['VYOS_TAGNODE_VALUE'] + pppoe['logfile'] = f"/var/log/vyatta/ppp_{pppoe['intf']}.log" # Check if interface has been removed if not conf.exists(base_path + [pppoe['intf']]): @@ -190,7 +116,7 @@ def get_config(): # Physical Interface used for this PPPoE session if conf.exists(['source-interface']): - pppoe['source_interface'] = conf.return_value('source-interface') + pppoe['source_interface'] = conf.return_value(['source-interface']) # Maximum Transmission Unit (MTU) if conf.exists(['mtu']): @@ -208,6 +134,10 @@ def get_config(): if conf.exists(['service-name']): pppoe['service_name'] = conf.return_value(['service-name']) + # retrieve VRF instance + if conf.exists('vrf'): + pppoe['vrf'] = conf.return_value(['vrf']) + return pppoe def verify(pppoe): @@ -216,32 +146,90 @@ def verify(pppoe): return None if not pppoe['source_interface']: - raise ConfigError('PPPoE source interface is missing') + raise ConfigError('PPPoE source interface missing') + + if not pppoe['source_interface'] in interfaces(): + raise ConfigError(f"PPPoE source interface {pppoe['source_interface']} does not exist") + + vrf_name = pppoe['vrf'] + if vrf_name and vrf_name not in interfaces(): + raise ConfigError(f'VRF {vrf_name} does not exist') - if pppoe['source_interface'] not in interfaces(): - raise ConfigError('PPPoE source interface does not exist') + if pppoe['on_demand'] and pppoe['vrf']: + raise ConfigError('On-demand dialing and VRF can not be used at the same time') return None def generate(pppoe): - config_file_pppoe = '/etc/ppp/peers/{}'.format(pppoe['intf']) + # Prepare Jinja2 template loader from files + tmpl_path = os.path.join(vyos_data_dir["data"], "templates", "pppoe") + fs_loader = FileSystemLoader(tmpl_path) + env = Environment(loader=fs_loader, trim_blocks=True) + + # set up configuration file path variables where our templates will be + # rendered into + intf = pppoe['intf'] + config_pppoe = f'/etc/ppp/peers/{intf}' + script_pppoe_pre_up = f'/etc/ppp/ip-pre-up.d/1000-vyos-pppoe-{intf}' + 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_files = [config_pppoe, script_pppoe_pre_up, script_pppoe_ip_up, + script_pppoe_ip_down, script_pppoe_ipv6_up] + + # 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 = 'systemctl stop ppp@{}.service'.format(pppoe['intf']) - subprocess_cmd(cmd) + cmd(f'systemctl stop ppp@{intf}.service') if pppoe['deleted']: # Delete PPP configuration files - if os.path.exists(config_file_pppoe): - os.unlink(config_file_pppoe) + for file in config_files: + if os.path.exists(file): + os.unlink(file) else: # Create PPP configuration files - tmpl = Template(config_pppoe_tmpl) + tmpl = env.get_template('peer.tmpl') + config_text = tmpl.render(pppoe) + with open(config_pppoe, 'w') as f: + f.write(config_text) + + # Create script for ip-pre-up.d + tmpl = env.get_template('ip-pre-up.script.tmpl') config_text = tmpl.render(pppoe) - with open(config_file_pppoe, 'w') as f: + with open(script_pppoe_pre_up, 'w') as f: f.write(config_text) + # Create script for ip-up.d + tmpl = env.get_template('ip-up.script.tmpl') + config_text = tmpl.render(pppoe) + with open(script_pppoe_ip_up, 'w') as f: + f.write(config_text) + + # Create script for ip-down.d + tmpl = env.get_template('ip-down.script.tmpl') + config_text = tmpl.render(pppoe) + with open(script_pppoe_ip_down, 'w') as f: + f.write(config_text) + + # Create script for ipv6-up.d + tmpl = env.get_template('ipv6-up.script.tmpl') + config_text = tmpl.render(pppoe) + with open(script_pppoe_ipv6_up, 'w') as f: + f.write(config_text) + + # make generated script file executable + chmod_x(script_pppoe_pre_up) + chmod_x(script_pppoe_ip_up) + chmod_x(script_pppoe_ip_down) + chmod_x(script_pppoe_ipv6_up) + return None def apply(pppoe): @@ -250,33 +238,12 @@ def apply(pppoe): return None if not pppoe['disable']: - # dial PPPoE connection - cmd = 'systemctl start ppp@{}.service'.format(pppoe['intf']) - subprocess_cmd(cmd) + # "dial" PPPoE connection + intf = pppoe['intf'] + cmd(f'systemctl start ppp@{intf}.service') # make logfile owned by root / vyattacfg - if os.path.isfile(pppoe['logfile']): - uid = getpwnam('root').pw_uid - gid = getgrnam('vyattacfg').gr_gid - os.chown(pppoe['logfile'], uid, gid) - - # better late then sorry ... but we can only set interface alias after - # pppd has been launched and created the interface - cnt = 0 - while pppoe['intf'] not in interfaces(): - cnt += 1 - if cnt == 50: - break - - # sleep 250ms - sleep(0.250) - - try: - # we need to catch the exception if the interface is not up due to - # reason stated above - Interface(pppoe['intf']).set_alias(pppoe['description']) - except: - pass + chown(pppoe['logfile'], 'root', 'vyattacfg') return None diff --git a/src/conf_mode/interfaces-pseudo-ethernet.py b/src/conf_mode/interfaces-pseudo-ethernet.py index 864e28936..ce3d472c4 100755 --- a/src/conf_mode/interfaces-pseudo-ethernet.py +++ b/src/conf_mode/interfaces-pseudo-ethernet.py @@ -21,8 +21,10 @@ from sys import exit from netifaces import interfaces from vyos.ifconfig import MACVLANIf -from vyos.configdict import list_diff +from vyos.ifconfig_vlan import apply_vlan_config, verify_vlan_config +from vyos.configdict import list_diff, vlan_to_dict from vyos.config import Config +from vyos.util import is_bridge_member from vyos import ConfigError default_config_data = { @@ -44,15 +46,20 @@ default_config_data = { '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, 'intf': '', - 'link': '', - 'link_changed': False, + 'source_interface': '', + 'source_interface_changed': False, 'mac': '', 'mode': 'private', 'vif_s': [], 'vif_s_remove': [], 'vif': [], - 'vif_remove': [] + 'vif_remove': [], + 'vrf': '' } def get_config(): @@ -60,11 +67,10 @@ def get_config(): conf = Config() # determine tagNode instance - try: - peth['intf'] = os.environ['VYOS_TAGNODE_VALUE'] - except KeyError as E: - print("Interface not specified") + 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']] # Check if interface has been removed @@ -144,12 +150,28 @@ def get_config(): if conf.exists(['ip', 'proxy-arp-pvlan']): peth['ip_proxy_arp_pvlan'] = 1 - # Lower link device - if conf.exists(['link']): - peth['link'] = conf.return_value(['link']) - tmp = conf.return_effective_value(['link']) - if tmp != peth['link']: - peth['link_changed'] = True + # 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']) + tmp = conf.return_effective_value(['source-interface']) + if tmp != peth['source_interface']: + peth['source_interface_changed'] = True # Media Access Control (MAC) address if conf.exists(['mac']): @@ -159,6 +181,10 @@ def get_config(): 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 @@ -192,11 +218,27 @@ def get_config(): def verify(peth): if peth['deleted']: + interface = peth['intf'] + is_member, bridge = is_bridge_member(interface) + if is_member: + # can not use a f'' formatted-string here as bridge would not get + # expanded in the print statement + raise ConfigError('Can not delete interface "{0}" as it ' \ + 'is a member of bridge "{1}"!'.format(interface, bridge)) return None - if not peth['link']: + if not peth['source_interface']: raise ConfigError('Link device must be set for virtual ethernet {}'.format(peth['intf'])) + if not peth['source_interface'] in interfaces(): + raise ConfigError('Pseudo-ethernet source interface does not exist') + + vrf_name = peth['vrf'] + if vrf_name and vrf_name not in interfaces(): + raise ConfigError(f'VRF "{vrf_name}" does not exist') + + # use common function to verify VLAN configuration + verify_vlan_config(peth) return None def generate(peth): @@ -211,12 +253,12 @@ def apply(peth): p.remove() return None - elif peth['link_changed']: + elif peth['source_interface_changed']: # Check if MACVLAN interface already exists. Parameters like the - # underlaying link device can not be changed on the fly and the - # interface needs to be recreated from the bottom. + # underlaying source-interface device can not be changed on the fly + # and the interface needs to be recreated from the bottom. # - # link_changed also means - the interface was not present in the + # source_interface_changed also means - the interface was not present in the # beginning and is newly created if peth['intf'] in interfaces(): p = MACVLANIf(peth['intf']) @@ -227,12 +269,12 @@ def apply(peth): conf = deepcopy(MACVLANIf.get_config()) # Assign MACVLAN instance configuration parameters to config dict - conf['link'] = peth['link'] + conf['source_interface'] = peth['source_interface'] conf['mode'] = peth['mode'] # It is safe to "re-create" the interface always, there is a sanity check # that the interface will only be create if its non existent - p = MACVLANIf(peth['intf'], config=conf) + p = MACVLANIf(peth['intf'], **conf) else: p = MACVLANIf(peth['intf']) @@ -282,6 +324,17 @@ 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 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']) # Change interface MAC address if peth['mac']: @@ -292,9 +345,9 @@ def apply(peth): # Enable/Disable interface if peth['disable']: - p.set_state('down') + p.set_admin_state('down') else: - p.set_state('up') + p.set_admin_state('up') # Configure interface address(es) # - not longer required addresses get removed first @@ -304,6 +357,35 @@ 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) + return None if __name__ == '__main__': diff --git a/src/conf_mode/interfaces-tunnel.py b/src/conf_mode/interfaces-tunnel.py new file mode 100755 index 000000000..28b1cf60f --- /dev/null +++ b/src/conf_mode/interfaces-tunnel.py @@ -0,0 +1,646 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2019 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +import os +import netifaces + +from sys import exit +from copy import deepcopy + +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 +from vyos import ConfigError + + +class FixedDict(dict): + """ + FixedDict: A dictionnary not allowing new keys to be created after initialisation. + + >>> f = FixedDict(**{'count':1}) + >>> f['count'] = 2 + >>> f['king'] = 3 + File "...", line ..., in __setitem__ + raise ConfigError(f'Option "{k}" has no defined default') + """ + def __init__ (self, **options): + self._allowed = options.keys() + super().__init__(**options) + + def __setitem__ (self, k, v): + """ + __setitem__ is a builtin which is called by python when setting dict values: + >>> d = dict() + >>> d['key'] = 'value' + >>> d + {'key': 'value'} + + is syntaxic sugar for + + >>> d = dict() + >>> d.__setitem__('key','value') + >>> d + {'key': 'value'} + """ + if k not in self._allowed: + raise ConfigError(f'Option "{k}" has no defined default') + super().__setitem__(k, v) + + +class ConfigurationState(Config): + """ + The current API require a dict to be generated by get_config() + which is then consumed by verify(), generate() and apply() + + ConfiguartionState is an helper class wrapping Config and providing + an common API to this dictionary structure + + Its to_dict() function return a dictionary containing three fields, + each a dict, called options, changes, actions. + + options: + + contains the configuration options for the dict and its value + {'options': {'commment': 'test'}} will be set if + 'set interface dummy dum1 description test' was used and + the key 'commment' is used to index the description info. + + changes: + + per key, let us know how the data was modified using one of the action + a special key called 'section' is used to indicate what happened to the + section. for example: + + 'set interface dummy dum1 description test' when no interface was setup + will result in the following changes + {'changes': {'section': 'create', 'comment': 'create'}} + + on an existing interface, depending if there was a description + 'set interface dummy dum1 description test' will result in one of + {'changes': {'comment': 'create'}} (not present before) + {'changes': {'comment': 'static'}} (unchanged) + {'changes': {'comment': 'modify'}} (changed from half) + + and 'delete interface dummy dummy1 description' will result in: + {'changes': {'comment': 'delete'}} + + actions: + + for each action list the configuration key which were changes + in our example if we added the 'description' and added an IP we would have + {'actions': { 'create': ['comment'], 'modify': ['addresses-add']}} + + the actions are: + 'create': it did not exist previously and was created + 'modify': it did exist previously but its content changed + 'static': it did exist and did not change + 'delete': it was present but was removed from the configuration + 'absent': it was not and is not present + which for each field represent how it was modified since the last commit + """ + + def __init__ (self, section, default): + """ + initialise the class for a given configuration path: + + >>> conf = ConfigurationState('interfaces ethernet eth1') + all further references to get_value(s) and get_effective(s) + will be for this part of the configuration (eth1) + """ + super().__init__() + self.section = section + self.default = deepcopy(default) + self.options = FixedDict(**default) + self.actions = { + 'create': [], # the key did not exist and was added + 'static': [], # the key exists and its value was not modfied + 'modify': [], # the key exists and its value was modified + 'absent': [], # the key is not present + 'delete': [], # the key was present and was deleted + } + self.changes = {} + if not self.exists(section): + self.changes['section'] = 'delete' + elif self.exists_effective(section): + self.changes['section'] = 'modify' + else: + self.changes['section'] = 'create' + + def _act(self, section): + """ + Returns for a given configuration field determine what happened to it + + 'create': it did not exist previously and was created + 'modify': it did exist previously but its content changed + 'static': it did exist and did not change + 'delete': it was present but was removed from the configuration + 'absent': it was not and is not present + """ + if self.exists(section): + if self.exists_effective(section): + if self.return_value(section) != self.return_effective_value(section): + return 'modify' + return 'static' + return 'create' + else: + if self.exists_effective(section): + return 'delete' + return 'absent' + + def _action (self, name, key): + action = self._act(key) + self.changes[name] = action + self.actions[action].append(name) + return action + + def _get(self, name, key, default, getter): + value = getter(key) + if not value: + if default: + self.options[name] = default + return + self.options[name] = self.default[name] + return + self.options[name] = value + + def get_value(self, name, key, default=None): + """ + >>> conf.get_value('comment', 'description') + will place the string of 'interface dummy description test' + into the dictionnary entry 'comment' using Config.return_value + (the data in the configuration to apply) + """ + if self._action(name, key) in ('delete', 'absent'): + return + return self._get(name, key, default, self.return_value) + + def get_values(self, name, key, default=None): + """ + >>> conf.get_values('addresses-add', 'address') + will place a list made of the IP present in 'interface dummy dum1 address' + into the dictionnary entry 'addr' using Config.return_values + (the data in the configuration to apply) + """ + if self._action(name, key) in ('delete', 'absent'): + return + return self._get(name, key, default, self.return_values) + + def get_effective(self, name, key, default=None): + """ + >>> conf.get_value('comment', 'description') + will place the string of 'interface dummy description test' + into the dictionnary entry 'comment' using Config.return_effective_value + (the data in the configuration to apply) + """ + self._action(name, key) + return self._get(name, key, default, self.return_effective_value) + + def get_effectives(self, name, key, default=None): + """ + >>> conf.get_effectives('addresses-add', 'address') + will place a list made of the IP present in 'interface ethernet eth1 address' + into the dictionnary entry 'addresses-add' using Config.return_effectives_value + (the data in the un-modified configuration) + """ + self._action(name, key) + return self._get(name, key, default, self.return_effectives_value) + + def load(self, mapping): + """ + load will take a dictionary defining how we wish the configuration + to be parsed and apply this definition to set the data. + + >>> mapping = { + 'addresses-add' : ('address', True, None), + 'comment' : ('description', False, 'auto'), + } + >>> conf.load(mapping) + + mapping is a dictionary where each key represents the name we wish + to have (such as 'addresses-add'), with a list a content representing + how the data should be parsed: + - the configuration section name + such as 'address' under 'interface ethernet eth1' + - boolean indicating if this data can have multiple values + for 'address', True, as multiple IPs can be set + for 'description', False, as it is a single string + - default represent the default value if absent from the configuration + 'None' indicate that no default should be set if the configuration + does not have the configuration section + + """ + for local_name, (config_name, multiple, default) in mapping.items(): + if multiple: + self.get_values(local_name, config_name, default) + else: + self.get_value(local_name, config_name, default) + + def remove_default (self,*options): + """ + 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]: + continue + del self.options[option] + + def to_dict (self): + """ + provide a dictionary with the generated data for the configuration + options: the configuration value for the key + changes: per key how they changed from the previous configuration + actions: per changes all the options which were changed + """ + # as we have to use a dict() for the API for verify and apply the options + return { + 'options': self.options, + 'changes': self.changes, + 'actions': self.actions, + } + +default_config_data = { + # interface definition + 'vrf': '', + 'addresses-add': [], + 'addresses-del': [], + 'state': 'up', + 'dhcp-interface': '', + 'link_detect': 1, + 'ip': False, + 'ipv6': False, + 'nhrp': [], + 'ipv6_autoconf': 0, + 'ipv6_forwarding': 1, + 'ipv6_dad_transmits': 1, + # internal + 'tunnel': {}, + # the following names are exactly matching the name + # for the ip command and must not be changed + 'ifname': '', + 'type': '', + 'alias': '', + 'mtu': '1476', + 'local': '', + 'remote': '', + 'multicast': 'disable', + 'allmulticast': 'disable', + 'ttl': '255', + 'tos': 'inherit', + 'key': '', + 'encaplimit': '4', + 'flowlabel': 'inherit', + 'hoplimit': '64', + 'tclass': 'inherit', + '6rd-prefix': '', + '6rd-relay-prefix': '', +} + +# dict name -> config name, multiple values, default +mapping = { + 'type': ('encapsulation', False, None), + 'alias': ('description', False, None), + 'mtu': ('mtu', False, None), + 'local': ('local-ip', False, None), + 'remote': ('remote-ip', False, None), + 'multicast': ('multicast', False, None), + 'ttl': ('parameters ip ttl', False, None), + 'tos': ('parameters ip tos', False, None), + 'key': ('parameters ip key', False, None), + 'encaplimit': ('parameters ipv6 encaplimit', False, None), + 'flowlabel': ('parameters ipv6 flowlabel', False, None), + 'hoplimit': ('parameters ipv6 hoplimit', False, None), + 'tclass': ('parameters ipv6 tclass', False, None), + '6rd-prefix': ('6rd-prefix', False, None), + '6rd-relay-prefix': ('6rd-relay-prefix', False, None), + 'dhcp-interface': ('dhcp-interface', False, None), + 'state': ('disable', False, 'down'), + 'link_detect': ('disable-link-detect', False, 2), + 'vrf': ('vrf', False, None), + 'addresses-add': ('address', True, None), + 'ipv6_autoconf': ('ipv6 address autoconf', False, 1), + 'ipv6_forwarding': ('ipv6 disable-forwarding', False, 0), + 'ipv6_dad_transmits:': ('ipv6 dup-addr-detect-transmits', False, None) +} + +def get_class (options): + dispatch = { + 'gre': GREIf, + 'gre-bridge': GRETapIf, + 'ipip': IPIPIf, + 'ipip6': IPIP6If, + 'ip6ip6': IP6IP6If, + 'ip6gre': IP6GREIf, + 'sit': SitIf, + } + + kls = dispatch[options['type']] + if options['type'] == 'gre' and not options['remote'] \ + and not options['key'] and not options['multicast']: + # will use GreTapIf on GreIf deletion but it does not matter + return GRETapIf + elif options['type'] == 'sit' and options['6rd-prefix']: + # will use SitIf on Sit6RDIf deletion but it does not matter + return Sit6RDIf + return kls + +def get_interface_ip (ifname): + if not ifname: + return '' + try: + addrs = Interface(ifname).get_addr() + if addrs: + return addrs[0].split('/')[0] + except Exception: + return '' + +def get_afi (ip): + return IP6 if is_ipv6(ip) else IP4 + +def ip_proto (afi): + return 6 if afi == IP6 else 4 + + +def get_config(): + ifname = os.environ.get('VYOS_TAGNODE_VALUE','') + if not ifname: + raise ConfigError('Interface not specified') + + conf = ConfigurationState('interfaces tunnel ' + ifname, default_config_data) + options = conf.options + changes = conf.changes + options['ifname'] = ifname + + # set new configuration level + conf.set_level(conf.section) + + if changes['section'] == 'delete': + conf.get_effective('type', mapping['type'][0]) + conf.set_level('protocols nhrp tunnel') + options['nhrp'] = conf.list_nodes('') + return conf.to_dict() + + # load all the configuration option according to the mapping + conf.load(mapping) + + # remove default value if not set and not required + afi_local = get_afi(options['local']) + if afi_local == IP6: + conf.remove_default('ttl', 'tos', 'key') + if afi_local == IP4: + conf.remove_default('encaplimit', 'flowlabel', 'hoplimit', 'tclass') + + # if the local-ip is not set, pick one from the interface ! + # hopefully there is only one, otherwise it will not be very deterministic + # at time of writing the code currently returns ipv4 before ipv6 in the list + + # XXX: There is no way to trigger an update of the interface source IP if + # XXX: the underlying interface IP address does change, I believe this + # XXX: limit/issue is present in vyatta too + + if not options['local'] and options['dhcp-interface']: + # XXX: This behaviour changes from vyatta which would return 127.0.0.1 if + # XXX: the interface was not DHCP. As there is no easy way to find if an + # XXX: interface is using DHCP, and using this feature to get 127.0.0.1 + # XXX: makes little sense, I feel the change in behaviour is acceptable + picked = get_interface_ip(options['dhcp-interface']) + if picked == '': + picked = '127.0.0.1' + print('Could not get an IP address from {dhcp-interface} using 127.0.0.1 instead') + options['local'] = picked + options['dhcp-interface'] = '' + + # get interface addresses (currently effective) - to determine which + # address is no longer valid and needs to be removed + # could be done within ConfigurationState + eff_addr = conf.return_effective_values('address') + options['addresses-del'] = list_diff(eff_addr, options['addresses-add']) + + # allmulticast fate is linked to multicast + options['allmulticast'] = options['multicast'] + + # check that per encapsulation all local-remote pairs are unique + conf.set_level('interfaces tunnel') + ct = conf.get_config_dict()['tunnel'] + options['tunnel'] = {} + + for name in ct: + tunnel = ct[name] + encap = tunnel.get('encapsulation', '') + local = tunnel.get('local-ip', '') + if not local: + local = get_interface_ip(tunnel.get('dhcp-interface', '')) + remote = tunnel.get('remote-ip', '<unset>') + pair = f'{local}-{remote}' + options['tunnel'][encap][pair] = options['tunnel'].setdefault(encap, {}).get(pair, 0) + 1 + + return conf.to_dict() + + +def verify(conf): + options = conf['options'] + changes = conf['changes'] + actions = conf['actions'] + + ifname = options['ifname'] + iftype = options['type'] + + if changes['section'] == 'delete': + if ifname in options['nhrp']: + raise ConfigError(f'Can not delete interface tunnel {iftype} {ifname}, it is used by nhrp') + # done, bail out early + return None + + # tunnel encapsulation checks + + if not iftype: + raise ConfigError(f'Must provide an "encapsulation" for tunnel {iftype} {ifname}') + + if changes['type'] in ('modify', 'delete'): + # TODO: we could now deal with encapsulation modification by deleting / recreating + raise ConfigError(f'Encapsulation can only be set at tunnel creation for tunnel {iftype} {ifname}') + + if iftype != 'sit' and options['6rd-prefix']: + # XXX: should be able to remove this and let the definition catch it + print(f'6RD can only be configured for sit interfaces not tunnel {iftype} {ifname}') + + # what are the tunnel options we can set / modified / deleted + + kls = get_class(options) + valid = kls.updates + ['alias', 'addresses-add', 'addresses-del', 'vrf'] + + if changes['section'] == 'create': + valid.extend(['type',]) + valid.extend([o for o in kls.options if o not in kls.updates]) + + for create in actions['create']: + if create not in valid: + raise ConfigError(f'Can not set "{create}" for tunnel {iftype} {ifname} at tunnel creation') + + for modify in actions['modify']: + if modify not in valid: + raise ConfigError(f'Can not modify "{modify}" for tunnel {iftype} {ifname}. it must be set at tunnel creation') + + for delete in actions['delete']: + if delete in kls.required: + raise ConfigError(f'Can not remove "{delete}", it is an mandatory option for tunnel {iftype} {ifname}') + + # tunnel information + + tun_local = options['local'] + afi_local = get_afi(tun_local) + tun_remote = options['remote'] or tun_local + afi_remote = get_afi(tun_remote) + tun_ismgre = iftype == 'gre' and not options['remote'] + tun_is6rd = iftype == 'sit' and options['6rd-prefix'] + + # incompatible options + + if not tun_local and not options['dhcp-interface'] and not tun_is6rd: + raise ConfigError(f'Must configure either local-ip or dhcp-interface for tunnel {iftype} {ifname}') + + if tun_local and options['dhcp-interface']: + raise ConfigError(f'Must configure only one of local-ip or dhcp-interface for tunnel {iftype} {ifname}') + + # tunnel endpoint + + if afi_local != afi_remote: + raise ConfigError(f'IPv4/IPv6 mismatch between local-ip and remote-ip for tunnel {iftype} {ifname}') + + if afi_local != kls.tunnel: + version = 4 if tun_local == IP4 else 6 + raise ConfigError(f'Invalid IPv{version} local-ip for tunnel {iftype} {ifname}') + + ipv4_count = len([ip for ip in options['addresses-add'] if is_ipv4(ip)]) + ipv6_count = len([ip for ip in options['addresses-add'] if is_ipv6(ip)]) + + if tun_ismgre and afi_local == IP6: + raise ConfigError(f'Using an IPv6 address is forbidden for mGRE tunnels such as tunnel {iftype} {ifname}') + + # check address family use + # checks are not enforced (but ip command failing) for backward compatibility + + if ipv4_count and not IP4 in kls.ip: + print(f'Should not use IPv4 addresses on tunnel {iftype} {ifname}') + + if ipv6_count and not IP6 in kls.ip: + print(f'Should not use IPv6 addresses on tunnel {iftype} {ifname}') + + # tunnel encapsulation check + + convert = { + (6, 4, 'gre'): 'ip6gre', + (6, 6, 'gre'): 'ip6gre', + (4, 6, 'ipip'): 'ipip6', + (6, 6, 'ipip'): 'ip6ip6', + } + + iprotos = [] + if ipv4_count: + iprotos.append(4) + if ipv6_count: + iprotos.append(6) + + for iproto in iprotos: + replace = convert.get((kls.tunnel, iproto, iftype), '') + if replace: + raise ConfigError( + f'Using IPv6 address in local-ip or remote-ip is not possible with "encapsulation {iftype}". ' + + f'Use "encapsulation {replace}" for tunnel {iftype} {ifname} instead.' + ) + + # tunnel options + + incompatible = [] + if afi_local == IP6: + incompatible.extend(['ttl', 'tos', 'key',]) + if afi_local == IP4: + incompatible.extend(['encaplimit', 'flowlabel', 'hoplimit', 'tclass']) + + for option in incompatible: + if option in options: + # TODO: raise converted to print as not enforced by vyatta + # raise ConfigError(f'{option} is not valid for tunnel {iftype} {ifname}') + print(f'Using "{option}" is invalid for tunnel {iftype} {ifname}') + + # duplicate tunnel pairs + + pair = '{}-{}'.format(options['local'], options['remote']) + if options['tunnel'].get(iftype, {}).get(pair, 0) > 1: + raise ConfigError(f'More than one tunnel configured for with the same encapulation and IPs for tunnel {iftype} {ifname}') + + return None + + +def generate(gre): + return None + +def apply(conf): + options = conf['options'] + changes = conf['changes'] + actions = conf['actions'] + kls = get_class(options) + + # extract ifname as otherwise it is duplicated on the interface creation + ifname = options.pop('ifname') + + # only the valid keys for creation of a Interface + config = dict((k, options[k]) for k in kls.options if options[k]) + + # setup or create the tunnel interface if it does not exist + tunnel = kls(ifname, **config) + + if changes['section'] == 'delete': + tunnel.remove() + # The perl code was calling/opt/vyatta/sbin/vyatta-tunnel-cleanup + # which identified tunnels type which were not used anymore to remove them + # (ie: gre0, gretap0, etc.) The perl code did however nothing + # This feature is also not implemented yet + return + + # A GRE interface without remote will be mGRE + # if the interface does not suppor the option, it skips the change + for option in tunnel.updates: + if changes['section'] in 'create' and option in tunnel.options: + # it was setup at creation + continue + tunnel.set_interface(option, options[option]) + + # set other interface properties + for option in ('alias', 'mtu', 'link_detect', 'multicast', 'allmulticast', + 'vrf', 'ipv6_autoconf', 'ipv6_forwarding', 'ipv6_dad_transmits'): + tunnel.set_interface(option, options[option]) + + # Configure interface address(es) + for addr in options['addresses-del']: + tunnel.del_addr(addr) + for addr in options['addresses-add']: + tunnel.add_addr(addr) + + # now bring it up (or not) + tunnel.set_admin_state(options['state']) + + +if __name__ == '__main__': + try: + c = get_config() + verify(c) + generate(c) + apply(c) + except ConfigError as e: + print(e) + exit(1) diff --git a/src/conf_mode/interfaces-vxlan.py b/src/conf_mode/interfaces-vxlan.py index efdc21f89..6639a9b0d 100755 --- a/src/conf_mode/interfaces-vxlan.py +++ b/src/conf_mode/interfaces-vxlan.py @@ -18,11 +18,12 @@ import os from sys import exit from copy import deepcopy +from netifaces import interfaces from vyos.config import Config from vyos.ifconfig import VXLANIf, Interface +from vyos.util import is_bridge_member from vyos import ConfigError -from netifaces import interfaces default_config_data = { 'address': [], @@ -37,7 +38,12 @@ default_config_data = { 'ip_enable_arp_announce': 0, 'ip_enable_arp_ignore': 0, 'ip_proxy_arp': 0, - 'link': '', + 'ipv6_autoconf': 0, + 'ipv6_eui64_prefix': '', + 'ipv6_forwarding': 1, + 'ipv6_dup_addr_detect': 1, + 'source_address': '', + 'source_interface': '', 'mtu': 1450, 'remote': '', 'remote_port': 8472, # The Linux implementation of VXLAN pre-dates @@ -50,10 +56,10 @@ def get_config(): conf = Config() # determine tagNode instance - try: - vxlan['intf'] = os.environ['VYOS_TAGNODE_VALUE'] - except KeyError as E: - print("Interface not specified") + if 'VYOS_TAGNODE_VALUE' not in os.environ: + raise ConfigError('Interface (VYOS_TAGNODE_VALUE) not specified') + + vxlan['intf'] = os.environ['VYOS_TAGNODE_VALUE'] # Check if interface has been removed if not conf.exists('interfaces vxlan ' + vxlan['intf']): @@ -103,9 +109,29 @@ def get_config(): if conf.exists('ip enable-proxy-arp'): vxlan['ip_proxy_arp'] = 1 + # Enable acquisition of IPv6 address using stateless autoconfig (SLAAC) + if conf.exists('ipv6 address autoconf'): + vxlan['ipv6_autoconf'] = 1 + + # Get prefix 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') + + # Disable IPv6 forwarding on this interface + if conf.exists('ipv6 disable-forwarding'): + vxlan['ipv6_forwarding'] = 0 + + # IPv6 Duplicate Address Detection (DAD) tries + if conf.exists('ipv6 dup-addr-detect-transmits'): + vxlan['ipv6_dup_addr_detect'] = int(conf.return_value('ipv6 dup-addr-detect-transmits')) + + # VXLAN source address + if conf.exists('source-address'): + vxlan['source_address'] = conf.return_value('source-address') + # VXLAN underlay interface - if conf.exists('link'): - vxlan['link'] = conf.return_value('link') + if conf.exists('source-interface'): + vxlan['source_interface'] = conf.return_value('source-interface') # Maximum Transmission Unit (MTU) if conf.exists('mtu'): @@ -128,25 +154,35 @@ def get_config(): def verify(vxlan): if vxlan['deleted']: - # bail out early + interface = vxlan['intf'] + is_member, bridge = is_bridge_member(interface) + if is_member: + # can not use a f'' formatted-string here as bridge would not get + # expanded in the print statement + raise ConfigError('Can not delete interface "{0}" as it ' \ + 'is a member of bridge "{1}"!'.format(interface, bridge)) return None if vxlan['mtu'] < 1500: print('WARNING: RFC7348 recommends VXLAN tunnels preserve a 1500 byte MTU') - if vxlan['group'] and not vxlan['link']: - raise ConfigError('Multicast VXLAN requires an underlaying interface ') + if vxlan['group']: + if not vxlan['source_interface']: + raise ConfigError('Multicast VXLAN requires an underlaying interface ') + + if not vxlan['source_interface'] in interfaces(): + raise ConfigError('VXLAN source interface does not exist') - if not (vxlan['group'] or vxlan['remote']): - raise ConfigError('Group or remote must be configured') + if not (vxlan['group'] or vxlan['remote'] or vxlan['source_address']): + raise ConfigError('Group, remote or source-address must be configured') if not vxlan['vni']: raise ConfigError('Must configure VNI for VXLAN') - if vxlan['link']: + if vxlan['source_interface']: # VXLAN adds a 50 byte overhead - we need to check the underlaying MTU # if our configured MTU is at least 50 bytes less - underlay_mtu = int(Interface(vxlan['link']).get_mtu()) + underlay_mtu = int(Interface(vxlan['source_interface']).get_mtu()) if underlay_mtu < (vxlan['mtu'] + 50): raise ConfigError('VXLAN has a 50 byte overhead, underlaying device ' \ 'MTU is to small ({})'.format(underlay_mtu)) @@ -175,12 +211,13 @@ def apply(vxlan): # Assign VXLAN instance configuration parameters to config dict conf['vni'] = vxlan['vni'] conf['group'] = vxlan['group'] - conf['dev'] = vxlan['link'] + conf['src_address'] = vxlan['source_address'] + conf['src_interface'] = vxlan['source_interface'] conf['remote'] = vxlan['remote'] conf['port'] = vxlan['remote_port'] # Finally create the new interface - v = VXLANIf(vxlan['intf'], config=conf) + v = VXLANIf(vxlan['intf'], **conf) # update interface description used e.g. by SNMP v.set_alias(vxlan['description']) # Maximum Transfer Unit (MTU) @@ -198,6 +235,14 @@ 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 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 + v.set_ipv6_dad_messages(vxlan['ipv6_dup_addr_detect']) # Configure interface address(es) - no need to implicitly delete the # old addresses as they have already been removed by deleting the @@ -209,7 +254,7 @@ def apply(vxlan): # parameters we will only re-enable the interface if it is not # administratively disabled if not vxlan['disable']: - v.set_state('up') + v.set_admin_state('up') return None diff --git a/src/conf_mode/interfaces-wireguard.py b/src/conf_mode/interfaces-wireguard.py index ff12a5172..8bf81c747 100755 --- a/src/conf_mode/interfaces-wireguard.py +++ b/src/conf_mode/interfaces-wireguard.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2018 VyOS maintainers and contributors +# Copyright (C) 2018-2020 VyOS maintainers and contributors # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 or later as @@ -13,263 +13,284 @@ # # You should have received a copy of the GNU General Public License # along with this program. If not, see <http://www.gnu.org/licenses/>. -# -# -import sys import os import re -import subprocess + +from sys import exit from copy import deepcopy from netifaces import interfaces -from vyos import ConfigError from vyos.config import Config from vyos.configdict import list_diff from vyos.ifconfig import WireGuardIf +from vyos.util import chown, is_bridge_member, chmod_750 +from vyos.util import call +from vyos import ConfigError kdir = r'/config/auth/wireguard' +default_config_data = { + 'intfc': '', + 'address': [], + 'address_remove': [], + 'description': '', + 'lport': None, + 'deleted': False, + 'disable': False, + 'fwmark': 0x00, + 'mtu': 1420, + 'peer': [], + 'peer_remove': [], # stores public keys of peers to remove + 'pk': f'{kdir}/default/private.key', + 'vrf': '' +} def _check_kmod(): - if not os.path.exists('/sys/module/wireguard'): - if os.system('sudo modprobe wireguard') != 0: - raise ConfigError("modprobe wireguard failed") + modules = ['wireguard'] + 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 _migrate_default_keys(): - if os.path.exists('{}/private.key'.format(kdir)) and not os.path.exists('{}/default/private.key'.format(kdir)): - old_umask = os.umask(0o027) - location = '{}/default'.format(kdir) - subprocess.call(['sudo mkdir -p ' + location], shell=True) - subprocess.call(['sudo chgrp vyattacfg ' + location], shell=True) - subprocess.call(['sudo chmod 750 ' + location], shell=True) - os.rename('{}/private.key'.format(kdir), - '{}/private.key'.format(location)) - os.rename('{}/public.key'.format(kdir), - '{}/public.key'.format(location)) - os.umask(old_umask) + if os.path.exists(f'{kdir}/private.key') and not os.path.exists(f'{kdir}/default/private.key'): + location = f'{kdir}/default' + if not os.path.exists(location): + os.makedirs(location) + + chown(location, 'root', 'vyattacfg') + chmod_750(location) + os.rename(f'{kdir}/private.key', f'{location}/private.key') + os.rename(f'{kdir}/public.key', f'{location}/public.key') def get_config(): - c = Config() - if not c.exists(['interfaces', 'wireguard']): - return None + conf = Config() + base = ['interfaces', 'wireguard'] + + # determine tagNode instance + if 'VYOS_TAGNODE_VALUE' not in os.environ: + raise ConfigError('Interface (VYOS_TAGNODE_VALUE) not specified') + + wg = deepcopy(default_config_data) + wg['intf'] = os.environ['VYOS_TAGNODE_VALUE'] + + # Check if interface has been removed + if not conf.exists(base + [wg['intf']]): + wg['deleted'] = True + return wg + + conf.set_level(base + [wg['intf']]) + + # retrieve configured interface addresses + if conf.exists(['address']): + wg['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']) + wg['address_remove'] = list_diff(eff_addr, wg['address']) + + # retrieve interface description + if conf.exists(['description']): + wg['description'] = conf.return_value(['description']) + + # disable interface + if conf.exists(['disable']): + wg['disable'] = True + + # local port to listen on + if conf.exists(['port']): + wg['lport'] = conf.return_value(['port']) + + # fwmark value + if conf.exists(['fwmark']): + wg['fwmark'] = int(conf.return_value(['fwmark'])) + + # Maximum Transmission Unit (MTU) + if conf.exists('mtu'): + wg['mtu'] = int(conf.return_value(['mtu'])) + + # retrieve VRF instance + if conf.exists('vrf'): + wg['vrf'] = conf.return_value('vrf') + + # private key + if conf.exists(['private-key']): + wg['pk'] = "{0}/{1}/private.key".format( + kdir, conf.return_value(['private-key'])) + + # peer removal, wg identifies peers by its pubkey + peer_eff = conf.list_effective_nodes(['peer']) + peer_rem = list_diff(peer_eff, conf.list_nodes(['peer'])) + for peer in peer_rem: + wg['peer_remove'].append( + conf.return_effective_value(['peer', peer, 'pubkey'])) + + # peer settings + if conf.exists(['peer']): + for p in conf.list_nodes(['peer']): + # set new config level for this peer + conf.set_level(base + [wg['intf'], 'peer', p]) + peer = { + 'allowed-ips': [], + 'address': '', + 'name': p, + 'persistent_keepalive': '', + 'port': '', + 'psk': '', + 'pubkey': '' + } + + # peer allowed-ips + if conf.exists(['allowed-ips']): + peer['allowed-ips'] = conf.return_values(['allowed-ips']) + + # peer address + if conf.exists(['address']): + peer['address'] = conf.return_value(['address']) + + # peer port + if conf.exists(['port']): + peer['port'] = conf.return_value(['port']) + + # persistent-keepalive + if conf.exists(['persistent-keepalive']): + peer['persistent_keepalive'] = conf.return_value(['persistent-keepalive']) + + # preshared-key + if conf.exists(['preshared-key']): + peer['psk'] = conf.return_value(['preshared-key']) + + # peer pubkeys + if conf.exists(['pubkey']): + key_eff = conf.return_effective_value(['pubkey']) + key_cfg = conf.return_value(['pubkey']) + peer['pubkey'] = key_cfg + + # on a pubkey change we need to remove the pubkey first + # peers are identified by pubkey, so key update means + # peer removal and re-add + if key_eff != key_cfg and key_eff != None: + wg['peer_remove'].append(key_cfg) + + # if a peer is disabled, we have to exec a remove for it's pubkey + if conf.exists(['disable']): + wg['peer_remove'].append(peer['pubkey']) + else: + wg['peer'].append(peer) - dflt_cnf = { - 'intfc': '', - 'addr': [], - 'addr_remove': [], - 'descr': '', - 'lport': None, - 'delete': False, - 'state': 'up', - 'fwmark': 0x00, - 'mtu': 1420, - 'peer': {}, - 'peer_remove': [], - 'pk': '{}/default/private.key'.format(kdir) - } - - if os.getenv('VYOS_TAGNODE_VALUE'): - ifname = str(os.environ['VYOS_TAGNODE_VALUE']) - wg = deepcopy(dflt_cnf) - wg['intfc'] = ifname - wg['descr'] = ifname - else: - print("ERROR: VYOS_TAGNODE_VALUE undefined") - sys.exit(1) - - c.set_level(['interfaces', 'wireguard']) - - # interface removal state - if not c.exists(ifname) and c.exists_effective(ifname): - wg['delete'] = True - - if not wg['delete']: - c.set_level(['interfaces', 'wireguard', ifname]) - if c.exists(['address']): - wg['addr'] = c.return_values(['address']) - - # determine addresses which need to be removed - eff_addr = c.return_effective_values(['address']) - wg['addr_remove'] = list_diff(eff_addr, wg['addr']) - - # ifalias description - if c.exists(['description']): - wg['descr'] = c.return_value(['description']) - - # link state - if c.exists(['disable']): - wg['state'] = 'down' - - # local port to listen on - if c.exists(['port']): - wg['lport'] = c.return_value(['port']) - - # fwmark value - if c.exists(['fwmark']): - wg['fwmark'] = c.return_value(['fwmark']) - - # mtu - if c.exists('mtu'): - wg['mtu'] = c.return_value('mtu') - - # private key - if c.exists(['private-key']): - wg['pk'] = "{0}/{1}/private.key".format( - kdir, c.return_value(['private-key'])) - - # peer removal, wg identifies peers by its pubkey - peer_eff = c.list_effective_nodes(['peer']) - peer_rem = list_diff(peer_eff, c.list_nodes(['peer'])) - for p in peer_rem: - wg['peer_remove'].append( - c.return_effective_value(['peer', p, 'pubkey'])) - - # peer settings - if c.exists(['peer']): - for p in c.list_nodes(['peer']): - if not c.exists(['peer', p, 'disable']): - wg['peer'].update( - { - p: { - 'allowed-ips': [], - 'endpoint': '', - 'pubkey': '' - } - } - ) - # peer allowed-ips - if c.exists(['peer', p, 'allowed-ips']): - wg['peer'][p]['allowed-ips'] = c.return_values( - ['peer', p, 'allowed-ips']) - # peer endpoint - if c.exists(['peer', p, 'endpoint']): - wg['peer'][p]['endpoint'] = c.return_value( - ['peer', p, 'endpoint']) - # persistent-keepalive - if c.exists(['peer', p, 'persistent-keepalive']): - wg['peer'][p]['persistent-keepalive'] = c.return_value( - ['peer', p, 'persistent-keepalive']) - # preshared-key - if c.exists(['peer', p, 'preshared-key']): - wg['peer'][p]['psk'] = c.return_value( - ['peer', p, 'preshared-key']) - # peer pubkeys - key_eff = c.return_effective_value(['peer', p, 'pubkey']) - key_cfg = c.return_value(['peer', p, 'pubkey']) - wg['peer'][p]['pubkey'] = key_cfg - - # on a pubkey change we need to remove the pubkey first - # peers are identified by pubkey, so key update means - # peer removal and re-add - if key_eff != key_cfg and key_eff != None: - wg['peer_remove'].append(key_cfg) - - # if a peer is disabled, we have to exec a remove for it's pubkey - else: - peer_key = c.return_value(['peer', p, 'pubkey']) - wg['peer_remove'].append(peer_key) return wg -def verify(c): - if not c: - return None +def verify(wg): + interface = wg['intf'] - if not os.path.exists(c['pk']): - raise ConfigError( - "No keys found, generate them by executing: \'run generate wireguard [keypair|named-keypairs]\'") - - if not c['delete']: - if not c['addr']: - raise ConfigError("ERROR: IP address required") - if not c['peer']: - raise ConfigError("ERROR: peer required") - for p in c['peer']: - if not c['peer'][p]['allowed-ips']: - raise ConfigError("ERROR: allowed-ips required for peer " + p) - if not c['peer'][p]['pubkey']: - raise ConfigError("peer pubkey required for peer " + p) - - -def apply(c): - # no wg configs left, remove all interface from system - # maybe move it into ifconfig.py - if not c: - net_devs = os.listdir('/sys/class/net/') - for dev in net_devs: - if os.path.isdir('/sys/class/net/' + dev): - buf = open('/sys/class/net/' + dev + '/uevent', 'r').read() - if re.search("DEVTYPE=wireguard", buf, re.I | re.M): - wg_intf = re.sub("INTERFACE=", "", re.search( - "INTERFACE=.*", buf, re.I | re.M).group(0)) - subprocess.call( - ['ip l d dev ' + wg_intf + ' >/dev/null'], shell=True) + if wg['deleted']: + is_member, bridge = is_bridge_member(interface) + if is_member: + # can not use a f'' formatted-string here as bridge would not get + # expanded in the print statement + raise ConfigError('Can not delete interface "{0}" as it ' \ + 'is a member of bridge "{1}"!'.format(interface, bridge)) 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 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}"!') + + if not wg['peer']: + raise ConfigError(f'Peer required for interface "{interface}"!') + + # 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}"!') + + if not peer['pubkey']: + raise ConfigError(f'Peer public-key required for peer "{peer_name}"!') + + +def apply(wg): # init wg class - intfc = WireGuardIf(c['intfc']) + w = WireGuardIf(wg['intf']) # single interface removal - if c['delete']: - intfc.remove() + if wg['deleted']: + w.remove() return None - # remove IP addresses - for ip in c['addr_remove']: - intfc.del_addr(ip) + # Configure interface address(es) + # - not longer required addresses get removed first + # - newly addresses will be added second + for addr in wg['address_remove']: + w.del_addr(addr) + for addr in wg['address']: + w.add_addr(addr) - # add IP addresses - for ip in c['addr']: - intfc.add_addr(ip) + # Maximum Transmission Unit (MTU) + w.set_mtu(wg['mtu']) - # interface mtu - intfc.set_mtu(int(c['mtu'])) + # update interface description used e.g. within SNMP + w.set_alias(wg['description']) - # ifalias for snmp from description - intfc.set_alias(str(c['descr'])) + # assign/remove VRF + w.set_vrf(wg['vrf']) # remove peers - if c['peer_remove']: - for pkey in c['peer_remove']: - intfc.remove_peer(pkey) + for pub_key in wg['peer_remove']: + w.remove_peer(pub_key) # peer pubkey # setting up the wg interface - intfc.config['private-key'] = c['pk'] - for p in c['peer']: + w.config['private-key'] = c['pk'] + + for peer in wg['peer']: # peer pubkey - intfc.config['pubkey'] = str(c['peer'][p]['pubkey']) + w.config['pubkey'] = peer['pubkey'] # peer allowed-ips - intfc.config['allowed-ips'] = c['peer'][p]['allowed-ips'] + w.config['allowed-ips'] = peer['allowed-ips'] # local listen port - if c['lport']: - intfc.config['port'] = c['lport'] + if wg['lport']: + w.config['port'] = wg['lport'] # fwmark if c['fwmark']: - intfc.config['fwmark'] = c['fwmark'] + w.config['fwmark'] = wg['fwmark'] + # endpoint - if c['peer'][p]['endpoint']: - intfc.config['endpoint'] = c['peer'][p]['endpoint'] + if peer['address'] and peer['port']: + w.config['endpoint'] = '{}:{}'.format( + peer['address'], peer['port']) # persistent-keepalive - if 'persistent-keepalive' in c['peer'][p]: - intfc.config['keepalive'] = c['peer'][p]['persistent-keepalive'] + if peer['persistent_keepalive']: + w.config['keepalive'] = peer['persistent_keepalive'] # maybe move it into ifconfig.py # preshared-key - needs to be read from a file - if 'psk' in c['peer'][p]: + if peer['psk']: psk_file = '/config/auth/wireguard/psk' - old_umask = os.umask(0o077) - open(psk_file, 'w').write(str(c['peer'][p]['psk'])) - os.umask(old_umask) - intfc.config['psk'] = psk_file - intfc.update() + with open(psk_file, 'w') as f: + f.write(peer['psk']) + w.config['psk'] = psk_file - # interface state - intfc.set_state(c['state']) + w.update() + + # Enable/Disable interface + if wg['disable']: + w.set_admin_state('down') + else: + w.set_admin_state('up') return None @@ -282,4 +303,4 @@ if __name__ == '__main__': apply(c) except ConfigError as e: print(e) - sys.exit(1) + exit(1) diff --git a/src/conf_mode/interfaces-wireless.py b/src/conf_mode/interfaces-wireless.py index 098aa8d97..138f27755 100755 --- a/src/conf_mode/interfaces-wireless.py +++ b/src/conf_mode/interfaces-wireless.py @@ -15,756 +15,26 @@ # along with this program. If not, see <http://www.gnu.org/licenses/>. import os +from sys import exit +from re import findall -from jinja2 import Template from copy import deepcopy -from sys import exit -from stat import S_IRWXU,S_IRGRP,S_IXGRP,S_IROTH,S_IXOTH -from pwd import getpwnam -from grp import getgrnam +from jinja2 import FileSystemLoader, Environment -from subprocess import Popen, PIPE -from psutil import pid_exists +from netifaces import interfaces +from netaddr import EUI, mac_unix_expanded -from vyos.ifconfig import EthernetIf -from vyos.ifconfig_vlan import apply_vlan_config -from vyos.configdict import list_diff, vlan_to_dict from vyos.config import Config +from vyos.configdict import list_diff, vlan_to_dict +from vyos.defaults import directories as vyos_data_dir +from vyos.ifconfig import WiFiIf +from vyos.ifconfig_vlan import apply_vlan_config, verify_vlan_config +from vyos.util import process_running, chmod_x, chown, run, is_bridge_member from vyos import ConfigError user = 'root' group = 'vyattacfg' -# Please be careful if you edit the template. -config_hostapd_tmpl = """ -### Autogenerated by interfaces-wireless.py ### -{% if description %} -# Description: {{ description }} -# User-friendly description of device; up to 32 octets encoded in UTF-8 -device_name={{ description | truncate(32, True) }} -{% endif %} - -# AP netdevice name (without 'ap' postfix, i.e., wlan0 uses wlan0ap for -# management frames with the Host AP driver); wlan0 with many nl80211 drivers -# Note: This attribute can be overridden by the values supplied with the '-i' -# command line parameter. -interface={{ intf }} - -# Driver interface type (hostap/wired/none/nl80211/bsd); -# default: hostap). nl80211 is used with all Linux mac80211 drivers. -# Use driver=none if building hostapd as a standalone RADIUS server that does -# not control any wireless/wired driver. -driver=nl80211 - -# -# What about bridge? -# bridge=br0 -# wds_sta=1 -# - -# Levels (minimum value for logged events): -# 0 = verbose debugging -# 1 = debugging -# 2 = informational messages -# 3 = notification -# 4 = warning -logger_syslog=-1 -logger_syslog_level=0 -logger_stdout=-1 -logger_stdout_level=0 - -{%- if country_code %} - -# Country code (ISO/IEC 3166-1). Used to set regulatory domain. -# Set as needed to indicate country in which device is operating. -# This can limit available channels and transmit power. -country_code={{ country_code }} - -# Enable IEEE 802.11d. This advertises the country_code and the set of allowed -# channels and transmit power levels based on the regulatory limits. The -# country_code setting must be configured with the correct country for -# IEEE 802.11d functions. -ieee80211d=1 -{% endif %} - -{%- if ssid %} - -# SSID to be used in IEEE 802.11 management frames -ssid={{ ssid }} -{% endif %} - -{%- if channel %} - -# Channel number (IEEE 802.11) -# (default: 0, i.e., not set) -# Please note that some drivers do not use this value from hostapd and the -# channel will need to be configured separately with iwconfig. -# -# If CONFIG_ACS build option is enabled, the channel can be selected -# automatically at run time by setting channel=acs_survey or channel=0, both of -# which will enable the ACS survey based algorithm. -channel={{ channel }} -{% endif %} - -{%- if mode %} - -# Operation mode (a = IEEE 802.11a (5 GHz), b = IEEE 802.11b (2.4 GHz), -# g = IEEE 802.11g (2.4 GHz), ad = IEEE 802.11ad (60 GHz); a/g options are used -# with IEEE 802.11n (HT), too, to specify band). For IEEE 802.11ac (VHT), this -# needs to be set to hw_mode=a. For IEEE 802.11ax (HE) on 6 GHz this needs -# to be set to hw_mode=a. When using ACS (see channel parameter), a -# special value "any" can be used to indicate that any support band can be used. -# This special case is currently supported only with drivers with which -# offloaded ACS is used. -{% if 'n' in mode -%} -hw_mode=g -ieee80211n=1 -{% elif 'ac' in mode -%} -hw_mode=a -ieee80211h=1 -ieee80211ac=1 -{% else -%} -hw_mode={{ mode }} -{% endif %} -{% endif %} - -# ieee80211w: Whether management frame protection (MFP) is enabled -# 0 = disabled (default) -# 1 = optional -# 2 = required -{% if 'disabled' in mgmt_frame_protection -%} -ieee80211w=0 -{% elif 'optional' in mgmt_frame_protection -%} -ieee80211w=1 -{% elif 'required' in mgmt_frame_protection -%} -ieee80211w=2 -{% endif %} - -# ht_capab: HT capabilities (list of flags) -# LDPC coding capability: [LDPC] = supported -# Supported channel width set: [HT40-] = both 20 MHz and 40 MHz with secondary -# channel below the primary channel; [HT40+] = both 20 MHz and 40 MHz -# with secondary channel above the primary channel -# (20 MHz only if neither is set) -# Note: There are limits on which channels can be used with HT40- and -# HT40+. Following table shows the channels that may be available for -# HT40- and HT40+ use per IEEE 802.11n Annex J: -# freq HT40- HT40+ -# 2.4 GHz 5-13 1-7 (1-9 in Europe/Japan) -# 5 GHz 40,48,56,64 36,44,52,60 -# (depending on the location, not all of these channels may be available -# for use) -# Please note that 40 MHz channels may switch their primary and secondary -# channels if needed or creation of 40 MHz channel maybe rejected based -# on overlapping BSSes. These changes are done automatically when hostapd -# is setting up the 40 MHz channel. -# Spatial Multiplexing (SM) Power Save: [SMPS-STATIC] or [SMPS-DYNAMIC] -# (SMPS disabled if neither is set) -# HT-greenfield: [GF] (disabled if not set) -# Short GI for 20 MHz: [SHORT-GI-20] (disabled if not set) -# Short GI for 40 MHz: [SHORT-GI-40] (disabled if not set) -# Tx STBC: [TX-STBC] (disabled if not set) -# Rx STBC: [RX-STBC1] (one spatial stream), [RX-STBC12] (one or two spatial -# streams), or [RX-STBC123] (one, two, or three spatial streams); Rx STBC -# disabled if none of these set -# HT-delayed Block Ack: [DELAYED-BA] (disabled if not set) -# Maximum A-MSDU length: [MAX-AMSDU-7935] for 7935 octets (3839 octets if not -# set) -# DSSS/CCK Mode in 40 MHz: [DSSS_CCK-40] = allowed (not allowed if not set) -# 40 MHz intolerant [40-INTOLERANT] (not advertised if not set) -# L-SIG TXOP protection support: [LSIG-TXOP-PROT] (disabled if not set) -{% if cap_ht %} -ht_capab= -{%- endif -%} - -{%- if cap_ht_40mhz_incapable -%} -[40-INTOLERANT] -{%- endif -%} - -{%- if cap_ht_delayed_block_ack -%} -[DELAYED-BA] -{%- endif -%} - -{%- if cap_ht_dsss_cck_40 -%} -[DSSS_CCK-40] -{%- endif -%} - -{%- if cap_ht_greenfield -%} -[GF] -{%- endif -%} - -{%- if cap_ht_ldpc -%} -[LDPC] -{%- endif -%} - -{%- if cap_ht_lsig_protection -%} -[LSIG-TXOP-PROT] -{%- endif -%} - -{%- if cap_ht_max_amsdu -%} -[MAX-AMSDU-{{ cap_ht_max_amsdu }}] -{%- endif -%} - -{%- if cap_ht_smps -%} -[SMPS-{{ cap_ht_smps | upper }}] -{%- endif -%} - -{%- if cap_ht_chan_set_width -%} -{%- for csw in cap_ht_chan_set_width -%} -[{{ csw | upper }}] -{%- endfor -%} -{%- endif -%} - -{%- if cap_ht_short_gi -%} -{%- for gi in cap_ht_short_gi -%} -[SHORT-GI-{{ gi }}] -{%- endfor -%} -{%- endif -%} - -{%- if cap_ht_stbc_tx -%} -[TX-STBC] -{%- endif -%} -{%- if cap_ht_stbc_rx -%} -[RX-STBC{{ cap_ht_stbc_rx }}] -{%- endif %} - -# Required for full HT and VHT functionality -wme_enabled=1 - -{% if cap_ht_powersave -%} -# WMM-PS Unscheduled Automatic Power Save Delivery [U-APSD] -# Enable this flag if U-APSD supported outside hostapd (eg., Firmware/driver) -uapsd_advertisement_enabled=1 -{%- endif %} - -{% if cap_req_ht -%} -# Require stations to support HT PHY (reject association if they do not) -require_ht=1 -{% endif %} - -# vht_capab: VHT capabilities (list of flags) -# -# vht_max_mpdu_len: [MAX-MPDU-7991] [MAX-MPDU-11454] -# Indicates maximum MPDU length -# 0 = 3895 octets (default) -# 1 = 7991 octets -# 2 = 11454 octets -# 3 = reserved -# -# supported_chan_width: [VHT160] [VHT160-80PLUS80] -# Indicates supported Channel widths -# 0 = 160 MHz & 80+80 channel widths are not supported (default) -# 1 = 160 MHz channel width is supported -# 2 = 160 MHz & 80+80 channel widths are supported -# 3 = reserved -# -# Rx LDPC coding capability: [RXLDPC] -# Indicates support for receiving LDPC coded pkts -# 0 = Not supported (default) -# 1 = Supported -# -# Short GI for 80 MHz: [SHORT-GI-80] -# Indicates short GI support for reception of packets transmitted with TXVECTOR -# params format equal to VHT and CBW = 80Mhz -# 0 = Not supported (default) -# 1 = Supported -# -# Short GI for 160 MHz: [SHORT-GI-160] -# Indicates short GI support for reception of packets transmitted with TXVECTOR -# params format equal to VHT and CBW = 160Mhz -# 0 = Not supported (default) -# 1 = Supported -# -# Tx STBC: [TX-STBC-2BY1] -# Indicates support for the transmission of at least 2x1 STBC -# 0 = Not supported (default) -# 1 = Supported -# -# Rx STBC: [RX-STBC-1] [RX-STBC-12] [RX-STBC-123] [RX-STBC-1234] -# Indicates support for the reception of PPDUs using STBC -# 0 = Not supported (default) -# 1 = support of one spatial stream -# 2 = support of one and two spatial streams -# 3 = support of one, two and three spatial streams -# 4 = support of one, two, three and four spatial streams -# 5,6,7 = reserved -# -# SU Beamformer Capable: [SU-BEAMFORMER] -# Indicates support for operation as a single user beamformer -# 0 = Not supported (default) -# 1 = Supported -# -# SU Beamformee Capable: [SU-BEAMFORMEE] -# Indicates support for operation as a single user beamformee -# 0 = Not supported (default) -# 1 = Supported -# -# Compressed Steering Number of Beamformer Antennas Supported: -# [BF-ANTENNA-2] [BF-ANTENNA-3] [BF-ANTENNA-4] -# Beamformee's capability indicating the maximum number of beamformer -# antennas the beamformee can support when sending compressed beamforming -# feedback -# If SU beamformer capable, set to maximum value minus 1 -# else reserved (default) -# -# Number of Sounding Dimensions: -# [SOUNDING-DIMENSION-2] [SOUNDING-DIMENSION-3] [SOUNDING-DIMENSION-4] -# Beamformer's capability indicating the maximum value of the NUM_STS parameter -# in the TXVECTOR of a VHT NDP -# If SU beamformer capable, set to maximum value minus 1 -# else reserved (default) -# -# MU Beamformer Capable: [MU-BEAMFORMER] -# Indicates support for operation as an MU beamformer -# 0 = Not supported or sent by Non-AP STA (default) -# 1 = Supported -# -# VHT TXOP PS: [VHT-TXOP-PS] -# Indicates whether or not the AP supports VHT TXOP Power Save Mode -# or whether or not the STA is in VHT TXOP Power Save mode -# 0 = VHT AP doesn't support VHT TXOP PS mode (OR) VHT STA not in VHT TXOP PS -# mode -# 1 = VHT AP supports VHT TXOP PS mode (OR) VHT STA is in VHT TXOP power save -# mode -# -# +HTC-VHT Capable: [HTC-VHT] -# Indicates whether or not the STA supports receiving a VHT variant HT Control -# field. -# 0 = Not supported (default) -# 1 = supported -# -# Maximum A-MPDU Length Exponent: [MAX-A-MPDU-LEN-EXP0]..[MAX-A-MPDU-LEN-EXP7] -# Indicates the maximum length of A-MPDU pre-EOF padding that the STA can recv -# This field is an integer in the range of 0 to 7. -# The length defined by this field is equal to -# 2 pow(13 + Maximum A-MPDU Length Exponent) -1 octets -# -# VHT Link Adaptation Capable: [VHT-LINK-ADAPT2] [VHT-LINK-ADAPT3] -# Indicates whether or not the STA supports link adaptation using VHT variant -# HT Control field -# If +HTC-VHTcapable is 1 -# 0 = (no feedback) if the STA does not provide VHT MFB (default) -# 1 = reserved -# 2 = (Unsolicited) if the STA provides only unsolicited VHT MFB -# 3 = (Both) if the STA can provide VHT MFB in response to VHT MRQ and if the -# STA provides unsolicited VHT MFB -# Reserved if +HTC-VHTcapable is 0 -# -# Rx Antenna Pattern Consistency: [RX-ANTENNA-PATTERN] -# Indicates the possibility of Rx antenna pattern change -# 0 = Rx antenna pattern might change during the lifetime of an association -# 1 = Rx antenna pattern does not change during the lifetime of an association -# -# Tx Antenna Pattern Consistency: [TX-ANTENNA-PATTERN] -# Indicates the possibility of Tx antenna pattern change -# 0 = Tx antenna pattern might change during the lifetime of an association -# 1 = Tx antenna pattern does not change during the lifetime of an association -{% if cap_vht %} -vht_capab= -{%- endif -%} - -{%- if cap_vht_max_mpdu -%} -[MAX-MPDU-{{ cap_vht_max_mpdu }}] -{%- endif -%} - -{%- if cap_vht_max_mpdu_exp -%} -[MAX-A-MPDU-LEN-EXP{{ cap_vht_max_mpdu_exp }}] -{%- endif -%} - -{%- if cap_vht_chan_set_width -%} -[MAX-A-MPDU-LEN-EXP{{ cap_vht_max_mpdu_exp }}] -{%- endif -%} - -{%- if cap_vht_chan_set_width -%} -{%- if '2' in cap_vht_chan_set_width -%} -[VHT160] -{%- elif '3' in cap_vht_chan_set_width -%} -[VHT160-80PLUS80] -{%- endif -%} -{%- endif -%} - -{%- if cap_vht_stbc_tx -%} -[TX-STBC-2BY1] -{%- endif -%} - -{%- if cap_vht_stbc_rx -%} -[RX-STBC-{{ cap_vht_stbc_rx }}] -{%- endif -%} - -{%- if cap_vht_link_adaptation -%} -{%- if 'unsolicited' in cap_vht_link_adaptation -%} -[VHT-LINK-ADAPT2] -{%- elif 'both' in cap_vht_link_adaptation -%} -[VHT-LINK-ADAPT3] -{%- endif -%} -{%- endif -%} - -{%- if cap_vht_short_gi -%} -{%- for gi in cap_vht_short_gi -%} -[SHORT-GI-{{ gi }}] -{%- endfor -%} -{%- endif -%} - -{%- if cap_vht_ldpc -%} -[RXLDPC] -{%- endif -%} - -{%- if cap_vht_tx_powersave -%} -[VHT-TXOP-PS] -{%- endif -%} - -{%- if cap_vht_vht_cf -%} -[HTC-VHT] -{%- endif -%} - -{%- if cap_vht_beamform -%} -{%- for beamform in cap_vht_beamform -%} -{%- if 'single-user-beamformer' in beamform -%} -[SU-BEAMFORMER] -{%- elif 'single-user-beamformee' in beamform -%} -[SU-BEAMFORMEE] -{%- elif 'multi-user-beamformer' in beamform -%} -[MU-BEAMFORMER] -{%- elif 'multi-user-beamformee' in beamform -%} -[MU-BEAMFORMEE] -{%- endif -%} -{%- endfor -%} -{%- endif -%} - -{%- if cap_vht_antenna_fixed -%} -[RX-ANTENNA-PATTERN][TX-ANTENNA-PATTERN] -{%- endif -%} - -{%- if cap_vht_antenna_cnt -%} -{%- for beamform in cap_vht_beamform -%} -{%- if 'single-user-beamformer' in beamform -%} -[BF-ANTENNA-{{ cap_vht_antenna_cnt|int -1 }}][SOUNDING-DIMENSION-{{ cap_vht_antenna_cnt|int -1}}] -{%- else -%} -[BF-ANTENNA-{{ cap_vht_antenna_cnt }}][SOUNDING-DIMENSION-{{ cap_vht_antenna_cnt }}] -{%- endif -%} -{%- endfor -%} -{%- endif %} - -# ieee80211n: Whether IEEE 802.11n (HT) is enabled -# 0 = disabled (default) -# 1 = enabled -# Note: You will also need to enable WMM for full HT functionality. -# Note: hw_mode=g (2.4 GHz) and hw_mode=a (5 GHz) is used to specify the band. -{% if cap_req_vht -%} -ieee80211n=0 -# Require stations to support VHT PHY (reject association if they do not) -require_vht=1 -{% endif %} - -{% if cap_vht_center_freq_1 -%} -# center freq = 5 GHz + (5 * index) -# So index 42 gives center freq 5.210 GHz -# which is channel 42 in 5G band -vht_oper_centr_freq_seg0_idx={{ cap_vht_center_freq_1 }} -{% endif %} - -{% if cap_vht_center_freq_2 -%} -# center freq = 5 GHz + (5 * index) -# So index 159 gives center freq 5.795 GHz -# which is channel 159 in 5G band -vht_oper_centr_freq_seg1_idx={{ cap_vht_center_freq_2 }} -{% endif %} - -{% if disable_broadcast_ssid -%} -# Send empty SSID in beacons and ignore probe request frames that do not -# specify full SSID, i.e., require stations to know SSID. -# default: disabled (0) -# 1 = send empty (length=0) SSID in beacon and ignore probe request for -# broadcast SSID -# 2 = clear SSID (ASCII 0), but keep the original length (this may be required -# with some clients that do not support empty SSID) and ignore probe -# requests for broadcast SSID -ignore_broadcast_ssid=1 -{% endif %} - -# Station MAC address -based authentication -# Please note that this kind of access control requires a driver that uses -# hostapd to take care of management frame processing and as such, this can be -# used with driver=hostap or driver=nl80211, but not with driver=atheros. -# 0 = accept unless in deny list -# 1 = deny unless in accept list -# 2 = use external RADIUS server (accept/deny lists are searched first) -macaddr_acl=0 - -{% if max_stations -%} -# Maximum number of stations allowed in station table. New stations will be -# rejected after the station table is full. IEEE 802.11 has a limit of 2007 -# different association IDs, so this number should not be larger than that. -# (default: 2007) -max_num_sta={{ max_stations }} -{% endif %} - -{% if isolate_stations -%} -# Client isolation can be used to prevent low-level bridging of frames between -# associated stations in the BSS. By default, this bridging is allowed. -ap_isolate=1 -{% endif %} - -{% if reduce_transmit_power -%} -# Add Power Constraint element to Beacon and Probe Response frames -# This config option adds Power Constraint element when applicable and Country -# element is added. Power Constraint element is required by Transmit Power -# Control. This can be used only with ieee80211d=1. -# Valid values are 0..255. -local_pwr_constraint={{ reduce_transmit_power }} -{% endif %} - -{% if expunge_failing_stations -%} -# Disassociate stations based on excessive transmission failures or other -# indications of connection loss. This depends on the driver capabilities and -# may not be available with all drivers. -disassoc_low_ack=1 -{% endif %} - -{% if sec_wep -%} -# IEEE 802.11 specifies two authentication algorithms. hostapd can be -# configured to allow both of these or only one. Open system authentication -# should be used with IEEE 802.1X. -# Bit fields of allowed authentication algorithms: -# bit 0 = Open System Authentication -# bit 1 = Shared Key Authentication (requires WEP) -auth_algs=2 - -# WEP rekeying (disabled if key lengths are not set or are set to 0) -# Key lengths for default/broadcast and individual/unicast keys: -# 5 = 40-bit WEP (also known as 64-bit WEP with 40 secret bits) -# 13 = 104-bit WEP (also known as 128-bit WEP with 104 secret bits) -wep_key_len_broadcast=5 -wep_key_len_unicast=5 - -# Static WEP key configuration -# -# The key number to use when transmitting. -# It must be between 0 and 3, and the corresponding key must be set. -# default: not set -wep_default_key=0 - -# The WEP keys to use. -# A key may be a quoted string or unquoted hexadecimal digits. -# The key length should be 5, 13, or 16 characters, or 10, 26, or 32 -# digits, depending on whether 40-bit (64-bit), 104-bit (128-bit), or -# 128-bit (152-bit) WEP is used. -# Only the default key must be supplied; the others are optional. -{% if sec_wep_key -%} -{% for key in sec_wep_key -%} -wep_key{{ loop.index -1 }}={{ key}} -{% endfor %} -{%- endif %} - -{% elif sec_wpa -%} -##### WPA/IEEE 802.11i configuration ########################################## - -# Enable WPA. Setting this variable configures the AP to require WPA (either -# WPA-PSK or WPA-RADIUS/EAP based on other configuration). For WPA-PSK, either -# wpa_psk or wpa_passphrase must be set and wpa_key_mgmt must include WPA-PSK. -# Instead of wpa_psk / wpa_passphrase, wpa_psk_radius might suffice. -# For WPA-RADIUS/EAP, ieee8021x must be set (but without dynamic WEP keys), -# RADIUS authentication server must be configured, and WPA-EAP must be included -# in wpa_key_mgmt. -# This field is a bit field that can be used to enable WPA (IEEE 802.11i/D3.0) -# and/or WPA2 (full IEEE 802.11i/RSN): -# bit0 = WPA -# bit1 = IEEE 802.11i/RSN (WPA2) (dot11RSNAEnabled) -{% if 'both' in sec_wpa_mode -%} -wpa=3 -{%- elif 'wpa2' in sec_wpa_mode -%} -wpa=2 -{%- elif 'wpa' in sec_wpa_mode -%} -wpa=1 -{%- endif %} - -{% if sec_wpa_cipher -%} -# Set of accepted cipher suites (encryption algorithms) for pairwise keys -# (unicast packets). This is a space separated list of algorithms: -# CCMP = AES in Counter mode with CBC-MAC (CCMP-128) -# TKIP = Temporal Key Integrity Protocol -# CCMP-256 = AES in Counter mode with CBC-MAC with 256-bit key -# GCMP = Galois/counter mode protocol (GCMP-128) -# GCMP-256 = Galois/counter mode protocol with 256-bit key -# Group cipher suite (encryption algorithm for broadcast and multicast frames) -# is automatically selected based on this configuration. If only CCMP is -# allowed as the pairwise cipher, group cipher will also be CCMP. Otherwise, -# TKIP will be used as the group cipher. The optional group_cipher parameter can -# be used to override this automatic selection. -{% if 'wpa2' in sec_wpa_mode -%} -# Pairwise cipher for RSN/WPA2 (default: use wpa_pairwise value) -rsn_pairwise={{ sec_wpa_cipher | join(" ") }} -{% else -%} -# Pairwise cipher for WPA (v1) (default: TKIP) -wpa_pairwise={{ sec_wpa_cipher | join(" ") }} -{%- endif -%} -{% endif %} - -{% if sec_wpa_passphrase -%} -# IEEE 802.11 specifies two authentication algorithms. hostapd can be -# configured to allow both of these or only one. Open system authentication -# should be used with IEEE 802.1X. -# Bit fields of allowed authentication algorithms: -# bit 0 = Open System Authentication -# bit 1 = Shared Key Authentication (requires WEP) -auth_algs=1 - -# WPA pre-shared keys for WPA-PSK. This can be either entered as a 256-bit -# secret in hex format (64 hex digits), wpa_psk, or as an ASCII passphrase -# (8..63 characters) that will be converted to PSK. This conversion uses SSID -# so the PSK changes when ASCII passphrase is used and the SSID is changed. -wpa_passphrase={{ sec_wpa_passphrase }} - -# Set of accepted key management algorithms (WPA-PSK, WPA-EAP, or both). The -# entries are separated with a space. WPA-PSK-SHA256 and WPA-EAP-SHA256 can be -# added to enable SHA256-based stronger algorithms. -# WPA-PSK = WPA-Personal / WPA2-Personal -# WPA-PSK-SHA256 = WPA2-Personal using SHA256 -wpa_key_mgmt=WPA-PSK - -{% elif sec_wpa_radius -%} -##### IEEE 802.1X-2004 related configuration ################################## -# Require IEEE 802.1X authorization -ieee8021x=1 - -# Set of accepted key management algorithms (WPA-PSK, WPA-EAP, or both). The -# entries are separated with a space. WPA-PSK-SHA256 and WPA-EAP-SHA256 can be -# added to enable SHA256-based stronger algorithms. -# WPA-EAP = WPA-Enterprise / WPA2-Enterprise -# WPA-EAP-SHA256 = WPA2-Enterprise using SHA256 -wpa_key_mgmt=WPA-EAP - -{% if sec_wpa_radius_source -%} -# RADIUS client forced local IP address for the access point -# Normally the local IP address is determined automatically based on configured -# IP addresses, but this field can be used to force a specific address to be -# used, e.g., when the device has multiple IP addresses. -radius_client_addr={{ sec_wpa_radius_source }} -{% endif %} - -{% for radius in sec_wpa_radius -%} -# RADIUS authentication server -auth_server_addr={{ radius.server }} -auth_server_port={{ radius.port }} -auth_server_shared_secret={{ radius.key }} -{% if radius.acc_port -%} -# RADIUS accounting server -acct_server_addr={{ radius.server }} -acct_server_port={{ radius.acc_port }} -acct_server_shared_secret={{ radius.key }} -{% endif %} -{% endfor %} - -{% endif %} - -{% else %} -# Open system -auth_algs=1 -{% endif %} - -# TX queue parameters (EDCF / bursting) -# tx_queue_<queue name>_<param> -# queues: data0, data1, data2, data3 -# (data0 is the highest priority queue) -# parameters: -# aifs: AIFS (default 2) -# cwmin: cwMin (1, 3, 7, 15, 31, 63, 127, 255, 511, 1023, 2047, 4095, 8191, -# 16383, 32767) -# cwmax: cwMax (same values as cwMin, cwMax >= cwMin) -# burst: maximum length (in milliseconds with precision of up to 0.1 ms) for -# bursting -# -# Default WMM parameters (IEEE 802.11 draft; 11-03-0504-03-000e): -# These parameters are used by the access point when transmitting frames -# to the clients. -# -# Low priority / AC_BK = background -tx_queue_data3_aifs=7 -tx_queue_data3_cwmin=15 -tx_queue_data3_cwmax=1023 -tx_queue_data3_burst=0 -# Note: for IEEE 802.11b mode: cWmin=31 cWmax=1023 burst=0 -# -# Normal priority / AC_BE = best effort -tx_queue_data2_aifs=3 -tx_queue_data2_cwmin=15 -tx_queue_data2_cwmax=63 -tx_queue_data2_burst=0 -# Note: for IEEE 802.11b mode: cWmin=31 cWmax=127 burst=0 -# -# High priority / AC_VI = video -tx_queue_data1_aifs=1 -tx_queue_data1_cwmin=7 -tx_queue_data1_cwmax=15 -tx_queue_data1_burst=3.0 -# Note: for IEEE 802.11b mode: cWmin=15 cWmax=31 burst=6.0 -# -# Highest priority / AC_VO = voice -tx_queue_data0_aifs=1 -tx_queue_data0_cwmin=3 -tx_queue_data0_cwmax=7 -tx_queue_data0_burst=1.5 - -# Default WMM parameters (IEEE 802.11 draft; 11-03-0504-03-000e): -# for 802.11a or 802.11g networks -# These parameters are sent to WMM clients when they associate. -# The parameters will be used by WMM clients for frames transmitted to the -# access point. -# -# note - txop_limit is in units of 32microseconds -# note - acm is admission control mandatory flag. 0 = admission control not -# required, 1 = mandatory -# note - Here cwMin and cmMax are in exponent form. The actual cw value used -# will be (2^n)-1 where n is the value given here. The allowed range for these -# wmm_ac_??_{cwmin,cwmax} is 0..15 with cwmax >= cwmin. -# -wmm_enabled=1 - -# Low priority / AC_BK = background -wmm_ac_bk_cwmin=4 -wmm_ac_bk_cwmax=10 -wmm_ac_bk_aifs=7 -wmm_ac_bk_txop_limit=0 -wmm_ac_bk_acm=0 -# Note: for IEEE 802.11b mode: cWmin=5 cWmax=10 -# -# Normal priority / AC_BE = best effort -wmm_ac_be_aifs=3 -wmm_ac_be_cwmin=4 -wmm_ac_be_cwmax=10 -wmm_ac_be_txop_limit=0 -wmm_ac_be_acm=0 -# Note: for IEEE 802.11b mode: cWmin=5 cWmax=7 -# -# High priority / AC_VI = video -wmm_ac_vi_aifs=2 -wmm_ac_vi_cwmin=3 -wmm_ac_vi_cwmax=4 -wmm_ac_vi_txop_limit=94 -wmm_ac_vi_acm=0 -# Note: for IEEE 802.11b mode: cWmin=4 cWmax=5 txop_limit=188 -# -# Highest priority / AC_VO = voice -wmm_ac_vo_aifs=2 -wmm_ac_vo_cwmin=2 -wmm_ac_vo_cwmax=3 -wmm_ac_vo_txop_limit=47 -wmm_ac_vo_acm=0 - -""" - -# Please be careful if you edit the template. -config_wpa_suppl_tmpl = """ -# WPA supplicant config -network={ - ssid="{{ ssid }}" -{%- if sec_wpa_passphrase %} - psk="{{ sec_wpa_passphrase }}" -{% endif %} -} - -""" - default_config_data = { 'address': [], 'address_remove': [], @@ -792,7 +62,7 @@ default_config_data = { 'cap_vht_center_freq_2' : '', 'cap_vht_chan_set_width' : '', 'cap_vht_ldpc' : False, - 'cap_vht_link_adaptation' : False, + 'cap_vht_link_adaptation' : '', 'cap_vht_max_mpdu_exp' : '', 'cap_vht_max_mpdu' : '', 'cap_vht_short_gi' : [], @@ -820,6 +90,10 @@ default_config_data = { '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, 'mac' : '', 'max_stations' : '', 'mgmt_frame_protection' : 'disabled', @@ -834,9 +108,10 @@ default_config_data = { 'sec_wpa_passphrase' : '', 'sec_wpa_radius' : [], 'ssid' : '', - 'type' : 'monitor', + 'op_mode' : 'monitor', 'vif': [], - 'vif_remove': [] + 'vif_remove': [], + 'vrf': '' } def get_conf_file(conf_type, intf): @@ -845,11 +120,8 @@ def get_conf_file(conf_type, intf): # create directory on demand if not os.path.exists(cfg_dir): os.mkdir(cfg_dir) - # fix permissions - corresponds to mode 755 - os.chmod(cfg_dir, S_IRWXU|S_IRGRP|S_IXGRP|S_IROTH|S_IXOTH) - uid = getpwnam(user).pw_uid - gid = getgrnam(group).gr_gid - os.chown(cfg_dir, uid, gid) + chmod_x(cfg_dir) + chown(cfg_dir, user, group) cfg_file = cfg_dir + r'/{}.cfg'.format(intf) return cfg_file @@ -860,11 +132,8 @@ def get_pid(conf_type, intf): # create directory on demand if not os.path.exists(cfg_dir): os.mkdir(cfg_dir) - # fix permissions - corresponds to mode 755 - os.chmod(cfg_dir, S_IRWXU|S_IRGRP|S_IXGRP|S_IROTH|S_IXOTH) - uid = getpwnam(user).pw_uid - gid = getgrnam(group).gr_gid - os.chown(cfg_dir, uid, gid) + chmod_x(cfg_dir) + chown(cfg_dir, user, group) cfg_file = cfg_dir + r'/{}.pid'.format(intf) return cfg_file @@ -876,28 +145,22 @@ def get_wpa_suppl_config_name(intf): # create directory on demand if not os.path.exists(cfg_dir): os.mkdir(cfg_dir) - # fix permissions - corresponds to mode 755 - os.chmod(cfg_dir, S_IRWXU|S_IRGRP|S_IXGRP|S_IROTH|S_IXOTH) - uid = getpwnam(user).pw_uid - gid = getgrnam(group).gr_gid - os.chown(cfg_dir, uid, gid) + chmod_x(cfg_dir) + chown(cfg_dir, user, group) cfg_file = cfg_dir + r'/{}.cfg'.format(intf) return cfg_file -def subprocess_cmd(command): - p = Popen(command, stdout=PIPE, shell=True) - p.communicate() def get_config(): wifi = deepcopy(default_config_data) conf = Config() # determine tagNode instance - try: - wifi['intf'] = os.environ['VYOS_TAGNODE_VALUE'] - except KeyError as E: - print("Interface not specified") + if 'VYOS_TAGNODE_VALUE' not in os.environ: + raise ConfigError('Interface (VYOS_TAGNODE_VALUE) not specified') + + wifi['intf'] = os.environ['VYOS_TAGNODE_VALUE'] # check if wireless interface has been removed cfg_base = 'interfaces wireless ' + wifi['intf'] @@ -1031,7 +294,7 @@ def get_config(): # VHT link adaptation capabilities if conf.exists('capabilities vht link-adaptation'): wifi['cap_vht'] = True - wifi['cap_vht_link_adaptation'] = True + wifi['cap_vht_link_adaptation'] = conf.return_value('capabilities vht link-adaptation') # Set the maximum length of A-MPDU pre-EOF padding that the station can receive if conf.exists('capabilities vht max-mpdu-exp'): @@ -1128,10 +391,30 @@ def get_config(): 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') @@ -1148,9 +431,9 @@ def get_config(): if conf.exists('mode'): wifi['mode'] = conf.return_value('mode') - # Wireless physical device - if conf.exists('phy'): - wifi['phy'] = conf.return_value('phy') + # retrieve VRF instance + if conf.exists('vrf'): + wifi['vrf'] = conf.return_value('vrf') # Transmission power reduction in dBm if conf.exists('reduce-transmit-power'): @@ -1204,6 +487,7 @@ def get_config(): radius = { 'server' : server, 'acc_port' : '', + 'disabled': False, 'port' : 1812, 'key' : '' } @@ -1216,6 +500,10 @@ def get_config(): if conf.exists('accounting'): radius['acc_port'] = radius['port'] + 1 + # Check if RADIUS server was temporary disabled + if conf.exists(['disable']): + radius['disabled'] = True + # RADIUS server shared-secret if conf.exists('key'): radius['key'] = conf.return_value('key') @@ -1232,7 +520,11 @@ def get_config(): # Wireless device type for this interface if conf.exists('type'): - wifi['type'] = conf.return_value('type') + tmp = conf.return_value('type') + if tmp == 'access-point': + tmp = 'ap' + + wifi['op_mode'] = tmp # re-set configuration level to parse new nodes conf.set_level(cfg_base) @@ -1248,6 +540,9 @@ def get_config(): 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') @@ -1259,12 +554,23 @@ def get_config(): def verify(wifi): if wifi['deleted']: + interface = wifi['intf'] + is_member, bridge = is_bridge_member(interface) + if is_member: + # can not use a f'' formatted-string here as bridge would not get + # expanded in the print statement + raise ConfigError('Can not delete interface "{0}" as it ' \ + 'is a member of bridge "{1}"!'.format(interface, bridge)) return None - if wifi['type'] != 'monitor' and not wifi['ssid']: + + if wifi['op_mode'] != 'monitor' and not wifi['ssid']: raise ConfigError('SSID must be set for {}'.format(wifi['intf'])) - if wifi['type'] == 'access-point': + if not wifi['phy']: + raise ConfigError('You must specify physical-device') + + if wifi['op_mode'] == 'ap': c = Config() if not c.exists('system wifi-regulatory-domain'): raise ConfigError('Wireless regulatory domain is mandatory,\n' \ @@ -1273,7 +579,6 @@ def verify(wifi): if not wifi['channel']: raise ConfigError('Channel must be set for {}'.format(wifi['intf'])) - if len(wifi['sec_wep_key']) > 4: raise ConfigError('No more then 4 WEP keys configurable') @@ -1283,6 +588,10 @@ def verify(wifi): if wifi['cap_vht_beamform'] and wifi['cap_vht_antenna_cnt'] == 1: raise ConfigError('Cannot use beam forming with just one antenna!') + if wifi['cap_vht_beamform'] == 'single-user-beamformer' and wifi['cap_vht_antenna_cnt'] < 3: + # Nasty Gotcha: see https://w1.fi/cgit/hostap/plain/hostapd/hostapd.conf lines 692-705 + raise ConfigError('Single-user beam former requires at least 3 antennas!') + if wifi['sec_wep'] and (len(wifi['sec_wep_key']) == 0): raise ConfigError('Missing WEP keys') @@ -1293,35 +602,55 @@ 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') + # use common function to verify VLAN configuration + verify_vlan_config(wifi) + + conf = Config() + # Only one wireless interface per phy can be in station mode + base = ['interfaces', 'wireless'] + for phy in os.listdir('/sys/class/ieee80211'): + stations = [] + for wlan in conf.list_nodes(base): + # the following node is mandatory + if conf.exists(base + [wlan, 'physical-device', phy]): + tmp = conf.return_value(base + [wlan, 'type']) + if tmp == 'station': + stations.append(wlan) + + if len(stations) > 1: + raise ConfigError('Only one station per wireless physical interface possible!') return None def generate(wifi): - pid = 0 + # Prepare Jinja2 template loader from files + tmpl_path = os.path.join(vyos_data_dir["data"], "templates", "wifi") + fs_loader = FileSystemLoader(tmpl_path) + env = Environment(loader=fs_loader) + # always stop hostapd service first before reconfiguring it pidfile = get_pid('hostapd', wifi['intf']) - if os.path.isfile(pidfile): - pid = 0 - with open(pidfile, 'r') as f: - pid = int(f.read()) - - if pid_exists(pid): - cmd = 'start-stop-daemon --stop --quiet' - cmd += ' --pidfile ' + pidfile - subprocess_cmd(cmd) + if process_running(pidfile): + command = 'start-stop-daemon' + command += ' --stop ' + command += ' --quiet' + command += ' --oknodo' + command += ' --pidfile ' + pidfile + run(command) # always stop wpa_supplicant service first before reconfiguring it pidfile = get_pid('wpa_supplicant', wifi['intf']) - if os.path.isfile(pidfile): - pid = 0 - with open(pidfile, 'r') as f: - pid = int(f.read()) - - if pid_exists(pid): - cmd = 'start-stop-daemon --stop --quiet' - cmd += ' --pidfile ' + pidfile - subprocess_cmd(cmd) + if process_running(pidfile): + command = 'start-stop-daemon' + command += ' --stop ' + command += ' --quiet' + command += ' --oknodo' + command += ' --pidfile ' + pidfile + run(command) # Delete config files if interface is removed if wifi['deleted']: @@ -1333,15 +662,37 @@ def generate(wifi): return None + if not wifi['mac']: + # http://wiki.stocksy.co.uk/wiki/Multiple_SSIDs_with_hostapd + # generate locally administered MAC address from used phy interface + with open('/sys/class/ieee80211/{}/addresses'.format(wifi['phy']), 'r') as f: + # some PHYs tend to have multiple interfaces and thus supply multiple MAC + # addresses - we only need the first one for our calculation + tmp = f.readline().rstrip() + tmp = EUI(tmp).value + # mask last nibble from the MAC address + tmp &= 0xfffffffffff0 + # set locally administered bit in MAC address + tmp |= 0x020000000000 + # we now need to add an offset to our MAC address indicating this + # subinterfaces index + tmp += int(findall(r'\d+', wifi['intf'])[0]) + + # convert integer to "real" MAC address representation + mac = EUI(hex(tmp).split('x')[-1]) + # change dialect to use : as delimiter instead of - + mac.dialect = mac_unix_expanded + wifi['mac'] = str(mac) + # render appropriate new config files depending on access-point or station mode - if wifi['type'] == 'access-point': - tmpl = Template(config_hostapd_tmpl) + if wifi['op_mode'] == 'ap': + tmpl = env.get_template('hostapd.conf.tmpl') config_text = tmpl.render(wifi) with open(get_conf_file('hostapd', wifi['intf']), 'w') as f: f.write(config_text) - elif wifi['type'] == 'station': - tmpl = Template(config_wpa_suppl_tmpl) + elif wifi['op_mode'] == 'station': + tmpl = env.get_template('wpa_supplicant.conf.tmpl') config_text = tmpl.render(wifi) with open(get_conf_file('wpa_supplicant', wifi['intf']), 'w') as f: f.write(config_text) @@ -1349,13 +700,24 @@ def generate(wifi): return None def apply(wifi): - w = EthernetIf(wifi['intf']) if wifi['deleted']: + w = WiFiIf(wifi['intf']) # delete interface w.remove() else: - # Some parts e.g. MAC address can't be changed when interface is up - w.set_state('down') + # WiFi interface needs to be created on-block (e.g. mode or physical + # interface) instead of passing a ton of arguments, I just use a dict + # that is managed by vyos.ifconfig + conf = deepcopy(WiFiIf.get_config()) + + # Assign WiFi instance configuration parameters to config dict + conf['phy'] = wifi['phy'] + + # Finally create the new interface + w = WiFiIf(wifi['intf'], **conf) + + # assign/remove VRF + w.set_vrf(wifi['vrf']) # update interface description used e.g. within SNMP w.set_alias(wifi['description']) @@ -1394,7 +756,7 @@ def apply(wifi): # if custom mac is removed if wifi['mac']: w.set_mac(wifi['mac']) - else: + elif wifi['hw_id']: w.set_mac(wifi['hw_id']) # configure ARP filter configuration @@ -1405,10 +767,14 @@ def apply(wifi): w.set_arp_announce(wifi['ip_enable_arp_announce']) # configure ARP ignore w.set_arp_ignore(wifi['ip_enable_arp_ignore']) - - # enable interface - if not wifi['disable']: - w.set_state('up') + # 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 + w.set_ipv6_dad_messages(wifi['ipv6_dup_addr_detect']) # Configure interface address(es) # - not longer required addresses get removed first @@ -1437,30 +803,46 @@ def apply(wifi): vlan = e.add_vlan(vif['id']) apply_vlan_config(vlan, vif) - # Physical interface is now configured. Proceed by starting hostapd or - # wpa_supplicant daemon. When type is monitor we can just skip this. - if wifi['type'] == 'access-point': - cmd = 'start-stop-daemon --start --quiet' - cmd += ' --exec /usr/sbin/hostapd' - # now pass arguments to hostapd binary - cmd += ' -- -B' - cmd += ' -P {}'.format(get_pid('hostapd', wifi['intf'])) - cmd += ' {}'.format(get_conf_file('hostapd', wifi['intf'])) - - # execute assembled command - subprocess_cmd(cmd) - - elif wifi['type'] == 'station': - cmd = 'start-stop-daemon --start --quiet' - cmd += ' --exec /sbin/wpa_supplicant' - # now pass arguments to hostapd binary - cmd += ' -- -s -B -D nl80211' - cmd += ' -P {}'.format(get_pid('wpa_supplicant', wifi['intf'])) - cmd += ' -i {}'.format(wifi['intf']) - cmd += ' -c {}'.format(get_conf_file('wpa_supplicant', wifi['intf'])) - - # execute assembled command - subprocess_cmd(cmd) + # Enable/Disable interface - interface is always placed in + # administrative down state in WiFiIf class + if not wifi['disable']: + w.set_admin_state('up') + + # Physical interface is now configured. Proceed by starting hostapd or + # wpa_supplicant daemon. When type is monitor we can just skip this. + if wifi['op_mode'] == 'ap': + command = 'start-stop-daemon' + command += ' --start ' + command += ' --quiet' + command += ' --oknodo' + command += ' --pidfile ' + get_pid('hostapd', wifi['intf']) + command += ' --exec /usr/sbin/hostapd' + # now pass arguments to hostapd binary + command += ' -- ' + command += ' -B' + command += ' -P ' + get_pid('hostapd', wifi['intf']) + command += ' ' + get_conf_file('hostapd', wifi['intf']) + + # execute assembled command + run(command) + + elif wifi['op_mode'] == 'station': + command = 'start-stop-daemon' + command += ' --start ' + command += ' --quiet' + command += ' --oknodo' + command += ' --pidfile ' + get_pid('hostapd', wifi['intf']) + command += ' --exec /sbin/wpa_supplicant' + # now pass arguments to hostapd binary + command += ' -- ' + command += ' -s -B -D nl80211' + command += ' -P ' + get_pid('wpa_supplicant', wifi['intf']) + command += ' -i ' + wifi['intf'] + command += ' -c ' + \ + get_conf_file('wpa_supplicant', wifi['intf']) + + # execute assembled command + run(command) return None diff --git a/src/conf_mode/interfaces-wirelessmodem.py b/src/conf_mode/interfaces-wirelessmodem.py new file mode 100755 index 000000000..c44a993c4 --- /dev/null +++ b/src/conf_mode/interfaces-wirelessmodem.py @@ -0,0 +1,237 @@ +#!/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 os + +from sys import exit +from copy import deepcopy +from jinja2 import FileSystemLoader, Environment +from netifaces import interfaces + +from vyos.config import Config +from vyos.defaults import directories as vyos_data_dir +from vyos.util import chown, chmod_x, is_bridge_member +from vyos.util import cmd +from vyos.util import call +from vyos import ConfigError + +default_config_data = { + 'address': [], + 'apn': '', + 'chat_script': '', + 'deleted': False, + 'description': '', + 'device': 'ttyUSB0', + 'disable': False, + 'disable_link_detect': 1, + 'on_demand': False, + 'logfile': '', + 'metric': '10', + 'mtu': '1500', + 'name_server': True, + 'intf': '', + 'vrf': '' +} + +def check_kmod(): + modules = ['option', 'usb_wwan', 'usbserial'] + 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_config(): + wwan = 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') + + wwan['intf'] = os.environ['VYOS_TAGNODE_VALUE'] + wwan['logfile'] = f"/var/log/vyatta/ppp_{wwan['intf']}.log" + wwan['chat_script'] = f"/etc/ppp/peers/chat.{wwan['intf']}" + + # Check if interface has been removed + if not conf.exists('interfaces wirelessmodem ' + wwan['intf']): + wwan['deleted'] = True + return wwan + + # set new configuration level + conf.set_level('interfaces wirelessmodem ' + wwan['intf']) + + # get metrick for backup default route + if conf.exists(['apn']): + wwan['apn'] = conf.return_value(['apn']) + + # get metrick for backup default route + if conf.exists(['backup', 'distance']): + wwan['metric'] = conf.return_value(['backup', 'distance']) + + # Retrieve interface description + if conf.exists(['description']): + wwan['description'] = conf.return_value(['description']) + + # System device name + if conf.exists(['device']): + wwan['device'] = conf.return_value(['device']) + + # disable interface + if conf.exists('disable'): + wwan['disable'] = True + + # ignore link state changes + if conf.exists('disable-link-detect'): + wwan['disable_link_detect'] = 2 + + # Do not use DNS servers provided by the peer + if conf.exists(['mtu']): + wwan['mtu'] = conf.return_value(['mtu']) + + # Do not use DNS servers provided by the peer + if conf.exists(['no-peer-dns']): + wwan['name_server'] = False + + # Access concentrator name (only connect to this concentrator) + if conf.exists(['ondemand']): + wwan['on_demand'] = True + + # retrieve VRF instance + if conf.exists('vrf'): + wwan['vrf'] = conf.return_value(['vrf']) + + return wwan + +def verify(wwan): + if wwan['deleted']: + interface = wwan['intf'] + is_member, bridge = is_bridge_member(interface) + if is_member: + # can not use a f'' formatted-string here as bridge would not get + # expanded in the print statement + raise ConfigError('Can not delete interface "{0}" as it ' \ + 'is a member of bridge "{1}"!'.format(interface, bridge)) + return None + + if not wwan['apn']: + raise ConfigError(f"APN for {wwan['intf']} not configured") + + # we can not use isfile() here as Linux device files are no regular files + # thus the check will return False + 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') + + return None + +def generate(wwan): + # Prepare Jinja2 template loader from files + tmpl_path = os.path.join(vyos_data_dir['data'], 'templates', 'wwan') + fs_loader = FileSystemLoader(tmpl_path) + env = Environment(loader=fs_loader) + + # set up configuration file path variables where our templates will be + # rendered into + intf = wwan['intf'] + config_wwan = f'/etc/ppp/peers/{intf}' + config_wwan_chat = wwan['chat_script'] + script_wwan_pre_up = f'/etc/ppp/ip-pre-up.d/1010-vyos-wwan-{intf}' + script_wwan_ip_up = f'/etc/ppp/ip-up.d/1010-vyos-wwan-{intf}' + script_wwan_ip_down = f'/etc/ppp/ip-down.d/1010-vyos-wwan-{intf}' + + 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') + + if wwan['deleted']: + # Delete PPP configuration files + for file in config_files: + if os.path.exists(file): + os.unlink(file) + + else: + # Create PPP configuration files + tmpl = env.get_template('peer.tmpl') + config_text = tmpl.render(wwan) + with open(config_wwan, 'w') as f: + f.write(config_text) + + # Create PPP chat script + tmpl = env.get_template('chat.tmpl') + config_text = tmpl.render(wwan) + with open(config_wwan_chat, 'w') as f: + f.write(config_text) + + # Create script for ip-pre-up.d + tmpl = env.get_template('ip-pre-up.script.tmpl') + config_text = tmpl.render(wwan) + with open(script_wwan_pre_up, 'w') as f: + f.write(config_text) + + # Create script for ip-up.d + tmpl = env.get_template('ip-up.script.tmpl') + config_text = tmpl.render(wwan) + with open(script_wwan_ip_up, 'w') as f: + f.write(config_text) + + # Create script for ip-down.d + tmpl = env.get_template('ip-down.script.tmpl') + config_text = tmpl.render(wwan) + with open(script_wwan_ip_down, 'w') as f: + f.write(config_text) + + # make generated script file executable + chmod_x(script_wwan_pre_up) + chmod_x(script_wwan_ip_up) + chmod_x(script_wwan_ip_down) + + return None + +def apply(wwan): + if wwan['deleted']: + # bail out early + return None + + if not wwan['disable']: + # "dial" WWAN connection + intf = wwan['intf'] + cmd(f'systemctl start ppp@{intf}.service') + # make logfile owned by root / vyattacfg + chown(wwan['logfile'], 'root', 'vyattacfg') + + 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/ipsec-settings.py b/src/conf_mode/ipsec-settings.py index e80c6caf0..dc04e9131 100755 --- a/src/conf_mode/ipsec-settings.py +++ b/src/conf_mode/ipsec-settings.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2018 VyOS maintainers and contributors +# Copyright (C) 2018-2020 VyOS maintainers and contributors # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 or later as @@ -13,21 +13,18 @@ # # 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 re import os -import jinja2 -import syslog as sl -import time -import vyos.config -import vyos.defaults +from time import sleep +from jinja2 import FileSystemLoader, Environment +from sys import exit +from vyos.config import Config +from vyos.defaults import directories as vyos_data_dir from vyos import ConfigError - +from vyos.util import call ra_conn_name = "remote-access" charon_conf_file = "/etc/strongswan.d/charon.conf" @@ -42,55 +39,8 @@ delim_ipsec_l2tp_begin = "### VyOS L2TP VPN Begin ###" delim_ipsec_l2tp_end = "### VyOS L2TP VPN End ###" charon_pidfile = "/var/run/charon.pid" -l2pt_ipsec_conf = ''' -{{delim_ipsec_l2tp_begin}} -include {{ipsec_ra_conn_file}} -{{delim_ipsec_l2tp_end}} -''' - -l2pt_ipsec_secrets_conf = ''' -{{delim_ipsec_l2tp_begin}} -{% if ipsec_l2tp_auth_mode == 'pre-shared-secret' %} -{{outside_addr}} %any : PSK "{{ipsec_l2tp_secret}}" -{% elif ipsec_l2tp_auth_mode == 'x509' %} -: RSA {{server_key_file_copied}} -{% endif%} -{{delim_ipsec_l2tp_end}} -''' - -l2tp_ipsec_ra_conn_conf = ''' -{{delim_ipsec_l2tp_begin}} -conn {{ra_conn_name}} - type=transport - left={{outside_addr}} - leftsubnet=%dynamic[/1701] - rightsubnet=%dynamic - mark_in=%unique - auto=add - ike=aes256-sha1-modp1024,3des-sha1-modp1024,3des-sha1-modp1024! - dpddelay=15 - dpdtimeout=45 - dpdaction=clear - esp=aes256-sha1,3des-sha1! - rekey=no -{% if ipsec_l2tp_auth_mode == 'pre-shared-secret' %} - authby=secret - leftauth=psk - rightauth=psk -{% elif ipsec_l2tp_auth_mode == 'x509' %} - authby=rsasig - leftrsasigkey=%cert - rightrsasigkey=%cert - rightca=%same - leftcert={{server_cert_file_copied}} -{% endif %} - ikelifetime={{ipsec_l2tp_ike_lifetime}} - keylife={{ipsec_l2tp_lifetime}} -{{delim_ipsec_l2tp_end}} -''' - def get_config(): - config = vyos.config.Config() + config = Config() data = {"install_routes": "yes"} if config.exists("vpn ipsec options disable-route-autoinstall"): @@ -146,44 +96,12 @@ def get_config(): return data -### ipsec secret l2tp -def write_ipsec_secrets(c): - tmpl = jinja2.Template(l2pt_ipsec_secrets_conf, trim_blocks=True) - l2pt_ipsec_secrets_txt = tmpl.render(c) - old_umask = os.umask(0o077) - open(ipsec_secrets_flie,'w').write(l2pt_ipsec_secrets_txt) - os.umask(old_umask) - sl.syslog(sl.LOG_NOTICE, ipsec_secrets_flie + ' written') - -### ipsec remote access connection config -def write_ipsec_ra_conn(c): - tmpl = jinja2.Template(l2tp_ipsec_ra_conn_conf, trim_blocks=True) - ipsec_ra_conn_txt = tmpl.render(c) - old_umask = os.umask(0o077) - - # Create tunnels directory if does not exist - if not os.path.exists(ipsec_ra_conn_dir): - os.makedirs(ipsec_ra_conn_dir) - sl.syslog(sl.LOG_NOTICE, ipsec_ra_conn_dir + " created") - - open(ipsec_ra_conn_file,'w').write(ipsec_ra_conn_txt) - os.umask(old_umask) - sl.syslog(sl.LOG_NOTICE, ipsec_ra_conn_file + ' written') ### Remove config from file by delimiter def remove_confs(delim_begin, delim_end, conf_file): - os.system("sed -i '/"+delim_begin+"/,/"+delim_end+"/d' "+conf_file) + call("sed -i '/"+delim_begin+"/,/"+delim_end+"/d' "+conf_file) -### Append "include /path/to/ra_conn" to ipsec conf file -def append_ipsec_conf(c): - tmpl = jinja2.Template(l2pt_ipsec_conf, trim_blocks=True) - l2pt_ipsec_conf_txt = tmpl.render(c) - old_umask = os.umask(0o077) - open(ipsec_conf_flie,'a').write(l2pt_ipsec_conf_txt) - os.umask(old_umask) - sl.syslog(sl.LOG_NOTICE, ipsec_conf_flie + ' written') - ### Checking certificate storage and notice if certificate not in /config directory def check_cert_file_store(cert_name, file_path, dts_path): if not re.search('^\/config\/.+', file_path): @@ -194,11 +112,9 @@ def check_cert_file_store(cert_name, file_path, dts_path): else: ### Cpy file to /etc/ipsec.d/certs/ /etc/ipsec.d/cacerts/ # todo make check - ret = os.system('cp -f '+file_path+' '+dts_path) + ret = call('cp -f '+file_path+' '+dts_path) if ret: raise ConfigError("L2TP VPN configuration error: Cannot copy "+file_path) - else: - sl.syslog(sl.LOG_NOTICE, file_path + ' copied to '+dts_path) def verify(data): # l2tp ipsec check @@ -231,22 +147,45 @@ def verify(data): raise ConfigError("L2TP VPN configuration error: \"vpn ipsec ipsec-interfaces\" must be specified.") def generate(data): - tmpl_path = os.path.join(vyos.defaults.directories["data"], "templates", "ipsec") - fs_loader = jinja2.FileSystemLoader(tmpl_path) - env = jinja2.Environment(loader=fs_loader) - - - charon_conf_tmpl = env.get_template("charon.tmpl") - charon_conf = charon_conf_tmpl.render(data) + tmpl_path = os.path.join(vyos_data_dir['data'], 'templates', 'ipsec') + fs_loader = FileSystemLoader(tmpl_path) + env = Environment(loader=fs_loader, trim_blocks=True) + tmpl = env.get_template('charon.tmpl') + config_text = tmpl.render(data) with open(charon_conf_file, 'w') as f: - f.write(charon_conf) + f.write(config_text) if data["ipsec_l2tp"]: remove_confs(delim_ipsec_l2tp_begin, delim_ipsec_l2tp_end, ipsec_conf_flie) - write_ipsec_secrets(data) - write_ipsec_ra_conn(data) - append_ipsec_conf(data) + + tmpl = env.get_template('ipsec.secrets.tmpl') + l2pt_ipsec_secrets_txt = tmpl.render(c) + old_umask = os.umask(0o077) + with open(ipsec_secrets_flie,'w') as f: + f.write(l2pt_ipsec_secrets_txt) + os.umask(old_umask) + + tmpl = env.get_template('remote-access.tmpl') + ipsec_ra_conn_txt = tmpl.render(c) + old_umask = os.umask(0o077) + + # Create tunnels directory if does not exist + if not os.path.exists(ipsec_ra_conn_dir): + os.makedirs(ipsec_ra_conn_dir) + + with open(ipsec_ra_conn_file,'w') as f: + f.write(ipsec_ra_conn_txt) + os.umask(old_umask) + + + tmpl = env.get_template('ipsec.conf.tmpl') + l2pt_ipsec_conf_txt = tmpl.render(c) + old_umask = os.umask(0o077) + with open(ipsec_conf_flie,'a') as f: + f.write(l2pt_ipsec_conf_txt) + os.umask(old_umask) + else: if os.path.exists(ipsec_ra_conn_file): remove_confs(delim_ipsec_l2tp_begin, delim_ipsec_l2tp_end, ipsec_ra_conn_file) @@ -254,15 +193,15 @@ def generate(data): remove_confs(delim_ipsec_l2tp_begin, delim_ipsec_l2tp_end, ipsec_conf_flie) def restart_ipsec(): - os.system("ipsec restart >&/dev/null") + call('ipsec restart >&/dev/null') # counter for apply swanctl config counter = 10 while counter <= 10: if os.path.exists(charon_pidfile): - os.system("swanctl -q >&/dev/null") + call('swanctl -q >&/dev/null') break counter -=1 - time.sleep(1) + sleep(1) if counter == 0: raise ConfigError('VPN configuration error: IPSec is not running.') @@ -278,4 +217,4 @@ if __name__ == '__main__': apply(c) except ConfigError as e: print(e) - sys.exit(1) + exit(1) diff --git a/src/conf_mode/le_cert.py b/src/conf_mode/le_cert.py index c657098e1..4b365a566 100755 --- a/src/conf_mode/le_cert.py +++ b/src/conf_mode/le_cert.py @@ -18,11 +18,13 @@ import sys import os -import subprocess import vyos.defaults from vyos.config import Config from vyos import ConfigError +from vyos.util import cmd +from vyos.util import call + vyos_conf_scripts_dir = vyos.defaults.directories['conf_mode'] @@ -45,9 +47,9 @@ def request_certbot(cert): certbot_cmd = 'certbot certonly -n --nginx --agree-tos --no-eff-email --expand {0} {1}'.format(email_flag, domain_flag) - completed = subprocess.run(certbot_cmd, shell=True) - - return completed.returncode + cmd(certbot_cmd, + raising=ConfigError, + message="The certbot request failed for the specified domains.") def get_config(): conf = Config() @@ -84,28 +86,21 @@ def generate(cert): # certbot will attempt to reload nginx, even with 'certonly'; # start nginx if not active - ret = os.system('systemctl is-active --quiet nginx.ervice') + ret = call('systemctl is-active --quiet nginx.ervice') if ret: - os.system('sudo systemctl start nginx.service') + call('sudo systemctl start nginx.service') - ret = request_certbot(cert) - if ret: - raise ConfigError("The certbot request failed for the" - " specified domains.") + request_certbot(cert) def apply(cert): if cert is not None: - os.system('sudo systemctl restart certbot.timer') + call('sudo systemctl restart certbot.timer') else: - os.system('sudo systemctl stop certbot.timer') + call('sudo systemctl stop certbot.timer') return None for dep in dependencies: - cmd = '{0}/{1}'.format(vyos_conf_scripts_dir, dep) - try: - subprocess.check_call(cmd, shell=True) - except subprocess.CalledProcessError as err: - raise ConfigError(str(err)) + cmd(f'{vyos_conf_scripts_dir}/{dep}', raising=ConfigError) if __name__ == '__main__': try: diff --git a/src/conf_mode/lldp.py b/src/conf_mode/lldp.py index b72916ab8..ec59c68d0 100755 --- a/src/conf_mode/lldp.py +++ b/src/conf_mode/lldp.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2017-2019 VyOS maintainers and contributors +# Copyright (C) 2017-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 @@ -14,47 +14,24 @@ # 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 sys import os -import jinja2 +import re from copy import deepcopy +from jinja2 import FileSystemLoader, Environment +from sys import exit + from vyos.config import Config from vyos.validate import is_addr_assigned,is_loopback_addr +from vyos.defaults import directories as vyos_data_dir +from vyos.version import get_version_data from vyos import ConfigError +from vyos.util import call -# Please be careful if you edit the template. -config_file = "/etc/default/lldpd" -lldp_tmpl = """ -### Autogenerated by lldp.py ### -DAEMON_ARGS="-M 4{% if options.snmp %} -x{% endif %}{% if options.cdp %} -c{% endif %}{% if options.edp %} -e{% endif %}{% if options.fdp %} -f{% endif %}{% if options.sonmp %} -s{% endif %}" - -""" +config_file = "/etc/default/lldpd" vyos_config_file = "/etc/lldpd.d/01-vyos.conf" -vyos_tmpl = """ -### Autogenerated by lldp.py ### - -configure system platform VyOS -configure system description "VyOS {{ options.description }}" -{% if options.listen_on -%} -configure system interface pattern "{{ ( options.listen_on | select('equalto','all') | map('replace','all','*') | list + options.listen_on | select('equalto','!all') | map('replace','!all','!*') | list + options.listen_on | reject('equalto','all') | reject('equalto','!all') | list ) | unique | join(",") }}" -{%- endif %} -{% if options.mgmt_addr -%} -configure system ip management pattern {{ options.mgmt_addr | join(",") }} -{%- endif %} -{%- for loc in location -%} -{%- if loc.elin %} -configure ports {{ loc.name }} med location elin "{{ loc.elin }}" -{%- endif %} -{%- if loc.coordinate_based %} -configure ports {{ loc.name }} med location coordinate {% if loc.coordinate_based.latitude %}latitude {{ loc.coordinate_based.latitude }}{% endif %} {% if loc.coordinate_based.longitude %}longitude {{ loc.coordinate_based.longitude }}{% endif %} {% if loc.coordinate_based.altitude %}altitude {{ loc.coordinate_based.altitude }} m{% endif %} {% if loc.coordinate_based.datum %}datum {{ loc.coordinate_based.datum }}{% endif %} -{%- endif %} - - -{% endfor %} -""" +base = ['service', 'lldp'] default_config_data = { "options": '', @@ -64,7 +41,7 @@ default_config_data = { def get_options(config): options = {} - config.set_level('service lldp') + config.set_level(base) options['listen_vlan'] = config.exists('listen-vlan') options['mgmt_addr'] = [] @@ -84,30 +61,31 @@ def get_options(config): if snmp: config.set_level('') options["sys_snmp"] = config.exists('service snmp') - config.set_level('service lldp') + config.set_level(base) - config.set_level('service lldp legacy-protocols') + config.set_level(base + ['legacy-protocols']) options['cdp'] = config.exists('cdp') options['edp'] = config.exists('edp') options['fdp'] = config.exists('fdp') options['sonmp'] = config.exists('sonmp') # start with an unknown version information - options['description'] = 'unknown' + version_data = get_version_data() + options['description'] = version_data['version'] options['listen_on'] = [] return options def get_interface_list(config): - config.set_level('service lldp') - intfs_names = config.list_nodes('interface') + config.set_level(base) + intfs_names = config.list_nodes(['interface']) if len(intfs_names) < 0: return 0 interface_list = [] for name in intfs_names: - config.set_level('service lldp interface {0}'.format(name)) - disable = config.exists('disable') + config.set_level(base + ['interface', name]) + disable = config.exists(['disable']) intf = { 'name': name, 'disable': disable @@ -117,10 +95,10 @@ def get_interface_list(config): def get_location_intf(config, name): - path = 'service lldp interface {0}'.format(name) + path = base + ['interface', name] config.set_level(path) - config.set_level('{} location'.format(path)) + config.set_level(path + ['location']) elin = '' coordinate_based = {} @@ -128,18 +106,18 @@ def get_location_intf(config, name): elin = config.return_value('elin') if config.exists('coordinate-based'): - config.set_level('{} location coordinate-based'.format(path)) + config.set_level(path + ['location', 'coordinate-based']) - coordinate_based['latitude'] = config.return_value('latitude') - coordinate_based['longitude'] = config.return_value('longitude') + coordinate_based['latitude'] = config.return_value(['latitude']) + coordinate_based['longitude'] = config.return_value(['longitude']) coordinate_based['altitude'] = '0' - if config.exists('altitude'): - coordinate_based['altitude'] = config.return_value('altitude') + if config.exists(['altitude']): + coordinate_based['altitude'] = config.return_value(['altitude']) coordinate_based['datum'] = 'WGS84' - if config.exists('datum'): - coordinate_based['datum'] = config.return_value('datum') + if config.exists(['datum']): + coordinate_based['datum'] = config.return_value(['datum']) intf = { 'name': name, @@ -151,8 +129,8 @@ def get_location_intf(config, name): def get_location(config): - config.set_level('service lldp') - intfs_names = config.list_nodes('interface') + config.set_level(base) + intfs_names = config.list_nodes(['interface']) if len(intfs_names) < 0: return 0 @@ -170,7 +148,7 @@ def get_location(config): def get_config(): lldp = deepcopy(default_config_data) conf = Config() - if not conf.exists('service lldp'): + if not conf.exists(base): return None else: lldp['options'] = get_options(conf) @@ -232,10 +210,10 @@ def generate(lldp): if lldp is None: return - with open('/opt/vyatta/etc/version', 'r') as f: - tmp = f.read() - lldp['options']['description'] = tmp.split()[1] - + # Prepare Jinja2 template loader from files + tmpl_path = os.path.join(vyos_data_dir['data'], 'templates', 'lldp') + fs_loader = FileSystemLoader(tmpl_path) + env = Environment(loader=fs_loader) # generate listen on interfaces for intf in lldp['interface_list']: @@ -248,13 +226,13 @@ def generate(lldp): lldp['options']['listen_on'].append(tmp) # generate /etc/default/lldpd - tmpl = jinja2.Template(lldp_tmpl) + tmpl = env.get_template('lldpd.tmpl') config_text = tmpl.render(lldp) with open(config_file, 'w') as f: f.write(config_text) # generate /etc/lldpd.d/01-vyos.conf - tmpl = jinja2.Template(vyos_tmpl) + tmpl = env.get_template('vyos.conf.tmpl') config_text = tmpl.render(lldp) with open(vyos_config_file, 'w') as f: f.write(config_text) @@ -263,10 +241,10 @@ def generate(lldp): def apply(lldp): if lldp: # start/restart lldp service - os.system('sudo systemctl restart lldpd.service') + call('sudo systemctl restart lldpd.service') else: # LLDP service has been terminated - os.system('sudo systemctl stop lldpd.service') + call('sudo systemctl stop lldpd.service') os.unlink(config_file) os.unlink(vyos_config_file) @@ -278,5 +256,5 @@ if __name__ == '__main__': apply(c) except ConfigError as e: print(e) - sys.exit(1) + exit(1) diff --git a/src/conf_mode/mdns_repeater.py b/src/conf_mode/mdns_repeater.py index cef735c0d..9230aaf61 100755 --- a/src/conf_mode/mdns_repeater.py +++ b/src/conf_mode/mdns_repeater.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2017 VyOS maintainers and contributors +# Copyright (C) 2017-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 @@ -13,23 +13,21 @@ # # You should have received a copy of the GNU General Public License # along with this program. If not, see <http://www.gnu.org/licenses/>. -# -# -import sys import os -import jinja2 -import netifaces + +from sys import exit +from copy import deepcopy +from jinja2 import FileSystemLoader, Environment +from netifaces import ifaddresses, AF_INET from vyos.config import Config +from vyos.defaults import directories as vyos_data_dir from vyos import ConfigError +from vyos.util import call -config_file = r'/etc/default/mdns-repeater' -config_tmpl = """ -### Autogenerated by mdns_repeater.py ### -DAEMON_ARGS="{{ interfaces | join(' ') }}" -""" +config_file = r'/etc/default/mdns-repeater' default_config_data = { 'disabled': False, @@ -37,21 +35,22 @@ default_config_data = { } def get_config(): - mdns = default_config_data + mdns = deepcopy(default_config_data) conf = Config() - if not conf.exists('service mdns repeater'): + base = ['service', 'mdns', 'repeater'] + if not conf.exists(base): return None else: - conf.set_level('service mdns repeater') + conf.set_level(base) # Service can be disabled by user - if conf.exists('disable'): + if conf.exists(['disable']): mdns['disabled'] = True return mdns # Interface to repeat mDNS advertisements - if conf.exists('interface'): - mdns['interfaces'] = conf.return_values('interface') + if conf.exists(['interface']): + mdns['interfaces'] = conf.return_values(['interface']) return mdns @@ -69,8 +68,8 @@ def verify(mdns): # For mdns-repeater to work it is essential that the interfaces has # an IPv4 address assigned for interface in mdns['interfaces']: - if netifaces.AF_INET in netifaces.ifaddresses(interface).keys(): - if len(netifaces.ifaddresses(interface)[netifaces.AF_INET]) < 1: + if AF_INET in ifaddresses(interface).keys(): + if len(ifaddresses(interface)[AF_INET]) < 1: raise ConfigError('mDNS repeater requires an IPv6 address configured on interface %s!'.format(interface)) return None @@ -83,7 +82,12 @@ def generate(mdns): print('Warning: mDNS repeater will be deactivated because it is disabled') return None - tmpl = jinja2.Template(config_tmpl) + # Prepare Jinja2 template loader from files + tmpl_path = os.path.join(vyos_data_dir['data'], 'templates', 'mdns-repeater') + fs_loader = FileSystemLoader(tmpl_path) + env = Environment(loader=fs_loader) + + tmpl = env.get_template('mdns-repeater.tmpl') config_text = tmpl.render(mdns) with open(config_file, 'w') as f: f.write(config_text) @@ -92,11 +96,11 @@ def generate(mdns): def apply(mdns): if (mdns is None) or mdns['disabled']: - os.system('sudo systemctl stop mdns-repeater') + call('sudo systemctl stop mdns-repeater') if os.path.exists(config_file): os.unlink(config_file) else: - os.system('sudo systemctl restart mdns-repeater') + call('sudo systemctl restart mdns-repeater') return None @@ -108,4 +112,4 @@ if __name__ == '__main__': apply(c) except ConfigError as e: print(e) - sys.exit(1) + exit(1) diff --git a/src/conf_mode/ntp.py b/src/conf_mode/ntp.py index 8f32e6e81..75328dfd7 100755 --- a/src/conf_mode/ntp.py +++ b/src/conf_mode/ntp.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2018 VyOS maintainers and contributors +# Copyright (C) 2018-2020 VyOS maintainers and contributors # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 or later as @@ -13,64 +13,21 @@ # # You should have received a copy of the GNU General Public License # along with this program. If not, see <http://www.gnu.org/licenses/>. -# -# -import sys import os -import jinja2 -import ipaddress -import copy +from copy import deepcopy +from ipaddress import ip_network +from jinja2 import FileSystemLoader, Environment +from sys import exit from vyos.config import Config +from vyos.defaults import directories as vyos_data_dir from vyos import ConfigError +from vyos.util import call -config_file = r'/etc/ntp.conf' - -# Please be careful if you edit the template. -config_tmpl = """ -### Autogenerated by ntp.py ### -# -# Non-configurable defaults -# -driftfile /var/lib/ntp/ntp.drift -# By default, only allow ntpd to query time sources, ignore any incoming requests -restrict default noquery nopeer notrap nomodify -# Local users have unrestricted access, allowing reconfiguration via ntpdc -restrict 127.0.0.1 -restrict -6 ::1 - -# Do not listen on any interface address by default -interface ignore wildcard -# -# Configurable section -# - -{% if servers -%} -{% for s in servers -%} -# Server configuration for: {{ s.name }} -server {{ s.name }} iburst {{ s.options | join(" ") }} -{% endfor -%} -{% endif %} - -{% if allowed_networks -%} -{% for n in allowed_networks -%} -# Client configuration for network: {{ n.network }} -restrict {{ n.address }} mask {{ n.netmask }} nomodify notrap nopeer - -{% endfor -%} -{% endif %} - -{% if listen_address -%} -# NTP should listen on configured addresses only -{% for a in listen_address -%} -interface listen {{ a }} -{% endfor -%} -{% endif %} - -""" +config_file = r'/etc/ntp.conf' default_config_data = { 'servers': [], @@ -79,7 +36,7 @@ default_config_data = { } def get_config(): - ntp = copy.deepcopy(default_config_data) + ntp = deepcopy(default_config_data) conf = Config() if not conf.exists('system ntp'): return None @@ -89,7 +46,7 @@ def get_config(): if conf.exists('allow-clients address'): networks = conf.return_values('allow-clients address') for n in networks: - addr = ipaddress.ip_network(n) + addr = ip_network(n) net = { "network" : n, "address" : addr.network_address, @@ -131,7 +88,7 @@ def verify(ntp): for n in ntp['allowed_networks']: try: - addr = ipaddress.ip_network( n['network'] ) + addr = ip_network( n['network'] ) break except ValueError: raise ConfigError("{0} does not appear to be a valid IPv4 or IPv6 network, check host bits!".format(n['network'])) @@ -143,7 +100,12 @@ def generate(ntp): if ntp is None: return None - tmpl = jinja2.Template(config_tmpl) + # Prepare Jinja2 template loader from files + tmpl_path = os.path.join(vyos_data_dir['data'], 'templates', 'ntp') + fs_loader = FileSystemLoader(tmpl_path) + env = Environment(loader=fs_loader) + + tmpl = env.get_template('ntp.conf.tmpl') config_text = tmpl.render(ntp) with open(config_file, 'w') as f: f.write(config_text) @@ -152,10 +114,10 @@ def generate(ntp): def apply(ntp): if ntp is not None: - os.system('sudo systemctl restart ntp.service') + call('sudo systemctl restart ntp.service') else: # NTP support is removed in the commit - os.system('sudo systemctl stop ntp.service') + call('sudo systemctl stop ntp.service') os.unlink(config_file) return None @@ -168,4 +130,4 @@ if __name__ == '__main__': apply(c) except ConfigError as e: print(e) - sys.exit(1) + exit(1) diff --git a/src/conf_mode/protocols_bfd.py b/src/conf_mode/protocols_bfd.py index 58f5b5a0e..cf4db5f54 100755 --- a/src/conf_mode/protocols_bfd.py +++ b/src/conf_mode/protocols_bfd.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 @@ -13,38 +13,21 @@ # # 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 jinja2 -import copy import os -import vyos.validate -from vyos import ConfigError +from sys import exit +from copy import deepcopy +from jinja2 import FileSystemLoader, Environment + from vyos.config import Config +from vyos.defaults import directories as vyos_data_dir +from vyos.validate import is_ipv6_link_local, is_ipv6 +from vyos import ConfigError +from vyos.util import call -config_file = r'/tmp/bfd.frr' -# Please be careful if you edit the template. -config_tmpl = """ -! -bfd -{% for peer in old_peers -%} - no peer {{ peer.remote }}{% if peer.multihop %} multihop{% endif %}{% if peer.src_addr %} local-address {{ peer.src_addr }}{% endif %}{% if peer.src_if %} interface {{ peer.src_if }}{% endif %} -{% endfor -%} -! -{% for peer in new_peers -%} - peer {{ peer.remote }}{% if peer.multihop %} multihop{% endif %}{% if peer.src_addr %} local-address {{ peer.src_addr }}{% endif %}{% if peer.src_if %} interface {{ peer.src_if }}{% endif %} - detect-multiplier {{ peer.multiplier }} - receive-interval {{ peer.rx_interval }} - transmit-interval {{ peer.tx_interval }} - {% if peer.echo_mode %}echo-mode{% endif %} - {% if peer.echo_interval != '' %}echo-interval {{ peer.echo_interval }}{% endif %} - {% if not peer.shutdown %}no {% endif %}shutdown -{% endfor -%} -! -""" +config_file = r'/tmp/bfd.frr' default_config_data = { 'new_peers': [], @@ -132,7 +115,7 @@ def get_bfd_peer_config(peer, conf_mode="proposed"): return bfd_peer def get_config(): - bfd = copy.deepcopy(default_config_data) + bfd = deepcopy(default_config_data) conf = Config() if not (conf.exists('protocols bfd') or conf.exists_effective('protocols bfd')): return None @@ -164,12 +147,12 @@ def verify(bfd): for peer in bfd['new_peers']: # IPv6 link local peers require an explicit local address/interface - if vyos.validate.is_ipv6_link_local(peer['remote']): + if is_ipv6_link_local(peer['remote']): if not (peer['src_if'] and peer['src_addr']): raise ConfigError('BFD IPv6 link-local peers require explicit local address and interface setting') # IPv6 peers require an explicit local address - if vyos.validate.is_ipv6(peer['remote']): + if is_ipv6(peer['remote']): if not peer['src_addr']: raise ConfigError('BFD IPv6 peers require explicit local address setting') @@ -208,18 +191,23 @@ def generate(bfd): if bfd is None: return None + # Prepare Jinja2 template loader from files + tmpl_path = os.path.join(vyos_data_dir['data'], 'templates', 'frr-bfd') + fs_loader = FileSystemLoader(tmpl_path) + env = Environment(loader=fs_loader) + + tmpl = env.get_template('bfd.frr.tmpl') + config_text = tmpl.render(bfd) + with open(config_file, 'w') as f: + f.write(config_text) + return None def apply(bfd): if bfd is None: return None - tmpl = jinja2.Template(config_tmpl) - config_text = tmpl.render(bfd) - with open(config_file, 'w') as f: - f.write(config_text) - - os.system("sudo vtysh -d bfdd -f " + config_file) + call("vtysh -d bfdd -f " + config_file) if os.path.exists(config_file): os.remove(config_file) @@ -233,4 +221,4 @@ if __name__ == '__main__': apply(c) except ConfigError as e: print(e) - sys.exit(1) + exit(1) diff --git a/src/conf_mode/protocols_igmp.py b/src/conf_mode/protocols_igmp.py new file mode 100755 index 000000000..141b1950d --- /dev/null +++ b/src/conf_mode/protocols_igmp.py @@ -0,0 +1,121 @@ +#!/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 os + +from ipaddress import IPv4Address +from jinja2 import FileSystemLoader, Environment +from sys import exit + +from vyos import ConfigError +from vyos.config import Config +from vyos.defaults import directories as vyos_data_dir +from vyos.util import call + + +config_file = r'/tmp/igmp.frr' + +def get_config(): + conf = Config() + igmp_conf = { + 'igmp_conf' : False, + 'old_ifaces' : {}, + 'ifaces' : {} + } + if not (conf.exists('protocols igmp') or conf.exists_effective('protocols igmp')): + return None + + if conf.exists('protocols igmp'): + igmp_conf['igmp_conf'] = True + + conf.set_level('protocols igmp') + + # # Get interfaces + for iface in conf.list_effective_nodes('interface'): + igmp_conf['old_ifaces'].update({ + iface : { + 'version' : conf.return_effective_value('interface {0} version'.format(iface)), + 'query_interval' : conf.return_effective_value('interface {0} query-interval'.format(iface)), + 'query_max_resp_time' : conf.return_effective_value('interface {0} query-max-response-time'.format(iface)), + 'gr_join' : {} + } + }) + for gr_join in conf.list_effective_nodes('interface {0} join'.format(iface)): + igmp_conf['old_ifaces'][iface]['gr_join'][gr_join] = conf.return_effective_values('interface {0} join {1} source'.format(iface, gr_join)) + + for iface in conf.list_nodes('interface'): + igmp_conf['ifaces'].update({ + iface : { + 'version' : conf.return_value('interface {0} version'.format(iface)), + 'query_interval' : conf.return_value('interface {0} query-interval'.format(iface)), + 'query_max_resp_time' : conf.return_value('interface {0} query-max-response-time'.format(iface)), + 'gr_join' : {} + } + }) + for gr_join in conf.list_nodes('interface {0} join'.format(iface)): + igmp_conf['ifaces'][iface]['gr_join'][gr_join] = conf.return_values('interface {0} join {1} source'.format(iface, gr_join)) + + return igmp_conf + +def verify(igmp): + if igmp is None: + return None + + if igmp['igmp_conf']: + # Check interfaces + if not igmp['ifaces']: + raise ConfigError(f"IGMP require defined interfaces!") + # Check, is this multicast group + for intfc in igmp['ifaces']: + for gr_addr in igmp['ifaces'][intfc]['gr_join']: + if IPv4Address(gr_addr) < IPv4Address('224.0.0.0'): + raise ConfigError(gr_addr + " not a multicast group") + +def generate(igmp): + if igmp is None: + return None + + # Prepare Jinja2 template loader from files + tmpl_path = os.path.join(vyos_data_dir['data'], 'templates', 'igmp') + fs_loader = FileSystemLoader(tmpl_path) + env = Environment(loader=fs_loader) + + tmpl = env.get_template('igmp.frr.tmpl') + config_text = tmpl.render(igmp) + with open(config_file, 'w') as f: + f.write(config_text) + + return None + +def apply(igmp): + if igmp is None: + return None + + if os.path.exists(config_file): + call("sudo vtysh -d pimd -f " + config_file) + os.remove(config_file) + + return None + +if __name__ == '__main__': + try: + c = get_config() + verify(c) + generate(c) + apply(c) + except ConfigError as e: + print(e) + exit(1) diff --git a/src/conf_mode/protocols_mpls.py b/src/conf_mode/protocols_mpls.py new file mode 100755 index 000000000..b5753aea8 --- /dev/null +++ b/src/conf_mode/protocols_mpls.py @@ -0,0 +1,178 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2019 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +import os + +from jinja2 import FileSystemLoader, Environment + +from vyos.config import Config +from vyos.defaults import directories as vyos_data_dir +from vyos import ConfigError +from vyos.util import call + + +config_file = r'/tmp/ldpd.frr' + +def sysctl(name, value): + call('sysctl -wq {}={}'.format(name, value)) + +def get_config(): + conf = Config() + mpls_conf = { + 'router_id' : None, + 'mpls_ldp' : False, + 'old_ldp' : { + 'interfaces' : [], + 'neighbors' : {}, + 'd_transp_ipv4' : None, + 'd_transp_ipv6' : None + }, + 'ldp' : { + 'interfaces' : [], + 'neighbors' : {}, + 'd_transp_ipv4' : None, + 'd_transp_ipv6' : None + } + } + if not (conf.exists('protocols mpls') or conf.exists_effective('protocols mpls')): + return None + + if conf.exists('protocols mpls ldp'): + mpls_conf['mpls_ldp'] = True + + conf.set_level('protocols mpls ldp') + + # Get router-id + if conf.exists_effective('router-id'): + mpls_conf['old_router_id'] = conf.return_effective_value('router-id') + if conf.exists('router-id'): + mpls_conf['router_id'] = conf.return_value('router-id') + + # Get discovery transport-ipv4-address + if conf.exists_effective('discovery transport-ipv4-address'): + mpls_conf['old_ldp']['d_transp_ipv4'] = conf.return_effective_value('discovery transport-ipv4-address') + + if conf.exists('discovery transport-ipv4-address'): + mpls_conf['ldp']['d_transp_ipv4'] = conf.return_value('discovery transport-ipv4-address') + + # Get discovery transport-ipv6-address + if conf.exists_effective('discovery transport-ipv6-address'): + mpls_conf['old_ldp']['d_transp_ipv6'] = conf.return_effective_value('discovery transport-ipv6-address') + + if conf.exists('discovery transport-ipv6-address'): + mpls_conf['ldp']['d_transp_ipv6'] = conf.return_value('discovery transport-ipv6-address') + + # Get interfaces + if conf.exists_effective('interface'): + mpls_conf['old_ldp']['interfaces'] = conf.return_effective_values('interface') + + if conf.exists('interface'): + mpls_conf['ldp']['interfaces'] = conf.return_values('interface') + + # Get neighbors + for neighbor in conf.list_effective_nodes('neighbor'): + mpls_conf['old_ldp']['neighbors'].update({ + neighbor : { + 'password' : conf.return_effective_value('neighbor {0} password'.format(neighbor)) + } + }) + + for neighbor in conf.list_nodes('neighbor'): + mpls_conf['ldp']['neighbors'].update({ + neighbor : { + 'password' : conf.return_value('neighbor {0} password'.format(neighbor)) + } + }) + + return mpls_conf + +def operate_mpls_on_intfc(interfaces, action): + rp_filter = 0 + if action == 1: + rp_filter = 2 + for iface in interfaces: + sysctl('net.mpls.conf.{0}.input'.format(iface), action) + # Operate rp filter + sysctl('net.ipv4.conf.{0}.rp_filter'.format(iface), rp_filter) + +def verify(mpls): + if mpls is None: + return None + + if mpls['mpls_ldp']: + # Requre router-id + if not mpls['router_id']: + raise ConfigError(f"MPLS ldp router-id is mandatory!") + + # Requre discovery transport-address + if not mpls['ldp']['d_transp_ipv4'] and not mpls['ldp']['d_transp_ipv6']: + raise ConfigError(f"MPLS ldp discovery transport address is mandatory!") + + # Requre interface + if not mpls['ldp']['interfaces']: + raise ConfigError(f"MPLS ldp interface is mandatory!") + +def generate(mpls): + if mpls is None: + return None + + # Prepare Jinja2 template loader from files + tmpl_path = os.path.join(vyos_data_dir['data'], 'templates', 'mpls') + fs_loader = FileSystemLoader(tmpl_path) + env = Environment(loader=fs_loader) + + tmpl = env.get_template('ldpd.frr.tmpl') + config_text = tmpl.render(mpls) + with open(config_file, 'w') as f: + f.write(config_text) + + return None + +def apply(mpls): + if mpls is None: + return None + + # Set number of entries in the platform label table + if mpls['mpls_ldp']: + sysctl('net.mpls.platform_labels', '1048575') + else: + sysctl('net.mpls.platform_labels', '0') + + # Do not copy IP TTL to MPLS header + sysctl('net.mpls.ip_ttl_propagate', '0') + + # Allow mpls on interfaces + operate_mpls_on_intfc(mpls['ldp']['interfaces'], 1) + + # Disable mpls on deleted interfaces + diactive_ifaces = set(mpls['old_ldp']['interfaces']).difference(mpls['ldp']['interfaces']) + operate_mpls_on_intfc(diactive_ifaces, 0) + + if os.path.exists(config_file): + call("sudo vtysh -d ldpd -f " + config_file) + os.remove(config_file) + + return None + +if __name__ == '__main__': + try: + c = get_config() + verify(c) + generate(c) + apply(c) + except ConfigError as e: + print(e) + exit(1) diff --git a/src/conf_mode/protocols_pim.py b/src/conf_mode/protocols_pim.py new file mode 100755 index 000000000..44fc9293b --- /dev/null +++ b/src/conf_mode/protocols_pim.py @@ -0,0 +1,148 @@ +#!/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 os + +from ipaddress import IPv4Address +from jinja2 import FileSystemLoader, Environment +from sys import exit + +from vyos.config import Config +from vyos.defaults import directories as vyos_data_dir +from vyos import ConfigError +from vyos.util import call + + +config_file = r'/tmp/pimd.frr' + +def get_config(): + conf = Config() + pim_conf = { + 'pim_conf' : False, + 'old_pim' : { + 'ifaces' : {}, + 'rp' : {} + }, + 'pim' : { + 'ifaces' : {}, + 'rp' : {} + } + } + if not (conf.exists('protocols pim') or conf.exists_effective('protocols pim')): + return None + + if conf.exists('protocols pim'): + pim_conf['pim_conf'] = True + + conf.set_level('protocols pim') + + # Get interfaces + for iface in conf.list_effective_nodes('interface'): + pim_conf['old_pim']['ifaces'].update({ + iface : { + 'hello' : conf.return_effective_value('interface {0} hello'.format(iface)), + 'dr_prio' : conf.return_effective_value('interface {0} dr-priority'.format(iface)) + } + }) + + for iface in conf.list_nodes('interface'): + pim_conf['pim']['ifaces'].update({ + iface : { + 'hello' : conf.return_value('interface {0} hello'.format(iface)), + 'dr_prio' : conf.return_value('interface {0} dr-priority'.format(iface)), + } + }) + + conf.set_level('protocols pim rp') + + # Get RPs addresses + for rp_addr in conf.list_effective_nodes('address'): + pim_conf['old_pim']['rp'][rp_addr] = conf.return_effective_values('address {0} group'.format(rp_addr)) + + for rp_addr in conf.list_nodes('address'): + pim_conf['pim']['rp'][rp_addr] = conf.return_values('address {0} group'.format(rp_addr)) + + # Get RP keep-alive-timer + if conf.exists_effective('rp keep-alive-timer'): + pim_conf['old_pim']['rp_keep_alive'] = conf.return_effective_value('rp keep-alive-timer') + if conf.exists('rp keep-alive-timer'): + pim_conf['pim']['rp_keep_alive'] = conf.return_value('rp keep-alive-timer') + + return pim_conf + +def verify(pim): + if pim is None: + return None + + if pim['pim_conf']: + # Check interfaces + if not pim['pim']['ifaces']: + raise ConfigError(f"PIM require defined interfaces!") + + if not pim['pim']['rp']: + raise ConfigError(f"RP address required") + + # Check unique multicast groups + uniq_groups = [] + for rp_addr in pim['pim']['rp']: + if not pim['pim']['rp'][rp_addr]: + raise ConfigError(f"Group should be specified for RP " + rp_addr) + for group in pim['pim']['rp'][rp_addr]: + if (group in uniq_groups): + raise ConfigError(f"Group range " + group + " specified cannot exact match another") + + # Check, is this multicast group + gr_addr = group.split('/') + if IPv4Address(gr_addr[0]) < IPv4Address('224.0.0.0'): + raise ConfigError(group + " not a multicast group") + + uniq_groups.extend(pim['pim']['rp'][rp_addr]) + +def generate(pim): + if pim is None: + return None + + # Prepare Jinja2 template loader from files + tmpl_path = os.path.join(vyos_data_dir['data'], 'templates', 'pim') + fs_loader = FileSystemLoader(tmpl_path) + env = Environment(loader=fs_loader) + + tmpl = env.get_template('pimd.frr.tmpl') + config_text = tmpl.render(pim) + with open(config_file, 'w') as f: + f.write(config_text) + + return None + +def apply(pim): + if pim is None: + return None + + if os.path.exists(config_file): + call("vtysh -d pimd -f " + config_file) + os.remove(config_file) + + return None + +if __name__ == '__main__': + try: + c = get_config() + verify(c) + generate(c) + apply(c) + except ConfigError as e: + print(e) + exit(1) diff --git a/src/conf_mode/salt-minion.py b/src/conf_mode/salt-minion.py new file mode 100755 index 000000000..bfc3a707e --- /dev/null +++ b/src/conf_mode/salt-minion.py @@ -0,0 +1,144 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2018-2020 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +import os + +from copy import deepcopy +from jinja2 import FileSystemLoader, Environment +from pwd import getpwnam +from socket import gethostname +from sys import exit +from urllib3 import PoolManager + +from vyos.config import Config +from vyos.defaults import directories as vyos_data_dir +from vyos import ConfigError +from vyos.util import call + + +config_file = r'/etc/salt/minion' + +default_config_data = { + 'hash_type': 'sha256', + 'log_file': '/var/log/salt/minion', + 'log_level': 'warning', + 'master' : 'salt', + 'user': 'minion', + 'salt_id': gethostname(), + 'mine_interval': '60', + 'verify_master_pubkey_sign': 'false' +} + +def get_config(): + salt = deepcopy(default_config_data) + conf = Config() + if not conf.exists('service salt-minion'): + 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') + + if conf.exists('log_level'): + salt['log_level'] = conf.return_value('log_level') + + if conf.exists('master'): + master = conf.return_values('master') + salt['master'] = master + + if conf.exists('id'): + salt['salt_id'] = conf.return_value('id') + + if conf.exists('user'): + salt['user'] = conf.return_value('user') + + if conf.exists('mine_interval'): + salt['mine_interval'] = conf.return_value('mine_interval') + + salt['master-key'] = None + 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() + + if salt is None: + return None + + # Prepare Jinja2 template loader from files + tmpl_path = os.path.join(vyos_data_dir['data'], 'templates', 'salt-minion') + fs_loader = FileSystemLoader(tmpl_path) + env = Environment(loader=fs_loader) + + if not os.path.exists(directory): + os.makedirs(directory) + + tmpl = env.get_template('minion.tmpl') + config_text = tmpl.render(salt) + with open(config_file, 'w') as f: + f.write(config_text) + + 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) + + 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) + + with open('/opt/vyatta/etc/config/salt/pki/minion/master_sign.pub', 'wb') as out: + while True: + data = r.read(1024) + if not data: + break + out.write(data) + + r.release_conn() + + return None + +def apply(salt): + if salt is not None: + call("sudo systemctl restart salt-minion") + else: + # Salt access is removed in the commit + call("sudo systemctl stop salt-minion") + os.unlink(config_file) + + return None + +if __name__ == '__main__': + try: + c = get_config() + generate(c) + apply(c) + except ConfigError as e: + print(e) + exit(1) diff --git a/src/conf_mode/service-ipoe.py b/src/conf_mode/service-ipoe.py index bd7a898d0..5bd4aea2e 100755 --- a/src/conf_mode/service-ipoe.py +++ b/src/conf_mode/service-ipoe.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2018 VyOS maintainers and contributors +# Copyright (C) 2018-2020 VyOS maintainers and contributors # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 or later as @@ -13,20 +13,19 @@ # # You should have received a copy of the GNU General Public License # along with this program. If not, see <http://www.gnu.org/licenses/>. -# -# -import sys import os import re -import time -import socket -import subprocess -import jinja2 -import syslog as sl + +from jinja2 import FileSystemLoader, Environment +from socket import socket, AF_INET, SOCK_STREAM +from sys import exit +from time import sleep from vyos.config import Config +from vyos.defaults import directories as vyos_data_dir from vyos import ConfigError +from vyos.util import run ipoe_cnf_dir = r'/etc/accel-ppp/ipoe' ipoe_cnf = ipoe_cnf_dir + r'/ipoe.config' @@ -37,157 +36,8 @@ cmd_port = r'2002' chap_secrets = ipoe_cnf_dir + '/chap-secrets' ## accel-pppd -d -c /etc/accel-ppp/pppoe/pppoe.config -p /var/run/accel_pppoe.pid -ipoe_config = ''' -### generated by ipoe.py ### -[modules] -log_syslog -ipoe -shaper -ipv6pool -ipv6_nd -ipv6_dhcp -{% if auth['mech'] == 'radius' %} -radius -{% endif -%} -ippool -{% if auth['mech'] == 'local' %} -chap-secrets -{% endif %} - -[core] -thread-count={{thread_cnt}} - -[log] -syslog=accel-ipoe,daemon -copy=1 -level=5 - -[ipoe] -verbose=1 -{% for intfc in interfaces %} -{% if interfaces[intfc]['vlan_mon'] %} -interface=re:{{intfc}}\.\d+,\ -{% else %} -interface={{intfc}},\ -{% endif %} -shared={{interfaces[intfc]['shared']}},\ -mode={{interfaces[intfc]['mode']}},\ -ifcfg={{interfaces[intfc]['ifcfg']}},\ -range={{interfaces[intfc]['range']}},\ -start={{interfaces[intfc]['sess_start']}},\ -ipv6=1 -{% endfor %} -{% if auth['mech'] == 'noauth' %} -noauth=1 -{% endif %} -{% if auth['mech'] == 'local' %} -username=ifname -password=csid -{% endif %} - -{%- for intfc in interfaces %} -{% if (interfaces[intfc]['shared'] == '0') and (interfaces[intfc]['vlan_mon']) %} -vlan-mon={{intfc}},{{interfaces[intfc]['vlan_mon']|join(',')}} -{% endif %} -{% endfor %} - -{% if (dns['server1']) or (dns['server2']) %} -[dns] -{% if dns['server1'] %} -dns1={{dns['server1']}} -{% endif -%} -{% if dns['server2'] %} -dns2={{dns['server2']}} -{% endif -%} -{% endif -%} - -{% if (dnsv6['server1']) or (dnsv6['server2']) or (dnsv6['server3']) %} -[dnsv6] -dns={{dnsv6['server1']}} -dns={{dnsv6['server2']}} -dns={{dnsv6['server3']}} -{% endif %} - -[ipv6-nd] -verbose=1 - -[ipv6-dhcp] -verbose=1 - -{% if ipv6['prfx'] %} -[ipv6-pool] -{% for prfx in ipv6['prfx'] %} -{{prfx}} -{% endfor %} -{% for pd in ipv6['pd'] %} -delegate={{pd}} -{% endfor %} -{% endif %} - -{% if auth['mech'] == 'local' %} -[chap-secrets] -chap-secrets=/etc/accel-ppp/ipoe/chap-secrets -{% endif %} - -{% if auth['mech'] == 'radius' %} -[radius] -verbose=1 -{% for srv in auth['radius'] %} -server={{srv}},{{auth['radius'][srv]['secret']}},\ -req-limit={{auth['radius'][srv]['req-limit']}},\ -fail-time={{auth['radius'][srv]['fail-time']}} -{% endfor %} -{% if auth['radsettings']['dae-server']['ip-address'] %} -dae-server={{auth['radsettings']['dae-server']['ip-address']}}:\ -{{auth['radsettings']['dae-server']['port']}},\ -{{auth['radsettings']['dae-server']['secret']}} -{% endif -%} -{% if auth['radsettings']['acct-timeout'] %} -acct-timeout={{auth['radsettings']['acct-timeout']}} -{% endif -%} -{% if auth['radsettings']['max-try'] %} -max-try={{auth['radsettings']['max-try']}} -{% endif -%} -{% if auth['radsettings']['timeout'] %} -timeout={{auth['radsettings']['timeout']}} -{% endif -%} -{% if auth['radsettings']['nas-ip-address'] %} -nas-ip-address={{auth['radsettings']['nas-ip-address']}} -{% endif -%} -{% if auth['radsettings']['nas-identifier'] %} -nas-identifier={{auth['radsettings']['nas-identifier']}} -{% endif -%} -{% endif %} - -[cli] -tcp=127.0.0.1:2002 -''' - -# chap secrets -chap_secrets_conf = ''' -# username server password acceptable local IP addresses shaper -{% for aifc in auth['auth_if'] %} -{% for mac in auth['auth_if'][aifc] %} -{% if (auth['auth_if'][aifc][mac]['up']) and (auth['auth_if'][aifc][mac]['down']) %} -{% if auth['auth_if'][aifc][mac]['vlan'] %} -{{aifc}}.{{auth['auth_if'][aifc][mac]['vlan']}}\t*\t{{mac.lower()}}\t*\t{{auth['auth_if'][aifc][mac]['down']}}/{{auth['auth_if'][aifc][mac]['up']}} -{% else %} -{{aifc}}\t*\t{{mac.lower()}}\t*\t{{auth['auth_if'][aifc][mac]['down']}}/{{auth['auth_if'][aifc][mac]['up']}} -{% endif %} -{% else %} -{% if auth['auth_if'][aifc][mac]['vlan'] %} -{{aifc}}.{{auth['auth_if'][aifc][mac]['vlan']}}\t*\t{{mac.lower()}}\t* -{% else %} -{{aifc}}\t*\t{{mac.lower()}}\t* -{% endif %} -{% endif %} -{% endfor %} -{% endfor %} -''' - if not os.path.exists(ipoe_cnf_dir): os.makedirs(ipoe_cnf_dir) - sl.syslog(sl.LOG_NOTICE, ipoe_cnf_dir + " created") def _get_cpu(): @@ -201,40 +51,21 @@ def _get_cpu(): def _chk_con(): cnt = 0 - s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + s = socket(AF_INET, SOCK_STREAM) while True: try: s.connect(("127.0.0.1", int(cmd_port))) break except ConnectionRefusedError: - time.sleep(0.5) + sleep(0.5) cnt += 1 if cnt == 100: raise("failed to start pppoe server") break -def _accel_cmd(cmd=''): - if not cmd: - return None - try: - ret = subprocess.check_output( - ['/usr/bin/accel-cmd', '-p', cmd_port, cmd]).decode().strip() - return ret - except: - return 1 - -# chap_secrets file if auth mode local - - -def _gen_chap_secrets(c): - - tmpl = jinja2.Template(chap_secrets_conf, trim_blocks=True) - chap_secrets_txt = tmpl.render(c) - old_umask = os.umask(0o077) - open(chap_secrets, 'w').write(chap_secrets_txt) - os.umask(old_umask) - sl.syslog(sl.LOG_NOTICE, chap_secrets + ' written') +def _accel_cmd(command): + return run('/usr/bin/accel-cmd -p {cmd_port} {command}') ##### Inline functions end #### @@ -388,14 +219,25 @@ def generate(c): if c == None or not c: return None + # Prepare Jinja2 template loader from files + tmpl_path = os.path.join(vyos_data_dir['data'], 'templates', 'ipoe-server') + fs_loader = FileSystemLoader(tmpl_path) + env = Environment(loader=fs_loader, trim_blocks=True) + c['thread_cnt'] = _get_cpu() if c['auth']['mech'] == 'local': - _gen_chap_secrets(c) - - tmpl = jinja2.Template(ipoe_config, trim_blocks=True) + tmpl = env.get_template('chap-secrets.tmpl') + chap_secrets_txt = tmpl.render(c) + old_umask = os.umask(0o077) + with open(chap_secrets, 'w') as f: + f.write(chap_secrets_txt) + os.umask(old_umask) + + tmpl = env.get_template('ipoe.config.tmpl') config_text = tmpl.render(c) - open(ipoe_cnf, 'w').write(config_text) + with open(ipoe_cnf, 'w') as f: + f.write(config_text) return c @@ -457,15 +299,13 @@ def apply(c): return None if not os.path.exists(pidfile): - ret = subprocess.call( - ['/usr/sbin/accel-pppd', '-c', ipoe_cnf, '-p', pidfile, '-d']) + ret = run(f'/usr/sbin/accel-pppd -c {ipoe_cnf} -p {pidfile} -d') _chk_con() if ret != 0 and os.path.exists(pidfile): os.remove(pidfile) raise ConfigError('accel-pppd failed to start') else: _accel_cmd('restart') - sl.syslog(sl.LOG_NOTICE, "reloading config via daemon restart") if __name__ == '__main__': @@ -476,4 +316,4 @@ if __name__ == '__main__': apply(c) except ConfigError as e: print(e) - sys.exit(1) + exit(1) diff --git a/src/conf_mode/service-pppoe.py b/src/conf_mode/service-pppoe.py index 22250d18b..d3fc82406 100755 --- a/src/conf_mode/service-pppoe.py +++ b/src/conf_mode/service-pppoe.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2018 VyOS maintainers and contributors +# Copyright (C) 2018-2020 VyOS maintainers and contributors # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 or later as @@ -13,20 +13,19 @@ # # You should have received a copy of the GNU General Public License # along with this program. If not, see <http://www.gnu.org/licenses/>. -# -# -import sys import os import re -import subprocess -import jinja2 -import socket -import time -import syslog as sl + +from jinja2 import FileSystemLoader, Environment +from socket import socket, AF_INET, SOCK_STREAM +from sys import exit +from time import sleep from vyos.config import Config +from vyos.defaults import directories as vyos_data_dir from vyos import ConfigError +from vyos.util import run pidfile = r'/var/run/accel_pppoe.pid' pppoe_cnf_dir = r'/etc/accel-ppp/pppoe' @@ -38,291 +37,28 @@ pppoe_conf = pppoe_cnf_dir + '/pppoe.config' # config path creation if not os.path.exists(pppoe_cnf_dir): os.makedirs(pppoe_cnf_dir) - sl.syslog(sl.LOG_NOTICE, pppoe_cnf_dir + " created") - -pppoe_config = ''' -### generated by accel_pppoe.py ### -[modules] -log_syslog -pppoe -{% if authentication['mode'] == 'radius' %} -radius -{% endif %} -ippool -{% if ppp_options['ipv6'] != 'deny' %} -ipv6pool -ipv6_nd -ipv6_dhcp -{% endif %} -chap-secrets -auth_pap -auth_chap_md5 -auth_mschap_v1 -auth_mschap_v2 -#pppd_compat -shaper -{% if snmp == 'enable' or snmp == 'enable-ma' %} -net-snmp -{% endif %} -{% if limits %} -connlimit -{% endif %} - -[core] -thread-count={{thread_cnt}} - -[log] -syslog=accel-pppoe,daemon -copy=1 -level=5 - -{% if snmp == 'enable-ma' %} -[snmp] -master=1 -{% endif -%} - -[client-ip-range] -disable - -{% if ppp_gw %} -[ip-pool] -gw-ip-address={{ppp_gw}} -{% if client_ip_pool %} -{{client_ip_pool}} -{% endif -%} - -{% if client_ip_subnets %} -{% for sn in client_ip_subnets %} -{{sn}} -{% endfor %} -{% endif %} -{% endif -%} - -{% if client_ipv6_pool %} -[ipv6-pool] -{% for prfx in client_ipv6_pool['prefix']: %} -{{prfx}} -{% endfor %} -{% for prfx in client_ipv6_pool['delegate-prefix']: %} -delegate={{prfx}} -{% endfor %} -{% endif %} - -{% if dns %} -[dns] -{% if dns[0] %} -dns1={{dns[0]}} -{% endif -%} -{% if dns[1] %} -dns2={{dns[1]}} -{% endif -%} -{% endif %} - -{% if dnsv6 %} -[ipv6-dns] -{% for srv in dnsv6: %} -{{srv}} -{% endfor %} -{% endif %} - -{% if wins %} -[wins] -{% if wins[0] %} -wins1={{wins[0]}} -{% endif %} -{% if wins[1] %} -wins2={{wins[1]}} -{% endif -%} -{% endif -%} - -{% if authentication['mode'] == 'local' %} -[chap-secrets] -chap-secrets=/etc/accel-ppp/pppoe/chap-secrets -{% endif -%} - -{% if authentication['mode'] == 'radius' %} -[radius] -{% for rsrv in authentication['radiussrv']: %} -server={{rsrv}},{{authentication['radiussrv'][rsrv]['secret']}},\ -req-limit={{authentication['radiussrv'][rsrv]['req-limit']}},\ -fail-time={{authentication['radiussrv'][rsrv]['fail-time']}} -{% endfor %} -{% if authentication['radiusopt']['timeout'] %} -timeout={{authentication['radiusopt']['timeout']}} -{% endif %} -{% if authentication['radiusopt']['acct-timeout'] %} -acct-timeout={{authentication['radiusopt']['acct-timeout']}} -{% endif %} -{% if authentication['radiusopt']['max-try'] %} -max-try={{authentication['radiusopt']['max-try']}} -{% endif %} -{% if authentication['radiusopt']['nas-id'] %} -nas-identifier={{authentication['radiusopt']['nas-id']}} -{% endif %} -{% if authentication['radiusopt']['nas-ip'] %} -nas-ip-address={{authentication['radiusopt']['nas-ip']}} -{% endif -%} -{% if authentication['radiusopt']['dae-srv'] %} -dae-server={{authentication['radiusopt']['dae-srv']['ip-addr']}}:\ -{{authentication['radiusopt']['dae-srv']['port']}},\ -{{authentication['radiusopt']['dae-srv']['secret']}} -{% endif -%} -gw-ip-address={{ppp_gw}} -verbose=1 - -{% if authentication['radiusopt']['shaper'] %} -[shaper] -verbose=1 -attr={{authentication['radiusopt']['shaper']['attr']}} -{% if authentication['radiusopt']['shaper']['vendor'] %} -vendor={{authentication['radiusopt']['shaper']['vendor']}} -{% endif -%} -{% endif -%} -{% endif %} - -[ppp] -verbose=1 -check-ip=1 -{% if not sesscrtl == 'disable' %} -single-session={{sesscrtl}} -{% endif -%} -{% if ppp_options['ccp'] %} -ccp=1 -{% endif %} -{% if ppp_options['min-mtu'] %} -min-mtu={{ppp_options['min-mtu']}} -{% else %} -min-mtu={{mtu}} -{% endif %} -{% if ppp_options['mru'] %} -mru={{ppp_options['mru']}} -{% endif %} -{% if ppp_options['mppe'] %} -mppe={{ppp_options['mppe']}} -{% else %} -mppe=prefer -{% endif %} -{% if ppp_options['lcp-echo-interval'] %} -lcp-echo-interval={{ppp_options['lcp-echo-interval']}} -{% else %} -lcp-echo-interval=30 -{% endif %} -{% if ppp_options['lcp-echo-timeout'] %} -lcp-echo-timeout={{ppp_options['lcp-echo-timeout']}} -{% endif %} -{% if ppp_options['lcp-echo-failure'] %} -lcp-echo-failure={{ppp_options['lcp-echo-failure']}} -{% else %} -lcp-echo-failure=3 -{% endif %} -{% if ppp_options['ipv4'] %} -ipv4={{ppp_options['ipv4']}} -{% endif %} -{% if client_ipv6_pool %} -ipv6=allow -{% endif %} - -{% if ppp_options['ipv6'] %} -ipv6={{ppp_options['ipv6']}} -{% if ppp_options['ipv6-intf-id'] %} -ipv6-intf-id={{ppp_options['ipv6-intf-id']}} -{% endif %} -{% if ppp_options['ipv6-peer-intf-id'] %} -ipv6-peer-intf-id={{ppp_options['ipv6-peer-intf-id']}} -{% endif %} -{% if ppp_options['ipv6-accept-peer-intf-id'] %} -ipv6-accept-peer-intf-id={{ppp_options['ipv6-accept-peer-intf-id']}} -{% endif %} -{% endif %} -mtu={{mtu}} - -[pppoe] -verbose=1 -{% if concentrator %} -ac-name={{concentrator}} -{% endif %} -{% if interface %} -{% for int in interface %} -interface={{int}} -{% if interface[int]['vlans'] %} -vlan-mon={{int}},{{interface[int]['vlans']|join(',')}} -interface=re:{{int}}\.\d+ -{% endif %} -{% endfor -%} -{% endif -%} - -{% if svc_name %} -service-name={{svc_name|join(',')}} -{% endif -%} - -{% if pado_delay %} -pado-delay={{pado_delay}} -{% endif %} - -{% if limits %} -[connlimit] -limit={{limits['conn-limit']}} -burst={{limits['burst']}} -timeout={{limits['timeout']}} -{% endif %} - -[cli] -tcp=127.0.0.1:2001 -''' - -# pppoe chap secrets -chap_secrets_conf = ''' -# username server password acceptable local IP addresses shaper -{% for user in authentication['local-users'] %} -{% if authentication['local-users'][user]['state'] == 'enabled' %} -{% if (authentication['local-users'][user]['upload']) and (authentication['local-users'][user]['download']) %} -{{user}}\t*\t{{authentication['local-users'][user]['passwd']}}\t{{authentication['local-users'][user]['ip']}}\t\ -{{authentication['local-users'][user]['download']}}/{{authentication['local-users'][user]['upload']}} -{% else %} -{{user}}\t*\t{{authentication['local-users'][user]['passwd']}}\t{{authentication['local-users'][user]['ip']}} -{% endif %} -{% endif %} -{% endfor %} -''' + # # depending on hw and threads, daemon needs a little to start # if it takes longer than 100 * 0.5 secs, exception is being raised # not sure if that's the best way to check it, but it worked so far quite well # - - def _chk_con(): cnt = 0 - s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + s = socket(AF_INET, SOCK_STREAM) while True: try: s.connect(("127.0.0.1", 2001)) break except ConnectionRefusedError: - time.sleep(0.5) + sleep(0.5) cnt += 1 if cnt == 100: raise("failed to start pppoe server") -def _write_chap_secrets(c): - tmpl = jinja2.Template(chap_secrets_conf, trim_blocks=True) - chap_secrets_txt = tmpl.render(c) - old_umask = os.umask(0o077) - open(chap_secrets, 'w').write(chap_secrets_txt) - os.umask(old_umask) - sl.syslog(sl.LOG_NOTICE, chap_secrets + ' written') - - -def _accel_cmd(cmd=''): - if not cmd: - return None - try: - ret = subprocess.check_output( - ['/usr/bin/accel-cmd', cmd]).decode().strip() - return ret - except: - return 1 +def _accel_cmd(command): + return run(f'/usr/bin/accel-cmd {command}') def get_config(): @@ -640,6 +376,11 @@ def generate(c): if c == None: return None + # Prepare Jinja2 template loader from files + tmpl_path = os.path.join(vyos_data_dir['data'], 'templates', 'pppoe-server') + fs_loader = FileSystemLoader(tmpl_path) + env = Environment(loader=fs_loader, trim_blocks=True) + # accel-cmd reload doesn't work so any change results in a restart of the # daemon try: @@ -653,12 +394,18 @@ def generate(c): else: c['thread_cnt'] = int(os.cpu_count() / 2) - tmpl = jinja2.Template(pppoe_config, trim_blocks=True) + tmpl = env.get_template('pppoe.config.tmpl') config_text = tmpl.render(c) - open(pppoe_conf, 'w').write(config_text) + with open(pppoe_conf, 'w') as f: + f.write(config_text) if c['authentication']['local-users']: - _write_chap_secrets(c) + tmpl = env.get_template('chap-secrets.tmpl') + chap_secrets_txt = tmpl.render(c) + old_umask = os.umask(0o077) + with open(chap_secrets, 'w') as f: + f.write(chap_secrets_txt) + os.umask(old_umask) return c @@ -672,15 +419,13 @@ def apply(c): return None if not os.path.exists(pidfile): - ret = subprocess.call( - ['/usr/sbin/accel-pppd', '-c', pppoe_conf, '-p', pidfile, '-d']) + ret = run(f'/usr/sbin/accel-pppd -c {pppoe_conf} -p {pidfile} -d') _chk_con() if ret != 0 and os.path.exists(pidfile): os.remove(pidfile) raise ConfigError('accel-pppd failed to start') else: _accel_cmd('restart') - sl.syslog(sl.LOG_NOTICE, "reloading config via daemon restart") if __name__ == '__main__': @@ -691,4 +436,4 @@ if __name__ == '__main__': apply(c) except ConfigError as e: print(e) - sys.exit(1) + exit(1) diff --git a/src/conf_mode/service-router-advert.py b/src/conf_mode/service-router-advert.py new file mode 100755 index 000000000..75a324260 --- /dev/null +++ b/src/conf_mode/service-router-advert.py @@ -0,0 +1,178 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2018-2019 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +import os + +from jinja2 import FileSystemLoader, Environment +from stat import S_IRUSR, S_IWUSR, S_IRGRP +from sys import exit + +from vyos.config import Config +from vyos.defaults import directories as vyos_data_dir +from vyos import ConfigError +from vyos.util import call + + +config_file = r'/etc/radvd.conf' + +default_config_data = { + 'interfaces': [] +} + +def get_config(): + rtradv = default_config_data + conf = Config() + base_level = ['service', 'router-advert'] + + if not conf.exists(base_level): + return rtradv + + for interface in conf.list_nodes(base_level + ['interface']): + intf = { + 'name': interface, + 'hop_limit' : '64', + 'default_lifetime': '', + 'default_preference': 'medium', + 'dnssl': [], + 'link_mtu': '', + 'managed_flag': 'off', + 'interval_max': '600', + 'interval_min': '', + 'name_server': [], + 'other_config_flag': 'off', + 'prefixes' : [], + 'reachable_time': '0', + 'retrans_timer': '0', + 'send_advert': 'on' + } + + # set config level first to reduce boilerplate code + conf.set_level(base_level + ['interface', interface]) + + if conf.exists(['hop-limit']): + intf['hop_limit'] = conf.return_value(['hop-limit']) + + if conf.exists(['default-lifetim']): + intf['default_lifetime'] = conf.return_value(['default-lifetim']) + + if conf.exists(['default-preference']): + intf['default_preference'] = conf.return_value(['default-preference']) + + if conf.exists(['dnssl']): + intf['dnssl'] = conf.return_values(['dnssl']) + + if conf.exists(['link-mtu']): + intf['link_mtu'] = conf.return_value(['link-mtu']) + + if conf.exists(['managed-flag']): + intf['managed_flag'] = 'on' + + if conf.exists(['interval', 'max']): + intf['interval_max'] = conf.return_value(['interval', 'max']) + + if conf.exists(['interval', 'min']): + intf['interval_min'] = conf.return_value(['interval', 'min']) + + if conf.exists(['name-server']): + intf['name_server'] = conf.return_values(['name-server']) + + if conf.exists(['other-config-flag']): + intf['other_config_flag'] = 'on' + + if conf.exists(['reachable-time']): + intf['reachable_time'] = conf.return_value(['reachable-time']) + + if conf.exists(['retrans-timer']): + intf['retrans_timer'] = conf.return_value(['retrans-timer']) + + if conf.exists(['no-send-advert']): + intf['send_advert'] = 'off' + + for prefix in conf.list_nodes(['prefix']): + tmp = { + 'prefix' : prefix, + 'autonomous_flag' : 'on', + 'on_link' : 'on', + 'preferred_lifetime': '14400', + 'valid_lifetime' : '2592000' + + } + + # set config level first to reduce boilerplate code + conf.set_level(base_level + ['interface', interface, 'prefix', prefix]) + + if conf.exists(['no-autonomous-flag']): + tmp['autonomous_flag'] = 'off' + + if conf.exists(['no-on-link-flag']): + tmp['on_link'] = 'off' + + if conf.exists(['preferred-lifetime']): + tmp['preferred_lifetime'] = conf.return_value(['preferred-lifetime']) + + if conf.exists(['valid-lifetime']): + tmp['valid_lifetime'] = conf.return_value(['valid-lifetime']) + + intf['prefixes'].append(tmp) + + rtradv['interfaces'].append(intf) + + return rtradv + +def verify(rtradv): + return None + +def generate(rtradv): + if not rtradv['interfaces']: + return None + + # Prepare Jinja2 template loader from files + tmpl_path = os.path.join(vyos_data_dir['data'], 'templates', 'router-advert') + fs_loader = FileSystemLoader(tmpl_path) + env = Environment(loader=fs_loader, trim_blocks=True) + + tmpl = env.get_template('radvd.conf.tmpl') + config_text = tmpl.render(rtradv) + with open(config_file, 'w') as f: + f.write(config_text) + + # adjust file permissions of new configuration file + if os.path.exists(config_file): + os.chmod(config_file, S_IRUSR | S_IWUSR | S_IRGRP) + + return None + +def apply(rtradv): + if not rtradv['interfaces']: + # bail out early - looks like removal from running config + call('systemctl stop radvd.service') + if os.path.exists(config_file): + os.unlink(config_file) + + return None + + call('systemctl restart radvd.service') + return None + +if __name__ == '__main__': + try: + c = get_config() + verify(c) + generate(c) + apply(c) + except ConfigError as e: + print(e) + exit(1) diff --git a/src/conf_mode/snmp.py b/src/conf_mode/snmp.py index ac94afb1a..4a69e8742 100755 --- a/src/conf_mode/snmp.py +++ b/src/conf_mode/snmp.py @@ -15,18 +15,20 @@ # along with this program. If not, see <http://www.gnu.org/licenses/>. import os -import jinja2 - -import vyos.version -import vyos.validate from binascii import hexlify from time import sleep from stat import S_IRWXU, S_IXGRP, S_IXOTH, S_IROTH, S_IRGRP from sys import exit +from jinja2 import FileSystemLoader, Environment from vyos.config import Config +from vyos.defaults import directories as vyos_data_dir +from vyos.validate import is_ipv4, is_addr_assigned +from vyos.version import get_version_data from vyos import ConfigError +from vyos.util import call + config_file_client = r'/etc/snmp/snmp.conf' config_file_daemon = r'/etc/snmp/snmpd.conf' @@ -43,171 +45,10 @@ OIDs = { 'none': '.1.3.6.1.6.3.10.1.2.1' } -# SNMP template (/etc/snmp/snmp.conf) - be careful if you edit the template. -client_config_tmpl = """ -### Autogenerated by snmp.py ### -{% if trap_source -%} -clientaddr {{ trap_source }} -{% endif %} - -""" - -# SNMP template (/usr/share/snmp/snmpd.conf) - be careful if you edit the template. -access_config_tmpl = """ -### Autogenerated by snmp.py ### -{%- for u in v3_users %} -{{ u.mode }}user {{ u.name }} -{%- endfor %} - -rwuser {{ vyos_user }} - -""" - -# SNMP template (/var/lib/snmp/snmpd.conf) - be careful if you edit the template. -user_config_tmpl = """ -### Autogenerated by snmp.py ### -# user -{%- for u in v3_users %} -{%- if u.authOID == 'none' %} -createUser {{ u.name }} -{%- elif u.authPassword %} -createUser {{ u.name }} {{ u.authProtocol | upper }} "{{ u.authPassword }}" {{ u.privProtocol | upper }} {{ u.privPassword }} -{%- else %} -usmUser 1 3 {{ v3_engineid }} "{{ u.name }}" "{{ u.name }}" NULL {{ u.authOID }} {{ u.authMasterKey }} {{ u.privOID }} {{ u.privMasterKey }} 0x -{%- endif %} -{%- endfor %} - -createUser {{ vyos_user }} MD5 "{{ vyos_user_pass }}" DES -{%- if v3_engineid %} -oldEngineID {{ v3_engineid }} -{%- endif %} -""" - -# SNMP template (/etc/snmp/snmpd.conf) - be careful if you edit the template. -daemon_config_tmpl = """ -### Autogenerated by snmp.py ### - -# non configurable defaults -sysObjectID 1.3.6.1.4.1.44641 -sysServices 14 -master agentx -agentXPerms 0777 0777 -pass .1.3.6.1.2.1.31.1.1.1.18 /opt/vyatta/sbin/if-mib-alias -smuxpeer .1.3.6.1.2.1.83 -smuxpeer .1.3.6.1.2.1.157 -smuxsocket localhost - -# linkUp/Down configure the Event MIB tables to monitor -# the ifTable for network interfaces being taken up or down -# for making internal queries to retrieve any necessary information -iquerySecName {{ vyos_user }} - -# Modified from the default linkUpDownNotification -# to include more OIDs and poll more frequently -notificationEvent linkUpTrap linkUp ifIndex ifDescr ifType ifAdminStatus ifOperStatus -notificationEvent linkDownTrap linkDown ifIndex ifDescr ifType ifAdminStatus ifOperStatus -monitor -r 10 -e linkUpTrap "Generate linkUp" ifOperStatus != 2 -monitor -r 10 -e linkDownTrap "Generate linkDown" ifOperStatus == 2 - -######################## -# configurable section # -######################## - -# Default system description is VyOS version -sysDescr VyOS {{ version }} - -{% if description %} -# Description -SysDescr {{ description }} -{%- endif %} - -# Listen -agentaddress unix:/run/snmpd.socket{% if listen_on %}{% for li in listen_on %},{{ li }}{% endfor %}{% else %},udp:161,udp6:161{% endif %} - -# SNMP communities -{%- for c in communities %} - -{%- if c.network_v4 %} -{%- for network in c.network_v4 %} -{{ c.authorization }}community {{ c.name }} {{ network }} -{%- endfor %} -{%- elif not c.has_source %} -{{ c.authorization }}community {{ c.name }} -{%- endif %} - -{%- if c.network_v6 %} -{%- for network in c.network_v6 %} -{{ c.authorization }}community6 {{ c.name }} {{ network }} -{%- endfor %} -{%- elif not c.has_source %} -{{ c.authorization }}community6 {{ c.name }} -{%- endif %} - -{%- endfor %} - -{% if contact %} -# system contact information -SysContact {{ contact }} -{%- endif %} - -{% if location %} -# system location information -SysLocation {{ location }} -{%- endif %} - -{% if smux_peers -%} -# additional smux peers -{%- for sp in smux_peers %} -smuxpeer {{ sp }} -{%- endfor %} -{%- endif %} - -{% if trap_targets -%} -# if there is a problem - tell someone! -{%- for t in trap_targets %} -trap2sink {{ t.target }}{% if t.port -%}:{{ t.port }}{% endif %} {{ t.community }} -{%- endfor %} -{%- endif %} - -{%- if v3_enabled %} -# -# SNMPv3 stuff goes here -# -# views -{%- for v in v3_views %} -{%- for oid in v.oids %} -view {{ v.name }} included .{{ oid.oid }} -{%- endfor %} -{%- endfor %} - -# access -# context sec.model sec.level match read write notif -{%- for g in v3_groups %} -access {{ g.name }} "" usm {{ g.seclevel }} exact {{ g.view }} {% if g.mode == 'ro' %}none{% else %}{{ g.view }}{% endif %} none -{%- endfor %} - -# trap-target -{%- for t in v3_traps %} -trapsess -v 3 {{ '-Ci' if t.type == 'inform' }} -e {{ v3_engineid }} -u {{ t.secName }} -l {{ t.secLevel }} -a {{ t.authProtocol }} {% if t.authPassword %}-A {{ t.authPassword }}{% elif t.authMasterKey %}-3m {{ t.authMasterKey }}{% endif %} -x {{ t.privProtocol }} {% if t.privPassword %}-X {{ t.privPassword }}{% elif t.privMasterKey %}-3M {{ t.privMasterKey }}{% endif %} {{ t.ipProto }}:{{ t.ipAddr }}:{{ t.ipPort }} -{%- endfor %} - -# group -{%- for u in v3_users %} -group {{ u.group }} usm {{ u.name }} -{% endfor %} -{%- endif %} - -{% if script_ext %} -# extension scripts -{%- for ext in script_ext|sort(attribute='name') %} -extend {{ ext.name }} {{ ext.script }} -{%- endfor %} -{% endif %} -""" - default_config_data = { 'listen_on': [], 'listen_address': [], + 'ipv6_enabled': 'True', 'communities': [], 'smux_peers': [], 'location' : '', @@ -237,9 +78,12 @@ def get_config(): if not conf.exists('service snmp'): return None else: + if conf.exists('system ipv6 disable'): + snmp['ipv6_enabled'] = False + conf.set_level('service snmp') - version_data = vyos.version.get_version_data() + version_data = get_version_data() snmp['version'] = version_data['version'] # create an internal snmpv3 user of the form 'vyosxxxxxxxxxxxxxxxx' @@ -263,7 +107,7 @@ def get_config(): # Subnet of SNMP client(s) allowed to contact system if conf.exists('community {0} network'.format(name)): for addr in conf.return_values('community {0} network'.format(name)): - if vyos.validate.is_ipv4(addr): + if is_ipv4(addr): community['network_v4'].append(addr) else: community['network_v6'].append(addr) @@ -271,7 +115,7 @@ def get_config(): # IP address of SNMP client allowed to contact system if conf.exists('community {0} client'.format(name)): for addr in conf.return_values('community {0} client'.format(name)): - if vyos.validate.is_ipv4(addr): + if is_ipv4(addr): community['network_v4'].append(addr) else: community['network_v6'].append(addr) @@ -554,16 +398,16 @@ def verify(snmp): addr = listen[0] port = listen[1] - if vyos.validate.is_ipv4(addr): + if is_ipv4(addr): # example: udp:127.0.0.1:161 listen = 'udp:' + addr + ':' + port - else: + elif snmp['ipv6_enabled']: # example: udp6:[::1]:161 listen = 'udp6:' + '[' + addr + ']' + ':' + port # We only wan't to configure addresses that exist on the system. # Hint the user if they don't exist - if vyos.validate.is_addr_assigned(addr): + if is_addr_assigned(addr): snmp['listen_on'].append(listen) else: print('WARNING: SNMP listen address {0} not configured!'.format(addr)) @@ -665,35 +509,40 @@ def generate(snmp): # # As we are manipulating the snmpd user database we have to stop it first! # This is even save if service is going to be removed - os.system("systemctl stop snmpd.service") - rmfile(config_file_client) - rmfile(config_file_daemon) - rmfile(config_file_access) - rmfile(config_file_user) + call('systemctl stop snmpd.service') + config_files = [config_file_client, config_file_daemon, config_file_access, + config_file_user] + for file in config_files: + rmfile(file) if snmp is None: return None + # Prepare Jinja2 template loader from files + tmpl_path = os.path.join(vyos_data_dir['data'], 'templates', 'snmp') + fs_loader = FileSystemLoader(tmpl_path) + env = Environment(loader=fs_loader) + # Write client config file - tmpl = jinja2.Template(client_config_tmpl) + tmpl = env.get_template('etc.snmp.conf.tmpl') config_text = tmpl.render(snmp) with open(config_file_client, 'w') as f: f.write(config_text) # Write server config file - tmpl = jinja2.Template(daemon_config_tmpl) + tmpl = env.get_template('etc.snmpd.conf.tmpl') config_text = tmpl.render(snmp) with open(config_file_daemon, 'w') as f: f.write(config_text) # Write access rights config file - tmpl = jinja2.Template(access_config_tmpl) + tmpl = env.get_template('usr.snmpd.conf.tmpl') config_text = tmpl.render(snmp) with open(config_file_access, 'w') as f: f.write(config_text) # Write access rights config file - tmpl = jinja2.Template(user_config_tmpl) + tmpl = env.get_template('var.snmpd.conf.tmpl') config_text = tmpl.render(snmp) with open(config_file_user, 'w') as f: f.write(config_text) @@ -705,7 +554,7 @@ def apply(snmp): return None # start SNMP daemon - os.system("systemctl restart snmpd.service") + call("systemctl restart snmpd.service") # Passwords are not available immediately in the configuration file, # after daemon startup - we wait until they have been processed by @@ -746,15 +595,15 @@ def apply(snmp): # Now update the running configuration # - # Currently when executing os.system() the environment does not + # Currently when executing call() the environment does not # have the vyos_libexec_dir variable set, see Phabricator T685. - os.system('/opt/vyatta/sbin/my_set service snmp v3 user "{0}" auth encrypted-key "{1}" > /dev/null'.format(cfg['user'], cfg['auth_pw'])) - os.system('/opt/vyatta/sbin/my_set service snmp v3 user "{0}" privacy encrypted-key "{1}" > /dev/null'.format(cfg['user'], cfg['priv_pw'])) - os.system('/opt/vyatta/sbin/my_delete service snmp v3 user "{0}" auth plaintext-key > /dev/null'.format(cfg['user'])) - os.system('/opt/vyatta/sbin/my_delete service snmp v3 user "{0}" privacy plaintext-key > /dev/null'.format(cfg['user'])) + call('/opt/vyatta/sbin/my_set service snmp v3 user "{0}" auth encrypted-key "{1}" > /dev/null'.format(cfg['user'], cfg['auth_pw'])) + call('/opt/vyatta/sbin/my_set service snmp v3 user "{0}" privacy encrypted-key "{1}" > /dev/null'.format(cfg['user'], cfg['priv_pw'])) + call('/opt/vyatta/sbin/my_delete service snmp v3 user "{0}" auth plaintext-key > /dev/null'.format(cfg['user'])) + call('/opt/vyatta/sbin/my_delete service snmp v3 user "{0}" privacy plaintext-key > /dev/null'.format(cfg['user'])) # Enable AgentX in FRR - os.system('vtysh -c "configure terminal" -c "agentx" >/dev/null') + call('vtysh -c "configure terminal" -c "agentx" >/dev/null') return None diff --git a/src/conf_mode/ssh.py b/src/conf_mode/ssh.py index 9fe22bfee..a6cdb7ccc 100755 --- a/src/conf_mode/ssh.py +++ b/src/conf_mode/ssh.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2018 VyOS maintainers and contributors +# Copyright (C) 2018-2020 VyOS maintainers and contributors # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 or later as @@ -13,148 +13,18 @@ # # You should have received a copy of the GNU General Public License # along with this program. If not, see <http://www.gnu.org/licenses/>. -# -# -import sys import os - -import jinja2 +from jinja2 import FileSystemLoader, Environment +from sys import exit from vyos.config import Config +from vyos.defaults import directories as vyos_data_dir from vyos import ConfigError +from vyos.util import call -config_file = r'/etc/ssh/sshd_config' -# Please be careful if you edit the template. -config_tmpl = """ - -### Autogenerated by ssh.py ### - -# Non-configurable defaults -Protocol 2 -HostKey /etc/ssh/ssh_host_rsa_key -HostKey /etc/ssh/ssh_host_dsa_key -HostKey /etc/ssh/ssh_host_ecdsa_key -HostKey /etc/ssh/ssh_host_ed25519_key -SyslogFacility AUTH -LoginGraceTime 120 -StrictModes yes -PubkeyAuthentication yes -IgnoreRhosts yes -HostbasedAuthentication no -PermitEmptyPasswords no -ChallengeResponseAuthentication no -X11Forwarding yes -X11DisplayOffset 10 -PrintMotd no -PrintLastLog yes -TCPKeepAlive yes -Banner /etc/issue.net -Subsystem sftp /usr/lib/openssh/sftp-server -UsePAM yes -HostKey /etc/ssh/ssh_host_rsa_key - -# Specifies whether sshd should look up the remote host name, -# and to check that the resolved host name for the remote IP -# address maps back to the very same IP address. -UseDNS {{ host_validation }} - -# Specifies the port number that sshd listens on. The default is 22. -# Multiple options of this type are permitted. -{% if mport|length != 0 %} -{% for p in mport %} -Port {{ p }} -{% endfor %} -{% else %} -Port {{ port }} -{% endif %} - -# Gives the verbosity level that is used when logging messages from sshd -LogLevel {{ log_level }} - -# Specifies whether root can log in using ssh -PermitRootLogin no - -# Specifies whether password authentication is allowed -PasswordAuthentication {{ password_authentication }} - -{% if listen_on %} -# Specifies the local addresses sshd should listen on -{% for a in listen_on %} -ListenAddress {{ a }} -{% endfor %} -{{ "\n" }} -{% endif %} - -{%- if ciphers %} -# Specifies the ciphers allowed. Multiple ciphers must be comma-separated. -# -# NOTE: As of now, there is no 'multi' node for 'ciphers', thus we have only one :/ -Ciphers {{ ciphers | join(",") }} -{{ "\n" }} -{% endif %} - -{%- if mac %} -# Specifies the available MAC (message authentication code) algorithms. The MAC -# algorithm is used for data integrity protection. Multiple algorithms must be -# comma-separated. -# -# NOTE: As of now, there is no 'multi' node for 'mac', thus we have only one :/ -MACs {{ mac | join(",") }} -{{ "\n" }} -{% endif %} - -{%- if key_exchange %} -# Specifies the available KEX (Key Exchange) algorithms. Multiple algorithms must -# be comma-separated. -# -# NOTE: As of now, there is no 'multi' node for 'key-exchange', thus we have only one :/ -KexAlgorithms {{ key_exchange | join(",") }} -{{ "\n" }} -{% endif %} - -{%- if allow_users %} -# This keyword can be followed by a list of user name patterns, separated by spaces. -# If specified, login is allowed only for user names that match one of the patterns. -# Only user names are valid, a numerical user ID is not recognized. -AllowUsers {{ allow_users | join(" ") }} -{{ "\n" }} -{% endif %} - -{%- if allow_groups %} -# This keyword can be followed by a list of group name patterns, separated by spaces. -# If specified, login is allowed only for users whose primary group or supplementary -# group list matches one of the patterns. Only group names are valid, a numerical group -# ID is not recognized. -AllowGroups {{ allow_groups | join(" ") }} -{{ "\n" }} -{% endif %} - -{%- if deny_users %} -# This keyword can be followed by a list of user name patterns, separated by spaces. -# Login is disallowed for user names that match one of the patterns. Only user names -# are valid, a numerical user ID is not recognized. -DenyUsers {{ deny_users | join(" ") }} -{{ "\n" }} -{% endif %} - -{%- if deny_groups %} -# This keyword can be followed by a list of group name patterns, separated by spaces. -# Login is disallowed for users whose primary group or supplementary group list matches -# one of the patterns. Only group names are valid, a numerical group ID is not recognized. -DenyGroups {{ deny_groups | join(" ") }} -{{ "\n" }} -{% endif %} - -{%- if client_keepalive %} -# Sets a timeout interval in seconds after which if no data has been received from the client, -# sshd will send a message through the encrypted channel to request a response from the client. -# The default is 0, indicating that these messages will not be sent to the client. -# This option applies to protocol version 2 only. -ClientAliveInterval {{ client_keepalive }} -{% endif %} -""" +config_file = r'/etc/ssh/sshd_config' default_config_data = { 'port' : '22', @@ -250,7 +120,12 @@ def generate(ssh): if ssh is None: return None - tmpl = jinja2.Template(config_tmpl, trim_blocks=True) + # Prepare Jinja2 template loader from files + tmpl_path = os.path.join(vyos_data_dir['data'], 'templates', 'ssh') + fs_loader = FileSystemLoader(tmpl_path) + env = Environment(loader=fs_loader, trim_blocks=True) + + tmpl = env.get_template('sshd_config.tmpl') config_text = tmpl.render(ssh) with open(config_file, 'w') as f: f.write(config_text) @@ -258,10 +133,10 @@ def generate(ssh): def apply(ssh): if ssh is not None and 'port' in ssh.keys(): - os.system("sudo systemctl restart ssh.service") + call("sudo systemctl restart ssh.service") else: # SSH access is removed in the commit - os.system("sudo systemctl stop ssh.service") + call("sudo systemctl stop ssh.service") if os.path.isfile(config_file): os.unlink(config_file) @@ -275,4 +150,4 @@ if __name__ == '__main__': apply(c) except ConfigError as e: print(e) - sys.exit(1) + exit(1) diff --git a/src/conf_mode/system-ip.py b/src/conf_mode/system-ip.py index 335507411..8a1ac8411 100755 --- a/src/conf_mode/system-ip.py +++ b/src/conf_mode/system-ip.py @@ -20,6 +20,8 @@ from sys import exit from copy import deepcopy from vyos.config import Config from vyos import ConfigError +from vyos.util import call + default_config_data = { 'arp_table': 8192, @@ -29,7 +31,7 @@ default_config_data = { } def sysctl(name, value): - os.system('sysctl -wq {}={}'.format(name, value)) + call('sysctl -wq {}={}'.format(name, value)) def get_config(): ip_opt = deepcopy(default_config_data) diff --git a/src/conf_mode/system-ipv6.py b/src/conf_mode/system-ipv6.py index bd28ec357..04a063564 100755 --- a/src/conf_mode/system-ipv6.py +++ b/src/conf_mode/system-ipv6.py @@ -21,6 +21,8 @@ from sys import exit from copy import deepcopy from vyos.config import Config from vyos import ConfigError +from vyos.util import call + ipv6_disable_file = '/etc/modprobe.d/vyos_disable_ipv6.conf' @@ -35,7 +37,7 @@ default_config_data = { } def sysctl(name, value): - os.system('sysctl -wq {}={}'.format(name, value)) + call('sysctl -wq {}={}'.format(name, value)) def get_config(): ip_opt = deepcopy(default_config_data) diff --git a/src/conf_mode/system-login-banner.py b/src/conf_mode/system-login-banner.py index e66d409bb..5a34a0b06 100755 --- a/src/conf_mode/system-login-banner.py +++ b/src/conf_mode/system-login-banner.py @@ -16,6 +16,7 @@ from sys import exit from vyos.config import Config +from vyos import ConfigError motd=""" The programs included with the Debian GNU/Linux system are free software; @@ -24,6 +25,7 @@ individual files in /usr/share/doc/*/copyright. Debian GNU/Linux comes with ABSOLUTELY NO WARRANTY, to the extent permitted by applicable law. + """ PRELOGIN_FILE = r'/etc/issue' @@ -31,8 +33,8 @@ PRELOGIN_NET_FILE = r'/etc/issue.net' POSTLOGIN_FILE = r'/etc/motd' default_config_data = { - 'issue': 'Welcome to VyOS - \n \l', - 'issue_net': 'Welcome to VyOS', + 'issue': 'Welcome to VyOS - \n \l\n', + 'issue_net': 'Welcome to VyOS\n', 'motd': motd } @@ -49,15 +51,29 @@ def get_config(): # Post-Login banner if conf.exists(['post-login']): tmp = conf.return_value(['post-login']) - tmp = tmp.replace('\\n','\n') - tmp = tmp.replace('\\t','\t') + # post-login banner can be empty as well + if tmp: + tmp = tmp.replace('\\n','\n') + tmp = tmp.replace('\\t','\t') + # always add newline character + tmp += '\n' + else: + tmp = '' + banner['motd'] = tmp # Pre-Login banner if conf.exists(['pre-login']): tmp = conf.return_value(['pre-login']) - tmp = tmp.replace('\\n','\n') - tmp = tmp.replace('\\t','\t') + # pre-login banner can be empty as well + if tmp: + tmp = tmp.replace('\\n','\n') + tmp = tmp.replace('\\t','\t') + # always add newline character + tmp += '\n' + else: + tmp = '' + banner['issue'] = banner['issue_net'] = tmp return banner diff --git a/src/conf_mode/system-login.py b/src/conf_mode/system-login.py index 23152fee0..43732cfae 100755 --- a/src/conf_mode/system-login.py +++ b/src/conf_mode/system-login.py @@ -14,36 +14,22 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see <http://www.gnu.org/licenses/>. -import sys import os -import jinja2 +from jinja2 import FileSystemLoader, Environment +from psutil import users from pwd import getpwall, getpwnam from stat import S_IRUSR, S_IWUSR, S_IRWXU, S_IRGRP, S_IXGRP -from subprocess import Popen, PIPE, STDOUT -from psutil import users +from sys import exit from vyos.config import Config from vyos.configdict import list_diff +from vyos.defaults import directories as vyos_data_dir from vyos import ConfigError +from vyos.util import cmd +from vyos.util import call radius_config_file = "/etc/pam_radius_auth.conf" -radius_config_tmpl = """ -# Automatically generated by VyOS -# RADIUS configuration file -{%- if radius_server %} -# server[:port] shared_secret timeout (s) source_ip -{% for s in radius_server %} -{%- if not s.disabled -%} -{{ s.address }}:{{ s.port }} {{ s.key }} {{ s.timeout }} {% if radius_source_address -%}{{ radius_source_address }}{% endif %} -{% endif %} -{%- endfor %} - -priv-lvl 15 -mapped_priv_user radius_priv_user -{% endif %} - -""" default_config_data = { 'deleted': False, @@ -67,10 +53,7 @@ def get_local_users(): def get_crypt_pw(password): - command = '/usr/bin/mkpasswd --method=sha-512 {}'.format(password) - p = Popen(command, stdout=PIPE, stderr=STDOUT, shell=True) - tmp = p.communicate()[0].strip() - return tmp.decode() + return cmd(f'/usr/bin/mkpasswd --method=sha-512 {password}') def get_config(): @@ -196,6 +179,14 @@ def verify(login): if cur_user in login['del_users']: raise ConfigError('Attempting to delete current user: {}'.format(cur_user)) + for user in login['add_users']: + for key in user['public_keys']: + if not key['type']: + raise ConfigError('SSH public key type missing for "{}"!'.format(key['name'])) + + if not key['key']: + raise ConfigError('SSH public key for id "{}" missing!'.format(key['name'])) + # At lease one RADIUS server must not be disabled if len(login['radius_server']) > 0: fail = True @@ -221,7 +212,12 @@ def generate(login): os.system("vyos_libexec_dir=/usr/libexec/vyos /opt/vyatta/sbin/my_set system login user '{}' authentication encrypted-password '{}' >/dev/null".format(user['name'], user['password_encrypted'])) if len(login['radius_server']) > 0: - tmpl = jinja2.Template(radius_config_tmpl) + # Prepare Jinja2 template loader from files + tmpl_path = os.path.join(vyos_data_dir['data'], 'templates', 'system-login') + fs_loader = FileSystemLoader(tmpl_path) + env = Environment(loader=fs_loader) + + tmpl = env.get_template('pam_radius_auth.conf.tmpl') config_text = tmpl.render(login) with open(radius_config_file, 'w') as f: f.write(config_text) @@ -240,40 +236,44 @@ def apply(login): for user in login['add_users']: # make new user using vyatta shell and make home directory (-m), # default group of 100 (users) - cmd = "useradd -m -N" + command = "useradd -m -N" # check if user already exists: if user['name'] in get_local_users(): # update existing account - cmd = "usermod" + command = "usermod" # we need to use '' quotes when passing formatted data to the shell # else it will not work as some data parts are lost in translation - cmd += " -p '{}'".format(user['password_encrypted']) - cmd += " -s /bin/vbash" + command += " -p '{}'".format(user['password_encrypted']) + command += " -s /bin/vbash" if user['full_name']: - cmd += " -c '{}'".format(user['full_name']) + command += " -c '{}'".format(user['full_name']) if user['home_dir']: - cmd += " -d '{}'".format(user['home_dir']) + command += " -d '{}'".format(user['home_dir']) - cmd += " -G frrvty,vyattacfg,sudo,adm,dip,disk" - cmd += " {}".format(user['name']) + command += " -G frrvty,vyattacfg,sudo,adm,dip,disk" + command += " {}".format(user['name']) try: - os.system(cmd) + call(command) uid = getpwnam(user['name']).pw_uid gid = getpwnam(user['name']).pw_gid + # we should not rely on the home dir value stored in user['home_dir'] + # as if a crazy user will choose username root or any other system + # user this will fail. should be deny using root at all? + home_dir = getpwnam(user['name']).pw_dir # install ssh keys - key_dir = '{}/.ssh'.format(user['home_dir']) - if not os.path.isdir(key_dir): - os.mkdir(key_dir) - os.chown(key_dir, uid, gid) - os.chmod(key_dir, S_IRWXU | S_IRGRP | S_IXGRP) - - key_file = key_dir + '/authorized_keys'; - with open(key_file, 'w') as f: + ssh_key_dir = home_dir + '/.ssh' + if not os.path.isdir(ssh_key_dir): + os.mkdir(ssh_key_dir) + os.chown(ssh_key_dir, uid, gid) + os.chmod(ssh_key_dir, S_IRWXU | S_IRGRP | S_IXGRP) + + ssh_key_file = ssh_key_dir + '/authorized_keys'; + with open(ssh_key_file, 'w') as f: f.write("# Automatically generated by VyOS\n") f.write("# Do not edit, all changes will be lost\n") @@ -285,8 +285,8 @@ def apply(login): line += '{} {} {}\n'.format(id['type'], id['key'], id['name']) f.write(line) - os.chown(key_file, uid, gid) - os.chmod(key_file, S_IRUSR | S_IWUSR) + os.chown(ssh_key_file, uid, gid) + os.chmod(ssh_key_file, S_IRUSR | S_IWUSR) except Exception as e: raise ConfigError('Adding user "{}" raised an exception: {}'.format(user['name'], e)) @@ -296,10 +296,10 @@ def apply(login): # Logout user if he is logged in if user in list(set([tmp[0] for tmp in users()])): print('{} is logged in, forcing logout'.format(user)) - os.system('pkill -HUP -u {}'.format(user)) + call('pkill -HUP -u {}'.format(user)) # Remove user account but leave home directory to be safe - os.system('userdel -r {} 2>/dev/null'.format(user)) + call('userdel -r {} 2>/dev/null'.format(user)) except Exception as e: raise ConfigError('Deleting user "{}" raised an exception: {}'.format(user, e)) @@ -313,7 +313,7 @@ def apply(login): os.system("DEBIAN_FRONTEND=noninteractive pam-auth-update --package --enable radius") # Make NSS system aware of RADIUS, too - cmd = "sed -i -e \'/\smapname/b\' \ + command = "sed -i -e \'/\smapname/b\' \ -e \'/^passwd:/s/\s\s*/&mapuid /\' \ -e \'/^passwd:.*#/s/#.*/mapname &/\' \ -e \'/^passwd:[^#]*$/s/$/ mapname &/\' \ @@ -321,7 +321,7 @@ def apply(login): -e \'/^group:[^#]*$/s/: */&mapname /\' \ /etc/nsswitch.conf" - os.system(cmd) + call(command) except Exception as e: raise ConfigError('RADIUS configuration failed: {}'.format(e)) @@ -331,13 +331,13 @@ def apply(login): # Disable RADIUS in PAM os.system("DEBIAN_FRONTEND=noninteractive pam-auth-update --package --remove radius") - cmd = "sed -i -e \'/^passwd:.*mapuid[ \t]/s/mapuid[ \t]//\' \ + command = "sed -i -e \'/^passwd:.*mapuid[ \t]/s/mapuid[ \t]//\' \ -e \'/^passwd:.*[ \t]mapname/s/[ \t]mapname//\' \ -e \'/^group:.*[ \t]mapname/s/[ \t]mapname//\' \ -e \'s/[ \t]*$//\' \ /etc/nsswitch.conf" - os.system(cmd) + call(command) except Exception as e: raise ConfigError('Removing RADIUS configuration failed'.format(e)) @@ -352,4 +352,4 @@ if __name__ == '__main__': apply(c) except ConfigError as e: print(e) - sys.exit(1) + exit(1) diff --git a/src/conf_mode/system-options.py b/src/conf_mode/system-options.py index a893e98b3..b3dbc82fb 100755 --- a/src/conf_mode/system-options.py +++ b/src/conf_mode/system-options.py @@ -20,6 +20,7 @@ from sys import exit from copy import deepcopy from vyos.config import Config from vyos import ConfigError +from vyos.util import run systemd_ctrl_alt_del = '/lib/systemd/system/ctrl-alt-del.target' @@ -51,9 +52,9 @@ def generate(opt): def apply(opt): # Beep action if opt['beep_if_fully_booted']: - os.system('systemctl enable vyos-beep.service >/dev/null 2>&1') + run('systemctl enable vyos-beep.service') else: - os.system('systemctl disable vyos-beep.service >/dev/null 2>&1') + run('systemctl disable vyos-beep.service') # Ctrl-Alt-Delete action if opt['ctrl_alt_del'] == 'ignore': diff --git a/src/conf_mode/system-syslog.py b/src/conf_mode/system-syslog.py index 2d47cc061..25b9b5bed 100755 --- a/src/conf_mode/system-syslog.py +++ b/src/conf_mode/system-syslog.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2018 VyOS maintainers and contributors +# Copyright (C) 2018-2020 VyOS maintainers and contributors # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 or later as @@ -13,84 +13,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/>. -# -# -import sys import os import re -import subprocess -import jinja2 + +from jinja2 import FileSystemLoader, Environment +from sys import exit from vyos.config import Config +from vyos.defaults import directories as vyos_data_dir from vyos import ConfigError - -# config templates - -# /etc/rsyslog.d/vyos-rsyslog.conf ### -configs = ''' -## generated by syslog.py ## -## file based logging -{% if files['global']['marker'] -%} -$ModLoad immark -{% if files['global']['marker-interval'] %} -$MarkMessagePeriod {{files['global']['marker-interval']}} -{% endif %} -{% endif -%} -{% if files['global']['preserver_fqdn'] -%} -$PreserveFQDN on -{% endif -%} -{% for file in files %} -$outchannel {{file}},{{files[file]['log-file']}},{{files[file]['max-size']}},{{files[file]['action-on-max-size']}} -{{files[file]['selectors']}} :omfile:${{file}} -{% endfor %} -{% if console %} -## console logging -{% for con in console %} -{{console[con]['selectors']}} /dev/console -{% endfor %} -{% endif %} -{% if hosts %} -## remote logging -{% for host in hosts %} -{% if hosts[host]['proto'] == 'tcp' %} -{% if hosts[host]['port'] %} -{{hosts[host]['selectors']}} @@{{host}}:{{hosts[host]['port']}} -{% else %} -{{hosts[host]['selectors']}} @@{{host}} -{% endif %} -{% else %} -{% if hosts[host]['port'] %} -{{hosts[host]['selectors']}} @{{host}}:{{hosts[host]['port']}} -{% else %} -{{hosts[host]['selectors']}} @{{host}} -{% endif %} -{% endif %} -{% endfor %} -{% endif %} -{% if user %} -{% for u in user %} -{{user[u]['selectors']}} :omusrmsg:{{u}} -{% endfor %} -{% endif %} -''' - -logrotate_configs = ''' -{% for file in files %} -{{files[file]['log-file']}} { - missingok - notifempty - create - rotate {{files[file]['max-files']}} - size={{files[file]['max-size']//1024}}k - postrotate - invoke-rc.d rsyslog rotate > /dev/null - endscript -} -{% endfor %} -''' -# config templates end - +from vyos.util import run def get_config(): c = Config() @@ -259,14 +192,19 @@ def generate(c): if c == None: return None - tmpl = jinja2.Template(configs, trim_blocks=True) + # Prepare Jinja2 template loader from files + tmpl_path = os.path.join(vyos_data_dir['data'], 'templates', 'syslog') + fs_loader = FileSystemLoader(tmpl_path) + env = Environment(loader=fs_loader, trim_blocks=True) + + tmpl = env.get_template('rsyslog.conf.tmpl') config_text = tmpl.render(c) with open('/etc/rsyslog.d/vyos-rsyslog.conf', 'w') as f: f.write(config_text) # eventually write for each file its own logrotate file, since size is # defined it shouldn't matter - tmpl = jinja2.Template(logrotate_configs, trim_blocks=True) + tmpl = env.get_template('logrotate.tmpl') config_text = tmpl.render(c) with open('/etc/logrotate.d/vyos-rsyslog', 'w') as f: f.write(config_text) @@ -315,10 +253,8 @@ def verify(c): def apply(c): if not c: - subprocess.call(['sudo', 'systemctl', 'stop', 'syslog']) - return 0 - - subprocess.call(['sudo', 'systemctl', 'restart', 'syslog']) + return run('systemctl stop syslog') + return run('systemctl restart syslog') if __name__ == '__main__': try: @@ -328,4 +264,4 @@ if __name__ == '__main__': apply(c) except ConfigError as e: print(e) - sys.exit(1) + exit(1) diff --git a/src/conf_mode/system-timezone.py b/src/conf_mode/system-timezone.py index d715bd27e..25b949a79 100755 --- a/src/conf_mode/system-timezone.py +++ b/src/conf_mode/system-timezone.py @@ -20,6 +20,8 @@ import os from copy import deepcopy from vyos.config import Config from vyos import ConfigError +from vyos.util import call + default_config_data = { 'name': 'UTC' @@ -40,9 +42,7 @@ def generate(tz): pass def apply(tz): - cmd = '/usr/bin/timedatectl set-timezone {}'.format(tz['name']) - os.system(cmd) - pass + call('/usr/bin/timedatectl set-timezone {}'.format(tz['name'])) if __name__ == '__main__': try: diff --git a/src/conf_mode/system-wifi-regdom.py b/src/conf_mode/system-wifi-regdom.py index 01dc92a20..943c42274 100755 --- a/src/conf_mode/system-wifi-regdom.py +++ b/src/conf_mode/system-wifi-regdom.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 @@ -15,52 +15,34 @@ # along with this program. If not, see <http://www.gnu.org/licenses/>. import os -import jinja2 from copy import deepcopy from sys import exit +from jinja2 import FileSystemLoader, Environment from vyos.config import Config +from vyos.defaults import directories as vyos_data_dir from vyos import ConfigError config_80211_file='/etc/modprobe.d/cfg80211.conf' config_crda_file='/etc/default/crda' -# Please be careful if you edit the template. -config_80211_tmpl = """ -{%- if regdom -%} -options cfg80211 ieee80211_regdom={{ regdom }} -{% endif %} -""" - -# Please be careful if you edit the template. -config_crda_tmpl = """ -{%- if regdom -%} -REGDOMAIN={{ regdom }} -{% endif %} -""" - default_config_data = { 'regdom' : '', 'deleted' : False } - def get_config(): regdom = deepcopy(default_config_data) conf = Config() - - # set new configuration level - conf.set_level('system') + base = ['system', 'wifi-regulatory-domain'] # Check if interface has been removed - if not conf.exists('wifi-regulatory-domain'): + if not conf.exists(base): regdom['deleted'] = True return regdom - - # retrieve configured regulatory domain - if conf.exists('wifi-regulatory-domain'): - regdom['regdom'] = conf.return_value('wifi-regulatory-domain') + else: + regdom['regdom'] = conf.return_value(base) return regdom @@ -85,12 +67,17 @@ def generate(regdom): return None - tmpl = jinja2.Template(config_80211_tmpl) + # Prepare Jinja2 template loader from files + tmpl_path = os.path.join(vyos_data_dir['data'], 'templates', 'wifi') + fs_loader = FileSystemLoader(tmpl_path) + env = Environment(loader=fs_loader) + + tmpl = env.get_template('cfg80211.conf.tmpl') config_text = tmpl.render(regdom) with open(config_80211_file, 'w') as f: f.write(config_text) - tmpl = jinja2.Template(config_crda_tmpl) + tmpl = env.get_template('crda.tmpl') config_text = tmpl.render(regdom) with open(config_crda_file, 'w') as f: f.write(config_text) diff --git a/src/conf_mode/tftp_server.py b/src/conf_mode/tftp_server.py index ff7cad0c9..7a7246783 100755 --- a/src/conf_mode/tftp_server.py +++ b/src/conf_mode/tftp_server.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2018 VyOS maintainers and contributors +# Copyright (C) 2018-2020 VyOS maintainers and contributors # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 or later as @@ -13,31 +13,24 @@ # # You should have received a copy of the GNU General Public License # along with this program. If not, see <http://www.gnu.org/licenses/>. -# -# -import sys import os import stat import pwd -import copy -import glob -import jinja2 -import vyos.validate +from copy import deepcopy +from glob import glob +from jinja2 import FileSystemLoader, Environment +from sys import exit from vyos.config import Config +from vyos.defaults import directories as vyos_data_dir +from vyos.validate import is_ipv4, is_addr_assigned from vyos import ConfigError +from vyos.util import call -config_file = r'/etc/default/tftpd' - -# Please be careful if you edit the template. -config_tmpl = """ -### Autogenerated by tftp_server.py ### -DAEMON_ARGS="--listen --user tftp --address {% for a in listen-%}{{ a }}{% endfor %}{% if allow_upload %} --create --umask 000{% endif %} --secure {{ directory }}" - -""" +config_file = r'/etc/default/tftpd' default_config_data = { 'directory': '', @@ -47,23 +40,25 @@ default_config_data = { } def get_config(): - tftpd = copy.deepcopy(default_config_data) + tftpd = deepcopy(default_config_data) conf = Config() - if not conf.exists('service tftp-server'): + base = ['service', 'tftp-server'] + if not conf.exists(base): return None else: - conf.set_level('service tftp-server') + conf.set_level(base) - if conf.exists('directory'): - tftpd['directory'] = conf.return_value('directory') + if conf.exists(['directory']): + tftpd['directory'] = conf.return_value(['directory']) - if conf.exists('allow-upload'): + if conf.exists(['allow-upload']): tftpd['allow_upload'] = True - if conf.exists('port'): - tftpd['port'] = conf.return_value('port') + if conf.exists(['port']): + tftpd['port'] = conf.return_value(['port']) - tftpd['listen'] = conf.return_values('listen-address') + if conf.exists(['listen-address']): + tftpd['listen'] = conf.return_values(['listen-address']) return tftpd @@ -80,7 +75,7 @@ def verify(tftpd): raise ConfigError('TFTP server listen address must be configured!') for addr in tftpd['listen']: - if not vyos.validate.is_addr_assigned(addr): + if not is_addr_assigned(addr): print('WARNING: TFTP server listen address {0} not assigned to any interface!'.format(addr)) return None @@ -88,22 +83,27 @@ def verify(tftpd): def generate(tftpd): # cleanup any available configuration file # files will be recreated on demand - for i in glob.glob(config_file + '*'): + for i in glob(config_file + '*'): os.unlink(i) # bail out early - looks like removal from running config if tftpd is None: return None + # Prepare Jinja2 template loader from files + tmpl_path = os.path.join(vyos_data_dir['data'], 'templates', 'tftp-server') + fs_loader = FileSystemLoader(tmpl_path) + env = Environment(loader=fs_loader) + idx = 0 for listen in tftpd['listen']: - config = copy.deepcopy(tftpd) - if vyos.validate.is_ipv4(listen): + config = deepcopy(tftpd) + if is_ipv4(listen): config['listen'] = [listen + ":" + tftpd['port'] + " -4"] else: config['listen'] = ["[" + listen + "]" + tftpd['port'] + " -6"] - tmpl = jinja2.Template(config_tmpl) + tmpl = env.get_template('default.tmpl') config_text = tmpl.render(config) file = config_file + str(idx) with open(file, 'w') as f: @@ -115,7 +115,7 @@ def generate(tftpd): def apply(tftpd): # stop all services first - then we will decide - os.system('sudo systemctl stop tftpd@{0..20}') + call('systemctl stop tftpd@{0..20}') # bail out early - e.g. service deletion if tftpd is None: @@ -140,7 +140,7 @@ def apply(tftpd): idx = 0 for listen in tftpd['listen']: - os.system('sudo systemctl restart tftpd@{0}.service'.format(idx)) + call('systemctl restart tftpd@{0}.service'.format(idx)) idx = idx + 1 return None @@ -153,4 +153,4 @@ if __name__ == '__main__': apply(c) except ConfigError as e: print(e) - sys.exit(1) + exit(1) diff --git a/src/conf_mode/vpn-pptp.py b/src/conf_mode/vpn-pptp.py index 355adf715..45b2c4b40 100755 --- a/src/conf_mode/vpn-pptp.py +++ b/src/conf_mode/vpn-pptp.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2018 VyOS maintainers and contributors +# Copyright (C) 2018-2020 VyOS maintainers and contributors # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 or later as @@ -13,20 +13,19 @@ # # You should have received a copy of the GNU General Public License # along with this program. If not, see <http://www.gnu.org/licenses/>. -# -# -import sys import os import re -import subprocess -import jinja2 -import socket -import time -import syslog as sl + +from jinja2 import FileSystemLoader, Environment +from socket import socket, AF_INET, SOCK_STREAM +from sys import exit +from time import sleep from vyos.config import Config +from vyos.defaults import directories as vyos_data_dir from vyos import ConfigError +from vyos.util import run pidfile = r'/var/run/accel_pptp.pid' pptp_cnf_dir = r'/etc/accel-ppp/pptp' @@ -36,143 +35,24 @@ pptp_conf = pptp_cnf_dir + '/pptp.config' # config path creation if not os.path.exists(pptp_cnf_dir): os.makedirs(pptp_cnf_dir) - sl.syslog(sl.LOG_NOTICE, pptp_cnf_dir + " created") - -pptp_config = ''' -### generated by accel_pptp.py ### -[modules] -log_syslog -pptp -ippool -chap-secrets -{% if authentication['auth_proto'] %} -{{ authentication['auth_proto'] }} -{% else %} -auth_mschap_v2 -{% endif %} -{% if authentication['mode'] == 'radius' %} -radius -{% endif -%} - -[core] -thread-count={{thread_cnt}} - -[log] -syslog=accel-pptp,daemon -copy=1 -level=5 - -{% if dns %} -[dns] -{% if dns[0] %} -dns1={{dns[0]}} -{% endif %} -{% if dns[1] %} -dns2={{dns[1]}} -{% endif %} -{% endif %} - -{% if wins %} -[wins] -{% if wins[0] %} -wins1={{wins[0]}} -{% endif %} -{% if wins[1] %} -wins2={{wins[1]}} -{% endif %} -{% endif %} - -[pptp] -ifname=pptp%d -{% if outside_addr %} -bind={{outside_addr}} -{% endif %} -verbose=1 -ppp-max-mtu={{mtu}} -mppe={{authentication['mppe']}} -echo-interval=10 -echo-failure=3 - - -[client-ip-range] -0.0.0.0/0 - -[ip-pool] -tunnel={{client_ip_pool}} -gw-ip-address={{gw_ip}} - -{% if authentication['mode'] == 'local' %} -[chap-secrets] -chap-secrets=/etc/accel-ppp/pptp/chap-secrets -{% endif %} - -[ppp] -verbose=5 -check-ip=1 -single-session=replace - -{% if authentication['mode'] == 'radius' %} -[radius] -{% for rsrv in authentication['radiussrv']: %} -server={{rsrv}},{{authentication['radiussrv'][rsrv]['secret']}},\ -req-limit={{authentication['radiussrv'][rsrv]['req-limit']}},\ -fail-time={{authentication['radiussrv'][rsrv]['fail-time']}} -{% endfor %} -timeout=30 -acct-timeout=30 -max-try=3 -{%endif %} - -[cli] -tcp=127.0.0.1:2003 -''' - -# pptp chap secrets -chap_secrets_conf = ''' -# username server password acceptable local IP addresses -{% for user in authentication['local-users'] %} -{% if authentication['local-users'][user]['state'] == 'enabled' %} -{{user}}\t*\t{{authentication['local-users'][user]['passwd']}}\t{{authentication['local-users'][user]['ip']}} -{% endif %} -{% endfor %} -''' - def _chk_con(): cnt = 0 - s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + s = socket(AF_INET, SOCK_STREAM) while True: try: s.connect(("127.0.0.1", 2003)) break except ConnectionRefusedError: - time.sleep(0.5) + sleep(0.5) cnt += 1 if cnt == 100: raise("failed to start pptp server") break -# chap_secrets file if auth mode local - - -def _write_chap_secrets(c): - tmpl = jinja2.Template(chap_secrets_conf, trim_blocks=True) - chap_secrets_txt = tmpl.render(c) - old_umask = os.umask(0o077) - open(chap_secrets, 'w').write(chap_secrets_txt) - os.umask(old_umask) - sl.syslog(sl.LOG_NOTICE, chap_secrets + ' written') - -def _accel_cmd(cmd=''): - if not cmd: - return None - try: - ret = subprocess.check_output( - ['/usr/bin/accel-cmd', '-p', '2003', cmd]).decode().strip() - return ret - except: - return 1 +def _accel_cmd(command): + return run('/usr/bin/accel-cmd -p 2003 {command}') ### # inline helper functions end @@ -326,6 +206,11 @@ def generate(c): if c == None: return None + # Prepare Jinja2 template loader from files + tmpl_path = os.path.join(vyos_data_dir['data'], 'templates', 'pptp') + fs_loader = FileSystemLoader(tmpl_path) + env = Environment(loader=fs_loader, trim_blocks=True) + # accel-cmd reload doesn't work so any change results in a restart of the daemon try: if os.cpu_count() == 1: @@ -338,12 +223,18 @@ def generate(c): else: c['thread_cnt'] = int(os.cpu_count()/2) - tmpl = jinja2.Template(pptp_config, trim_blocks=True) + tmpl = env.get_template('pptp.config.tmpl') config_text = tmpl.render(c) - open(pptp_conf, 'w').write(config_text) + with open(pptp_conf, 'w') as f: + f.write(config_text) if c['authentication']['local-users']: - _write_chap_secrets(c) + tmpl = env.get_template('chap-secrets.tmpl') + chap_secrets_txt = tmpl.render(c) + old_umask = os.umask(0o077) + with open(chap_secrets, 'w') as f: + f.write(chap_secrets_txt) + os.umask(old_umask) return c @@ -357,8 +248,7 @@ def apply(c): return None if not os.path.exists(pidfile): - ret = subprocess.call( - ['/usr/sbin/accel-pppd', '-c', pptp_conf, '-p', pidfile, '-d']) + ret = run(f'/usr/sbin/accel-pppd -c {pptp_conf} -p {pidfile} -d') _chk_con() if ret != 0 and os.path.exists(pidfile): os.remove(pidfile) @@ -366,8 +256,6 @@ def apply(c): else: # if gw ip changes, only restart doesn't work _accel_cmd('restart') - sl.syslog(sl.LOG_NOTICE, "reloading config via daemon restart") - if __name__ == '__main__': try: @@ -377,4 +265,4 @@ if __name__ == '__main__': apply(c) except ConfigError as e: print(e) - sys.exit(1) + exit(1) diff --git a/src/conf_mode/vpn_sstp.py b/src/conf_mode/vpn_sstp.py new file mode 100755 index 000000000..ca0844c50 --- /dev/null +++ b/src/conf_mode/vpn_sstp.py @@ -0,0 +1,402 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2018-2020 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +import os + +from time import sleep +from sys import exit +from socket import socket, AF_INET, SOCK_STREAM +from copy import deepcopy +from stat import S_IRUSR, S_IWUSR, S_IRGRP +from jinja2 import FileSystemLoader, Environment + +from vyos.config import Config +from vyos import ConfigError +from vyos.defaults import directories as vyos_data_dir +from vyos.util import process_running +from vyos.util import process_running, cmd, run + +pidfile = r'/var/run/accel_sstp.pid' +sstp_cnf_dir = r'/etc/accel-ppp/sstp' +chap_secrets = sstp_cnf_dir + '/chap-secrets' +sstp_conf = sstp_cnf_dir + '/sstp.config' + +# config path creation +if not os.path.exists(sstp_cnf_dir): + os.makedirs(sstp_cnf_dir) + +def chk_con(): + cnt = 0 + s = socket(AF_INET, SOCK_STREAM) + while True: + try: + s.connect(("127.0.0.1", 2005)) + s.close() + break + except ConnectionRefusedError: + sleep(0.5) + cnt += 1 + if cnt == 100: + raise("failed to start sstp server") + break + + +def _accel_cmd(command): + return run(f'/usr/bin/accel-cmd -p 2005 {command}') + +default_config_data = { + 'local_users' : [], + 'auth_mode' : 'local', + 'auth_proto' : [], + 'radius_server' : [], + 'radius_acct_tmo' : '3', + 'radius_max_try' : '3', + 'radius_timeout' : '3', + 'radius_nas_id' : '', + 'radius_nas_ip' : '', + 'radius_source_address' : '', + 'radius_shaper_attr' : '', + 'radius_shaper_vendor': '', + 'radius_dynamic_author' : '', + 'ssl_ca' : '', + 'ssl_cert' : '', + 'ssl_key' : '', + 'client_ip_pool' : [], + 'dnsv4' : [], + 'mtu' : '', + 'ppp_mppe' : '', + 'ppp_echo_failure' : '', + 'ppp_echo_interval' : '', + 'ppp_echo_timeout' : '', + 'thread_cnt' : '' +} + +def get_config(): + sstp = deepcopy(default_config_data) + base_path = ['vpn', 'sstp'] + conf = Config() + if not conf.exists(base_path): + return None + + conf.set_level(base_path) + + cpu = int(os.cpu_count()/2) + if cpu < 1: + cpu = 1 + sstp['thread_cnt'] = cpu + + if conf.exists(['authentication', 'mode']): + sstp['auth_mode'] = conf.return_value(['authentication', 'mode']) + + # + # local auth + if conf.exists(['authentication', 'local-users']): + for username in conf.list_nodes(['authentication', 'local-users', 'username']): + user = { + 'name' : username, + 'password' : '', + 'state' : 'enabled', + 'ip' : '*', + 'upload' : None, + 'download' : None + } + + conf.set_level(base_path + ['authentication', 'local-users', 'username', username]) + + if conf.exists(['password']): + user['password'] = conf.return_value(['password']) + + if conf.exists(['disable']): + user['state'] = 'disable' + + if conf.exists(['static-ip']): + user['ip'] = conf.return_value(['static-ip']) + + if conf.exists(['rate-limit', 'download']): + user['download'] = conf.return_value(['rate-limit', 'download']) + + if conf.exists(['rate-limit', 'upload']): + user['upload'] = conf.return_value(['rate-limit', 'upload']) + + sstp['local_users'].append(user) + + # + # RADIUS auth and settings + conf.set_level(base_path + ['authentication', 'radius']) + if conf.exists(['server']): + for server in conf.list_nodes(['server']): + radius = { + 'server' : server, + 'key' : '', + 'fail_time' : 0, + 'port' : '1812' + } + + conf.set_level(base_path + ['authentication', 'radius', 'server', server]) + + if conf.exists(['fail-time']): + radius['fail-time'] = conf.return_value(['fail-time']) + + if conf.exists(['port']): + radius['port'] = conf.return_value(['port']) + + if conf.exists(['key']): + radius['key'] = conf.return_value(['key']) + + if not conf.exists(['disable']): + sstp['radius_server'].append(radius) + + # + # advanced radius-setting + conf.set_level(base_path + ['authentication', 'radius']) + + if conf.exists(['acct-timeout']): + sstp['radius_acct_tmo'] = conf.return_value(['acct-timeout']) + + if conf.exists(['max-try']): + sstp['radius_max_try'] = conf.return_value(['max-try']) + + if conf.exists(['timeout']): + sstp['radius_timeout'] = conf.return_value(['timeout']) + + if conf.exists(['nas-identifier']): + sstp['radius_nas_id'] = conf.return_value(['nas-identifier']) + + if conf.exists(['nas-ip-address']): + sstp['radius_nas_ip'] = conf.return_value(['nas-ip-address']) + + if conf.exists(['source-address']): + sstp['radius_source_address'] = conf.return_value(['source-address']) + + # Dynamic Authorization Extensions (DOA)/Change Of Authentication (COA) + if conf.exists(['dynamic-author']): + dae = { + 'port' : '', + 'server' : '', + 'key' : '' + } + + if conf.exists(['dynamic-author', 'server']): + dae['server'] = conf.return_value(['dynamic-author', 'server']) + + if conf.exists(['dynamic-author', 'port']): + dae['port'] = conf.return_value(['dynamic-author', 'port']) + + if conf.exists(['dynamic-author', 'key']): + dae['key'] = conf.return_value(['dynamic-author', 'key']) + + sstp['radius_dynamic_author'] = dae + + if conf.exists(['rate-limit', 'enable']): + sstp['radius_shaper_attr'] = 'Filter-Id' + c_attr = ['rate-limit', 'enable', 'attribute'] + if conf.exists(c_attr): + sstp['radius_shaper_attr'] = conf.return_value(c_attr) + + c_vendor = ['rate-limit', 'enable', 'vendor'] + if conf.exists(c_vendor): + sstp['radius_shaper_vendor'] = conf.return_value(c_vendor) + + # + # authentication protocols + conf.set_level(base_path + ['authentication']) + if conf.exists(['protocols']): + auth_mods = { + 'pap': 'auth_pap', + 'chap': 'auth_chap_md5', + 'mschap': 'auth_mschap_v1', + 'mschap-v2': 'auth_mschap_v2' + } + + for proto in conf.return_values(['protocols']): + sstp['auth_proto'].append(auth_mods[proto]) + + else: + sstp['auth_proto'] = ['auth_mschap_v2'] + + # + # read in SSL certs + conf.set_level(base_path + ['ssl']) + if conf.exists(['ca-cert-file']): + sstp['ssl_ca'] = conf.return_value(['ca-cert-file']) + + if conf.exists(['cert-file']): + sstp['ssl_cert'] = conf.return_value(['cert-file']) + + if conf.exists(['key-file']): + sstp['ssl_key'] = conf.return_value(['key-file']) + + + # + # read in client ip pool settings + conf.set_level(base_path + ['network-settings', 'client-ip-settings']) + if conf.exists(['subnet']): + sstp['client_ip_pool'] = conf.return_values(['subnet']) + + if conf.exists(['gateway-address']): + sstp['client_gateway'] = conf.return_value(['gateway-address']) + + # + # read in network settings + conf.set_level(base_path + ['network-settings']) + if conf.exists(['name-server']): + sstp['dnsv4'] = conf.return_values(['name-server']) + + if conf.exists(['mtu']): + sstp['mtu'] = conf.return_value(['mtu']) + + # + # read in PPP stuff + conf.set_level(base_path + ['ppp-settings']) + if conf.exists('mppe'): + sstp['ppp_mppe'] = conf.return_value('ppp-settings mppe') + + if conf.exists(['lcp-echo-failure']): + sstp['ppp_echo_failure'] = conf.return_value(['lcp-echo-failure']) + + if conf.exists(['lcp-echo-interval']): + sstp['ppp_echo_interval'] = conf.return_value(['lcp-echo-interval']) + + if conf.exists(['lcp-echo-timeout']): + sstp['ppp_echo_timeout'] = conf.return_value(['lcp-echo-timeout']) + + return sstp + + +def verify(sstp): + if sstp is None: + return None + + # vertify auth settings + if sstp['auth_mode'] == 'local': + if not sstp['local_users']: + raise ConfigError('sstp-server authentication local-users required') + + for user in sstp['local_users']: + if not user['password']: + raise ConfigError(f"Password required for user {user['name']}") + + # if up/download is set, check that both have a value + if user['upload'] and not user['download']: + raise ConfigError(f"Download speed value required for user {user['name']}") + + if user['download'] and not user['upload']: + raise ConfigError(f"Upload speed value required for user {user['name']}") + + if not sstp['client_ip_pool']: + raise ConfigError("Client IP subnet required") + + if not sstp['client_gateway']: + raise ConfigError("Client gateway IP address required") + + if len(sstp['dnsv4']) > 2: + raise ConfigError("Only 2 DNS name-servers can be configured") + + if not sstp['ssl_ca'] or not sstp['ssl_cert'] or not sstp['ssl_key']: + raise ConfigError('One or more SSL certificates missing') + + if not os.path.exists(sstp['ssl_ca']): + raise ConfigError(f"CA cert file {sstp['ssl_ca']} does not exist") + + if not os.path.exists(sstp['ssl_cert']): + raise ConfigError(f"SSL cert file {sstp['ssl_cert']} does not exist") + + if not os.path.exists(sstp['ssl_key']): + raise ConfigError(f"SSL key file {sstp['ssl_key']} does not exist") + + if sstp['auth_mode'] == 'radius': + if len(sstp['radius_server']) == 0: + raise ConfigError("RADIUS authentication requires at least one server") + + for radius in sstp['radius_server']: + if not radius['key']: + raise ConfigError(f"Missing RADIUS secret for server {{ radius['key'] }}") + +def generate(sstp): + if sstp is None: + return None + + # Prepare Jinja2 template loader from files + tmpl_path = os.path.join(vyos_data_dir['data'], 'templates', 'sstp') + fs_loader = FileSystemLoader(tmpl_path) + env = Environment(loader=fs_loader, trim_blocks=True) + + # accel-cmd reload doesn't work so any change results in a restart of the daemon + tmpl = env.get_template('sstp.config.tmpl') + config_text = tmpl.render(sstp) + with open(sstp_conf, 'w') as f: + f.write(config_text) + + if sstp['local_users']: + tmpl = env.get_template('chap-secrets.tmpl') + config_text = tmpl.render(sstp) + with open(chap_secrets, 'w') as f: + f.write(config_text) + + os.chmod(chap_secrets, S_IRUSR | S_IWUSR | S_IRGRP) + else: + if os.path.exists(chap_secrets): + os.unlink(chap_secrets) + + return sstp + +def apply(sstp): + if sstp is None: + if process_running(pidfile): + command = 'start-stop-daemon' + command += ' --stop ' + command += ' --quiet' + command += ' --oknodo' + command += ' --pidfile ' + pidfile + cmd(command) + + if os.path.exists(pidfile): + os.remove(pidfile) + + return None + + if not process_running(pidfile): + if os.path.exists(pidfile): + os.remove(pidfile) + + command = 'start-stop-daemon' + command += ' --start ' + command += ' --quiet' + command += ' --oknodo' + command += ' --pidfile ' + pidfile + command += ' --exec /usr/sbin/accel-pppd' + # now pass arguments to accel-pppd binary + command += ' --' + command += ' -c ' + sstp_conf + command += ' -p ' + pidfile + command += ' -d' + cmd(command) + + chk_con() + + else: + _accel_cmd('restart') + + +if __name__ == '__main__': + try: + c = get_config() + verify(c) + generate(c) + apply(c) + except ConfigError as e: + print(e) + exit(1) diff --git a/src/conf_mode/vrf.py b/src/conf_mode/vrf.py new file mode 100755 index 000000000..586424c09 --- /dev/null +++ b/src/conf_mode/vrf.py @@ -0,0 +1,276 @@ +#!/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 os + +from sys import exit +from copy import deepcopy +from jinja2 import FileSystemLoader, Environment +from json import loads + +from vyos.config import Config +from vyos.configdict import list_diff +from vyos.defaults import directories as vyos_data_dir +from vyos.ifconfig import Interface +from vyos.util import read_file, cmd +from vyos import ConfigError + +config_file = r'/etc/iproute2/rt_tables.d/vyos-vrf.conf' + +default_config_data = { + 'bind_to_all': '0', + 'deleted': False, + 'vrf_add': [], + 'vrf_existing': [], + 'vrf_remove': [] +} + +def _cmd(command): + cmd(command, raising=ConfigError, message='Error changing VRF') + +def list_rules(): + command = 'ip -j -4 rule show' + answer = loads(cmd(command)) + return [_ for _ in answer if _] + +def vrf_interfaces(c, match): + matched = [] + old_level = c.get_level() + c.set_level(['interfaces']) + section = c.get_config_dict([]) + for type in section: + interfaces = section[type] + for name in interfaces: + interface = interfaces[name] + if 'vrf' in interface: + v = interface.get('vrf', '') + if v == match: + matched.append(name) + + c.set_level(old_level) + return matched + +def vrf_routing(c, match): + matched = [] + old_level = c.get_level() + c.set_level(['protocols', 'vrf']) + if match in c.list_nodes([]): + matched.append(match) + + c.set_level(old_level) + return matched + + +def get_config(): + conf = Config() + vrf_config = deepcopy(default_config_data) + + cfg_base = ['vrf'] + if not conf.exists(cfg_base): + # get all currently effetive VRFs and mark them for deletion + vrf_config['vrf_remove'] = conf.list_effective_nodes(cfg_base + ['name']) + else: + # set configuration level base + conf.set_level(cfg_base) + + # Should services be allowed to bind to all VRFs? + if conf.exists(['bind-to-all']): + vrf_config['bind_to_all'] = '1' + + # Determine vrf interfaces (currently effective) - to determine which + # vrf interface is no longer present and needs to be removed + eff_vrf = conf.list_effective_nodes(['name']) + act_vrf = conf.list_nodes(['name']) + vrf_config['vrf_remove'] = list_diff(eff_vrf, act_vrf) + + # read in individual VRF definition and build up + # configuration + for name in conf.list_nodes(['name']): + vrf_inst = { + 'description' : '', + 'members': [], + 'name' : name, + 'table' : '', + 'table_mod': False + } + conf.set_level(cfg_base + ['name', name]) + + if conf.exists(['table']): + # VRF table can't be changed on demand, thus we need to read in the + # current and the effective routing table number + act_table = conf.return_value(['table']) + eff_table = conf.return_effective_value(['table']) + vrf_inst['table'] = act_table + if eff_table and eff_table != act_table: + vrf_inst['table_mod'] = True + + if conf.exists(['description']): + vrf_inst['description'] = conf.return_value(['description']) + + # append individual VRF configuration to global configuration list + vrf_config['vrf_add'].append(vrf_inst) + + # set configuration level base + conf.set_level(cfg_base) + + # check VRFs which need to be removed as they are not allowed to have + # interfaces attached + tmp = [] + for name in vrf_config['vrf_remove']: + vrf_inst = { + 'interfaces': [], + 'name': name, + 'routes': [] + } + + # find member interfaces of this particulat VRF + vrf_inst['interfaces'] = vrf_interfaces(conf, name) + + # find routing protocols used by this VRF + vrf_inst['routes'] = vrf_routing(conf, name) + + # append individual VRF configuration to temporary configuration list + tmp.append(vrf_inst) + + # replace values in vrf_remove with list of dictionaries + # as we need it in verify() - we can't delete a VRF with members attached + vrf_config['vrf_remove'] = tmp + return vrf_config + +def verify(vrf_config): + # ensure VRF is not assigned to any interface + for vrf in vrf_config['vrf_remove']: + if len(vrf['interfaces']) > 0: + raise ConfigError(f"VRF {vrf['name']} can not be deleted. It has active member interfaces!") + + if len(vrf['routes']) > 0: + raise ConfigError(f"VRF {vrf['name']} can not be deleted. It has active routing protocols!") + + table_ids = [] + for vrf in vrf_config['vrf_add']: + # table id is mandatory + if not vrf['table']: + raise ConfigError(f"VRF {vrf['name']} table id is mandatory!") + + # routing table id can't be changed - OS restriction + if vrf['table_mod']: + raise ConfigError(f"VRF {vrf['name']} table id modification is not possible!") + + # VRf routing table ID must be unique on the system + if vrf['table'] in table_ids: + raise ConfigError(f"VRF {vrf['name']} table id {vrf['table']} is not unique!") + + table_ids.append(vrf['table']) + + return None + +def generate(vrf_config): + # Prepare Jinja2 template loader from files + tmpl_path = os.path.join(vyos_data_dir['data'], 'templates', 'vrf') + fs_loader = FileSystemLoader(tmpl_path) + env = Environment(loader=fs_loader) + + tmpl = env.get_template('vrf.conf.tmpl') + config_text = tmpl.render(vrf_config) + with open(config_file, 'w') as f: + f.write(config_text) + + return None + +def apply(vrf_config): + # Documentation + # + # - https://github.com/torvalds/linux/blob/master/Documentation/networking/vrf.txt + # - https://github.com/Mellanox/mlxsw/wiki/Virtual-Routing-and-Forwarding-(VRF) + # - https://github.com/Mellanox/mlxsw/wiki/L3-Tunneling + # - https://netdevconf.info/1.1/proceedings/slides/ahern-vrf-tutorial.pdf + # - https://netdevconf.info/1.2/slides/oct6/02_ahern_what_is_l3mdev_slides.pdf + + # set the default VRF global behaviour + bind_all = vrf_config['bind_to_all'] + if read_file('/proc/sys/net/ipv4/tcp_l3mdev_accept') != bind_all: + _cmd(f'sysctl -wq net.ipv4.tcp_l3mdev_accept={bind_all}') + _cmd(f'sysctl -wq net.ipv4.udp_l3mdev_accept={bind_all}') + + for vrf in vrf_config['vrf_remove']: + name = vrf['name'] + if os.path.isdir(f'/sys/class/net/{name}'): + _cmd(f'sudo ip -4 route del vrf {name} unreachable default metric 4278198272') + _cmd(f'sudo ip -6 route del vrf {name} unreachable default metric 4278198272') + _cmd(f'ip link delete dev {name}') + + for vrf in vrf_config['vrf_add']: + name = vrf['name'] + table = vrf['table'] + + if not os.path.isdir(f'/sys/class/net/{name}'): + # For each VRF apart from your default context create a VRF + # interface with a separate routing table + _cmd(f'ip link add {name} type vrf table {table}') + # Start VRf + _cmd(f'ip link set dev {name} up') + # The kernel Documentation/networking/vrf.txt also recommends + # adding unreachable routes to the VRF routing tables so that routes + # afterwards are taken. + _cmd(f'ip -4 route add vrf {name} unreachable default metric 4278198272') + _cmd(f'ip -6 route add vrf {name} unreachable default metric 4278198272') + + # set VRF description for e.g. SNMP monitoring + Interface(name).set_alias(vrf['description']) + + # Linux routing uses rules to find tables - routing targets are then + # looked up in those tables. If the lookup got a matching route, the + # process ends. + # + # TL;DR; first table with a matching entry wins! + # + # You can see your routing table lookup rules using "ip rule", sadly the + # local lookup is hit before any VRF lookup. Pinging an addresses from the + # VRF will usually find a hit in the local table, and never reach the VRF + # routing table - this is usually not what you want. Thus we will + # re-arrange the tables and move the local lookup furhter down once VRFs + # are enabled. + + # get current preference on local table + local_pref = [r.get('priority') for r in list_rules() if r.get('table') == 'local'][0] + + # change preference when VRFs are enabled and local lookup table is default + if not local_pref and vrf_config['vrf_add']: + for af in ['-4', '-6']: + _cmd(f'ip {af} rule add pref 32765 table local') + _cmd(f'ip {af} rule del pref 0') + + # return to default lookup preference when no VRF is configured + if not vrf_config['vrf_add']: + for af in ['-4', '-6']: + _cmd(f'ip {af} rule add pref 0 table local') + _cmd(f'ip {af} rule del pref 32765') + + # clean out l3mdev-table rule if present + if 1000 in [r.get('priority') for r in list_rules() if r.get('priority') == 1000]: + _cmd(f'ip {af} rule del pref 1000') + + return None + +if __name__ == '__main__': + try: + c = get_config() + verify(c) + generate(c) + apply(c) + except ConfigError as e: + print(e) + exit(1) diff --git a/src/conf_mode/vrrp.py b/src/conf_mode/vrrp.py index a09e55a2f..3f1b73385 100755 --- a/src/conf_mode/vrrp.py +++ b/src/conf_mode/vrrp.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2018 VyOS maintainers and contributors +# Copyright (C) 2018-2020 VyOS maintainers and contributors # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 or later as @@ -13,145 +13,25 @@ # # You should have received a copy of the GNU General Public License # along with this program. If not, see <http://www.gnu.org/licenses/>. -# import os -import sys -import subprocess -import ipaddress -import jinja2 +from sys import exit +from ipaddress import ip_address, ip_interface, IPv4Interface, IPv6Interface, IPv4Address, IPv6Address +from jinja2 import FileSystemLoader, Environment +from json import dumps +from pathlib import Path import vyos.config import vyos.keepalived +from vyos.defaults import directories as vyos_data_dir from vyos import ConfigError +from vyos.util import call daemon_file = "/etc/default/keepalived" config_file = "/etc/keepalived/keepalived.conf" - -config_tmpl = """ -# Autogenerated by VyOS -# Do not edit this file, all your changes will be lost -# on next commit or reboot - -global_defs { - dynamic_interfaces - script_user root -} - -{% for group in groups -%} - -{% if group.health_check_script -%} -vrrp_script healthcheck_{{ group.name }} { - script "{{ group.health_check_script }}" - interval {{ group.health_check_interval }} - fall {{ group.health_check_count }} - rise 1 - -} -{% endif %} - -vrrp_instance {{ group.name }} { - {% if group.description -%} - # {{ group.description }} - {% endif -%} - - state BACKUP - interface {{ group.interface }} - virtual_router_id {{ group.vrid }} - priority {{ group.priority }} - advert_int {{ group.advertise_interval }} - - {% if group.preempt -%} - preempt_delay {{ group.preempt_delay }} - {% else -%} - nopreempt - {% endif -%} - - {% if group.peer_address -%} - unicast_peer { {{ group.peer_address }} } - {% endif -%} - - {% if group.hello_source -%} - {%- if group.peer_address -%} - unicast_src_ip {{ group.hello_source }} - {%- else -%} - mcast_src_ip {{ group.hello_source }} - {%- endif %} - {% endif -%} - - {% if group.use_vmac and group.peer_address -%} - use_vmac {{group.interface}}v{{group.vrid}} - vmac_xmit_base - {% elif group.use_vmac -%} - use_vmac {{group.interface}}v{{group.vrid}} - {% endif -%} - - {% if group.auth_password -%} - authentication { - auth_pass "{{ group.auth_password }}" - auth_type {{ group.auth_type }} - } - {% endif -%} - - virtual_ipaddress { - {% for addr in group.virtual_addresses -%} - {{ addr }} - {% endfor -%} - } - - {% if group.health_check_script -%} - track_script { - healthcheck_{{ group.name }} - } - {% endif -%} - - {% if group.master_script -%} - notify_master "/usr/libexec/vyos/system/vrrp-script-wrapper.py --state master --group {{ group.name }} --interface {{ group.interface }} {{ group.master_script }}" - {% endif -%} - - {% if group.backup_script -%} - notify_backup "/usr/libexec/vyos/system/vrrp-script-wrapper.py --state backup --group {{ group.name }} --interface {{ group.interface }} {{ group.backup_script }}" - {% endif -%} - - {% if group.fault_script -%} - notify_fault "/usr/libexec/vyos/system/vrrp-script-wrapper.py --state fault --group {{ group.name }} --interface {{ group.interface }} {{ group.fault_script }}" - {% endif -%} - - {% if group.stop_script -%} - notify_stop "/usr/libexec/vyos/system/vrrp-script-wrapper.py --state stop --group {{ group.name }} --interface {{ group.interface }} {{ group.stop_script }}" - {% endif -%} -} - -{% endfor -%} - -{% for sync_group in sync_groups -%} -vrrp_sync_group {{ sync_group.name }} { - group { - {% for member in sync_group.members -%} - {{ member }} - {% endfor -%} - } - - {% if sync_group.conntrack_sync -%} - notify_master "/opt/vyatta/sbin/vyatta-vrrp-conntracksync.sh master {{ sync_group.name }}" - notify_backup "/opt/vyatta/sbin/vyatta-vrrp-conntracksync.sh backup {{ sync_group.name }}" - notify_fault "/opt/vyatta/sbin/vyatta-vrrp-conntracksync.sh fault {{ sync_group.name }}" - {% endif -%} -} - -{% endfor -%} - -""" - -daemon_tmpl = """ -# Autogenerated by VyOS -# Options to pass to keepalived - -# DAEMON_ARGS are appended to the keepalived command-line -DAEMON_ARGS="--snmp" -""" +config_dict_path = "/run/keepalived_config.dict" def get_config(): vrrp_groups = [] @@ -220,7 +100,7 @@ def get_config(): vrrp_groups.append(group) - config.set_level("") + config.set_level("") # Get the sync group used for conntrack-sync conntrack_sync_group = None @@ -238,10 +118,21 @@ def get_config(): if conntrack_sync_group == sync_group_name: sync_group["conntrack_sync"] = True + # add transition script configuration + sync_group["master_script"] = config.return_value("transition-script master") + sync_group["backup_script"] = config.return_value("transition-script backup") + sync_group["fault_script"] = config.return_value("transition-script fault") + sync_group["stop_script"] = config.return_value("transition-script stop") + sync_groups.append(sync_group) + # create a file with dict with proposed configuration + with open("{}.temp".format(config_dict_path), 'w') as dict_file: + dict_file.write(dumps({'vrrp_groups': vrrp_groups, 'sync_groups': sync_groups})) + return (vrrp_groups, sync_groups) + def verify(data): vrrp_groups, sync_groups = data @@ -262,31 +153,31 @@ def verify(data): # XXX: filter on map object is destructive, so we force it to list. # Additionally, filter objects always evaluate to True, empty or not, # so we force them to lists as well. - vaddrs = list(map(lambda i: ipaddress.ip_interface(i), group["virtual_addresses"])) - vaddrs4 = list(filter(lambda x: isinstance(x, ipaddress.IPv4Interface), vaddrs)) - vaddrs6 = list(filter(lambda x: isinstance(x, ipaddress.IPv6Interface), vaddrs)) + vaddrs = list(map(lambda i: ip_interface(i), group["virtual_addresses"])) + vaddrs4 = list(filter(lambda x: isinstance(x, IPv4Interface), vaddrs)) + vaddrs6 = list(filter(lambda x: isinstance(x, IPv6Interface), vaddrs)) if vaddrs4 and vaddrs6: raise ConfigError("VRRP group {0} mixes IPv4 and IPv6 virtual addresses, this is not allowed. Create separate groups for IPv4 and IPv6".format(group["name"])) if vaddrs4: if group["hello_source"]: - hsa = ipaddress.ip_address(group["hello_source"]) - if isinstance(hsa, ipaddress.IPv6Address): + hsa = ip_address(group["hello_source"]) + if isinstance(hsa, IPv6Address): raise ConfigError("VRRP group {0} uses IPv4 but its hello-source-address is IPv6".format(group["name"])) if group["peer_address"]: - pa = ipaddress.ip_address(group["peer_address"]) - if isinstance(pa, ipaddress.IPv6Address): + pa = ip_address(group["peer_address"]) + if isinstance(pa, IPv6Address): raise ConfigError("VRRP group {0} uses IPv4 but its peer-address is IPv6".format(group["name"])) if vaddrs6: if group["hello_source"]: - hsa = ipaddress.ip_address(group["hello_source"]) - if isinstance(hsa, ipaddress.IPv4Address): + hsa = ip_address(group["hello_source"]) + if isinstance(hsa, IPv4Address): raise ConfigError("VRRP group {0} uses IPv6 but its hello-source-address is IPv4".format(group["name"])) if group["peer_address"]: - pa = ipaddress.ip_address(group["peer_address"]) - if isinstance(pa, ipaddress.IPv4Address): + pa = ip_address(group["peer_address"]) + if isinstance(pa, IPv4Address): raise ConfigError("VRRP group {0} uses IPv6 but its peer-address is IPv4".format(group["name"])) # Disallow same VRID on multiple interfaces @@ -308,7 +199,13 @@ def verify(data): if not (m in vrrp_group_names): raise ConfigError("VRRP sync-group {0} refers to VRRP group {1}, but group {1} does not exist".format(sync_group["name"], m)) + def generate(data): + # Prepare Jinja2 template loader from files + tmpl_path = os.path.join(vyos_data_dir['data'], 'templates', 'vrrp') + fs_loader = FileSystemLoader(tmpl_path) + env = Environment(loader=fs_loader) + vrrp_groups, sync_groups = data # Remove disabled groups from the sync group member lists @@ -318,34 +215,44 @@ def generate(data): if g["disable"]: print("Warning: ignoring disabled VRRP group {0} in sync-group {1}".format(g["name"], sync_group["name"])) # Filter out disabled groups - vrrp_groups = list(filter(lambda x: x["disable"] != True, vrrp_groups)) + vrrp_groups = list(filter(lambda x: x["disable"] is not True, vrrp_groups)) - tmpl = jinja2.Template(config_tmpl) + tmpl = env.get_template('keepalived.conf.tmpl') config_text = tmpl.render({"groups": vrrp_groups, "sync_groups": sync_groups}) with open(config_file, 'w') as f: f.write(config_text) + tmpl = env.get_template('daemon.tmpl') + config_text = tmpl.render() with open(daemon_file, 'w') as f: - f.write(daemon_tmpl) + f.write(config_text) return None + def apply(data): vrrp_groups, sync_groups = data if vrrp_groups: + # safely rename a temporary file with configuration dict + try: + dict_file = Path("{}.temp".format(config_dict_path)) + dict_file.rename(Path(config_dict_path)) + except Exception as err: + print("Unable to rename the file with keepalived config for FIFO pipe: {}".format(err)) + if not vyos.keepalived.vrrp_running(): print("Starting the VRRP process") - ret = subprocess.call("sudo systemctl restart keepalived.service", shell=True) + ret = call("sudo systemctl restart keepalived.service") else: print("Reloading the VRRP process") - ret = subprocess.call("sudo systemctl reload keepalived.service", shell=True) + ret = call("sudo systemctl reload keepalived.service") if ret != 0: raise ConfigError("keepalived failed to start") else: # VRRP is removed in the commit print("Stopping the VRRP process") - subprocess.call("sudo systemctl stop keepalived.service", shell=True) + call("sudo systemctl stop keepalived.service") os.unlink(config_file) return None @@ -359,4 +266,4 @@ if __name__ == '__main__': apply(c) except ConfigError as e: print("VRRP error: {0}".format(str(e))) - sys.exit(1) + exit(1) diff --git a/src/conf_mode/vyos_cert.py b/src/conf_mode/vyos_cert.py index 4a44573ca..8b8953cb7 100755 --- a/src/conf_mode/vyos_cert.py +++ b/src/conf_mode/vyos_cert.py @@ -18,7 +18,6 @@ import sys import os -import subprocess import tempfile import pathlib import ssl @@ -26,6 +25,7 @@ import ssl import vyos.defaults from vyos.config import Config from vyos import ConfigError +from vyos.util import cmd vyos_conf_scripts_dir = vyos.defaults.directories['conf_mode'] @@ -49,16 +49,16 @@ def status_self_signed(cert_data): # check if certificate is 1/2 past lifetime, with openssl -checkend end_days = int(cert_data['lifetime']) end_seconds = int(0.5*60*60*24*end_days) - checkend_cmd = ('openssl x509 -checkend {end} -noout -in {crt}' - ''.format(end=end_seconds, **cert_data)) + checkend_cmd = 'openssl x509 -checkend {end} -noout -in {crt}'.format(end=end_seconds, **cert_data) try: - subprocess.check_call(checkend_cmd, shell=True) + cmd(checkend_cmd, message='Called process error') return True - except subprocess.CalledProcessError as err: - if err.returncode == 1: + except OSError as err: + if err.errno == 1: return False - else: - print("Called process error: {}.".format(err)) + print(err) + # XXX: This seems wrong to continue on failure + # implicitely returning None def generate_self_signed(cert_data): san_config = None @@ -86,9 +86,10 @@ def generate_self_signed(cert_data): ''.format(**cert_data)) try: - subprocess.check_call(openssl_req_cmd, shell=True) - except subprocess.CalledProcessError as err: - print("Called process error: {}.".format(err)) + cmd(openssl_req_cmd, message='Called process error') + except OSError as err: + print(err) + # XXX: seems wrong to ignore the failure os.chmod('{key}'.format(**cert_data), 0o400) @@ -126,11 +127,8 @@ def generate(vyos_cert): def apply(vyos_cert): for dep in dependencies: - cmd = '{0}/{1}'.format(vyos_conf_scripts_dir, dep) - try: - subprocess.check_call(cmd, shell=True) - except subprocess.CalledProcessError as err: - raise ConfigError("{}.".format(err)) + command = '{0}/{1}'.format(vyos_conf_scripts_dir, dep) + cmd(command, raising=ConfigError) if __name__ == '__main__': try: diff --git a/src/etc/init.d/isc-dhcpv4-server b/src/etc/init.d/isc-dhcpv4-server index 377634a13..94a1020ac 100755 --- a/src/etc/init.d/isc-dhcpv4-server +++ b/src/etc/init.d/isc-dhcpv4-server @@ -72,7 +72,7 @@ case "$1" in start) test_config log_daemon_msg "Starting $DESC" "$NAME" - start-stop-daemon --start --quiet --pidfile "$DHCPD_PID" \ + start-stop-daemon --start --oknodo --quiet --pidfile "$DHCPD_PID" \ --exec /usr/sbin/dhcpd -- \ -q $OPTIONS -cf "$DHCPD_CONF" -pf "$DHCPD_PID" $INTERFACES sleep 2 @@ -87,7 +87,7 @@ case "$1" in ;; stop) log_daemon_msg "Stopping $DESC" "$NAME" - start-stop-daemon --stop --quiet --pidfile "$DHCPD_PID" + start-stop-daemon --stop --oknodo --quiet --pidfile "$DHCPD_PID" log_end_msg $? rm -f "$DHCPD_PID" ;; diff --git a/src/etc/init.d/isc-dhcpv6-relay b/src/etc/init.d/isc-dhcpv6-relay index 5a8ce620c..e553eafd1 100755 --- a/src/etc/init.d/isc-dhcpv6-relay +++ b/src/etc/init.d/isc-dhcpv6-relay @@ -31,11 +31,11 @@ DHCRELAYPID=/var/run/dhcv6relay.pid case "$1" in start) - start-stop-daemon --start --quiet --pidfile $DHCRELAYPID \ + start-stop-daemon --start --oknodo --quiet --pidfile $DHCRELAYPID \ --exec /usr/sbin/dhcrelay -- -q $OPTIONS -pf $DHCRELAYPID ;; stop) - start-stop-daemon --stop --quiet --pidfile $DHCRELAYPID + start-stop-daemon --stop --oknodo --quiet --pidfile $DHCRELAYPID ;; restart | force-reload) $0 stop diff --git a/src/etc/init.d/isc-dhcpv6-server b/src/etc/init.d/isc-dhcpv6-server index 55f59b68e..f6b27cb4a 100755 --- a/src/etc/init.d/isc-dhcpv6-server +++ b/src/etc/init.d/isc-dhcpv6-server @@ -72,7 +72,7 @@ case "$1" in start) test_config log_daemon_msg "Starting $DESC" "$NAME" - start-stop-daemon --start --quiet --pidfile "$DHCPD_PID" \ + start-stop-daemon --start --oknodo --quiet --pidfile "$DHCPD_PID" \ --exec /usr/sbin/dhcpd -- \ -q $OPTIONS -cf "$DHCPD_CONF" -pf "$DHCPD_PID" $INTERFACES sleep 2 @@ -87,7 +87,7 @@ case "$1" in ;; stop) log_daemon_msg "Stopping $DESC" "$NAME" - start-stop-daemon --stop --quiet --pidfile "$DHCPD_PID" + start-stop-daemon --stop --oknodo --quiet --pidfile "$DHCPD_PID" log_end_msg $? rm -f "$DHCPD_PID" ;; diff --git a/src/etc/ppp/ip-pre-up b/src/etc/ppp/ip-pre-up new file mode 100755 index 000000000..05840650b --- /dev/null +++ b/src/etc/ppp/ip-pre-up @@ -0,0 +1,51 @@ +#!/bin/sh +# +# This script is run by the pppd when the link is created. +# It uses run-parts to run scripts in /etc/ppp/ip-pre-up.d, to +# change name, setup firewall,etc you should create script(s) there. +# +# Be aware that other packages may include /etc/ppp/ip-pre-up.d scripts (named +# after that package), so choose local script names with that in mind. +# +# This script is called with the following arguments: +# Arg Name Example +# $1 Interface name ppp0 +# $2 The tty ttyS1 +# $3 The link speed 38400 +# $4 Local IP number 12.34.56.78 +# $5 Peer IP number 12.34.56.99 +# $6 Optional ``ipparam'' value foo + +# The environment is cleared before executing this script +# so the path must be reset +PATH=/usr/local/sbin:/usr/sbin:/sbin:/usr/local/bin:/usr/bin:/bin +export PATH + +# These variables are for the use of the scripts run by run-parts +PPP_IFACE="$1" +PPP_TTY="$2" +PPP_SPEED="$3" +PPP_LOCAL="$4" +PPP_REMOTE="$5" +PPP_IPPARAM="$6" +export PPP_IFACE PPP_TTY PPP_SPEED PPP_LOCAL PPP_REMOTE PPP_IPPARAM + +# as an additional convenience, $PPP_TTYNAME is set to the tty name, +# stripped of /dev/ (if present) for easier matching. +PPP_TTYNAME=`/usr/bin/basename "$2"` +export PPP_TTYNAME + +# If /var/log/ppp-ipupdown.log exists use it for logging. +if [ -e /var/log/ppp-ipupdown.log ]; then + exec > /var/log/ppp-ipupdown.log 2>&1 + echo $0 $* + echo +fi + +# This script can be used to override the .d files supplied by other packages. +if [ -x /etc/ppp/ip-pre-up.local ]; then + exec /etc/ppp/ip-pre-up.local "$*" +fi + +run-parts /etc/ppp/ip-pre-up.d \ + --arg="$1" --arg="$2" --arg="$3" --arg="$4" --arg="$5" --arg="$6" diff --git a/src/etc/rsyslog.d/01-auth.conf b/src/etc/rsyslog.d/01-auth.conf new file mode 100644 index 000000000..cc64099d6 --- /dev/null +++ b/src/etc/rsyslog.d/01-auth.conf @@ -0,0 +1,14 @@ +# The lines below cause all listed daemons/processes to be logged into +# /var/log/auth.log, then drops the message so it does not also go to the +# regular syslog so that messages are not duplicated + +$outchannel auth_log,/var/log/auth.log +if $programname == 'CRON' or + $programname == 'sudo' or + $programname == 'su' + then :omfile:$auth_log + +if $programname == 'CRON' or + $programname == 'sudo' or + $programname == 'su' + then stop diff --git a/src/etc/udev/rules.d/99-vyos-wwan.rules b/src/etc/udev/rules.d/99-vyos-wwan.rules new file mode 100644 index 000000000..67f30a3dd --- /dev/null +++ b/src/etc/udev/rules.d/99-vyos-wwan.rules @@ -0,0 +1,11 @@ +ACTION!="add|change", GOTO="mbim_to_qmi_rules_end" + +SUBSYSTEM!="usb", GOTO="mbim_to_qmi_rules_end" + +# ignore any device with only one configuration +ATTR{bNumConfigurations}=="1", GOTO="mbim_to_qmi_rules_end" + +# force Sierra Wireless MC7710 to configuration #1 +ATTR{idVendor}=="1199",ATTR{idProduct}=="68a2",ATTR{bConfigurationValue}="1" + +LABEL="mbim_to_qmi_rules_end" diff --git a/src/etc/vmware-tools/scripts/resume-vm-default.d/ether-resume.py b/src/etc/vmware-tools/scripts/resume-vm-default.d/ether-resume.py index 72da317f5..dc751c45c 100755 --- a/src/etc/vmware-tools/scripts/resume-vm-default.d/ether-resume.py +++ b/src/etc/vmware-tools/scripts/resume-vm-default.d/ether-resume.py @@ -15,11 +15,12 @@ # along with this program. If not, see <http://www.gnu.org/licenses/>. import sys -import subprocess import syslog as sl from vyos.config import Config -from vyos.util import vyos +from vyos import ConfigError +from vyos.util import run + def get_config(): c = Config() @@ -28,7 +29,7 @@ def get_config(): # skip interfaces that are disabled or is configured for dhcp check_disable = "interfaces ethernet {} disable".format(intf) check_dhcp = "interfaces ethernet {} address dhcp".format(intf) - if c.exists_effective(check_disable) or c.exists_effective(check_dhcp): + if c.exists_effective(check_disable): continue # get addresses configured on the interface @@ -38,23 +39,28 @@ def get_config(): interfaces[intf] = [addr.strip("'") for addr in intf_addresses] return interfaces + def apply(config): for intf, addresses in config.items(): # bring the interface up cmd = ["ip", "link", "set", "dev", intf, "up"] sl.syslog(sl.LOG_NOTICE, " ".join(cmd)) - subprocess.call(cmd) + run(cmd) # add configured addresses to interface for addr in addresses: - cmd = ["ip", "address", "add", addr, "dev", intf] + if addr == "dhcp": + cmd = ["dhclient", intf] + else: + cmd = ["ip", "address", "add", addr, "dev", intf] sl.syslog(sl.LOG_NOTICE, " ".join(cmd)) - subprocess.call(cmd) + run(cmd) + if __name__ == '__main__': try: config = get_config() apply(config) - except vyos.ConfigError as e: + except ConfigError as e: print(e) sys.exit(1) diff --git a/src/helpers/run-config-migration.py b/src/helpers/run-config-migration.py index a57a19cdf..cc7166c22 100755 --- a/src/helpers/run-config-migration.py +++ b/src/helpers/run-config-migration.py @@ -19,7 +19,8 @@ import os import sys import argparse import datetime -import subprocess + +from vyos.util import cmd from vyos.migrator import Migrator, VirtualMigrator def main(): @@ -61,23 +62,25 @@ def main(): '{0:%Y-%m-%d-%H%M%S}'.format(datetime.datetime.now()), 'pre-migration']) - try: - subprocess.check_call(['cp', '-p', config_file_name, - backup_file_name]) - except subprocess.CalledProcessError as err: - print("Called process error: {}.".format(err)) - sys.exit(1) + cmd(f'cp -p {config_file_name} {backup_file_name}') if not virtual: - migration = Migrator(config_file_name, force=force_on, - set_vintage=vintage) + virtual_migration = VirtualMigrator(config_file_name) + virtual_migration.run() + + migration = Migrator(config_file_name, force=force_on) + migration.run() + + if not migration.config_changed(): + os.remove(backup_file_name) else: - migration = VirtualMigrator(config_file_name) + virtual_migration = VirtualMigrator(config_file_name, + set_vintage=vintage) - migration.run() + virtual_migration.run() - if not migration._changed: - os.remove(backup_file_name) + if not virtual_migration.config_changed(): + os.remove(backup_file_name) if __name__ == '__main__': main() diff --git a/src/helpers/validate-value.py b/src/helpers/validate-value.py index 36f996d38..a58ba61d1 100755 --- a/src/helpers/validate-value.py +++ b/src/helpers/validate-value.py @@ -5,6 +5,8 @@ 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') @@ -31,7 +33,7 @@ try: cmd = "{0} {1}".format(cmd, args.value) if debug: print(cmd) - res = os.system(cmd) + res = call(cmd) if res == 0: sys.exit(0) except Exception as exn: diff --git a/src/helpers/vyos-boot-config-loader.py b/src/helpers/vyos-boot-config-loader.py index 7c81a4c3c..c5bf22f10 100755 --- a/src/helpers/vyos-boot-config-loader.py +++ b/src/helpers/vyos-boot-config-loader.py @@ -20,19 +20,21 @@ import os import sys import pwd import grp -import subprocess import traceback from datetime import datetime from vyos.defaults import directories from vyos.configsession import ConfigSession, ConfigSessionError from vyos.configtree import ConfigTree +from vyos.util import cmd STATUS_FILE = '/tmp/vyos-config-status' TRACE_FILE = '/tmp/boot-config-trace' CFG_GROUP = 'vyattacfg' +trace_config = False + if 'log' in directories: LOG_DIR = directories['log'] else: @@ -45,6 +47,9 @@ try: cmdline = f.read() if 'vyos-debug' in cmdline: os.environ['VYOS_DEBUG'] = 'yes' + if 'vyos-config-debug' in cmdline: + os.environ['VYOS_DEBUG'] = 'yes' + trace_config = True except Exception as e: print('{0}'.format(e)) @@ -97,12 +102,7 @@ def failsafe(config_file_name): 'authentication', 'encrypted-password']) - cmd = ("useradd -s /bin/bash -G 'users,sudo' -m -N -p '{0}' " - "vyos".format(passwd)) - try: - subprocess.check_call(cmd, shell=True) - except subprocess.CalledProcessError as e: - sys.exit("{0}".format(e)) + cmd(f"useradd -s /bin/bash -G 'users,sudo' -m -N -p '{passwd}' vyos") if __name__ == '__main__': if len(sys.argv) < 2: @@ -130,8 +130,9 @@ if __name__ == '__main__': config_file = f.read() except Exception: write_config_status(1) - failsafe(default_file_name) - trace_to_file(TRACE_FILE) + if trace_config: + failsafe(default_file_name) + trace_to_file(TRACE_FILE) sys.exit(1) try: @@ -146,8 +147,9 @@ if __name__ == '__main__': # If here, there is no use doing session.discard, as we have no # recoverable config environment, and will only throw an error write_config_status(1) - failsafe(default_file_name) - trace_to_file(TRACE_FILE) + if trace_config: + failsafe(default_file_name) + trace_to_file(TRACE_FILE) sys.exit(1) time_elapsed_load = time_end_load - time_begin_load diff --git a/src/helpers/vyos-bridge-sync.py b/src/helpers/vyos-bridge-sync.py index 495eb5d40..097d28d85 100755 --- a/src/helpers/vyos-bridge-sync.py +++ b/src/helpers/vyos-bridge-sync.py @@ -21,16 +21,12 @@ # to the bridge automatically once it's available import argparse -import subprocess - from sys import exit from time import sleep + from vyos.config import Config +from vyos.util import cmd, run -def subprocess_cmd(command): - process = subprocess.Popen(command,stdout=subprocess.PIPE, shell=True) - proc_stdout = process.communicate()[0].strip() - pass if __name__ == '__main__': parser = argparse.ArgumentParser() @@ -45,9 +41,11 @@ if __name__ == '__main__': for bridge in conf.list_nodes('interfaces bridge'): for member_if in conf.list_nodes('interfaces bridge {} member interface'.format(bridge)): if args.interface == member_if: - cmd = 'brctl addif "{}" "{}"'.format(bridge, args.interface) + command = 'brctl addif "{}" "{}"'.format(bridge, args.interface) # let interfaces etc. settle - especially required for OpenVPN bridged interfaces sleep(4) - subprocess_cmd(cmd) + # XXX: This is ignoring any issue, should be cmd but kept as it + # XXX: during the migration to not cause any regression + run(command) exit(0) diff --git a/src/helpers/vyos-load-config.py b/src/helpers/vyos-load-config.py index 4e6d67efa..a9fa15778 100755 --- a/src/helpers/vyos-load-config.py +++ b/src/helpers/vyos-load-config.py @@ -28,27 +28,21 @@ import tempfile import vyos.defaults import vyos.remote from vyos.config import Config, VyOSError -from vyos.migrator import Migrator, MigratorError - -system_config_file = 'config.boot' +from vyos.migrator import Migrator, VirtualMigrator, MigratorError class LoadConfig(Config): """A subclass for calling 'loadFile'. This does not belong in config.py, and only has a single caller. """ - def load_config(self, file_path): - cmd = [self._cli_shell_api, 'loadFile', file_path] - self._run(cmd) + def load_config(self, path): + return self._run(['/bin/cli-shell-api','loadFile',path]) -if len(sys.argv) > 1: - file_name = sys.argv[1] -else: - file_name = system_config_file +file_name = sys.argv[1] if len(sys.argv) > 1 else 'config.boot' configdir = vyos.defaults.directories['config'] - protocols = ['scp', 'sftp', 'http', 'https', 'ftp', 'tftp'] + if any(x in file_name for x in protocols): config_file = vyos.remote.get_remote_config(file_name) if not config_file: @@ -73,6 +67,12 @@ with tempfile.NamedTemporaryFile() as fp: with open(fp.name, 'w') as fd: fd.write(config_file) + virtual_migration = VirtualMigrator(fp.name) + try: + virtual_migration.run() + except MigratorError as err: + sys.exit('{}'.format(err)) + migration = Migrator(fp.name) try: migration.run() diff --git a/src/helpers/vyos-merge-config.py b/src/helpers/vyos-merge-config.py index 7ae62cfb3..14df2734b 100755 --- a/src/helpers/vyos-merge-config.py +++ b/src/helpers/vyos-merge-config.py @@ -17,13 +17,13 @@ import sys import os -import subprocess import tempfile import vyos.defaults import vyos.remote -import vyos.migrator from vyos.config import Config from vyos.configtree import ConfigTree +from vyos.migrator import Migrator, VirtualMigrator +from vyos.util import cmd, DEVNULL if (len(sys.argv) < 2): @@ -61,19 +61,20 @@ with tempfile.NamedTemporaryFile() as file_to_migrate: with open(file_to_migrate.name, 'w') as fd: fd.write(config_file) - migration = vyos.migrator.Migrator(file_to_migrate.name) + virtual_migration = VirtualMigrator(file_to_migrate.name) + virtual_migration.run() + + migration = Migrator(file_to_migrate.name) migration.run() - if migration.config_changed(): + + if virtual_migration.config_changed() or migration.config_changed(): with open(file_to_migrate.name, 'r') as fd: config_file = fd.read() merge_config_tree = ConfigTree(config_file) effective_config = Config() - -output_effective_config = effective_config.show_config() - -effective_config_tree = ConfigTree(output_effective_config) +effective_config_tree = effective_config._running_config effective_cmds = effective_config_tree.to_commands() merge_cmds = merge_config_tree.to_commands() @@ -98,13 +99,11 @@ if (len(sys.argv) > 2): if path: add_cmds = [ cmd for cmd in add_cmds if path in cmd ] -for cmd in add_cmds: - cmd = "/opt/vyatta/sbin/my_" + cmd - +for add in add_cmds: try: - subprocess.check_call(cmd, shell=True) - except subprocess.CalledProcessError as err: - print("Called process error: {}.".format(err)) + cmd(f'/opt/vyatta/sbin/my_{add}', shell=True, stderr=DEVNULL) + except OSError as err: + print(err) if effective_config.session_changed(): print("Merge complete. Use 'commit' to make changes effective.") diff --git a/src/migration-scripts/dns-forwarding/1-to-2 b/src/migration-scripts/dns-forwarding/1-to-2 index 31ba5573f..9a50b6aa3 100755 --- a/src/migration-scripts/dns-forwarding/1-to-2 +++ b/src/migration-scripts/dns-forwarding/1-to-2 @@ -23,8 +23,8 @@ import sys from ipaddress import ip_interface +from vyos.ifconfig import Interface from vyos.configtree import ConfigTree -from vyos.interfaces import get_type_of_interface if (len(sys.argv) < 1): print("Must specify file name!") @@ -41,7 +41,10 @@ base = ['service', 'dns', 'forwarding'] if not config.exists(base): # Nothing to do sys.exit(0) + else: + # XXX: we can remove the else and un-indent this whole block + if config.exists(base + ['listen-on']): listen_intf = config.return_values(base + ['listen-on']) # Delete node with abandoned command @@ -60,7 +63,10 @@ else: # this is a QinQ VLAN interface intf = intf.split('.')[0] + ' vif-s ' + intf.split('.')[1] + ' vif-c ' + intf.split('.')[2] - path = ['interfaces', get_type_of_interface(intf), intf, 'address'] + section = Interface.section(intf) + if not section: + raise ValueError(f'Invalid interface name {intf}') + path = ['interfaces', section, intf, 'address'] # retrieve corresponding interface addresses in CIDR format # those need to be converted in pure IP addresses without network information diff --git a/src/migration-scripts/https/0-to-1 b/src/migration-scripts/https/0-to-1 new file mode 100755 index 000000000..23809f5ad --- /dev/null +++ b/src/migration-scripts/https/0-to-1 @@ -0,0 +1,69 @@ +#!/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/>. + +# * Move server block directives under 'virtual-host' tag node, instead of +# relying on 'listen-address' tag node + +import sys + +from vyos.configtree import ConfigTree + +if (len(sys.argv) < 2): + print("Must specify file name!") + sys.exit(1) + +file_name = sys.argv[1] + +with open(file_name, 'r') as f: + config_file = f.read() + +config = ConfigTree(config_file) + +old_base = ['service', 'https', 'listen-address'] +if not config.exists(old_base): + # Nothing to do + sys.exit(0) +else: + new_base = ['service', 'https', 'virtual-host'] + config.set(new_base) + config.set_tag(new_base) + + index = 0 + for addr in config.list_nodes(old_base): + tag_name = f'vhost{index}' + config.set(new_base + [tag_name]) + config.set(new_base + [tag_name, 'listen-address'], value=addr) + + if config.exists(old_base + [addr, 'listen-port']): + port = config.return_value(old_base + [addr, 'listen-port']) + config.set(new_base + [tag_name, 'listen-port'], value=port) + + if config.exists(old_base + [addr, 'server-name']): + names = config.return_values(old_base + [addr, 'server-name']) + for name in names: + config.set(new_base + [tag_name, 'server-name'], value=name, + replace=False) + + index += 1 + + config.delete(old_base) + + try: + with open(file_name, 'w') as f: + f.write(config.to_string()) + except OSError as e: + print("Failed to save the modified config: {}".format(e)) + sys.exit(1) diff --git a/src/migration-scripts/https/1-to-2 b/src/migration-scripts/https/1-to-2 new file mode 100755 index 000000000..b1cf37ea6 --- /dev/null +++ b/src/migration-scripts/https/1-to-2 @@ -0,0 +1,54 @@ +#!/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/>. + +# * Move 'api virtual-host' list to 'api-restrict virtual-host' so it +# is owned by https.py instead of http-api.py + +import sys + +from vyos.configtree import ConfigTree + +if (len(sys.argv) < 2): + print("Must specify file name!") + sys.exit(1) + +file_name = sys.argv[1] + +with open(file_name, 'r') as f: + config_file = f.read() + +config = ConfigTree(config_file) + +old_base = ['service', 'https', 'api', 'virtual-host'] +if not config.exists(old_base): + # Nothing to do + sys.exit(0) +else: + new_base = ['service', 'https', 'api-restrict', 'virtual-host'] + config.set(new_base) + + names = config.return_values(old_base) + for name in names: + config.set(new_base, value=name, replace=False) + + config.delete(old_base) + + try: + with open(file_name, 'w') as f: + f.write(config.to_string()) + except OSError as e: + print("Failed to save the modified config: {}".format(e)) + sys.exit(1) diff --git a/src/migration-scripts/interfaces/5-to-6 b/src/migration-scripts/interfaces/5-to-6 new file mode 100755 index 000000000..85a1994c6 --- /dev/null +++ b/src/migration-scripts/interfaces/5-to-6 @@ -0,0 +1,119 @@ +#!/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/>. + +# Migrate IPv6 router advertisments from a nested interface configuration to +# a denested "service router-advert" + +import sys +from vyos.configtree import ConfigTree + +def copy_rtradv(c, old_base, interface): + base = ['service', 'router-advert', 'interface'] + + if c.exists(old_base): + if not c.exists(base): + c.set(base) + c.set_tag(base) + + # take the old node as a whole and copy it to new new path, + # additional migrations will be done afterwards + new_base = base + [interface] + c.copy(old_base, new_base) + c.delete(old_base) + + # cur-hop-limit has been renamed to hop-limit + if c.exists(new_base + ['cur-hop-limit']): + c.rename(new_base + ['cur-hop-limit'], 'hop-limit') + + bool_cleanup = ['managed-flag', 'other-config-flag'] + for bool in bool_cleanup: + if c.exists(new_base + [bool]): + tmp = c.return_value(new_base + [bool]) + c.delete(new_base + [bool]) + if tmp == 'true': + c.set(new_base + [bool]) + + # max/min interval moved to subnode + intervals = ['max-interval', 'min-interval'] + for interval in intervals: + if c.exists(new_base + [interval]): + tmp = c.return_value(new_base + [interval]) + c.delete(new_base + [interval]) + min_max = interval.split('-')[0] + c.set(new_base + ['interval', min_max], value=tmp) + + # cleanup boolean nodes in individual prefix + prefix_base = new_base + ['prefix'] + if c.exists(prefix_base): + for prefix in config.list_nodes(prefix_base): + bool_cleanup = ['autonomous-flag', 'on-link-flag'] + for bool in bool_cleanup: + if c.exists(prefix_base + [prefix, bool]): + tmp = c.return_value(prefix_base + [prefix, bool]) + c.delete(prefix_base + [prefix, bool]) + if tmp == 'true': + c.set(prefix_base + [prefix, bool]) + + # router advertisement can be individually disabled per interface + # the node has been renamed from send-advert {true | false} to no-send-advert + if c.exists(new_base + ['send-advert']): + tmp = c.return_value(new_base + ['send-advert']) + c.delete(new_base + ['send-advert']) + if tmp == 'false': + c.set(new_base + ['no-send-advert']) + + # link-mtu advertisement was formerly disabled by setting its value to 0 + # ... this makes less sense - if it should not be send, just do not + # configure it + if c.exists(new_base + ['link-mtu']): + tmp = c.return_value(new_base + ['link-mtu']) + if tmp == '0': + c.delete(new_base + ['link-mtu']) + +if __name__ == '__main__': + if (len(sys.argv) < 1): + print("Must specify file name!") + exit(1) + + file_name = sys.argv[1] + with open(file_name, 'r') as f: + config_file = f.read() + + config = ConfigTree(config_file) + + # list all individual interface types like dummy, ethernet and so on + for if_type in config.list_nodes(['interfaces']): + base_if_type = ['interfaces', if_type] + + # for every individual interface we need to check if there is an + # ipv6 ra configured ... and also for every VIF (VLAN) interface + for intf in config.list_nodes(base_if_type): + old_base = base_if_type + [intf, 'ipv6', 'router-advert'] + copy_rtradv(config, old_base, intf) + + vif_base = base_if_type + [intf, 'vif'] + if config.exists(vif_base): + for vif in config.list_nodes(vif_base): + old_base = vif_base + [vif, 'ipv6', 'router-advert'] + vlan_name = f'{intf}.{vif}' + copy_rtradv(config, old_base, vlan_name) + + try: + with open(file_name, 'w') as f: + f.write(config.to_string()) + except OSError as e: + print("Failed to save the modified config: {}".format(e)) + sys.exit(1) diff --git a/src/migration-scripts/interfaces/6-to-7 b/src/migration-scripts/interfaces/6-to-7 new file mode 100755 index 000000000..220c7e601 --- /dev/null +++ b/src/migration-scripts/interfaces/6-to-7 @@ -0,0 +1,63 @@ +#!/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/>. + +# Remove network provider name from CLI and rather use provider APN from CLI + +import sys +from vyos.configtree import ConfigTree + +if __name__ == '__main__': + if (len(sys.argv) < 1): + print("Must specify file name!") + exit(1) + + file_name = sys.argv[1] + with open(file_name, 'r') as f: + config_file = f.read() + + config = ConfigTree(config_file) + base = ['interfaces', 'wirelessmodem'] + + if not config.exists(base): + # Nothing to do + sys.exit(0) + + # list all individual wwan/wireless modem interfaces + for i in config.list_nodes(base): + iface = base + [i] + + # only three carries have been supported in the past, thus + # this will be fairly simple \o/ - and only one (AT&T) did + # configure an APN + if config.exists(iface + ['network']): + network = config.return_value(iface + ['network']) + if network == "att": + apn = 'isp.cingular' + config.set(iface + ['apn'], value=apn) + + config.delete(iface + ['network']) + + # synchronize DNS configuration with PPPoE interfaces to have a + # uniform CLI experience + if config.exists(iface + ['no-dns']): + config.rename(iface + ['no-dns'], 'no-peer-dns') + + try: + with open(file_name, 'w') as f: + f.write(config.to_string()) + except OSError as e: + print("Failed to save the modified config: {}".format(e)) + sys.exit(1) diff --git a/src/migration-scripts/interfaces/7-to-8 b/src/migration-scripts/interfaces/7-to-8 new file mode 100755 index 000000000..8830ffdc7 --- /dev/null +++ b/src/migration-scripts/interfaces/7-to-8 @@ -0,0 +1,59 @@ +#!/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/>. + +# Split WireGuard endpoint into address / port nodes to make use of common +# validators + +from sys import exit, argv +from vyos.configtree import ConfigTree + +if __name__ == '__main__': + 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 = ['interfaces', 'wireguard'] + + if not config.exists(base): + # Nothing to do + exit(0) + + # list all individual wireguard interface isntance + for i in config.list_nodes(base): + iface = base + [i] + for peer in config.list_nodes(iface + ['peer']): + base_peer = iface + ['peer', peer] + if config.exists(base_peer + ['endpoint']): + endpoint = config.return_value(base_peer + ['endpoint']) + address = endpoint.split(':')[0] + port = endpoint.split(':')[1] + # delete old node + config.delete(base_peer + ['endpoint']) + # setup new nodes + config.set(base_peer + ['address'], value=address) + config.set(base_peer + ['port'], value=port) + + 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/interfaces/8-to-9 b/src/migration-scripts/interfaces/8-to-9 new file mode 100755 index 000000000..e0b9dd375 --- /dev/null +++ b/src/migration-scripts/interfaces/8-to-9 @@ -0,0 +1,52 @@ +#!/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/>. + +# Rename link nodes to source-interface for the following interface types: +# - vxlan +# - pseudo ethernet + +from sys import exit, argv +from vyos.configtree import ConfigTree + +if __name__ == '__main__': + 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) + + for if_type in ['vxlan', 'pseudo-ethernet']: + base = ['interfaces', if_type] + if not config.exists(base): + # Nothing to do + exit(0) + + # list all individual interface isntance + for i in config.list_nodes(base): + iface = base + [i] + if config.exists(iface + ['link']): + config.rename(iface + ['link'], 'source-interface') + + try: + with open(file_name, 'w') as f: + f.write(config.to_string()) + except OSError as e: + print("Failed to save the modified config: {}".format(e)) + exit(1) diff --git a/src/migration-scripts/quagga/3-to-4 b/src/migration-scripts/quagga/3-to-4 index f8c87ce8c..be3528391 100755 --- a/src/migration-scripts/quagga/3-to-4 +++ b/src/migration-scripts/quagga/3-to-4 @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2019 VyOS maintainers and contributors +# 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 @@ -16,6 +16,13 @@ # # +# Between 1.2.3 and 1.2.4, FRR added per-neighbor enforce-first-as option. +# Unfortunately they also removed the global enforce-first-as option, +# which broke all old configs that used to have it. +# +# To emulate the effect of the original option, we insert it in every neighbor +# if the config used to have the original global option + import sys from vyos.configtree import ConfigTree @@ -45,11 +52,16 @@ else: # There's actually no BGP, just its empty shell sys.exit(0) - # Check if BGP scan-time parameter exist - scan_time_param = ['protocols', 'bgp', asn, 'parameters', 'scan-time'] - if config.exists(scan_time_param): - # Delete BGP scan-time parameter - config.delete(scan_time_param) + # Check if BGP enforce-first-as option is set + enforce_first_as_path = ['protocols', 'bgp', asn, 'parameters', 'enforce-first-as'] + if config.exists(enforce_first_as_path): + # Delete the obsolete option + config.delete(enforce_first_as_path) + + # Now insert it in every peer + peers = config.list_nodes(['protocols', 'bgp', asn, 'neighbor']) + for p in peers: + config.set(['protocols', 'bgp', asn, 'neighbor', p, 'enforce-first-as']) else: # Do nothing sys.exit(0) @@ -61,3 +73,4 @@ else: except OSError as e: print("Failed to save the modified config: {}".format(e)) sys.exit(1) + diff --git a/src/migration-scripts/quagga/4-to-5 b/src/migration-scripts/quagga/4-to-5 new file mode 100755 index 000000000..f8c87ce8c --- /dev/null +++ b/src/migration-scripts/quagga/4-to-5 @@ -0,0 +1,63 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2019 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. +# +# + +import sys + +from vyos.configtree import ConfigTree + + +if (len(sys.argv) < 1): + print("Must specify file name!") + sys.exit(1) + +file_name = sys.argv[1] + +with open(file_name, 'r') as f: + config_file = f.read() + +config = ConfigTree(config_file) + +if not config.exists(['protocols', 'bgp']): + # Nothing to do + sys.exit(0) +else: + # Check if BGP is actually configured and obtain the ASN + asn_list = config.list_nodes(['protocols', 'bgp']) + if asn_list: + # There's always just one BGP node, if any + asn = asn_list[0] + else: + # There's actually no BGP, just its empty shell + sys.exit(0) + + # Check if BGP scan-time parameter exist + scan_time_param = ['protocols', 'bgp', asn, 'parameters', 'scan-time'] + if config.exists(scan_time_param): + # Delete BGP scan-time parameter + config.delete(scan_time_param) + else: + # Do nothing + sys.exit(0) + + # Save a new configuration file + try: + with open(file_name, 'w') as f: + f.write(config.to_string()) + except OSError as e: + print("Failed to save the modified config: {}".format(e)) + sys.exit(1) diff --git a/src/migration-scripts/quagga/5-to-6 b/src/migration-scripts/quagga/5-to-6 new file mode 100755 index 000000000..a71851942 --- /dev/null +++ b/src/migration-scripts/quagga/5-to-6 @@ -0,0 +1,63 @@ +#!/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/>. + +# * Remove parameter 'disable-network-import-check' which, as implemented, +# had no effect on boot. + +import sys + +from vyos.configtree import ConfigTree + + +if (len(sys.argv) < 2): + print("Must specify file name!") + sys.exit(1) + +file_name = sys.argv[1] + +with open(file_name, 'r') as f: + config_file = f.read() + +config = ConfigTree(config_file) + +if not config.exists(['protocols', 'bgp']): + # Nothing to do + sys.exit(0) +else: + # Check if BGP is actually configured and obtain the ASN + asn_list = config.list_nodes(['protocols', 'bgp']) + if asn_list: + # There's always just one BGP node, if any + asn = asn_list[0] + else: + # There's actually no BGP, just its empty shell + sys.exit(0) + + # Check if BGP parameter disable-network-import-check exists + param = ['protocols', 'bgp', asn, 'parameters', 'disable-network-import-check'] + if config.exists(param): + # Delete parameter + config.delete(param) + else: + # Do nothing + sys.exit(0) + + try: + with open(file_name, 'w') as f: + f.write(config.to_string()) + except OSError as e: + print("Failed to save the modified config: {}".format(e)) + sys.exit(1) diff --git a/src/migration-scripts/sstp/0-to-1 b/src/migration-scripts/sstp/0-to-1 new file mode 100755 index 000000000..0e8dd1c4b --- /dev/null +++ b/src/migration-scripts/sstp/0-to-1 @@ -0,0 +1,130 @@ +#!/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/>. + + +# - migrate from "service sstp-server" to "vpn sstp" +# - remove primary/secondary identifier from nameserver +# - migrate RADIUS configuration to a more uniform syntax accross the system +# - authentication radius-server x.x.x.x to authentication radius server x.x.x.x +# - authentication radius-settings to authentication radius +# - do not migrate radius server req-limit, use default of unlimited +# - migrate SSL certificate path + +import os +import sys + +from vyos.configtree import ConfigTree + +if (len(sys.argv) < 1): + print("Must specify file name!") + sys.exit(1) + +file_name = sys.argv[1] + +with open(file_name, 'r') as f: + config_file = f.read() + +config = ConfigTree(config_file) +old_base = ['service', 'sstp-server'] +if not config.exists(old_base): + # Nothing to do + sys.exit(0) +else: + # ensure new base path exists + if not config.exists(['vpn']): + config.set(['vpn']) + + new_base = ['vpn', 'sstp'] + # copy entire tree + config.copy(old_base, new_base) + config.delete(old_base) + + # migrate DNS servers + dns_base = new_base + ['network-settings', 'dns-server'] + if config.exists(dns_base): + if config.exists(dns_base + ['primary-dns']): + dns = config.return_value(dns_base + ['primary-dns']) + config.set(new_base + ['network-settings', 'name-server'], value=dns, replace=False) + + if config.exists(dns_base + ['secondary-dns']): + dns = config.return_value(dns_base + ['secondary-dns']) + config.set(new_base + ['network-settings', 'name-server'], value=dns, replace=False) + + config.delete(dns_base) + + + # migrate radius options - copy subtree + # thus must happen before migration of the individual RADIUS servers + old_options = new_base + ['authentication', 'radius-settings'] + if config.exists(old_options): + new_options = new_base + ['authentication', 'radius'] + config.copy(old_options, new_options) + config.delete(old_options) + + # migrate radius dynamic author / change of authorisation server + dae_old = new_base + ['authentication', 'radius', 'dae-server'] + if config.exists(dae_old): + config.rename(dae_old, 'dynamic-author') + dae_new = new_base + ['authentication', 'radius', 'dynamic-author'] + + if config.exists(dae_new + ['ip-address']): + config.rename(dae_new + ['ip-address'], 'server') + + if config.exists(dae_new + ['secret']): + config.rename(dae_new + ['secret'], 'key') + + + # migrate radius server + radius_server = new_base + ['authentication', 'radius-server'] + if config.exists(radius_server): + for server in config.list_nodes(radius_server): + base = radius_server + [server] + new = new_base + ['authentication', 'radius', 'server', server] + + # convert secret to key + if config.exists(base + ['secret']): + tmp = config.return_value(base + ['secret']) + config.set(new + ['key'], value=tmp) + + if config.exists(base + ['fail-time']): + tmp = config.return_value(base + ['fail-time']) + config.set(new + ['fail-time'], value=tmp) + + config.set_tag(new_base + ['authentication', 'radius', 'server']) + config.delete(radius_server) + + # migrate SSL certificates + old_ssl = new_base + ['sstp-settings', 'ssl-certs'] + new_ssl = new_base + ['ssl'] + config.copy(old_ssl, new_ssl) + config.delete(old_ssl) + + if config.exists(new_ssl + ['ca']): + config.rename(new_ssl + ['ca'], 'ca-cert-file') + + if config.exists(new_ssl + ['server-cert']): + config.rename(new_ssl + ['server-cert'], 'cert-file') + + if config.exists(new_ssl + ['server-key']): + config.rename(new_ssl + ['server-key'], 'key-file') + + + try: + with open(file_name, 'w') as f: + f.write(config.to_string()) + except OSError as e: + print("Failed to save the modified config: {}".format(e)) + sys.exit(1) diff --git a/src/migration-scripts/sstp/1-to-2 b/src/migration-scripts/sstp/1-to-2 new file mode 100755 index 000000000..94cb04831 --- /dev/null +++ b/src/migration-scripts/sstp/1-to-2 @@ -0,0 +1,110 @@ +#!/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/>. + +# - migrate relative path SSL certificate to absolute path, as certs are only +# allowed to stored in /config/user-data/sstp/ this is pretty straight +# forward move. Delete certificates from source directory + +import os +import sys + +from shutil import copy2 +from stat import S_IRUSR, S_IWUSR, S_IRGRP, S_IROTH +from vyos.configtree import ConfigTree + +if (len(sys.argv) < 1): + print("Must specify file name!") + sys.exit(1) + +file_name = sys.argv[1] + +with open(file_name, 'r') as f: + config_file = f.read() + +config = ConfigTree(config_file) +base_path = ['vpn', 'sstp', 'ssl'] +if not config.exists(base_path): + # Nothing to do + sys.exit(0) +else: + cert_path_old ='/config/user-data/sstp/' + cert_path_new ='/config/auth/sstp/' + + if not os.path.isdir(cert_path_new): + os.mkdir(cert_path_new) + + # + # migrate ca-cert-file to new path + if config.exists(base_path + ['ca-cert-file']): + tmp = config.return_value(base_path + ['ca-cert-file']) + cert_old = cert_path_old + tmp + cert_new = cert_path_new + tmp + + if os.path.isfile(cert_old): + # adjust file permissions on source file, + # permissions will be copied by copy2() + os.chmod(cert_old, S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH) + copy2(cert_old, cert_path_new) + # delete old certificate file + os.unlink(cert_old) + + config.set(base_path + ['ca-cert-file'], value=cert_new, replace=True) + + # + # migrate cert-file to new path + if config.exists(base_path + ['cert-file']): + tmp = config.return_value(base_path + ['cert-file']) + cert_old = cert_path_old + tmp + cert_new = cert_path_new + tmp + + if os.path.isfile(cert_old): + # adjust file permissions on source file, + # permissions will be copied by copy2() + os.chmod(cert_old, S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH) + copy2(cert_old, cert_path_new) + # delete old certificate file + os.unlink(cert_old) + + config.set(base_path + ['cert-file'], value=cert_new, replace=True) + + # + # migrate key-file to new path + if config.exists(base_path + ['key-file']): + tmp = config.return_value(base_path + ['key-file']) + cert_old = cert_path_old + tmp + cert_new = cert_path_new + tmp + + if os.path.isfile(cert_old): + # adjust file permissions on source file, + # permissions will be copied by copy2() + os.chmod(cert_old, S_IRUSR | S_IWUSR) + copy2(cert_old, cert_path_new) + # delete old certificate file + os.unlink(cert_old) + + config.set(base_path + ['key-file'], value=cert_new, replace=True) + + # + # check if old certificate directory exists but is empty + if os.path.isdir(cert_path_old) and not os.listdir(cert_path_old): + os.rmdir(cert_path_old) + + try: + with open(file_name, 'w') as f: + f.write(config.to_string()) + except OSError as e: + print("Failed to save the modified config: {}".format(e)) + sys.exit(1) diff --git a/src/migration-scripts/system/12-to-13 b/src/migration-scripts/system/12-to-13 index 5f7413d46..5b068f4fc 100755 --- a/src/migration-scripts/system/12-to-13 +++ b/src/migration-scripts/system/12-to-13 @@ -12,12 +12,8 @@ import re import sys from vyos.configtree import ConfigTree -from subprocess import Popen, PIPE, STDOUT +from vyos.util import cmd -def _cmd(cmd): - p = Popen(cmd, stdout=PIPE, stderr=STDOUT, shell=True) - tmp = p.communicate()[0].strip() - return tmp.decode() if (len(sys.argv) < 1): print("Must specify file name!") @@ -37,8 +33,11 @@ else: tz = config.return_value(tz_base) # retrieve all valid timezones - 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') + try: + tz_datas = cmd('find /usr/share/zoneinfo/posix -type f -or -type l | sed -e s:/usr/share/zoneinfo/posix/::') + except OSError: + tz_datas = '' + tz_data = tz_datas.split('\n') if re.match(r'[Ll][Oo][Ss].+', tz): tz = 'America/Los_Angeles' diff --git a/src/op_mode/clear_conntrack.py b/src/op_mode/clear_conntrack.py index 0e52b9086..423694187 100755 --- a/src/op_mode/clear_conntrack.py +++ b/src/op_mode/clear_conntrack.py @@ -14,13 +14,13 @@ # 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 subprocess import sys from vyos.util import ask_yes_no +from vyos.util import cmd, DEVNULL if not ask_yes_no("This will clear all currently tracked and expected connections. Continue?"): sys.exit(1) else: - subprocess.check_call(['/usr/sbin/conntrack -F'], shell=True, stderr=subprocess.DEVNULL) - subprocess.check_call(['/usr/sbin/conntrack -F expect'], shell=True, stderr=subprocess.DEVNULL) + cmd('/usr/sbin/conntrack -F', stderr=DEVNULL) + cmd('/usr/sbin/conntrack -F expect', stderr=DEVNULL) diff --git a/src/op_mode/connect_disconnect.py b/src/op_mode/connect_disconnect.py index a22615096..b191f630d 100755 --- a/src/op_mode/connect_disconnect.py +++ b/src/op_mode/connect_disconnect.py @@ -21,6 +21,9 @@ from sys import exit from psutil import process_iter from time import strftime, localtime, time +from vyos.util import call + + PPP_LOGFILE = '/var/log/vyatta/ppp_{}.log' def check_interface(interface): @@ -56,8 +59,7 @@ def connect(interface): tm = strftime("%a %d %b %Y %I:%M:%S %p %Z", localtime(time())) with open(PPP_LOGFILE.format(interface), 'a') as f: f.write('{}: user {} started PPP daemon for {} by connect command\n'.format(tm, user, interface)) - cmd = 'umask 0; setsid sh -c "nohup /usr/sbin/pppd call {0} > /tmp/{0}.log 2>&1 &"'.format(interface) - os.system(cmd) + call('umask 0; setsid sh -c "nohup /usr/sbin/pppd call {0} > /tmp/{0}.log 2>&1 &"'.format(interface)) def disconnect(interface): @@ -75,8 +77,7 @@ def disconnect(interface): tm = strftime("%a %d %b %Y %I:%M:%S %p %Z", localtime(time())) with open(PPP_LOGFILE.format(interface), 'a') as f: f.write('{}: user {} stopped PPP daemon for {} by disconnect command\n'.format(tm, user, interface)) - cmd = '/usr/bin/poff "{}"'.format(interface) - os.system(cmd) + call('/usr/bin/poff "{}"'.format(interface)) def main(): parser = argparse.ArgumentParser() diff --git a/src/op_mode/dns_forwarding_reset.py b/src/op_mode/dns_forwarding_reset.py index da4fba3a2..8e2ee546c 100755 --- a/src/op_mode/dns_forwarding_reset.py +++ b/src/op_mode/dns_forwarding_reset.py @@ -21,10 +21,11 @@ import os -import sys import argparse -import vyos.config +from sys import exit +from vyos.config import Config +from vyos.util import call parser = argparse.ArgumentParser() parser.add_argument("-a", "--all", action="store_true", help="Reset all cache") @@ -34,16 +35,18 @@ if __name__ == '__main__': args = parser.parse_args() # Do nothing if service is not configured - c = vyos.config.Config() - if not c.exists_effective('service dns forwarding'): + c = Config() + if not c.exists_effective(['service', 'dns', 'forwarding']): print("DNS forwarding is not configured") - sys.exit(0) + exit(0) if args.all: - os.system("rec_control wipe-cache \'.$\'") - sys.exit(1) + call("rec_control wipe-cache \'.$\'") + exit(0) + elif args.domain: - os.system("rec_control wipe-cache \'{0}$\'".format(args.domain)) + call("rec_control wipe-cache \'{0}$\'".format(args.domain)) + else: parser.print_help() - sys.exit(1) + exit(1) diff --git a/src/op_mode/dns_forwarding_statistics.py b/src/op_mode/dns_forwarding_statistics.py index f626244a8..c400a72cd 100755 --- a/src/op_mode/dns_forwarding_statistics.py +++ b/src/op_mode/dns_forwarding_statistics.py @@ -1,10 +1,10 @@ #!/usr/bin/env python3 -import subprocess import jinja2 import sys from vyos.config import Config +from vyos.config import cmd PDNS_CMD='/usr/bin/rec_control' @@ -26,8 +26,8 @@ if __name__ == '__main__': data = {} - data['cache_entries'] = subprocess.check_output([PDNS_CMD, 'get cache-entries']).decode() - data['cache_size'] = "{0:.2f}".format( int(subprocess.check_output([PDNS_CMD, 'get cache-bytes']).decode()) / 1024 ) + data['cache_entries'] = cmd(f'{PDNS_CMD} get cache-entries') + data['cache_size'] = "{0:.2f}".format( int(cmd(f'{PDNS_CMD} get cache-bytes')) / 1024 ) tmpl = jinja2.Template(OUT_TMPL_SRC) print(tmpl.render(data)) diff --git a/src/op_mode/dynamic_dns.py b/src/op_mode/dynamic_dns.py index 0d457e247..405dd9f04 100755 --- a/src/op_mode/dynamic_dns.py +++ b/src/op_mode/dynamic_dns.py @@ -21,6 +21,8 @@ import sys import time from vyos.config import Config +from vyos.util import call + cache_file = r'/var/cache/ddclient/ddclient.cache' @@ -84,9 +86,9 @@ def show_status(): def update_ddns(): - os.system('systemctl stop ddclient') + call('systemctl stop ddclient') os.remove(cache_file) - os.system('systemctl start ddclient') + call('systemctl start ddclient') def main(): diff --git a/src/op_mode/flow_accounting_op.py b/src/op_mode/flow_accounting_op.py index a39eaf871..7f3ad7476 100755 --- a/src/op_mode/flow_accounting_op.py +++ b/src/op_mode/flow_accounting_op.py @@ -19,10 +19,11 @@ import sys import argparse import re import ipaddress -import subprocess import os.path from tabulate import tabulate +from vyos.util import cmd, run + # some default values uacctd_pidfile = '/var/run/uacctd.pid' uacctd_pipefile = '/tmp/uacctd.pipe' @@ -69,26 +70,16 @@ def _is_host(host): # check if flow-accounting running def _uacctd_running(): - command = "/usr/bin/sudo /bin/systemctl status uacctd > /dev/null" - return_code = subprocess.call(command, shell=True) - if not return_code == 0: - return False - - # return True if all checks were passed - return True + command = '/usr/bin/sudo /bin/systemctl status uacctd > /dev/null' + return run(command) == 0 # get list of interfaces def _get_ifaces_dict(): # run command to get ifaces list - command = "/bin/ip link show" - process = subprocess.Popen(command.split(' '), stdout=subprocess.PIPE, universal_newlines=True) - stdout, stderr = process.communicate() - if not process.returncode == 0: - print("Failed to get interfaces list: command \"{}\" returned exit code: {}".format(command, process.returncode)) - sys.exit(1) + out = cmd('/bin/ip link show', universal_newlines=True) # read output - ifaces_out = stdout.splitlines() + ifaces_out = out.splitlines() # make a dictionary with interfaces and indexes ifaces_dict = {} @@ -103,15 +94,12 @@ def _get_ifaces_dict(): # get list of flows def _get_flows_list(): # run command to get flows list - command = "/usr/bin/pmacct -s -O json -T flows -p {}".format(uacctd_pipefile) - process = subprocess.Popen(command.split(' '), stdout=subprocess.PIPE, universal_newlines=True) - stdout, stderr = process.communicate() - if not process.returncode == 0: - print("Failed to get flows list: command \"{}\" returned exit code: {}\nError: {}".format(command, process.returncode, stderr)) - sys.exit(1) + out = cmd(f'/usr/bin/pmacct -s -O json -T flows -p {uacctd_pipefile}', + universal_newlines=True, + message='Failed to get flows list') # read output - flows_out = stdout.splitlines() + flows_out = out.splitlines() # make a list with flows flows_list = [] @@ -208,21 +196,15 @@ if not _uacctd_running(): # restart pmacct daemon if cmd_args.action == 'restart': # run command to restart flow-accounting - command = '/usr/bin/sudo /bin/systemctl restart uacctd' - return_code = subprocess.call(command.split(' ')) - if not return_code == 0: - print("Failed to restart flow-accounting: command \"{}\" returned exit code: {}".format(command, return_code)) - sys.exit(1) + cmd('/usr/bin/sudo /bin/systemctl restart uacctd', + message='Failed to restart flow-accounting') # clear in-memory collected flows if cmd_args.action == 'clear': _check_imt() # run command to clear flows - command = "/usr/bin/pmacct -e -p {}".format(uacctd_pipefile) - return_code = subprocess.call(command.split(' ')) - if not return_code == 0: - print("Failed to clear flows: command \"{}\" returned exit code: {}".format(command, return_code)) - sys.exit(1) + cmd(f'/usr/bin/pmacct -e -p {uacctd_pipefile}', + message='Failed to clear flows') # show table with flows if cmd_args.action == 'show': diff --git a/src/op_mode/format_disk.py b/src/op_mode/format_disk.py index 5a3b250ee..df4486bce 100755 --- a/src/op_mode/format_disk.py +++ b/src/op_mode/format_disk.py @@ -17,13 +17,14 @@ import argparse import os import re -import subprocess import sys from datetime import datetime from time import sleep from vyos.util import is_admin, ask_yes_no - +from vyos.util import call +from vyos.util import cmd +from vyos.util import DEVNULL def list_disks(): disks = set() @@ -37,10 +38,7 @@ def list_disks(): def is_busy(disk: str): """Check if given disk device is busy by re-reading it's partition table""" - - cmd = 'sudo blockdev --rereadpt /dev/{}'.format(disk) - status = subprocess.call([cmd], shell=True, stderr=subprocess.DEVNULL) - return status != 0 + return call(f'sudo blockdev --rereadpt /dev/{disk}', stderr=DEVNULL) != 0 def backup_partitions(disk: str): @@ -49,8 +47,7 @@ def backup_partitions(disk: str): device_path = '/dev/' + disk backup_ts = datetime.now().strftime('%Y-%m-%d-%H:%M') backup_file = '/var/tmp/backup_{}.{}'.format(disk, backup_ts) - cmd = 'sudo /sbin/sfdisk -d {} > {}'.format(device_path, backup_file) - subprocess.check_call([cmd], shell=True) + cmd(f'sudo /sbin/sfdisk -d {device_path} > {backup_file}') def list_partitions(disk: str): @@ -68,13 +65,11 @@ def list_partitions(disk: str): def delete_partition(disk: str, partition_idx: int): - cmd = 'sudo /sbin/parted /dev/{} rm {}'.format(disk, partition_idx) - subprocess.check_call([cmd], shell=True) + cmd(f'sudo /sbin/parted /dev/{disk} rm {partition_idx}') def format_disk_like(target: str, proto: str): - cmd = 'sudo /sbin/sfdisk -d /dev/{} | sudo /sbin/sfdisk --force /dev/{}'.format(proto, target) - subprocess.check_call([cmd], shell=True) + cmd(f'sudo /sbin/sfdisk -d /dev/{proto} | sudo /sbin/sfdisk --force /dev/{target}') if __name__ == '__main__': diff --git a/src/op_mode/generate_ssh_server_key.py b/src/op_mode/generate_ssh_server_key.py index f205919b8..f65d383c0 100755 --- a/src/op_mode/generate_ssh_server_key.py +++ b/src/op_mode/generate_ssh_server_key.py @@ -14,14 +14,14 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see <http://www.gnu.org/licenses/>. -import subprocess import sys from vyos.util import ask_yes_no +from vyos.util import cmd if not ask_yes_no('Do you really want to remove the existing SSH host keys?'): sys.exit(0) -else: - subprocess.check_call(['sudo rm -v /etc/ssh/ssh_host_*'], shell=True) - subprocess.check_call(['sudo dpkg-reconfigure openssh-server'], shell=True) - subprocess.check_call(['sudo systemctl restart ssh'], shell=True) + +cmd('sudo rm -v /etc/ssh/ssh_host_*') +cmd('sudo dpkg-reconfigure openssh-server') +cmd('sudo systemctl restart ssh') diff --git a/src/op_mode/lldp_op.py b/src/op_mode/lldp_op.py index 4d8fdbc99..5d48e3210 100755 --- a/src/op_mode/lldp_op.py +++ b/src/op_mode/lldp_op.py @@ -20,9 +20,11 @@ import jinja2 from xml.dom import minidom from sys import exit -from subprocess import Popen, PIPE, STDOUT from tabulate import tabulate +from vyos.util import popen +from vyos.config import Config + parser = argparse.ArgumentParser() parser.add_argument("-a", "--all", action="store_true", help="Show LLDP neighbors on all interfaces") parser.add_argument("-i", "--interface", action="store", help="Show LLDP neighbors on specific interface") @@ -40,9 +42,8 @@ Device ID Local Proto Cap Platform Port ID def _get_neighbors(): command = '/usr/sbin/lldpcli -f xml show neighbors' - p = Popen(command, stdout=PIPE, stderr=STDOUT, shell=True) - tmp = p.communicate()[0].strip() - return tmp.decode() + out,_ = popen(command) + return out def extract_neighbor(neighbor): """ @@ -141,6 +142,11 @@ if __name__ == '__main__': args = parser.parse_args() tmp = { 'neighbors' : [] } + c = Config() + if not c.exists_effective(['service', 'lldp']): + print('Service LLDP is not configured') + exit(0) + if args.all: neighbors = minidom.parseString(_get_neighbors()) for neighbor in neighbors.getElementsByTagName('interface'): diff --git a/src/op_mode/powerctrl.py b/src/op_mode/powerctrl.py index 54fc12be3..0f3619411 100755 --- a/src/op_mode/powerctrl.py +++ b/src/op_mode/powerctrl.py @@ -17,12 +17,13 @@ import os import sys import argparse -import subprocess import re from datetime import datetime, timedelta, time as type_time, date as type_date -from subprocess import check_output, CalledProcessError, STDOUT from vyos.util import ask_yes_no +from vyos.util import cmd +from vyos.util import call +from vyos.util import run systemd_sched_file = "/run/systemd/shutdown/scheduled" @@ -45,23 +46,20 @@ def parse_date(s): return None def get_shutdown_status(): - try: - 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 - except CalledProcessError: - 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() @@ -76,13 +74,13 @@ def check_shutdown(): def cancel_shutdown(): output = get_shutdown_status() if output and 'MODE' in output: + timenow = datetime.now().strftime('%Y-%m-%d %H:%M:%S') try: - timenow = datetime.now().strftime('%Y-%m-%d %H:%M:%S') - cmd = check_output(["/sbin/shutdown","-c","--no-wall"]) - message = "Scheduled %s has been cancelled %s" % (output['MODE'], timenow) - os.system("wall %s" % message) - except CalledProcessError as e: + 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") @@ -99,14 +97,14 @@ def execute_shutdown(time, reboot = True, ask=True): chk_vyatta_based_reboots() ### - cmd = check_output(["/sbin/shutdown",action,"now"],stderr=STDOUT) - print(cmd.decode().split(",",1)[0]) + out = cmd(f'/sbin/shutdown {action} now') + print(out.split(",",1)[0]) return elif len(time) == 1: # Assume the argument is just time ts = parse_time(time[0]) if ts: - cmd = check_output(["/sbin/shutdown", action, time[0]], stderr=STDOUT) + cmd(f'/sbin/shutdown {action} {time[0]}') else: sys.exit("Invalid time \"{0}\". The valid format is HH:MM".format(time[0])) elif len(time) == 2: @@ -117,7 +115,7 @@ def execute_shutdown(time, reboot = True, ask=True): t = datetime.combine(ds, ts) td = t - datetime.now() t2 = 1 + int(td.total_seconds())//60 # Get total minutes - cmd = check_output(["/sbin/shutdown", action, str(t2)], stderr=STDOUT) + cmd('/sbin/shutdown {action} {t2}') else: if not ts: sys.exit("Invalid time \"{0}\". The valid format is HH:MM".format(time[0])) @@ -136,7 +134,7 @@ def chk_vyatta_based_reboots(): if os.path.exists(f): jid = open(f).read().strip() if jid != 0: - subprocess.call(['sudo', 'atrm', jid]) + call(f'sudo atrm {jid}') os.remove(f) def main(): diff --git a/src/op_mode/reset_openvpn.py b/src/op_mode/reset_openvpn.py index 7043ac261..618cad5ea 100755 --- a/src/op_mode/reset_openvpn.py +++ b/src/op_mode/reset_openvpn.py @@ -17,10 +17,9 @@ import sys import os -from psutil import pid_exists -from subprocess import Popen, PIPE from time import sleep from netifaces import interfaces +from vyos.util import process_running, cmd def get_config_name(intf): cfg_file = r'/opt/vyatta/etc/openvpn/openvpn-{}.conf'.format(intf) @@ -30,9 +29,6 @@ def get_pid_file(intf): pid_file = r'/var/run/openvpn/{}.pid'.format(intf) return pid_file -def subprocess_cmd(command): - p = Popen(command, stdout=PIPE, shell=True) - p.communicate() if __name__ == '__main__': if (len(sys.argv) < 1): @@ -42,15 +38,13 @@ if __name__ == '__main__': interface = sys.argv[1] if os.path.isfile(get_config_name(interface)): pidfile = '/var/run/openvpn/{}.pid'.format(interface) - if os.path.isfile(pidfile): - pid = 0 - with open(pidfile, 'r') as f: - pid = int(f.read()) - - if pid_exists(pid): - cmd = 'start-stop-daemon --stop --quiet' - cmd += ' --pidfile ' + pidfile - subprocess_cmd(cmd) + if process_running(pidfile): + command = 'start-stop-daemon' + command += ' --stop' + command += ' --oknodo' + command += ' --quiet' + command += ' --pidfile ' + pidfile + cmd(command) # When stopping OpenVPN we need to wait for the 'old' interface to # vanish from the Kernel, if it is not gone, OpenVPN will report: @@ -59,14 +53,18 @@ if __name__ == '__main__': sleep(0.250) # 250ms # re-start OpenVPN process - cmd = 'start-stop-daemon --start --quiet' - cmd += ' --pidfile ' + get_pid_file(interface) - cmd += ' --exec /usr/sbin/openvpn' + command = 'start-stop-daemon' + command += ' --start' + command += ' --oknodo' + command += ' --quiet' + command += ' --pidfile ' + get_pid_file(interface) + command += ' --exec /usr/sbin/openvpn' # now pass arguments to openvpn binary - cmd += ' --' - cmd += ' --config ' + get_config_name(interface) + command += ' --' + command += ' --daemon openvpn-' + interface + command += ' --config ' + get_config_name(interface) - subprocess_cmd(cmd) + cmd(command) else: print("OpenVPN interface {} does not exist!".format(interface)) sys.exit(1) diff --git a/src/op_mode/reset_vpn.py b/src/op_mode/reset_vpn.py index 52677b58d..8962df212 100755 --- a/src/op_mode/reset_vpn.py +++ b/src/op_mode/reset_vpn.py @@ -16,52 +16,54 @@ # import os import sys -import subprocess import argparse #import re -pptp_cmd = ["/usr/bin/accel-cmd", "-p 2003"] -l2tp_cmd = ["/usr/bin/accel-cmd", "-p 2004"] +from vyos.util import run +from vyos.util import DEVNULL + +pptp_base = '/usr/bin/accel-cmd -p 2003 terminate {} {}' +l2tp_base = '/usr/bin/accel-cmd -p 2004 terminate {} {}' def terminate_sessions(username='', interface='', protocol=''): if username: if username == "all_users": if protocol == "pptp": - pptp_cmd.append("terminate all") - subprocess.call(pptp_cmd, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) + pptp_cmd = pptp_base.format('all','') + run(pptp_cmd, stdout=DEVNULL, stderr=DEVNULL) return elif protocol == "l2tp": - l2tp_cmd.append("terminate all") - subprocess.call(l2tp_cmd, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) + l2tp_cmd = l2tp_base.format('all', '') + run(l2tp_cmd, stdout=DEVNULL, stderr=DEVNULL) return else: - pptp_cmd.append("terminate all") - subprocess.call(pptp_cmd, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) - l2tp_cmd.append("terminate all") - subprocess.call(l2tp_cmd, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) + pptp_cmd = pptp_base.format('all', '') + run(pptp_cmd, stdout=DEVNULL, stderr=DEVNULL) + l2tp_cmd = l2tp_base.format('all', '') + run(l2tp_cmd, stdout=DEVNULL, stderr=DEVNULL) return if protocol == "pptp": - pptp_cmd.append("terminate username {0}".format(username)) - subprocess.call(pptp_cmd, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) + pptp_cmd = pptp_base.format('username', username) + run(pptp_cmd, stdout=DEVNULL, stderr=DEVNULL) return elif protocol == "l2tp": - l2tp_cmd.append("terminate username {0}".format(username)) - subprocess.call(l2tp_cmd, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) + l2tp_cmd = l2tp_base.format('username', username) + run(l2tp_cmd, stdout=DEVNULL, stderr=DEVNULL) return else: - pptp_cmd.append("terminate username {0}".format(username)) - subprocess.call(pptp_cmd, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) + pptp_cmd = pptp_base.format('username', username) + run(pptp_cmd, stdout=DEVNULL, stderr=DEVNULL) l2tp_cmd.append("terminate username {0}".format(username)) - subprocess.call(l2tp_cmd, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) + run(l2tp_cmd, stdout=DEVNULL, stderr=DEVNULL) return # rewrite `terminate by interface` if pptp will have pptp%d interface naming if interface: - pptp_cmd.append("terminate if {0}".format(interface)) - subprocess.call(pptp_cmd, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) - l2tp_cmd.append("terminate if {0}".format(interface)) - subprocess.call(l2tp_cmd, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) + pptp_cmd = pptp_base.format('if', interface) + run(pptp_cmd, stdout=DEVNULL, stderr=DEVNULL) + l2tp_cmd = l2tp_base.format('if', interface) + run(l2tp_cmd, stdout=DEVNULL, stderr=DEVNULL) def main(): diff --git a/src/op_mode/restart_dhcp_relay.py b/src/op_mode/restart_dhcp_relay.py index ab02d1eb3..66dc435b3 100755 --- a/src/op_mode/restart_dhcp_relay.py +++ b/src/op_mode/restart_dhcp_relay.py @@ -23,6 +23,8 @@ import argparse import os import vyos.config +from vyos.util import call + parser = argparse.ArgumentParser() parser.add_argument("--ipv4", action="store_true", help="Restart IPv4 DHCP relay") @@ -37,7 +39,7 @@ if __name__ == '__main__': if not c.exists_effective('service dhcp-relay'): print("DHCP relay service not configured") else: - os.system('sudo systemctl restart isc-dhcp-relay.service') + call('sudo systemctl restart isc-dhcp-relay.service') sys.exit(0) elif args.ipv6: @@ -45,7 +47,7 @@ if __name__ == '__main__': if not c.exists_effective('service dhcpv6-relay'): print("DHCPv6 relay service not configured") else: - os.system('sudo systemctl restart isc-dhcpv6-relay.service') + call('sudo systemctl restart isc-dhcpv6-relay.service') sys.exit(0) else: diff --git a/src/op_mode/restart_frr.py b/src/op_mode/restart_frr.py index da6407e23..d1b66b33f 100755 --- a/src/op_mode/restart_frr.py +++ b/src/op_mode/restart_frr.py @@ -17,12 +17,13 @@ import sys import argparse -import subprocess import logging from logging.handlers import SysLogHandler from pathlib import Path import psutil +from vyos.util import call + # some default values watchfrr = '/usr/lib/frr/watchfrr.sh' vtysh = '/usr/bin/vtysh' @@ -86,7 +87,7 @@ def _write_config(): Path(frrconfig_tmp).mkdir(parents=False, exist_ok=True) # save frr.conf to it command = "{} -n -w --config_dir {} 2> /dev/null".format(vtysh, frrconfig_tmp) - return_code = subprocess.call(command, shell=True) + return_code = call(command) if not return_code == 0: logger.error("Failed to save active config: \"{}\" returned exit code: {}".format(command, return_code)) return False @@ -108,7 +109,7 @@ def _cleanup(): # check if daemon is running def _daemon_check(daemon): command = "{} print_status {}".format(watchfrr, daemon) - return_code = subprocess.call(command, shell=True) + return_code = call(command) if not return_code == 0: logger.error("Daemon \"{}\" is not running".format(daemon)) return False @@ -119,7 +120,7 @@ def _daemon_check(daemon): # restart daemon def _daemon_restart(daemon): command = "{} restart {}".format(watchfrr, daemon) - return_code = subprocess.call(command, shell=True) + return_code = call(command) if not return_code == 0: logger.error("Failed to restart daemon \"{}\"".format(daemon)) return False @@ -135,7 +136,7 @@ def _reload_config(daemon): else: command = "{} -n -b --config_dir {} 2> /dev/null".format(vtysh, frrconfig_tmp) - return_code = subprocess.call(command, shell=True) + return_code = call(command) if not return_code == 0: logger.error("Failed to reinstall configuration") return False diff --git a/src/op_mode/show_acceleration.py b/src/op_mode/show_acceleration.py index 3ba0e85dd..6d44b0f66 100755 --- a/src/op_mode/show_acceleration.py +++ b/src/op_mode/show_acceleration.py @@ -19,14 +19,16 @@ import sys import os import re import argparse -import subprocess + from vyos.config import Config +from vyos.util import popen +from vyos.util import call + def detect_qat_dev(): - ret = subprocess.Popen(['sudo', 'lspci', '-nn'], stdout=subprocess.PIPE, stderr=subprocess.STDOUT) - (output, err) = ret.communicate() + output, err = popen('sudo lspci -nn', decode='utf-8') if not err: - data = re.findall('(8086:19e2)|(8086:37c8)|(8086:0435)|(8086:6f54)', output.decode("utf-8")) + data = re.findall('(8086:19e2)|(8086:37c8)|(8086:0435)|(8086:6f54)', output) #If QAT devices found if data: return @@ -42,15 +44,12 @@ def show_qat_status(): sys.exit(1) # Show QAT service - os.system('sudo /etc/init.d/vyos-qat-utilities status') + call('sudo /etc/init.d/vyos-qat-utilities status') # Return QAT devices def get_qat_devices(): - ret = subprocess.Popen(['sudo', '/etc/init.d/vyos-qat-utilities', 'status'], stdout=subprocess.PIPE, stderr=subprocess.STDOUT) - (output, err) = ret.communicate() + data_st, err = popen('sudo /etc/init.d/vyos-qat-utilities status', decode='utf-8') if not err: - #print(output) - data_st = output.decode("utf-8") elm_lst = re.findall('qat_dev\d', data_st) print('\n'.join(elm_lst)) @@ -58,11 +57,10 @@ def get_qat_devices(): def get_qat_proc_path(qat_dev): q_type = "" q_bsf = "" - ret = subprocess.Popen(['sudo', '/etc/init.d/vyos-qat-utilities', 'status'], stdout=subprocess.PIPE, stderr=subprocess.STDOUT) - (output, err) = ret.communicate() + output, err = popen('sudo /etc/init.d/vyos-qat-utilities status', decode='utf-8') if not err: # Parse QAT service output - data_st = output.decode("utf-8").split("\n") + data_st = output.split("\n") for elm_str in range(len(data_st)): if re.search(qat_dev, data_st[elm_str]): elm_list = data_st[elm_str].split(", ") @@ -97,22 +95,22 @@ args = parser.parse_args() if args.hw: detect_qat_dev() # Show availible Intel QAT devices - os.system('sudo lspci -nn | egrep -e \'8086:37c8|8086:19e2|8086:0435|8086:6f54\'') + call('sudo lspci -nn | egrep -e \'8086:37c8|8086:19e2|8086:0435|8086:6f54\'') elif args.flow and args.dev: check_qat_if_conf() - os.system('sudo cat '+get_qat_proc_path(args.dev)+"fw_counters") + call('sudo cat '+get_qat_proc_path(args.dev)+"fw_counters") elif args.interrupts: check_qat_if_conf() # Delete _dev from args.dev - os.system('sudo cat /proc/interrupts | grep qat') + call('sudo cat /proc/interrupts | grep qat') elif args.status: check_qat_if_conf() show_qat_status() elif args.conf and args.dev: check_qat_if_conf() - os.system('sudo cat '+get_qat_proc_path(args.dev)+"dev_cfg") + call('sudo cat '+get_qat_proc_path(args.dev)+"dev_cfg") elif args.dev_list: get_qat_devices() else: parser.print_help() - sys.exit(1)
\ No newline at end of file + sys.exit(1) diff --git a/src/op_mode/show_dhcp.py b/src/op_mode/show_dhcp.py index f801ba753..a79033f69 100755 --- a/src/op_mode/show_dhcp.py +++ b/src/op_mode/show_dhcp.py @@ -24,9 +24,12 @@ import collections import os from datetime import datetime -from vyos.config import Config from isc_dhcp_leases import Lease, IscDhcpLeases +from vyos.config import Config +from vyos.util import call + + lease_file = "/config/dhcpd.leases" pool_key = "shared-networkname" @@ -190,7 +193,7 @@ if __name__ == '__main__': sys.exit(0) # if dhcp server is down, inactive leases may still be shown as active, so warn the user. - if os.system('systemctl -q is-active isc-dhcpv4-server.service') != 0: + if call('systemctl -q is-active isc-dhcpv4-server.service') != 0: print("WARNING: DHCP server is configured but not started. Data may be stale.") if args.leases: diff --git a/src/op_mode/show_dhcpv6.py b/src/op_mode/show_dhcpv6.py index ae63af39b..18baa5517 100755 --- a/src/op_mode/show_dhcpv6.py +++ b/src/op_mode/show_dhcpv6.py @@ -24,9 +24,11 @@ import collections import os from datetime import datetime -from vyos.config import Config from isc_dhcp_leases import Lease, IscDhcpLeases +from vyos.config import Config +from vyos.util import call + lease_file = "/config/dhcpdv6.leases" pool_key = "shared-networkname" @@ -177,7 +179,7 @@ if __name__ == '__main__': sys.exit(0) # if dhcp server is down, inactive leases may still be shown as active, so warn the user. - if os.system('systemctl -q is-active isc-dhcpv6-server.service') != 0: + if call('systemctl -q is-active isc-dhcpv6-server.service') != 0: print("WARNING: DHCPv6 server is configured but not started. Data may be stale.") if args.leases: diff --git a/src/op_mode/show_openvpn.py b/src/op_mode/show_openvpn.py index 06b90296f..32918ddce 100755 --- a/src/op_mode/show_openvpn.py +++ b/src/op_mode/show_openvpn.py @@ -15,6 +15,7 @@ # along with this program. If not, see <http://www.gnu.org/licenses/>. # +import os import jinja2 import argparse @@ -63,6 +64,9 @@ def get_status(mode, interface): 'clients': [], } + if not os.path.exists(status_file): + return data + with open(status_file, 'r') as f: lines = f.readlines() for line_no, line in enumerate(lines): diff --git a/src/op_mode/show_vpn_ra.py b/src/op_mode/show_vpn_ra.py index cf6119c2f..2323193b1 100755 --- a/src/op_mode/show_vpn_ra.py +++ b/src/op_mode/show_vpn_ra.py @@ -17,8 +17,8 @@ import os import sys import re -import subprocess -# from subprocess import Popen, PIPE + +from vyos.util import popen # chech connection to pptp and l2tp daemon def get_sessions(): @@ -31,18 +31,16 @@ def get_sessions(): len_def_header = 170 # Check pptp - ret = subprocess.Popen(pptp_cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT) - (output, err) = ret.communicate() - if not err and len(output.decode("utf-8")) > len_def_header and not re.search(err_pattern, output.decode("utf-8")): - print(output.decode("utf-8")) + output, err = popen(pptp_cmd, decode='utf-8') + if not err and len(output) > len_def_header and not re.search(err_pattern, output): + print(output) else: absent_pptp = True # Check l2tp - ret = subprocess.Popen(l2tp_cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT) - (output, err) = ret.communicate() - if not err and len(output.decode("utf-8")) > len_def_header and not re.search(err_pattern, output.decode("utf-8")): - print(output.decode("utf-8")) + output, err = popen(l2tp_cmd, decode='utf-8') + if not err and len(output) > len_def_header and not re.search(err_pattern, output): + print(output) else: absent_l2tp = True diff --git a/src/op_mode/show_vrf.py b/src/op_mode/show_vrf.py new file mode 100755 index 000000000..b6bb73d01 --- /dev/null +++ b/src/op_mode/show_vrf.py @@ -0,0 +1,67 @@ +#!/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 argparse +import jinja2 +from json import loads + +from vyos.util import cmd + +vrf_out_tmpl = """ +VRF name state mac address flags interfaces +-------- ----- ----------- ----- ---------- +{%- for v in vrf %} +{{"%-16s"|format(v.ifname)}} {{ "%-8s"|format(v.operstate | lower())}} {{"%-17s"|format(v.address | lower())}} {{ v.flags|join(',')|lower()}} {{v.members|join(',')|lower()}} +{%- endfor %} + +""" + +def list_vrfs(): + command = 'ip -j -br link show type vrf' + answer = loads(cmd(command)) + return [_ for _ in answer if _] + +def list_vrf_members(vrf): + command = f'ip -j -br link show master {vrf}' + answer = loads(cmd(command)) + return [_ for _ in answer if _] + +parser = argparse.ArgumentParser() +group = parser.add_mutually_exclusive_group() +group.add_argument("-e", "--extensive", action="store_true", + help="provide detailed vrf informatio") +parser.add_argument('interface', metavar='I', type=str, nargs='?', + help='interface to display') + +args = parser.parse_args() + +if args.extensive: + data = { 'vrf': [] } + for vrf in list_vrfs(): + name = vrf['ifname'] + if args.interface and name != args.interface: + continue + + vrf['members'] = [] + for member in list_vrf_members(name): + vrf['members'].append(member['ifname']) + data['vrf'].append(vrf) + + tmpl = jinja2.Template(vrf_out_tmpl) + print(tmpl.render(data)) + +else: + print(" ".join([vrf['ifname'] for vrf in list_vrfs()])) diff --git a/src/op_mode/show_wireless.py b/src/op_mode/show_wireless.py index aff882559..b5ee3aee1 100755 --- a/src/op_mode/show_wireless.py +++ b/src/op_mode/show_wireless.py @@ -19,19 +19,15 @@ import re from sys import exit from copy import deepcopy -from subprocess import Popen, PIPE, STDOUT from vyos.config import Config +from vyos.util import popen parser = argparse.ArgumentParser() parser.add_argument("-s", "--scan", help="Scan for Wireless APs on given interface, e.g. 'wlan0'") parser.add_argument("-b", "--brief", action="store_true", help="Show wireless configuration") parser.add_argument("-c", "--stations", help="Show wireless clients connected on interface, e.g. 'wlan0'") -def _cmd(command): - p = Popen(command, stdout=PIPE, stderr=STDOUT, shell=True) - tmp = p.communicate()[0].strip() - return tmp.decode() def show_brief(): config = Config() @@ -57,7 +53,8 @@ def show_brief(): return interfaces def ssid_scan(intf): - tmp = _cmd('/sbin/iw dev {} scan ap-force'.format(intf)) + # XXX: This ignores errors + tmp, _ = popen(f'/sbin/iw dev {intf} scan ap-force') networks = [] data = { 'ssid': '', @@ -89,7 +86,8 @@ def ssid_scan(intf): return networks def show_clients(intf): - tmp = _cmd('/sbin/iw dev {} station dump'.format(intf)) + # XXX: This ignores errors + tmp, _ = popen(f'/sbin/iw dev {intf} station dump') clients = [] data = { 'mac': '', diff --git a/src/op_mode/snmp.py b/src/op_mode/snmp.py index e08441f0e..5fae67881 100755 --- a/src/op_mode/snmp.py +++ b/src/op_mode/snmp.py @@ -24,6 +24,7 @@ import sys import argparse from vyos.config import Config +from vyos.util import call config_file_daemon = r'/etc/snmp/snmpd.conf' @@ -53,7 +54,7 @@ def show_all(): def show_community(c, h): print('Status of SNMP community {0} on {1}'.format(c, h), flush=True) - os.system('/usr/bin/snmpstatus -t1 -v1 -c {0} {1}'.format(c, h)) + call('/usr/bin/snmpstatus -t1 -v1 -c {0} {1}'.format(c, h)) if __name__ == '__main__': args = parser.parse_args() diff --git a/src/op_mode/snmp_ifmib.py b/src/op_mode/snmp_ifmib.py index 3a0e0d4b2..2479936bd 100755 --- a/src/op_mode/snmp_ifmib.py +++ b/src/op_mode/snmp_ifmib.py @@ -22,36 +22,24 @@ import sys import argparse import netifaces -import subprocess from vyos.config import Config +from vyos.util import popen parser = argparse.ArgumentParser(description='Retrieve SNMP interfaces information') parser.add_argument('--ifindex', action='store', nargs='?', const='all', help='Show interface index') parser.add_argument('--ifalias', action='store', nargs='?', const='all', help='Show interface aliase') parser.add_argument('--ifdescr', action='store', nargs='?', const='all', help='Show interface description') -def show_ifindex(i): - proc = subprocess.Popen(['/bin/ip', 'link', 'show', i], stdout=subprocess.PIPE) - (out, err) = proc.communicate() - # convert output to string - string = out.decode("utf-8") - - index = 'ifIndex = ' + string.split(':')[0] +def show_ifindex(intf): + out, err = popen(f'/bin/ip link show {intf}', decode='utf-8') + index = 'ifIndex = ' + out.split(':')[0] return index.replace('\n', '') -def show_ifalias(i): - proc = subprocess.Popen(['/bin/ip', 'link', 'show', i], stdout=subprocess.PIPE) - (out, err) = proc.communicate() - # convert output to string - string = out.decode("utf-8") - - if 'alias' in string: - alias = 'ifAlias = ' + string.split('alias')[1].lstrip() - else: - alias = 'ifAlias = ' + i - - return alias.replace('\n', '') +def show_ifalias(intf): + out, err = popen(f'/bin/ip link show {intf}', decode='utf-8') + alias = out.split('alias')[1].lstrip() if 'alias' in out else intf + return 'ifAlias = ' + alias.replace('\n', '') def show_ifdescr(i): ven_id = '' @@ -74,14 +62,13 @@ def show_ifdescr(i): return ret device = str(ven_id) + ':' + str(dev_id) - proc = subprocess.Popen(['/usr/bin/lspci', '-mm', '-d', device], stdout=subprocess.PIPE) - (out, err) = proc.communicate() + out, err = popen(f'/usr/bin/lspci -mm -d {device}', decode='utf-8') vendor = "" device = "" # convert output to string - string = out.decode("utf-8").split('"') + string = out.split('"') if len(string) > 3: vendor = string[3] diff --git a/src/op_mode/system_integrity.py b/src/op_mode/system_integrity.py index 886d94f16..c0e3d1095 100755 --- a/src/op_mode/system_integrity.py +++ b/src/op_mode/system_integrity.py @@ -18,18 +18,19 @@ import sys import os -import subprocess import re import itertools from datetime import datetime, timedelta +from vyos.util import cmd + verf = r'/usr/libexec/vyos/op_mode/version.py' def get_sys_build_version(): if not os.path.exists(verf): return None - a = subprocess.check_output(['/usr/libexec/vyos/op_mode/version.py']).decode() + a = cmd('/usr/libexec/vyos/op_mode/version.py') if re.search('^Built on:.+',a, re.M) == None: return None diff --git a/src/op_mode/version.py b/src/op_mode/version.py index 5aff0f767..fe6ecbae5 100755 --- a/src/op_mode/version.py +++ b/src/op_mode/version.py @@ -22,7 +22,6 @@ import os import sys -import subprocess import argparse import json @@ -31,6 +30,10 @@ import pystache import vyos.version import vyos.limericks +from vyos.util import cmd +from vyos.util import call +from vyos.util import run + parser = argparse.ArgumentParser() parser.add_argument("-a", "--all", action="store_true", help="Include individual package versions") @@ -73,15 +76,15 @@ if __name__ == '__main__': version_data = vyos.version.get_version_data() # Get system architecture (well, kernel architecture rather) - version_data['system_arch'] = subprocess.check_output('uname -m', shell=True).decode().strip() + version_data['system_arch'] = cmd('uname -m') # Get hypervisor name, if any system_type = "bare metal" try: - hypervisor = subprocess.check_output('hvinfo 2>/dev/null', shell=True).decode().strip() + hypervisor = cmd('hvinfo 2>/dev/null') system_type = "{0} guest".format(hypervisor) - except subprocess.CalledProcessError: + except OSError: # hvinfo returns 1 if it cannot detect any hypervisor pass version_data['system_type'] = system_type @@ -93,9 +96,9 @@ if __name__ == '__main__': # while on livecd it's just "filesystem.squashfs", that's how we tell a livecd boot # from an installed image boot_via = "installed image" - if subprocess.call(""" grep -e '^overlay.*/filesystem.squashfs' /proc/mounts >/dev/null""", shell=True) == 0: + if run(""" grep -e '^overlay.*/filesystem.squashfs' /proc/mounts >/dev/null""") == 0: boot_via = "livecd" - elif subprocess.call(""" grep '^overlay /' /proc/mounts >/dev/null """, shell=True) != 0: + elif run(""" grep '^overlay /' /proc/mounts >/dev/null """) != 0: boot_via = "legacy non-image installation" version_data['boot_via'] = boot_via @@ -118,7 +121,7 @@ if __name__ == '__main__': if args.all: print("Package versions:") - os.system("dpkg -l") + call("dpkg -l") if args.funny: print(vyos.limericks.get_random()) diff --git a/src/op_mode/vrrp.py b/src/op_mode/vrrp.py index 8d1369823..8a993f92c 100755 --- a/src/op_mode/vrrp.py +++ b/src/op_mode/vrrp.py @@ -18,12 +18,28 @@ import sys import time import argparse - +import json import tabulate import vyos.keepalived import vyos.util +config_dict_path = '/run/keepalived_config.dict' + + +# get disabled instances from a config +def vrrp_get_disabled(): + # read the dictionary file with configuration + with open(config_dict_path, 'r') as dict_file: + vrrp_config_dict = json.load(dict_file) + vrrp_disabled = [] + # add disabled groups to the list + for vrrp_group in vrrp_config_dict['vrrp_groups']: + if vrrp_group['disable']: + vrrp_disabled.append([vrrp_group['name'], vrrp_group['interface'], vrrp_group['vrid'], 'DISABLED', '']) + # return list with disabled instances + return vrrp_disabled + def print_summary(): try: @@ -54,10 +70,13 @@ def print_summary(): row = [name, interface, vrid, state, ltrans_time] groups.append(row) + # add to the active list disabled instances + groups.extend(vrrp_get_disabled()) headers = ["Name", "Interface", "VRID", "State", "Last Transition"] output = tabulate.tabulate(groups, headers) print(output) + def print_statistics(): try: vyos.keepalived.force_stats_dump() @@ -69,6 +88,7 @@ def print_statistics(): print("VRRP statistics are not available") sys.exit(1) + def print_state_data(): try: vyos.keepalived.force_state_data_dump() @@ -80,6 +100,7 @@ def print_state_data(): print("VRRP information is not available") sys.exit(1) + parser = argparse.ArgumentParser() group = parser.add_mutually_exclusive_group() group.add_argument("-s", "--summary", action="store_true", help="Print VRRP summary") diff --git a/src/op_mode/wireguard.py b/src/op_mode/wireguard.py index 38c061cf4..1b90f4fa7 100755 --- a/src/op_mode/wireguard.py +++ b/src/op_mode/wireguard.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2018 VyOS maintainers and contributors +# Copyright (C) 2018-2020 VyOS maintainers and contributors # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 or later as @@ -13,14 +13,11 @@ # # You should have received a copy of the GNU General Public License # along with this program. If not, see <http://www.gnu.org/licenses/>. -# -# import argparse import os import sys import shutil -import subprocess import syslog as sl import re @@ -28,6 +25,7 @@ from vyos.ifconfig import WireGuardIf from vyos import ConfigError from vyos.config import Config +from vyos.util import cmd, run dir = r'/config/auth/wireguard' psk = dir + '/preshared.key' @@ -36,16 +34,14 @@ def check_kmod(): """ check if kmod is loaded, if not load it """ if not os.path.exists('/sys/module/wireguard'): sl.syslog(sl.LOG_NOTICE, "loading wirguard kmod") - if os.system('sudo modprobe wireguard') != 0: + if run('sudo modprobe wireguard') != 0: sl.syslog(sl.LOG_ERR, "modprobe wireguard failed") raise ConfigError("modprobe wireguard failed") def generate_keypair(pk, pub): """ generates a keypair which is stored in /config/auth/wireguard """ old_umask = os.umask(0o027) - ret = subprocess.call( - ['wg genkey | tee ' + pk + '|wg pubkey > ' + pub], shell=True) - if ret != 0: + if run(f'wg genkey | tee {pk} | wg pubkey > {pub}') != 0: raise ConfigError("wireguard key-pair generation failed") else: sl.syslog( @@ -69,9 +65,9 @@ def genkey(location): else: """ if keypair is bing executed from a running iso """ if not os.path.exists(location): - subprocess.call(['sudo mkdir -p ' + location], shell=True) - subprocess.call(['sudo chgrp vyattacfg ' + location], shell=True) - subprocess.call(['sudo chmod 750 ' + location], shell=True) + run(f'sudo mkdir -p {location}') + run(f'sudo chgrp vyattacfg {location}') + run(f'sudo chmod 750 {location}') generate_keypair(pk, pub) os.umask(old_umask) @@ -90,10 +86,11 @@ def genpsk(): it's stored only in the cli config """ - subprocess.call(['wg genpsk'], shell=True) + psk = cmd('wg genpsk') + print(psk) def list_key_dirs(): - """ lists all dirs under /config/auth/wireguard """ + """ lists all dirs under /config/auth/wireguard """ if os.path.exists(dir): nks = next(os.walk(dir))[1] for nk in nks: @@ -150,7 +147,7 @@ if __name__ == '__main__': if args.listkdir: list_key_dirs() if args.showinterface: - intf = WireGuardIf(args.showinterface) + intf = WireGuardIf(args.showinterface, create=False, debug=False) intf.op_show_interface() if args.delkdir: if args.location: diff --git a/src/pam-configs/radius b/src/pam-configs/radius index 0d9fbbb03..0e2c71e38 100644 --- a/src/pam-configs/radius +++ b/src/pam-configs/radius @@ -3,12 +3,18 @@ Default: yes Priority: 257 Auth-Type: Primary Auth: + [default=ignore success=1] pam_succeed_if.so uid eq 1001 quiet + [default=ignore success=ignore] pam_succeed_if.so uid eq 1002 quiet [authinfo_unavail=ignore success=end default=ignore] pam_radius_auth.so Account-Type: Primary Account: + [default=ignore success=1] pam_succeed_if.so uid eq 1001 quiet + [default=ignore success=ignore] pam_succeed_if.so uid eq 1002 quiet [authinfo_unavail=ignore success=end perm_denied=bad default=ignore] pam_radius_auth.so Session-Type: Additional Session: + [default=ignore success=1] pam_succeed_if.so uid eq 1001 quiet + [default=ignore success=ignore] pam_succeed_if.so uid eq 1002 quiet [authinfo_unavail=ignore success=ok default=ignore] pam_radius_auth.so diff --git a/src/services/vyos-http-api-server b/src/services/vyos-http-api-server index da6e5b118..b5ad8b159 100755 --- a/src/services/vyos-http-api-server +++ b/src/services/vyos-http-api-server @@ -308,6 +308,49 @@ def config_file_op(): return success(res) +@app.route('/generate', method='POST') +@auth_required +def generate_op(): + session = app.config['vyos_session'] + + command = bottle.request.forms.get("data") + command = json.loads(command) + + try: + cmd = command['cmd'] + res = session.generate(cmd) + except KeyError: + return error(400, "Missing required field \"cmd\"") + except VyOSError 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('/show', method='POST') +@auth_required +def show_op(): + session = app.config['vyos_session'] + + command = bottle.request.forms.get("data") + command = json.loads(command) + + try: + cmd = command['cmd'] + res = session.show(cmd) + except KeyError: + return error(400, "Missing required field \"cmd\"") + except VyOSError 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) + + if __name__ == '__main__': # systemd's user and group options don't work, do it by hand here, # else no one else will be able to commit diff --git a/src/system/keepalived-fifo.py b/src/system/keepalived-fifo.py new file mode 100755 index 000000000..2778deaab --- /dev/null +++ b/src/system/keepalived-fifo.py @@ -0,0 +1,188 @@ +#!/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 os +import time +import signal +import argparse +import threading +import re +import json +from pathlib import Path +from queue import Queue +import logging +from logging.handlers import SysLogHandler + +from vyos.util import cmd + +# configure logging +logger = logging.getLogger(__name__) +logs_format = logging.Formatter('%(filename)s: %(message)s') +logs_handler_syslog = SysLogHandler('/dev/log') +logs_handler_syslog.setFormatter(logs_format) +logger.addHandler(logs_handler_syslog) +logger.setLevel(logging.DEBUG) + + +# class for all operations +class KeepalivedFifo: + # init - read command arguments + def __init__(self): + logger.info("Starting FIFO pipe for Keepalived") + # define program arguments + cmd_args_parser = argparse.ArgumentParser(description='Create FIFO pipe for keepalived and process notify events', add_help=False) + cmd_args_parser.add_argument('PIPE', help='path to the FIFO pipe') + # parse arguments + cmd_args = cmd_args_parser.parse_args() + self._config_load() + self.pipe_path = cmd_args.PIPE + + # create queue for messages and events for syncronization + self.message_queue = Queue(maxsize=100) + self.stopme = threading.Event() + self.message_event = threading.Event() + + # load configuration + def _config_load(self): + try: + # read the dictionary file with configuration + with open('/run/keepalived_config.dict', 'r') as dict_file: + vrrp_config_dict = json.load(dict_file) + self.vrrp_config = {'vrrp_groups': {}, 'sync_groups': {}} + # save VRRP instances to the new dictionary + for vrrp_group in vrrp_config_dict['vrrp_groups']: + self.vrrp_config['vrrp_groups'][vrrp_group['name']] = { + 'STOP': vrrp_group.get('stop_script'), + 'FAULT': vrrp_group.get('fault_script'), + 'BACKUP': vrrp_group.get('backup_script'), + 'MASTER': vrrp_group.get('master_script') + } + # save VRRP sync groups to the new dictionary + for sync_group in vrrp_config_dict['sync_groups']: + self.vrrp_config['sync_groups'][sync_group['name']] = { + 'STOP': sync_group.get('stop_script'), + 'FAULT': sync_group.get('fault_script'), + 'BACKUP': sync_group.get('backup_script'), + 'MASTER': sync_group.get('master_script') + } + logger.debug("Loaded configuration: {}".format(self.vrrp_config)) + except Exception as err: + logger.error("Unable to load configuration: {}".format(err)) + + # run command + def _run_command(self, command): + logger.debug("Running the command: {}".format(command)) + try: + cmd(command, universal_newlines=True) + except OSError as err: + logger.error(f'Unable to execute command "{command}": {err}') + + # create FIFO pipe + def pipe_create(self): + if Path(self.pipe_path).exists(): + logger.info("PIPE already exist: {}".format(self.pipe_path)) + else: + os.mkfifo(self.pipe_path) + + # process message from pipe + def pipe_process(self): + logger.debug("Message processing start") + regex_notify = re.compile(r'^(?P<type>\w+) "(?P<name>[\w-]+)" (?P<state>\w+) (?P<priority>\d+)$', re.MULTILINE) + while self.stopme.is_set() is False: + # wait for a new message event from pipe_wait + self.message_event.wait() + try: + # clear mesage event flag + self.message_event.clear() + # get all messages from queue and try to process them + while self.message_queue.empty() is not True: + message = self.message_queue.get() + logger.debug("Received message: {}".format(message)) + notify_message = regex_notify.search(message) + # try to process a message if it looks valid + if notify_message: + n_type = notify_message.group('type') + n_name = notify_message.group('name') + n_state = notify_message.group('state') + logger.info("{} {} changed state to {}".format(n_type, n_name, n_state)) + # check and run commands for VRRP instances + if n_type == 'INSTANCE': + if n_name in self.vrrp_config['vrrp_groups'] and n_state in self.vrrp_config['vrrp_groups'][n_name]: + n_script = self.vrrp_config['vrrp_groups'][n_name].get(n_state) + if n_script: + self._run_command(n_script) + # check and run commands for VRRP sync groups + # currently, this is not available in VyOS CLI + if n_type == 'GROUP': + if n_name in self.vrrp_config['sync_groups'] and n_state in self.vrrp_config['sync_groups'][n_name]: + n_script = self.vrrp_config['sync_groups'][n_name].get(n_state) + if n_script: + self._run_command(n_script) + # mark task in queue as done + self.message_queue.task_done() + except Exception as err: + logger.error("Error processing message: {}".format(err)) + logger.debug("Terminating messages processing thread") + + # wait for messages + def pipe_wait(self): + logger.debug("Message reading start") + self.pipe_read = os.open(self.pipe_path, os.O_RDONLY | os.O_NONBLOCK) + while self.stopme.is_set() is False: + # sleep a bit to not produce 100% CPU load + time.sleep(0.1) + try: + # try to read a message from PIPE + message = os.read(self.pipe_read, 500) + if message: + # split PIPE content by lines and put them into queue + for line in message.decode().strip().splitlines(): + self.message_queue.put(line) + # set new message flag to start processing + self.message_event.set() + except Exception as err: + # ignore the "Resource temporarily unavailable" error + if err.errno != 11: + logger.error("Error receiving message: {}".format(err)) + + logger.debug("Closing FIFO pipe") + os.close(self.pipe_read) + + +# handle SIGTERM signal to allow finish all messages processing +def sigterm_handle(signum, frame): + logger.info("Ending processing: Received SIGTERM signal") + fifo.stopme.set() + thread_wait_message.join() + fifo.message_event.set() + thread_process_message.join() + + +signal.signal(signal.SIGTERM, sigterm_handle) + +# init our class +fifo = KeepalivedFifo() +# try to create PIPE if it is not exist yet +# It looks like keepalived do it before the script will be running, but if we +# will decide to run this not from keepalived config, then we may get in +# trouble. So it is betteer to leave this here. +fifo.pipe_create() +# create and run dedicated threads for reading and processing messages +thread_wait_message = threading.Thread(target=fifo.pipe_wait) +thread_process_message = threading.Thread(target=fifo.pipe_process) +thread_wait_message.start() +thread_process_message.start() diff --git a/src/system/vrrp-script-wrapper.py b/src/system/vrrp-script-wrapper.py deleted file mode 100755 index c28ecba55..000000000 --- a/src/system/vrrp-script-wrapper.py +++ /dev/null @@ -1,64 +0,0 @@ -#!/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 os -import sys -import subprocess -import argparse -import syslog - -import vyos.util - - -parser = argparse.ArgumentParser() -parser.add_argument("-t", "--state", type=str, help="VRRP state") -parser.add_argument("-g", "--group", type=str, help="VRRP group") -parser.add_argument("-i", "--interface", type=str, help="Network interface") -parser.add_argument("script", nargs='+') - -syslog.openlog('vyos-vrrp-wrapper') - -args = parser.parse_args() -if not args.script or not args.state or not args.group \ - or not args.interface: - parser.print_usage() - sys.exit(1) - -# Fixup: the reason we take multiple "script" arguments is that people may want -# to pass arguments to the script -args.script = " ".join(args.script) - -exitcode = 0 -# Change the process GID to the config owners group to avoid screwing up -# running config permissions -os.setgid(vyos.util.get_cfg_group_id()) -syslog.syslog(syslog.LOG_NOTICE, 'Running transition script {0} for VRRP group {1}'.format(args.script, args.group)) -try: - ret = subprocess.call("%s %s %s %s" % ( args.script, args.state, args.interface, args.group), shell=True) - if ret != 0: - syslog.syslog(syslog.LOG_ERR, "Transition script {0} failed, exit status: {1}".format(args.script, ret)) - exitcode = ret -except Exception as e: - syslog.syslog(syslog.LOG_ERR, "Failed to execute transition script {0}: {1}".format(args.script, e)) - exitcode = 1 - -if exitcode == 0: - syslog.syslog(syslog.LOG_NOTICE, "Transition script {0} executed successfully".format(args.script)) - -syslog.closelog() -sys.exit(exitcode) diff --git a/src/validators/fqdn b/src/validators/fqdn new file mode 100755 index 000000000..9f4ed764f --- /dev/null +++ b/src/validators/fqdn @@ -0,0 +1,27 @@ +#!/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/>. + +from re import match +from sys import argv,exit + +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) + diff --git a/src/validators/mac-address b/src/validators/mac-address index d6ec6d712..435920b84 100755 --- a/src/validators/mac-address +++ b/src/validators/mac-address @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2018 VyOS maintainers and contributors +# Copyright (C) 2018-2020 VyOS maintainers and contributors # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 or later as @@ -13,16 +13,14 @@ # # You should have received a copy of the GNU General Public License # along with this program. If not, see <http://www.gnu.org/licenses/>. -# -# -import sys import re +from sys import exit, argv -if len(sys.argv) == 2: - pattern = "^([0-9A-Fa-f]{2}[:]){5}([0-9A-Fa-f]{2})$" - if re.match(pattern, sys.argv[1]): - sys.exit(0) - else: - sys.exit(1) +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) diff --git a/src/validators/timezone b/src/validators/timezone index d7a8f64c4..ec845e755 100755 --- a/src/validators/timezone +++ b/src/validators/timezone @@ -17,12 +17,8 @@ import argparse from sys import exit -from subprocess import Popen, PIPE, STDOUT -def _cmd(cmd): - p = Popen(cmd, stdout=PIPE, stderr=STDOUT, shell=True) - tmp = p.communicate()[0].strip() - return tmp.decode() +from vyos.util import cmd parser = argparse.ArgumentParser() parser.add_argument("--validate", action="store", help="Check if timezone is valid") @@ -31,7 +27,7 @@ if __name__ == '__main__': 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 = 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: diff --git a/src/validators/vrf-name b/src/validators/vrf-name new file mode 100755 index 000000000..11c453f4d --- /dev/null +++ b/src/validators/vrf-name @@ -0,0 +1,40 @@ +#!/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 exit, argv + +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) + + # 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) + + # 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) + +exit(1) diff --git a/tests/data/config.valid b/tests/data/config.valid index a21c6a4d1..1fbdd1505 100644 --- a/tests/data/config.valid +++ b/tests/data/config.valid @@ -35,5 +35,5 @@ empty-node { trailing-leaf-node-without-value -/* Trailing commend */ -/* Another trailing comment */ +// Trailing comment +// Another trailing comment |