diff options
134 files changed, 4858 insertions, 3890 deletions
diff --git a/data/templates/accel-ppp/chap-secrets.ipoe.tmpl b/data/templates/accel-ppp/chap-secrets.ipoe.tmpl new file mode 100644 index 000000000..a7d899354 --- /dev/null +++ b/data/templates/accel-ppp/chap-secrets.ipoe.tmpl @@ -0,0 +1,18 @@ +# username server password acceptable local IP addresses shaper +{% for interface in auth_interfaces -%} +{% for mac in interface.mac -%} +{% if mac.rate_upload and mac.rate_download -%} +{% if mac.vlan_id -%} +{{ interface.name }}.{{ mac.vlan_id }} * {{ mac.address | lower }} * {{ mac.rate_download }}/{{ mac.rate_upload }} +{% else -%} +{{ interface.name }} * {{ mac.address | lower }} * {{ mac.rate_download }}/{{ mac.rate_upload }} +{% endif -%} +{% else -%} +{% if mac.vlan_id -%} +{{ interface.name }}.{{ mac.vlan_id }} * {{ mac.address | lower }} * +{% else -%} +{{ interface.name }} * {{ mac.address | lower }} * +{% endif -%} +{% endif -%} +{% endfor -%} +{% endfor -%} diff --git a/data/templates/l2tp/chap-secrets.tmpl b/data/templates/accel-ppp/chap-secrets.tmpl index dd00d7bd0..dd00d7bd0 100644 --- a/data/templates/l2tp/chap-secrets.tmpl +++ b/data/templates/accel-ppp/chap-secrets.tmpl diff --git a/data/templates/accel-ppp/ipoe.config.tmpl b/data/templates/accel-ppp/ipoe.config.tmpl new file mode 100644 index 000000000..84de5bf51 --- /dev/null +++ b/data/templates/accel-ppp/ipoe.config.tmpl @@ -0,0 +1,111 @@ +### generated by ipoe.py ### +[modules] +log_syslog +ipoe +shaper +ipv6pool +ipv6_nd +ipv6_dhcp +ippool +{% if auth_mode == 'radius' %} +radius +{% elif auth_mode == 'local' %} +chap-secrets +{% endif %} + +[core] +thread-count={{ thread_cnt }} + +[log] +syslog=accel-ipoe,daemon +copy=1 +level=5 + +[ipoe] +verbose=1 +{% for interface in interfaces %} +{% if interface.vlan_mon %} +interface=re:{{ interface.name }}\.\d+,{% else %}interface={{ interface.name }},{% endif %}shared={{ interface.shared }},mode={{ interface.mode }},ifcfg={{ interface.ifcfg }},range={{ interface.range }},start={{ interface.sess_start }},ipv6=1 +{% endfor %} +{% if auth_mode == 'noauth' %} +noauth=1 +{% elif auth_mode == 'local' %} +username=ifname +password=csid +{% endif %} + +{%- for interface in interfaces %} +{% if (interface.shared == '0') and (interface.vlan_mon) %} +vlan-mon={{ interface.name }},{{ interface.vlan_mon | join(',') }} +{% endif %} +{% endfor %} + +{% if dnsv4 %} +[dns] +{% for dns in dnsv4 -%} +dns{{ loop.index }}={{ dns }} +{% endfor -%} +{% endif %} + +{% if dnsv6 %} +[ipv6-dns] +{% for dns in dnsv6 -%} +{{ dns }} +{% endfor -%} +{% endif %} + +[ipv6-nd] +verbose=1 + +[ipv6-dhcp] +verbose=1 + +{% if client_ipv6_pool %} +[ipv6-pool] +{% for p in client_ipv6_pool %} +{{ p.prefix }},{{ p.mask }} +{% endfor %} +{% for p in client_ipv6_delegate_prefix %} +delegate={{ p.prefix }},{{ p.mask }} +{% endfor %} +{% endif %} + +{% if auth_mode == 'local' %} +[chap-secrets] +chap-secrets={{ chap_secrets_file }} +{% 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 -%} + +{% if radius_shaper_attr %} +[shaper] +verbose=1 +attr={{ radius_shaper_attr }} +{% if radius_shaper_vendor %} +vendor={{ radius_shaper_vendor }} +{% endif -%} +{% endif -%} +{% endif %} + +[cli] +tcp=127.0.0.1:2002 diff --git a/data/templates/l2tp/l2tp.config.tmpl b/data/templates/accel-ppp/l2tp.config.tmpl index ba78cadcd..8878e3175 100644 --- a/data/templates/l2tp/l2tp.config.tmpl +++ b/data/templates/accel-ppp/l2tp.config.tmpl @@ -85,7 +85,6 @@ 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 -%} -{% endif %} acct-timeout={{ radius_acct_tmo }} timeout={{ radius_timeout }} @@ -100,6 +99,7 @@ nas-ip-address={{ radius_nas_ip }} {% if radius_source_address %} bind={{ radius_source_address }} {% endif -%} +{% endif %} [ppp] verbose=1 @@ -124,7 +124,6 @@ ipv6=allow {% for p in client_ipv6_delegate_prefix %} delegate={{ p.prefix }},{{ p.mask }} {% endfor %} - {% endif %} {% if client_ipv6_delegate_prefix %} @@ -144,4 +143,3 @@ vendor={{ radius_shaper_vendor }} [cli] tcp=127.0.0.1:2004 sessions-columns=ifname,username,calling-sid,ip,{{ ip6_column | join(',') }}{{ ',' if ip6_column }}rate-limit,type,comp,state,rx-bytes,tx-bytes,uptime - diff --git a/data/templates/accel-ppp/pppoe.config.tmpl b/data/templates/accel-ppp/pppoe.config.tmpl new file mode 100644 index 000000000..aece47a66 --- /dev/null +++ b/data/templates/accel-ppp/pppoe.config.tmpl @@ -0,0 +1,197 @@ +### generated by accel_pppoe.py ### +[modules] +log_syslog +pppoe +{% if auth_mode == 'radius' %} +radius +{% endif %} +chap-secrets +ippool +{% if ppp_ipv6 != 'deny' %} +ipv6pool +ipv6_nd +ipv6_dhcp +{% endif %} +{% for proto in auth_proto: %} +{{proto}} +{% endfor%} +shaper +{% if snmp %} +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 subnet in client_ip_subnets %} +{{ subnet }} +{% endfor %} +{% endif %} +{% endif %} + +{% if client_ipv6_pool %} +[ipv6-pool] +{% for p in client_ipv6_pool %} +{{ p.prefix }},{{ p.mask }} +{% endfor %} +{% for p in client_ipv6_delegate_prefix %} +delegate={{ p.prefix }},{{ p.mask }} +{% endfor %} +{% endif %} + +{% if dnsv4 %} +[dns] +{% for dns in dnsv4 -%} +dns{{ loop.index }}={{ dns }} +{% endfor -%} +{% endif %} + +{% if dnsv6 %} +[ipv6-dns] +{% for dns in dnsv6 -%} +{{ dns }} +{% endfor -%} +{% endif %} + +{% if wins %} +[wins] +{% for server in wins -%} +wins{{ loop.index }}={{ server }} +{% endfor -%} +{% endif %} + +{% if auth_mode == 'local' %} +[chap-secrets] +gw-ip-address={{ ppp_gw }} +chap-secrets={{ chap_secrets_file }} +{% 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 %} + +{% if sesscrtl != 'disable' %} +[common] +single-session={{ sesscrtl }} +{% endif %} + +[ppp] +verbose=1 +check-ip=1 +{% if ppp_ccp %} +ccp=1 +{% endif %} +{% if ppp_min_mtu %} +min-mtu={{ ppp_min_mtu }} +{% else %} +min-mtu={{ mtu }} +{% endif %} +{% if ppp_mru %} +mru={{ ppp_mru }} +{% endif %} +mppe={{ ppp_mppe }} +lcp-echo-interval={{ ppp_echo_interval }} +lcp-echo-timeout={{ ppp_echo_timeout }} +lcp-echo-failure={{ ppp_echo_failure }} +{% if ppp_ipv4 %} +ipv4={{ ppp_ipv4 }} +{% endif %} +{% if client_ipv6_pool %} +ipv6=allow +{% endif %} + +{% if ppp_ipv6 %} +ipv6={{ ppp_ipv6 }} +{% if ppp_ipv6_intf_id %} +ipv6-intf-id={{ ppp_ipv6_intf_id }} +{% endif %} +{% if ppp_ipv6_peer_intf_id %} +ipv6-peer-intf-id={{ ppp_ipv6_peer_intf_id }} +{% endif %} +{% if ppp_ipv6_accept_peer_intf_id %} +ipv6-accept-peer-intf-id={{ ppp_ipv6_accept_peer_intf_id }} +{% endif %} +{% endif %} +mtu={{ mtu }} + +[pppoe] +verbose=1 +ac-name={{ concentrator }} + +{% if interfaces %} +{% for interface in interfaces %} +interface={{ interface.name }} +{% if interface.vlans %} +vlan-mon={{ interface.name }},{{ interface.vlans | join(',') }} +interface=re:{{ interface.name }}\.\d+ +{% endif %} +{% endfor -%} +{% endif -%} + +{% if svc_name %} +service-name={{ svc_name|join(',') }} +{% endif -%} + +{% if pado_delay %} +pado-delay={{ pado_delay }} +{% endif %} + +{% if limits_burst or limits_connections or limits_connections %} +[connlimit] +{% if limits_connections %} +limit={{ limits_connections }} +{% endif %} +{% if limits_burst %} +burst={{ limits_burst }} +{% endif %} +{% if limits_timeout %} +timeout={{ limits_timeout }} +{% endif %} +{% endif %} + +[cli] +tcp=127.0.0.1:2001 diff --git a/data/templates/accel-ppp/pptp.config.tmpl b/data/templates/accel-ppp/pptp.config.tmpl new file mode 100644 index 000000000..0bbfc13c5 --- /dev/null +++ b/data/templates/accel-ppp/pptp.config.tmpl @@ -0,0 +1,89 @@ +### generated by accel_pptp.py ### +[modules] +log_syslog +pptp +ippool +{% if auth_mode == 'local' %} +chap-secrets +{% elif auth_mode == 'radius' %} +radius +{% endif -%} +{% for proto in auth_proto %} +{{proto}} +{% endfor %} + +[core] +thread-count={{ thread_cnt }} + +[log] +syslog=accel-pptp,daemon +copy=1 +level=5 + +{% if dnsv4 %} +[dns] +{% for dns in dnsv4 -%} +dns{{ loop.index }}={{ dns }} +{% endfor -%} +{% endif %} + +{% if wins %} +[wins] +{% for server in wins -%} +wins{{ loop.index }}={{ server }} +{% endfor -%} +{% endif %} + + +[pptp] +ifname=pptp%d +{% if outside_addr %} +bind={{ outside_addr }} +{% endif %} +verbose=1 +ppp-max-mtu={{mtu}} +mppe={{ ppp_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 }} + +[ppp] +verbose=5 +check-ip=1 +single-session=replace + +{% if auth_mode == 'local' %} +[chap-secrets] +chap-secrets={{ chap_secrets_file }} +{% 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 -%} +{% endif %} + +[cli] +tcp=127.0.0.1:2003 + diff --git a/data/templates/sstp/sstp.config.tmpl b/data/templates/accel-ppp/sstp.config.tmpl index acdb6c76b..c3dc83429 100644 --- a/data/templates/sstp/sstp.config.tmpl +++ b/data/templates/accel-ppp/sstp.config.tmpl @@ -112,4 +112,3 @@ vendor={{ radius_shaper_vendor }} [cli] tcp=127.0.0.1:2005 - diff --git a/data/templates/dhcp-client/daemon-options.tmpl b/data/templates/dhcp-client/daemon-options.tmpl new file mode 100644 index 000000000..b5a10c3b8 --- /dev/null +++ b/data/templates/dhcp-client/daemon-options.tmpl @@ -0,0 +1 @@ +DHCLIENT_OPTS="-nw -cf {{ conf_file }} -pf {{ pid_file }} -lf {{ lease_file }} {{ '-S' if dhcpv6_prm_only }} {{ '-T' if dhcpv6_temporary }} {{ ifname }}"
diff --git a/data/templates/dhcp-client/ipv4.tmpl b/data/templates/dhcp-client/ipv4.tmpl index 43f273077..ab772b5f6 100644 --- a/data/templates/dhcp-client/ipv4.tmpl +++ b/data/templates/dhcp-client/ipv4.tmpl @@ -1,4 +1,4 @@ -# generated by ifconfig.py +# generated by dhcp.py option rfc3442-classless-static-routes code 121 = array of unsigned integer 8; timeout 60; retry 300; diff --git a/data/templates/dhcp-client/ipv6.tmpl b/data/templates/dhcp-client/ipv6.tmpl index 83db40c5f..be0235add 100644 --- a/data/templates/dhcp-client/ipv6.tmpl +++ b/data/templates/dhcp-client/ipv6.tmpl @@ -1,4 +1,4 @@ -# generated by ifconfig.py +# generated by dhcp.py interface "{{ ifname }}" { request routers, domain-name-servers, domain-name; } diff --git a/data/templates/dhcpv6-server/dhcpdv6.conf.tmpl b/data/templates/dhcpv6-server/dhcpdv6.conf.tmpl index 80d620fcf..d6b0ae935 100644 --- a/data/templates/dhcpv6-server/dhcpdv6.conf.tmpl +++ b/data/templates/dhcpv6-server/dhcpdv6.conf.tmpl @@ -21,7 +21,7 @@ shared-network {{ network.name }} { range6 {{ range.start }} {{ range.stop }}; {%- endfor %} {%- if subnet.domain_search %} - option dhcp6.domain-search {{ subnet.domain_search | join(', ') }}; + option dhcp6.domain-search "{{ subnet.domain_search | join('", "') }}"; {%- endif %} {%- if subnet.lease_def %} default-lease-time {{ subnet.lease_def }}; @@ -51,7 +51,7 @@ shared-network {{ network.name }} { option dhcp6.sip-servers-addresses {{ subnet.sip_address | join(', ') }}; {%- endif %} {%- if subnet.sip_hostname %} - option dhcp6.sip-servers-names {{ subnet.sip_hostname | join(', ') }}; + option dhcp6.sip-servers-names "{{ subnet.sip_hostname | join('", "') }}"; {%- endif %} {%- if subnet.sntp_server %} option dhcp6.sntp-servers {{ subnet.sntp_server | join(', ') }}; diff --git a/data/templates/frr-mcast/static_mcast.frr.tmpl b/data/templates/frr-mcast/static_mcast.frr.tmpl new file mode 100644 index 000000000..86d619ab0 --- /dev/null +++ b/data/templates/frr-mcast/static_mcast.frr.tmpl @@ -0,0 +1,20 @@ +! +{% for route_gr in old_mroute -%} +{% for nh in old_mroute[route_gr] -%} +{% if old_mroute[route_gr][nh] -%} +no ip mroute {{ route_gr }} {{ nh }} {{ old_mroute[route_gr][nh] }} +{% else -%} +no ip mroute {{ route_gr }} {{ nh }} +{% endif -%} +{% endfor -%} +{% endfor -%} +{% for route_gr in mroute -%} +{% for nh in mroute[route_gr] -%} +{% if mroute[route_gr][nh] -%} +ip mroute {{ route_gr }} {{ nh }} {{ mroute[route_gr][nh] }} +{% else -%} +ip mroute {{ route_gr }} {{ nh }} +{% endif -%} +{% endfor -%} +{% endfor -%} +! diff --git a/data/templates/ipoe-server/chap-secrets.tmpl b/data/templates/ipoe-server/chap-secrets.tmpl deleted file mode 100644 index 5e35d5775..000000000 --- a/data/templates/ipoe-server/chap-secrets.tmpl +++ /dev/null @@ -1,18 +0,0 @@ -# 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']}} * {{mac.lower()}} * {{auth['auth_if'][aifc][mac]['down']}}/{{auth['auth_if'][aifc][mac]['up']}} -{% else -%} -{{aifc}} * {{mac.lower()}} * {{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']}} * {{mac.lower()}} * -{% else -%} -{{aifc}} * {{mac.lower()}} * -{% endif -%} -{% endif -%} -{% endfor -%} -{% endfor -%} diff --git a/data/templates/ipoe-server/ipoe.config.tmpl b/data/templates/ipoe-server/ipoe.config.tmpl deleted file mode 100644 index 0a5ee09a6..000000000 --- a/data/templates/ipoe-server/ipoe.config.tmpl +++ /dev/null @@ -1,114 +0,0 @@ -### 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={{chap_secrets_file}} -{% 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/openvpn/server.conf.tmpl b/data/templates/openvpn/server.conf.tmpl index 0f563dc2b..75ab602f8 100644 --- a/data/templates/openvpn/server.conf.tmpl +++ b/data/templates/openvpn/server.conf.tmpl @@ -18,7 +18,7 @@ dev {{ intf }} persist-key iproute /usr/libexec/vyos/system/unpriv-ip -proto {% if 'tcp-active' in protocol -%}tcp6-client{% elif 'tcp-passive' in protocol -%}tcp6-server{% else %}udp6{% endif %} +proto {{ protocol_real }} {%- if local_host %} local {{ local_host }} @@ -98,7 +98,7 @@ ccd-exclusive {%- endif %} keepalive {{ ping_interval }} {{ ping_restart }} -management /tmp/openvpn-mgmt-intf unix +management /run/openvpn/openvpn-mgmt-intf unix {% for route in server_push_route -%} push "route {{ route }}" @@ -233,7 +233,7 @@ auth {{ hash }} {%- endif -%} {%- if auth %} -auth-user-pass /tmp/openvpn-{{ intf }}-pw +auth-user-pass {{ auth_user_pass_file }} auth-retry nointeract {%- endif %} diff --git a/data/templates/pppoe-server/chap-secrets.tmpl b/data/templates/pppoe-server/chap-secrets.tmpl deleted file mode 100644 index 907ac6ed7..000000000 --- a/data/templates/pppoe-server/chap-secrets.tmpl +++ /dev/null @@ -1,10 +0,0 @@ -# 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 deleted file mode 100644 index d44c0aa93..000000000 --- a/data/templates/pppoe-server/pppoe.config.tmpl +++ /dev/null @@ -1,228 +0,0 @@ - -### 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/pptp/chap-secrets.tmpl b/data/templates/pptp/chap-secrets.tmpl deleted file mode 100644 index f93f4607b..000000000 --- a/data/templates/pptp/chap-secrets.tmpl +++ /dev/null @@ -1,6 +0,0 @@ -# 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 deleted file mode 100644 index 2596507af..000000000 --- a/data/templates/pptp/pptp.config.tmpl +++ /dev/null @@ -1,87 +0,0 @@ - -### 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/salt-minion/minion.tmpl b/data/templates/salt-minion/minion.tmpl index 5e50d588c..9369573a4 100644 --- a/data/templates/salt-minion/minion.tmpl +++ b/data/templates/salt-minion/minion.tmpl @@ -12,7 +12,7 @@ # # Prior to changing this value, the master should be stopped and all Salt # caches should be cleared. -hash_type: {{ hash_type }} +hash_type: {{ hash }} ##### Logging settings ##### ########################################## @@ -21,11 +21,7 @@ hash_type: {{ hash_type }} # location. Remote logging works best when configured to use rsyslogd(8) (e.g.: # ``file:///dev/log``), with rsyslogd(8) configured for network logging. The URI # format is: <file|udp|tcp>://<host|socketpath>:<port-if-required>/<log-facility> -#log_file: /var/log/salt/minion -#log_file: file:///dev/log -#log_file: udp://loghost:10514 -# -log_file: {{ log_file }} +log_file: file:///dev/log # The level of messages to send to the console. # One of 'garbage', 'trace', 'debug', info', 'warning', 'error', 'critical'. @@ -58,6 +54,6 @@ id: {{ salt_id }} # The number of minutes between mine updates. -mine_interval: {{ mine_interval }} +mine_interval: {{ interval }} verify_master_pubkey_sign: {{ verify_master_pubkey_sign }} diff --git a/data/templates/sstp/chap-secrets.tmpl b/data/templates/sstp/chap-secrets.tmpl deleted file mode 100644 index dd00d7bd0..000000000 --- a/data/templates/sstp/chap-secrets.tmpl +++ /dev/null @@ -1,10 +0,0 @@ -# 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/system-login/pam_radius_auth.conf.tmpl b/data/templates/system-login/pam_radius_auth.conf.tmpl index 6cff67867..ad196fa3d 100644 --- a/data/templates/system-login/pam_radius_auth.conf.tmpl +++ b/data/templates/system-login/pam_radius_auth.conf.tmpl @@ -10,4 +10,8 @@ priv-lvl 15 mapped_priv_user radius_priv_user + +{% if radius_vrf %} +vrf-name {{ radius_vrf }} +{% endif %} {% endif %} diff --git a/data/templates/wifi/hostapd.conf.tmpl b/data/templates/wifi/hostapd.conf.tmpl index e2fb9ca8f..d6068e4db 100644 --- a/data/templates/wifi/hostapd.conf.tmpl +++ b/data/templates/wifi/hostapd.conf.tmpl @@ -572,6 +572,16 @@ wpa_pairwise={{ sec_wpa_cipher | join(" ") }} {%- endif -%} {% endif %} +{% if sec_wpa_group_cipher -%} +# Optional override for automatic group cipher selection +# This can be used to select a specific group cipher regardless of which +# pairwise ciphers were enabled for WPA and RSN. It should be noted that +# overriding the group cipher with an unexpected value can result in +# interoperability issues and in general, this parameter is mainly used for +# testing purposes. +group_cipher={{ sec_wpa_group_cipher | join(" ") }} +{% 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 diff --git a/debian/control b/debian/control index 7b95b2c75..5c176f40a 100644 --- a/debian/control +++ b/debian/control @@ -89,6 +89,7 @@ Depends: python3, pmacct (>= 1.6.0), python3-certbot-nginx, pppoe, + salt-minion, ${shlibs:Depends}, ${misc:Depends} Description: VyOS configuration scripts and data diff --git a/debian/vyos-1x.install b/debian/vyos-1x.install index dd8eebc0b..599f3f3f5 100644 --- a/debian/vyos-1x.install +++ b/debian/vyos-1x.install @@ -2,6 +2,7 @@ etc/dhcp etc/ppp etc/rsyslog.d etc/systemd +etc/sysctl.d etc/udev etc/vyos lib/ diff --git a/debian/vyos-1x.postinst b/debian/vyos-1x.postinst new file mode 100644 index 000000000..a308401ee --- /dev/null +++ b/debian/vyos-1x.postinst @@ -0,0 +1,21 @@ +#!/bin/sh -e +if ! deb-systemd-helper --quiet was-enabled salt-minion.service; then + # Enables the unit on first installation, creates new + # symlinks on upgrades if the unit file has changed. + deb-systemd-helper disable salt-minion.service >/dev/null || true +fi + +if [ -x "/etc/init.d/salt-minion" ]; then + update-rc.d -f salt-minion remove >/dev/null +fi + +# Add minion user for salt-minion +if ! grep -q '^minion' /etc/passwd; then + adduser --quiet --firstuid 100 --system --disabled-login --ingroup vyattacfg --gecos "salt minion user" --shell /bin/vbash minion + adduser --quiet minion frrvty + adduser --quiet minion sudo + adduser --quiet minion adm + adduser --quiet minion dip + adduser --quiet minion disk + adduser --quiet minion users +fi diff --git a/interface-definitions/dhcpv6-server.xml.in b/interface-definitions/dhcpv6-server.xml.in index 7d4c0de23..4073b46b2 100644 --- a/interface-definitions/dhcpv6-server.xml.in +++ b/interface-definitions/dhcpv6-server.xml.in @@ -126,16 +126,37 @@ <leafNode name="default"> <properties> <help>Default time (in seconds) that will be assigned to a lease</help> + <valueHelp> + <format>1-4294967295</format> + <description>DHCPv6 valid lifetime</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 1-4294967295"/> + </constraint> </properties> </leafNode> <leafNode name="maximum"> <properties> <help>Maximum time (in seconds) that will be assigned to a lease</help> + <valueHelp> + <format>1-4294967295</format> + <description>Maximum lease time in seconds</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 1-4294967295"/> + </constraint> </properties> </leafNode> <leafNode name="minimum"> <properties> <help>Minimum time (in seconds) that will be assigned to a lease</help> + <valueHelp> + <format>1-4294967295</format> + <description>Minimum lease time in seconds</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 1-4294967295"/> + </constraint> </properties> </leafNode> </children> @@ -243,29 +264,24 @@ </tagNode> </children> </node> - <leafNode name="sip-server-address"> + <leafNode name="sip-server"> <properties> <help>IPv6 address of SIP server</help> <valueHelp> <format>ipv6</format> <description>IPv6 address of SIP server</description> </valueHelp> + <valueHelp> + <format>hostname</format> + <description>FQDN of SIP server</description> + </valueHelp> <constraint> <validator name="ipv6-address"/> + <validator name="fqdn"/> </constraint> <multi/> </properties> </leafNode> - <leafNode name="sip-server-name"> - <properties> - <help>SIP server name</help> - <constraint> - <regex>[-_a-zA-Z0-9.]+</regex> - </constraint> - <constraintErrorMessage>Invalid SIP server name. May only contain letters, numbers and .-_</constraintErrorMessage> - <multi/> - </properties> - </leafNode> <leafNode name="sntp-server"> <properties> <help>IPv6 address of an SNTP server for client to use</help> diff --git a/interface-definitions/include/accel-auth-mode.xml.i b/interface-definitions/include/accel-auth-mode.xml.i new file mode 100644 index 000000000..e719112db --- /dev/null +++ b/interface-definitions/include/accel-auth-mode.xml.i @@ -0,0 +1,19 @@ +<leafNode name="mode"> + <properties> + <help>Authentication mode used by this server</help> + <valueHelp> + <format>local</format> + <description>Use local username/password configuration</description> + </valueHelp> + <valueHelp> + <format>radius</format> + <description>Use RADIUS server for user autentication</description> + </valueHelp> + <constraint> + <regex>(local|radius)</regex> + </constraint> + <completionHelp> + <list>local radius</list> + </completionHelp> + </properties> +</leafNode> diff --git a/interface-definitions/include/accel-client-ipv6-pool.xml.in b/interface-definitions/include/accel-client-ipv6-pool.xml.in new file mode 100644 index 000000000..455ada6ef --- /dev/null +++ b/interface-definitions/include/accel-client-ipv6-pool.xml.in @@ -0,0 +1,59 @@ +<node name="client-ipv6-pool"> + <properties> + <help>Pool of client IPv6 addresses</help> + </properties> + <children> + <tagNode name="prefix"> + <properties> + <help>Pool of addresses used to assign to clients</help> + <valueHelp> + <format>ipv6net</format> + <description>IPv6 address and prefix length</description> + </valueHelp> + <constraint> + <validator name="ipv6-prefix"/> + </constraint> + </properties> + <children> + <leafNode name="mask"> + <properties> + <help>Prefix length used for individual client</help> + <valueHelp> + <format><48-128></format> + <description>Client prefix length (default: 64)</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 48-128"/> + </constraint> + </properties> + </leafNode> + </children> + </tagNode> + <tagNode name="delegate"> + <properties> + <help>Subnet used to delegate prefix through DHCPv6-PD (RFC3633)</help> + <valueHelp> + <format>ipv6net</format> + <description>IPv6 address and prefix length</description> + </valueHelp> + <constraint> + <validator name="ipv6-prefix"/> + </constraint> + </properties> + <children> + <leafNode name="delegation-prefix"> + <properties> + <help>Prefix length delegated to client</help> + <valueHelp> + <format><32-64></format> + <description>Delegated prefix length</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 32-64"/> + </constraint> + </properties> + </leafNode> + </children> + </tagNode> + </children> +</node> diff --git a/interface-definitions/include/accel-name-server.xml.in b/interface-definitions/include/accel-name-server.xml.in new file mode 100644 index 000000000..82ed6771d --- /dev/null +++ b/interface-definitions/include/accel-name-server.xml.in @@ -0,0 +1,18 @@ +<leafNode name="name-server"> + <properties> + <help>Domain Name Server (DNS) propagated to client</help> + <valueHelp> + <format>ipv4</format> + <description>Domain Name Server (DNS) IPv4 address</description> + </valueHelp> + <valueHelp> + <format>ipv6</format> + <description>Domain Name Server (DNS) IPv6 address</description> + </valueHelp> + <constraint> + <validator name="ipv4-address"/> + <validator name="ipv6-address"/> + </constraint> + <multi/> + </properties> +</leafNode> diff --git a/interface-definitions/include/accel-radius-additions.xml.in b/interface-definitions/include/accel-radius-additions.xml.in new file mode 100644 index 000000000..227a043cd --- /dev/null +++ b/interface-definitions/include/accel-radius-additions.xml.in @@ -0,0 +1,113 @@ +<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 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 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>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>NAS-Identifier attribute sent to RADIUS</help> + </properties> + </leafNode> + <leafNode name="nas-ip-address"> + <properties> + <help>NAS-IP-Address attribute sent to RADIUS</help> + <constraint> + <validator name="ipv4-address"/> + </constraint> + <valueHelp> + <format>ipv4</format> + <description>NAS-IP-Address attribute</description> + </valueHelp> + </properties> + </leafNode> + <node name="dynamic-author"> + <properties> + <help>Dynamic Authorization Extension/Change of Authorization server</help> + </properties> + <children> + <leafNode name="server"> + <properties> + <help>IP address for Dynamic Authorization Extension server (DM/CoA)</help> + <constraint> + <validator name="ipv4-address"/> + </constraint> + <valueHelp> + <format>ipv4</format> + <description>IPv4 address for aynamic authorization server</description> + </valueHelp> + </properties> + </leafNode> + <leafNode name="port"> + <properties> + <help>Port for Dynamic Authorization Extension server (DM/CoA)</help> + <valueHelp> + <format>number</format> + <description>TCP port</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 1-65535"/> + </constraint> + </properties> + </leafNode> + <leafNode name="key"> + <properties> + <help>Shared secret for Dynamic Authorization Extension server</help> + </properties> + </leafNode> + </children> + </node> + </children> +</node> diff --git a/interface-definitions/include/accel-wins-server.xml.i b/interface-definitions/include/accel-wins-server.xml.i new file mode 100644 index 000000000..461a65ddf --- /dev/null +++ b/interface-definitions/include/accel-wins-server.xml.i @@ -0,0 +1,13 @@ +<leafNode name="wins-server"> + <properties> + <help>Windows Internet Name Service (WINS) servers propagated to client</help> + <valueHelp> + <format>ipv4</format> + <description>Domain Name Server (DNS) IPv4 address</description> + </valueHelp> + <constraint> + <validator name="ipv4-address"/> + </constraint> + <multi/> + </properties> +</leafNode> diff --git a/interface-definitions/include/dhcp-dhcpv6-options.xml.i b/interface-definitions/include/dhcp-dhcpv6-options.xml.i index 104b1fbe0..e4387863b 100644 --- a/interface-definitions/include/dhcp-dhcpv6-options.xml.i +++ b/interface-definitions/include/dhcp-dhcpv6-options.xml.i @@ -23,7 +23,6 @@ <node name="dhcpv6-options"> <properties> <help>DHCPv6 options</help> - <priority>319</priority> </properties> <children> <leafNode name="parameters-only"> diff --git a/interface-definitions/include/interface-hw-id.xml.i b/interface-definitions/include/interface-hw-id.xml.i index cefc9f0a0..318ddd1c4 100644 --- a/interface-definitions/include/interface-hw-id.xml.i +++ b/interface-definitions/include/interface-hw-id.xml.i @@ -1,4 +1,4 @@ -<leafNode name="mac"> +<leafNode name="hw-id"> <properties> <help>Associate Ethernet Interface with given Media Access Control (MAC) address</help> <valueHelp> diff --git a/interface-definitions/include/ipv6-address.xml.i b/interface-definitions/include/ipv6-address.xml.i index 507d5dcc1..34f54e4c1 100644 --- a/interface-definitions/include/ipv6-address.xml.i +++ b/interface-definitions/include/ipv6-address.xml.i @@ -8,14 +8,21 @@ </leafNode> <leafNode name="eui64"> <properties> - <help>ssign IPv6 address using EUI-64 based on MAC address</help> + <help>Prefix for IPv6 address with MAC-based EUI-64</help> <valueHelp> <format>ipv6net</format> - <description>IPv6 address and prefix length</description> + <description>IPv6 network and prefix length</description> </valueHelp> <constraint> <validator name="ipv6-prefix"/> </constraint> + <multi/> + </properties> + </leafNode> + <leafNode name="no-default-link-local"> + <properties> + <help>Remove the default link-local address from the interface</help> + <valueless/> </properties> </leafNode> </children> diff --git a/interface-definitions/interfaces-pseudo-ethernet.xml.in b/interface-definitions/interfaces-pseudo-ethernet.xml.in index c6e61d19a..ea267cf81 100644 --- a/interface-definitions/interfaces-pseudo-ethernet.xml.in +++ b/interface-definitions/interfaces-pseudo-ethernet.xml.in @@ -5,7 +5,7 @@ <tagNode name="pseudo-ethernet" owner="${vyos_conf_scripts_dir}/interfaces-pseudo-ethernet.py"> <properties> <help>Pseudo Ethernet</help> - <priority>319</priority> + <priority>321</priority> <constraint> <regex>^peth[0-9]+$</regex> </constraint> @@ -45,7 +45,7 @@ <help>Physical Interface used for this device</help> <valueHelp> <format>interface</format> - <description>Interface used for VXLAN underlay</description> + <description>Physical interface used for this pseudo device</description> </valueHelp> <completionHelp> <script>${vyos_completion_dir}/list_interfaces.py -t ethernet</script> diff --git a/interface-definitions/interfaces-tunnel.xml.in b/interface-definitions/interfaces-tunnel.xml.in index e1ac60319..a38a73e15 100644 --- a/interface-definitions/interfaces-tunnel.xml.in +++ b/interface-definitions/interfaces-tunnel.xml.in @@ -66,6 +66,14 @@ </constraint> </properties> </leafNode> + <leafNode name="source-interface"> + <properties> + <help>Physical Interface used for underlaying traffic</help> + <completionHelp> + <script>${vyos_completion_dir}/list_interfaces.py</script> + </completionHelp> + </properties> + </leafNode> <leafNode name="6rd-prefix"> <properties> <help>6rd network prefix</help> diff --git a/interface-definitions/interfaces-wireless.xml.in b/interface-definitions/interfaces-wireless.xml.in index a5c6315fa..3edcbb8ff 100644 --- a/interface-definitions/interfaces-wireless.xml.in +++ b/interface-definitions/interfaces-wireless.xml.in @@ -605,22 +605,67 @@ <children> <leafNode name="cipher"> <properties> - <help>Cipher suite for WPA</help> + <help>Cipher suite for WPA unicast packets</help> <completionHelp> - <list>TKIP CCMP</list> + <list>GCMP-256 GCMP CCMP-256 CCMP TKIP</list> </completionHelp> <valueHelp> + <format>GCMP-256</format> + <description>AES in Galois/counter mode with 256-bit key</description> + </valueHelp> + <valueHelp> + <format>GCMP</format> + <description>AES in Galois/counter mode with 128-bit key</description> + </valueHelp> + <valueHelp> + <format>CCMP-256</format> + <description>AES in Counter mode with CBC-MAC with 256-bit key</description> + </valueHelp> + <valueHelp> <format>CCMP</format> - <description>AES in Counter mode with CBC-MAC [RFC 3610, IEEE 802.11i/D7.0]</description> + <description>AES in Counter mode with CBC-MAC [RFC 3610, IEEE 802.11i/D7.0] (supported on all WPA2 APs)</description> </valueHelp> <valueHelp> <format>TKIP</format> <description>Temporal Key Integrity Protocol [IEEE 802.11i/D7.0]</description> </valueHelp> <constraint> - <regex>(CCMP|TKIP)</regex> + <regex>(GCMP-256|GCMP|CCMP-256|CCMP|TKIP)</regex> </constraint> - <constraintErrorMessage>Invalid WEP key</constraintErrorMessage> + <constraintErrorMessage>Invalid cipher selection</constraintErrorMessage> + <multi/> + </properties> + </leafNode> + <leafNode name="group-cipher"> + <properties> + <help>Cipher suite for WPA multicast and broadcast packets</help> + <completionHelp> + <list>GCMP-256 GCMP CCMP-256 CCMP TKIP</list> + </completionHelp> + <valueHelp> + <format>GCMP-256</format> + <description>AES in Galois/counter mode with 256-bit key</description> + </valueHelp> + <valueHelp> + <format>GCMP</format> + <description>AES in Galois/counter mode with 128-bit key</description> + </valueHelp> + <valueHelp> + <format>CCMP-256</format> + <description>AES in Counter mode with CBC-MAC with 256-bit key</description> + </valueHelp> + <valueHelp> + <format>CCMP</format> + <description>AES in Counter mode with CBC-MAC [RFC 3610, IEEE 802.11i/D7.0] (supported on all WPA2 APs)</description> + </valueHelp> + <valueHelp> + <format>TKIP</format> + <description>Temporal Key Integrity Protocol [IEEE 802.11i/D7.0]</description> + </valueHelp> + <constraint> + <regex>(GCMP-256|GCMP|CCMP-256|CCMP|TKIP)</regex> + </constraint> + <constraintErrorMessage>Invalid group cipher selection</constraintErrorMessage> <multi/> </properties> </leafNode> diff --git a/interface-definitions/protocols-multicast.xml.in b/interface-definitions/protocols-multicast.xml.in new file mode 100644 index 000000000..a06f2b287 --- /dev/null +++ b/interface-definitions/protocols-multicast.xml.in @@ -0,0 +1,95 @@ +<?xml version="1.0"?> +<!-- Multicast static routing configuration --> +<interfaceDefinition> + <node name="protocols"> + <children> + <node name="static"> + <children> + <node name="multicast" owner="${vyos_conf_scripts_dir}/protocols_static_multicast.py"> + <properties> + <help>Multicast static route</help> + </properties> + <children> + <tagNode name="route"> + <properties> + <help>Configure static unicast route into MRIB for multicast RPF lookup</help> + <valueHelp> + <format>ipv4net</format> + <description>Network</description> + </valueHelp> + <constraint> + <validator name="ip-prefix"/> + </constraint> + </properties> + <children> + <tagNode name="next-hop"> + <properties> + <help>Nexthop IPv4 address</help> + <valueHelp> + <format>ipv4</format> + <description>Nexthop IPv4 address</description> + </valueHelp> + <constraint> + <validator name="ipv4-address"/> + </constraint> + </properties> + <children> + <leafNode name="distance"> + <properties> + <help>Distance value for this route</help> + <valueHelp> + <format>1-255</format> + <description>Distance for this route</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 1-255"/> + </constraint> + </properties> + </leafNode> + </children> + </tagNode> + </children> + </tagNode> + <tagNode name="interface-route"> + <properties> + <help>Multicast interface based route</help> + <valueHelp> + <format>ipv4net</format> + <description>Network</description> + </valueHelp> + <constraint> + <validator name="ip-prefix"/> + </constraint> + </properties> + <children> + <tagNode name="next-hop-interface"> + <properties> + <help>Next-hop interface</help> + <completionHelp> + <script>${vyos_completion_dir}/list_interfaces.py</script> + </completionHelp> + </properties> + <children> + <leafNode name="distance"> + <properties> + <help>Distance value for this route</help> + <valueHelp> + <format>1-255</format> + <description>Distance for this route</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 1-255"/> + </constraint> + </properties> + </leafNode> + </children> + </tagNode> + </children> + </tagNode> + </children> + </node> + </children> + </node> + </children> + </node> +</interfaceDefinition> diff --git a/interface-definitions/salt-minion.xml.in b/interface-definitions/salt-minion.xml.in index 9aa60249a..97f882a6a 100644 --- a/interface-definitions/salt-minion.xml.in +++ b/interface-definitions/salt-minion.xml.in @@ -1,5 +1,4 @@ <?xml version="1.0"?> -<!--Salt-minion configuration --> <interfaceDefinition> <node name="service"> <children> @@ -9,73 +8,56 @@ <priority>500</priority> </properties> <children> - <leafNode name="hash_type"> + <leafNode name="hash"> <properties> - <help>The hash_type is the hash to use when discovering the hash of a file on the master server.</help> + <help>Hash used when discovering file on master server (default: sha256)</help> + <completionHelp> + <list>md5 sha1 sha224 sha256 sha384 sha512</list> + </completionHelp> + <constraint> + <regex>(md5|sha1|sha224|sha256|sha384|sha512)</regex> + </constraint> </properties> </leafNode> - <leafNode name="log_file"> - <properties> - <help>The location of the minion log file.</help> - </properties> - </leafNode> - <leafNode name="log_level"> + <leafNode name="master"> <properties> - <help>Log level</help> - <valueHelp> - <format>garbage</format> - <description>log garbage info</description> - </valueHelp> - <valueHelp> - <format>trace</format> - <description>log trace info</description> - </valueHelp> - <valueHelp> - <format>debug</format> - <description>log debug info</description> - </valueHelp> - <valueHelp> - <format>info</format> - <description>log info</description> - </valueHelp> - <valueHelp> - <format>warning</format> - <description>log warning info</description> - </valueHelp> + <help>The hostname or IP address of the master.</help> <valueHelp> - <format>error</format> - <description>log error info</description> + <format>ipv4</format> + <description>Remote syslog server IPv4 address</description> </valueHelp> <valueHelp> - <format>critical</format> - <description>log critical info</description> + <format>hostname</format> + <description>Remote syslog server FQDN</description> </valueHelp> - </properties> - </leafNode> - <leafNode name="master"> - <properties> - <help>The hostname or IP address of the master.</help> + <constraint> + <validator name="ip-address"/> + <validator name="fqdn"/> + </constraint> + <constraintErrorMessage>Invalid FQDN or IP address</constraintErrorMessage> <multi/> </properties> </leafNode> <leafNode name="id"> <properties> - <help>Explicitly declare the id for this minion to use.</help> - </properties> - </leafNode> - <leafNode name="user"> - <properties> - <help>The user to run the Salt processes.</help> + <help>Explicitly declare ID for this minion to use (default: hostname)</help> </properties> </leafNode> - <leafNode name="mine_interval"> + <leafNode name="interval"> <properties> - <help>The number of minutes between mine updates.</help> + <help>Interval in minutes between updates (default: 60)</help> + <valueHelp> + <format><1-1440></format> + <description>Update interval in minutes</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 1-1440"/> + </constraint> </properties> </leafNode> <leafNode name="master-key"> <properties> - <help>Enables verification of the master-public-signature returned by the master in auth-replies.</help> + <help>URL with signature of master for auth reply verification</help> </properties> </leafNode> </children> diff --git a/interface-definitions/service-ipoe.xml.in b/interface-definitions/service_ipoe-server.xml.in index 6804469cb..9ee5d5156 100644 --- a/interface-definitions/service-ipoe.xml.in +++ b/interface-definitions/service_ipoe-server.xml.in @@ -2,7 +2,7 @@ <interfaceDefinition> <node name="service"> <children> - <node name="ipoe-server" owner="${vyos_conf_scripts_dir}/service-ipoe.py"> + <node name="ipoe-server" owner="${vyos_conf_scripts_dir}/service_ipoe-server.py"> <properties> <help>Internet Protocol over Ethernet (IPoE) Server</help> <priority>900</priority> @@ -111,79 +111,8 @@ </leafNode> </children> </tagNode> - <node name="dns-server"> - <properties> - <help>DNS servers offered via internal DHCP</help> - </properties> - <children> - <leafNode name="server-1"> - <properties> - <help>IP address of the primary DNS server</help> - <constraint> - <validator name="ipv4-address"/> - </constraint> - </properties> - </leafNode> - <leafNode name="server-2"> - <properties> - <help>IP address of the secondary DNS server</help> - <constraint> - <validator name="ipv4-address"/> - </constraint> - </properties> - </leafNode> - </children> - </node> - <node name="dnsv6-server"> - <properties> - <help>DNSv6 servers offered via internal DHCPv6</help> - </properties> - <children> - <leafNode name="server-1"> - <properties> - <help>IP address of the primary DNS server</help> - <constraint> - <validator name="ipv6-address"/> - </constraint> - </properties> - </leafNode> - <leafNode name="server-2"> - <properties> - <help>IP address of the secondary DNS server</help> - <constraint> - <validator name="ipv6-address"/> - </constraint> - </properties> - </leafNode> - <leafNode name="server-3"> - <properties> - <help>IP address of the tertiary DNS server</help> - <constraint> - <validator name="ipv6-address"/> - </constraint> - </properties> - </leafNode> - </children> - </node> - <node name="client-ipv6-pool"> - <properties> - <help>Pool of client IPv6 addresses</help> - </properties> - <children> - <leafNode name="prefix"> - <properties> - <help>Format: ipv6prefix/mask,prefix_len (e.g.: fc00:0:1::/48,64 - divides prefix into /64 subnets for clients)</help> - <multi/> - </properties> - </leafNode> - <leafNode name="delegate-prefix"> - <properties> - <help>Format: ipv6prefix/mask,prefix_len (delegates prefix to clients via DHCPv6 prefix delegation</help> - <multi/> - </properties> - </leafNode> - </children> - </node> + #include <include/accel-name-server.xml.in> + #include <include/accel-client-ipv6-pool.xml.in> <node name="authentication"> <properties> <help>Client authentication methods</help> @@ -268,107 +197,8 @@ </tagNode> </children> </tagNode> - <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 respond, mark it unavailable for this time (seconds)</help> - </properties> - </leafNode> - </children> - </tagNode> - <node name="radius-settings"> - <properties> - <help>RADIUS settings</help> - </properties> - <children> - <leafNode name="timeout"> - <properties> - <help>Timeout to wait response from server (seconds)</help> - </properties> - </leafNode> - <leafNode name="acct-timeout"> - <properties> - <help>Timeout to wait reply for Interim-Update packets. (default 3 seconds)</help> - </properties> - </leafNode> - <leafNode name="max-try"> - <properties> - <help>Maximum number of tries to send Access-Request/Accounting-Request queries</help> - </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> - </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> - <valueHelp> - <format>ipv4</format> - <description>IPv4 address of the DAE Server</description> - </valueHelp> - <constraint> - <validator name="ipv4-address"/> - </constraint> - </properties> - </leafNode> - <node name="dae-server"> - <properties> - <help>IPv4 address and port to bind Dynamic Authorization Extension server (DM/CoA)</help> - </properties> - <children> - <leafNode name="ip-address"> - <properties> - <help>IP address for Dynamic Authorization Extension server (DM/CoA)</help> - <valueHelp> - <format>ipv4</format> - <description>IPv4 address of the DAE Server</description> - </valueHelp> - <constraint> - <validator name="ipv4-address"/> - </constraint> - </properties> - </leafNode> - <leafNode name="port"> - <properties> - <help>Port for Dynamic Authorization Extension server (DM/CoA)</help> - <valueHelp> - <format>1-65535</format> - <description>port number</description> - </valueHelp> - <constraint> - <validator name="numeric" argument="--range 1-65535"/> - </constraint> - </properties> - </leafNode> - <leafNode name="secret"> - <properties> - <help>Secret for Dynamic Authorization Extension server (DM/CoA)</help> - </properties> - </leafNode> - </children> - </node> - </children> - </node> + #include <include/radius-server.xml.i> + #include <include/accel-radius-additions.xml.in> </children> </node> </children> diff --git a/interface-definitions/service-pppoe.xml.in b/interface-definitions/service_pppoe-server.xml.in index b4950ede1..c7ba2617a 100644 --- a/interface-definitions/service-pppoe.xml.in +++ b/interface-definitions/service_pppoe-server.xml.in @@ -2,7 +2,7 @@ <interfaceDefinition> <node name="service"> <children> - <node name="pppoe-server" owner="${vyos_conf_scripts_dir}/service-pppoe.py"> + <node name="pppoe-server" owner="${vyos_conf_scripts_dir}/service_pppoe-server.py"> <properties> <help>Point to Point over Ethernet (PPPoE) Server</help> <priority>900</priority> @@ -107,103 +107,11 @@ </tagNode> </children> </node> - <leafNode name="mode"> - <properties> - <help>Authentication mode for PPPoE Server</help> - <valueHelp> - <format>local</format> - <description>Use local username/password configuration</description> - </valueHelp> - <valueHelp> - <format>radius</format> - <description>Use a RADIUS server to autenticate users</description> - </valueHelp> - <constraint> - <regex>(local|radius)</regex> - </constraint> - <completionHelp> - <list>local radius</list> - </completionHelp> - </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 amount of time in seconds</help> - </properties> - </leafNode> - </children> - </tagNode> - <node name="radius-settings"> - <properties> - <help>RADIUS settings</help> - </properties> + #include <include/accel-auth-mode.xml.i> + #include <include/radius-server.xml.i> + #include <include/accel-radius-additions.xml.in> + <node name="radius"> <children> - <leafNode name="timeout"> - <properties> - <help>Timeout to wait response from server (seconds)</help> - </properties> - </leafNode> - <leafNode name="acct-timeout"> - <properties> - <help>Timeout to wait reply for Interim-Update packets. (default 3 seconds)</help> - </properties> - </leafNode> - <leafNode name="max-try"> - <properties> - <help>Maximum number of tries to send Access-Request/Accounting-Request queries</help> - </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> - </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> - </properties> - </leafNode> - <node name="dae-server"> - <properties> - <help>IPv4 address and port to bind Dynamic Authorization Extension server (DM/CoA)</help> - </properties> - <children> - <leafNode name="ip-address"> - <properties> - <help>IP address for Dynamic Authorization Extension server (DM/CoA)</help> - </properties> - </leafNode> - <leafNode name="port"> - <properties> - <help>Port for Dynamic Authorization Extension server (DM/CoA)</help> - </properties> - </leafNode> - <leafNode name="secret"> - <properties> - <help>Secret for Dynamic Authorization Extension server (DM/CoA)</help> - </properties> - </leafNode> - </children> - </node> <node name="rate-limit"> <properties> <help>Upload/Download speed limits</help> @@ -229,6 +137,34 @@ </node> </children> </node> + <leafNode name="protocols"> + <properties> + <help>Authentication protocol</help> + <valueHelp> + <format>pap</format> + <description>Allow PAP authentication [Password Authentication Protocol]</description> + </valueHelp> + <valueHelp> + <format>chap</format> + <description>Allow CHAP authentication [Challenge Handshake Authentication Protocol]</description> + </valueHelp> + <valueHelp> + <format>mschap</format> + <description>Allow MS-CHAP authentication [Microsoft Challenge Handshake Authentication Protocol, Version 1]</description> + </valueHelp> + <valueHelp> + <format>mschap-v2</format> + <description>Allow MS-CHAPv2 authentication [Microsoft Challenge Handshake Authentication Protocol, Version 2]</description> + </valueHelp> + <constraint> + <regex>(pap|chap|mschap|mschap-v2)</regex> + </constraint> + <completionHelp> + <list>pap chap mschap mschap-v2</list> + </completionHelp> + <multi /> + </properties> + </leafNode> </children> </node> <node name="client-ip-pool"> @@ -264,100 +200,8 @@ </leafNode> </children> </node> - <node name="client-ipv6-pool"> - <properties> - <help>Pool of client IPv6 addresses</help> - </properties> - <children> - <leafNode name="prefix"> - <properties> - <help>Format: ipv6prefix/mask,prefix_len (e.g.: fc00:0:1::/48,64 - divides prefix into /64 subnets for clients)</help> - <multi /> - </properties> - </leafNode> - <leafNode name="delegate-prefix"> - <properties> - <help>Format: ipv6prefix/mask,prefix_len (delegate to clients through DHCPv6 prefix delegation - rfc3633)</help> - <multi /> - </properties> - </leafNode> - </children> - </node> - <node name="dns-servers"> - <properties> - <help>IPv4 Domain Name Service (DNS) server</help> - </properties> - <children> - <leafNode name="server-1"> - <properties> - <help>Primary DNS server</help> - <valueHelp> - <format>ipv4</format> - <description>IPv4 address</description> - </valueHelp> - <constraint> - <validator name="ipv4-address"/> - </constraint> - </properties> - </leafNode> - <leafNode name="server-2"> - <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> - <node name="dnsv6-servers"> - <properties> - <help>IPv6 Domain Name Service (DNS) server</help> - </properties> - <children> - <leafNode name="server-1"> - <properties> - <valueHelp> - <format>ipv6</format> - <description>IPv6 address</description> - </valueHelp> - <help>Primary DNS server</help> - <constraint> - <validator name="ipv6-address"/> - </constraint> - </properties> - </leafNode> - <leafNode name="server-2"> - <properties> - <valueHelp> - <format>ipv6</format> - <description>IPv6 address</description> - </valueHelp> - <help>Secondary DNS server</help> - <constraint> - <validator name="ipv6-address"/> - </constraint> - </properties> - </leafNode> - <leafNode name="server-3"> - <properties> - <valueHelp> - <format>ipv6</format> - <description>IPv6 address</description> - </valueHelp> - <help>Tertiary DNS server</help> - <constraint> - <validator name="ipv6-address"/> - </constraint> - </properties> - </leafNode> - </children> - </node> - + #include <include/accel-client-ipv6-pool.xml.in> + #include <include/accel-name-server.xml.in> <tagNode name="interface"> <properties> <help>interface(s) to listen on</help> @@ -439,29 +283,7 @@ <multi/> </properties> </leafNode> - <node name="wins-servers"> - <properties> - <help>Windows Internet Name Service (WINS) server settings</help> - </properties> - <children> - <leafNode name="server-1"> - <properties> - <help>Primary WINS server</help> - <constraint> - <validator name="ipv4-address"/> - </constraint> - </properties> - </leafNode> - <leafNode name="server-2"> - <properties> - <help>Secondary WINS server</help> - <constraint> - <validator name="ipv4-address"/> - </constraint> - </properties> - </leafNode> - </children> - </node> + #include <include/accel-wins-server.xml.i> <node name="ppp-options"> <properties> <help>Advanced protocol options</help> diff --git a/interface-definitions/service-router-advert.xml.in b/interface-definitions/service_router-advert.xml.in index bd63b15a3..6a4706ab7 100644 --- a/interface-definitions/service-router-advert.xml.in +++ b/interface-definitions/service_router-advert.xml.in @@ -2,7 +2,7 @@ <interfaceDefinition> <node name="service"> <children> - <node name="router-advert" owner="${vyos_conf_scripts_dir}/service-router-advert.py"> + <node name="router-advert" owner="${vyos_conf_scripts_dir}/service_router-advert.py"> <properties> <help>IPv6 Router Advertisements (RAs) service</help> <priority>900</priority> diff --git a/interface-definitions/system-login.xml.in b/interface-definitions/system-login.xml.in index 2499a192c..053b6babd 100644 --- a/interface-definitions/system-login.xml.in +++ b/interface-definitions/system-login.xml.in @@ -130,6 +130,7 @@ </leafNode> </children> </tagNode> + #include <include/interface-vrf.xml.i> </children> </node> </children> diff --git a/interface-definitions/vpn-l2tp.xml.in b/interface-definitions/vpn_l2tp.xml.in index d4286a810..702ef8b5a 100644 --- a/interface-definitions/vpn-l2tp.xml.in +++ b/interface-definitions/vpn_l2tp.xml.in @@ -36,24 +36,7 @@ </constraint> </properties> </leafNode> - <leafNode name="name-server"> - <properties> - <help>Domain Name Server (DNS) propagated to client</help> - <valueHelp> - <format>ipv4</format> - <description>Domain Name Server (DNS) IPv4 address</description> - </valueHelp> - <valueHelp> - <format>ipv6</format> - <description>Domain Name Server (DNS) IPv6 address</description> - </valueHelp> - <constraint> - <validator name="ipv4-address"/> - <validator name="ipv6-address"/> - </constraint> - <multi/> - </properties> - </leafNode> + #include <include/accel-name-server.xml.in> <node name="lns"> <properties> <help>L2TP Network Server (LNS)</help> @@ -182,19 +165,7 @@ </leafNode> </children> </node> - <leafNode name="wins-server"> - <properties> - <help>Windows Internet Name Service (WINS) servers propagated to client</help> - <valueHelp> - <format>ipv4</format> - <description>Domain Name Server (DNS) IPv4 address</description> - </valueHelp> - <constraint> - <validator name="ipv4-address"/> - </constraint> - <multi/> - </properties> - </leafNode> + #include <include/accel-wins-server.xml.i> <node name="client-ip-pool"> <properties> <help>Pool of client IP addresses (must be within a /24)</help> @@ -232,65 +203,7 @@ </leafNode> </children> </node> - <node name="client-ipv6-pool"> - <properties> - <help>Pool of client IPv6 addresses</help> - </properties> - <children> - <tagNode name="prefix"> - <properties> - <help>Pool of addresses used to assign to clients</help> - <valueHelp> - <format>ipv6net</format> - <description>IPv6 address and prefix length</description> - </valueHelp> - <constraint> - <validator name="ipv6-prefix"/> - </constraint> - </properties> - <children> - <leafNode name="mask"> - <properties> - <help>Prefix length used for individual client</help> - <valueHelp> - <format><48-128></format> - <description>Client prefix length (default: 64)</description> - </valueHelp> - <constraint> - <validator name="numeric" argument="--range 48-128"/> - </constraint> - </properties> - </leafNode> - </children> - </tagNode> - <tagNode name="delegate"> - <properties> - <help>Subnet used to delegate prefix through DHCPv6-PD (RFC3633)</help> - <valueHelp> - <format>ipv6net</format> - <description>IPv6 address and prefix length</description> - </valueHelp> - <constraint> - <validator name="ipv6-prefix"/> - </constraint> - </properties> - <children> - <leafNode name="delegation-prefix"> - <properties> - <help>Prefix length delegated to client</help> - <valueHelp> - <format><32-64></format> - <description>Delegated prefix length</description> - </valueHelp> - <constraint> - <validator name="numeric" argument="--range 32-64"/> - </constraint> - </properties> - </leafNode> - </children> - </tagNode> - </children> - </node> + #include <include/accel-client-ipv6-pool.xml.in> <leafNode name="description"> <properties> <help>Description for L2TP remote-access settings</help> @@ -369,25 +282,7 @@ </completionHelp> </properties> </leafNode> - <leafNode name="mode"> - <properties> - <help>Authentication mode for remote access L2TP VPN</help> - <valueHelp> - <format>local</format> - <description>Use local username/password configuration</description> - </valueHelp> - <valueHelp> - <format>radius</format> - <description>Use a RADIUS server to autenticate users</description> - </valueHelp> - <constraint> - <regex>(local|radius)</regex> - </constraint> - <completionHelp> - <list>local radius</list> - </completionHelp> - </properties> - </leafNode> + #include <include/accel-auth-mode.xml.i> <node name="local-users"> <properties> <help>Local user authentication for remote access L2TP VPN</help> diff --git a/interface-definitions/vpn-pptp.xml.in b/interface-definitions/vpn_pptp.xml.in index 9636c3b39..032455b4d 100644 --- a/interface-definitions/vpn-pptp.xml.in +++ b/interface-definitions/vpn_pptp.xml.in @@ -2,7 +2,7 @@ <interfaceDefinition> <node name="vpn"> <children> - <node name="pptp" owner="${vyos_conf_scripts_dir}/vpn-pptp.py"> + <node name="pptp" owner="${vyos_conf_scripts_dir}/vpn_pptp.py"> <properties> <help>Point to Point Tunneling Protocol (PPTP) Virtual Private Network (VPN)</help> </properties> @@ -28,60 +28,20 @@ </constraint> </properties> </leafNode> - <node name="dns-servers"> + <leafNode name="name-server"> <properties> - <help>IPv4 Domain Name Service (DNS) server</help> - </properties> - <children> - <leafNode name="server-1"> - <properties> - <help>Primary DNS server</help> - <valueHelp> - <format>ipv4</format> - <description>IPv4 address</description> - </valueHelp> - <constraint> - <validator name="ipv4-address"/> - </constraint> - </properties> - </leafNode> - <leafNode name="server-2"> - <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> - <node name="wins-servers"> - <properties> - <help>Windows Internet Name Service (WINS) server settings</help> + <help>Domain Name Server (DNS) propagated to client</help> + <valueHelp> + <format>ipv4</format> + <description>Domain Name Server (DNS) IPv4 address</description> + </valueHelp> + <constraint> + <validator name="ipv4-address"/> + </constraint> + <multi/> </properties> - <children> - <leafNode name="server-1"> - <properties> - <help>Primary WINS server</help> - <constraint> - <validator name="ipv4-address"/> - </constraint> - </properties> - </leafNode> - <leafNode name="server-2"> - <properties> - <help>Secondary WINS server</help> - <constraint> - <validator name="ipv4-address"/> - </constraint> - </properties> - </leafNode> - </children> - </node> + </leafNode> + #include <include/accel-wins-server.xml.i> <node name="client-ip-pool"> <properties> <help>Pool of client IP addresses (must be within a /24)</help> @@ -162,25 +122,7 @@ </completionHelp> </properties> </leafNode> - <leafNode name="mode"> - <properties> - <help>Authentication mode for remote access PPTP VPN</help> - <valueHelp> - <format>local</format> - <description>Use local username/password configuration</description> - </valueHelp> - <valueHelp> - <format>radius</format> - <description>Use a RADIUS server to autenticate users</description> - </valueHelp> - <constraint> - <regex>(local|radius)</regex> - </constraint> - <completionHelp> - <list>local radius</list> - </completionHelp> - </properties> - </leafNode> + #include <include/accel-auth-mode.xml.i> <node name="local-users"> <properties> <help>Local user authentication for remote access PPTP VPN</help> @@ -210,39 +152,8 @@ </tagNode> </children> </node> - <node name="radius"> - <properties> - <help>RADIUS specific configuration</help> - </properties> - <children> - <tagNode name="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="key"> - <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> - </children> - </node> + #include <include/radius-server.xml.i> + #include <include/accel-radius-additions.xml.in> </children> </node> </children> diff --git a/interface-definitions/vpn-sstp.xml.in b/interface-definitions/vpn_sstp.xml.in index b026417b3..7e4471015 100644 --- a/interface-definitions/vpn-sstp.xml.in +++ b/interface-definitions/vpn_sstp.xml.in @@ -66,25 +66,7 @@ </tagNode> </children> </node> - <leafNode name="mode"> - <properties> - <help>Authentication mode for SSTP Server</help> - <valueHelp> - <format>local</format> - <description>Use local username/password configuration</description> - </valueHelp> - <valueHelp> - <format>radius</format> - <description>Use a RADIUS server to autenticate users</description> - </valueHelp> - <constraint> - <regex>(local|radius)</regex> - </constraint> - <completionHelp> - <list>local radius</list> - </completionHelp> - </properties> - </leafNode> + #include <include/accel-auth-mode.xml.i> <leafNode name="protocols"> <properties> <help>Authentication protocol for remote access peer SSTP VPN</help> @@ -114,117 +96,9 @@ </properties> </leafNode> #include <include/radius-server.xml.i> + #include <include/accel-radius-additions.xml.in> <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 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 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>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>NAS-Identifier attribute sent to RADIUS</help> - </properties> - </leafNode> - <leafNode name="nas-ip-address"> - <properties> - <help>NAS-IP-Address attribute sent to RADIUS</help> - <constraint> - <validator name="ipv4-address"/> - </constraint> - <valueHelp> - <format>ipv4</format> - <description>NAS-IP-Address attribute</description> - </valueHelp> - </properties> - </leafNode> - <node name="dynamic-author"> - <properties> - <help>Dynamic Authorization Extension/Change of Authorization server</help> - </properties> - <children> - <leafNode name="server"> - <properties> - <help>IP address for Dynamic Authorization Extension server (DM/CoA)</help> - <constraint> - <validator name="ipv4-address"/> - </constraint> - <valueHelp> - <format>ipv4</format> - <description>IPv4 address for aynamic authorization server</description> - </valueHelp> - </properties> - </leafNode> - <leafNode name="port"> - <properties> - <help>Port for Dynamic Authorization Extension server (DM/CoA)</help> - <valueHelp> - <format>number</format> - <description>TCP port</description> - </valueHelp> - <constraint> - <validator name="numeric" argument="--range 1-65535"/> - </constraint> - </properties> - </leafNode> - <leafNode name="key"> - <properties> - <help>Shared secret for Dynamic Authorization Extension server</help> - </properties> - </leafNode> - </children> - </node> <node name="rate-limit"> <properties> <help>Upload/Download speed limits</help> diff --git a/op-mode-definitions/dhcp.xml b/op-mode-definitions/dhcp.xml index f142cdd0e..2013d0014 100644 --- a/op-mode-definitions/dhcp.xml +++ b/op-mode-definitions/dhcp.xml @@ -149,7 +149,7 @@ <properties> <help>Restart the DHCPv6 server process</help> </properties> - <command>sudo systemctl restart isc-dhcpv6-server.service</command> + <command>sudo systemctl restart isc-dhcp-server6.service</command> </node> <node name="relay-agent"> <properties> diff --git a/op-mode-definitions/openvpn.xml b/op-mode-definitions/openvpn.xml index 53c0157c6..b9cb06dca 100644 --- a/op-mode-definitions/openvpn.xml +++ b/op-mode-definitions/openvpn.xml @@ -59,7 +59,7 @@ <script>sudo ${vyos_completion_dir}/list_openvpn_clients.py --all</script> </completionHelp> </properties> - <command>echo kill $4 | socat - UNIX-CONNECT:/tmp/openvpn-mgmt-intf > /dev/null</command> + <command>echo kill $4 | socat - UNIX-CONNECT:/run/openvpn/openvpn-mgmt-intf > /dev/null</command> </tagNode> <tagNode name="interface"> <properties> diff --git a/op-mode-definitions/show-interfaces-pppoe.xml b/op-mode-definitions/show-interfaces-pppoe.xml index e68d05da9..211ad9808 100644 --- a/op-mode-definitions/show-interfaces-pppoe.xml +++ b/op-mode-definitions/show-interfaces-pppoe.xml @@ -11,7 +11,7 @@ <script>${vyos_completion_dir}/list_pppoe_peers.sh</script> </completionHelp> </properties> - <command>${vyos_op_scripts_dir}/show_interfaces.py pppoe --intf="$4"</command> + <command>${vyos_op_scripts_dir}/show_interfaces.py --intf="$4"</command> <children> <node name="log"> <properties> diff --git a/op-mode-definitions/traceroute.xml b/op-mode-definitions/traceroute.xml index d623fe103..1aea8eef6 100644 --- a/op-mode-definitions/traceroute.xml +++ b/op-mode-definitions/traceroute.xml @@ -1,71 +1,70 @@ <?xml version="1.0"?> <interfaceDefinition> - <node name="traceroute"> + <tagNode name="traceroute"> <properties> <help>Track network path to node</help> + <completionHelp> + <list><hostname> <x.x.x.x> <h:h:h:h:h:h:h:h></list> + </completionHelp> </properties> + <command>/usr/bin/traceroute "$2"</command> + </tagNode> + <node name="traceroute"> <children> - <tagNode name=""> - <properties> - <help>Track network path to specified node</help> - <completionHelp> - <list><hostname> <x.x.x.x> <h:h:h:h:h:h:h:h></list> - </completionHelp> - </properties> - <command>/usr/bin/traceroute $2</command> - </tagNode> <tagNode name="ipv4"> <properties> - <help>Track network path to <hostname|IPv4 address></help> + <help>Explicitly use IPv4 when tracing the path</help> <completionHelp> <list><hostname> <x.x.x.x></list> </completionHelp> </properties> - <command>/usr/bin/traceroute -4 $3</command> + <command>/usr/bin/traceroute -4 "$3"</command> </tagNode> <tagNode name="ipv6"> <properties> - <help>Track network path to <hostname|IPv6 address></help> + <help>Explicitly use IPv6 when tracing the path</help> <completionHelp> <list><hostname> <h:h:h:h:h:h:h:h></list> </completionHelp> </properties> - <command>/usr/bin/traceroute -6 $3</command> + <command>/usr/bin/traceroute -6 "$3"</command> </tagNode> <tagNode name="vrf"> <properties> - <help>Track network path to specified node via given VRF instance</help> + <help>Track network path to specified node via given VRF</help> <completionHelp> <path>vrf name</path> </completionHelp> </properties> <children> + <!-- we need an empty tagNode to pass in a plain fqdn/ip address and + let traceroute decide how to handle this parameter --> <tagNode name=""> <properties> - <help>Track network path to specified node</help> + <help>Track network path to specified node via given VRF</help> <completionHelp> <list><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> + <command>sudo /usr/sbin/ip vrf exec "$3" /usr/bin/traceroute "$4"</command> </tagNode> <tagNode name="ipv4"> <properties> - <help>Track network path to <hostname|IPv4 address></help> + <help>Explicitly use IPv4 when tracing the path via given VRF</help> <completionHelp> <list><hostname> <x.x.x.x></list> </completionHelp> </properties> - <command>sudo ip vrf exec "$3" traceroute -4 "$5"</command> + <command>sudo /usr/sbin/ip vrf exec "$3" /usr/bin/traceroute -4 "$5"</command> </tagNode> <tagNode name="ipv6"> <properties> - <help>Track network path to <hostname|IPv6 address></help> + <help>Explicitly use IPv6 when tracing the path via given VRF</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> + <command>sudo /usr/sbin/ip vrf exec "$3" /usr/bin/traceroute -6 "$5"</command> </tagNode> </children> </tagNode> @@ -75,13 +74,38 @@ <children> <tagNode name="traceroute"> <properties> - <help>Monitor the path to a destination in realtime</help> + <help>Monitor path to destination in realtime</help> <completionHelp> <list><hostname> <x.x.x.x> <h:h:h:h:h:h:h:h></list> </completionHelp> </properties> - <command>/usr/bin/mtr $3</command> + <command>/usr/bin/mtr "$3"</command> </tagNode> + <node name="traceroute"> + <children> + <tagNode name="vrf"> + <properties> + <help>Monitor path to destination in realtime via given VRF</help> + <completionHelp> + <path>vrf name</path> + </completionHelp> + </properties> + <children> + <!-- we need an empty tagNode to pass in a plain fqdn/ip address and + let traceroute decide how to handle this parameter --> + <tagNode name=""> + <properties> + <help>Track network path to specified node via given VRF</help> + <completionHelp> + <list><hostname> <x.x.x.x> <h:h:h:h:h:h:h:h></list> + </completionHelp> + </properties> + <command>sudo /usr/sbin/ip vrf exec "$4" /usr/bin/mtr "$5"</command> + </tagNode> + </children> + </tagNode> + </children> + </node> </children> </node> </interfaceDefinition> diff --git a/python/vyos/airbag.py b/python/vyos/airbag.py index b0565192d..b7838d8a2 100644 --- a/python/vyos/airbag.py +++ b/python/vyos/airbag.py @@ -13,22 +13,30 @@ # 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 import debug from vyos.config import Config +from vyos.logger import syslog from vyos.version import get_version -from vyos.util import run - +from vyos.version import get_full_version_data # we allow to disable the extra logging DISABLE = False +_noteworthy = [] + +def noteworthy(msg): + """ + noteworthy can be use to take note things which we may not want to + report to the user may but be worth including in bug report + if something goes wrong later on + """ + _noteworthy.append(msg) + + # emulate a file object class _IO(object): def __init__(self, std, log): @@ -59,12 +67,19 @@ def bug_report(dtype, value, trace): sys.stdout.flush() sys.stderr.flush() - information = { + information = get_full_version_data() + trace = '\n'.join(format_exception(dtype, value, trace)).replace('\n\n','\n') + note = '' + if _noteworthy: + note = 'noteworthy:\n' + note += '\n'.join(_noteworthy) + + information.update({ 'date': datetime.now().strftime('%Y-%m-%d %H:%M:%S'), - 'version': get_version(), - 'trace': format_exception(dtype, value, trace), + 'trace': trace, 'instructions': COMMUNITY if 'rolling' in get_version() else SUPPORTED, - } + 'note': note, + }) sys.stdout.write(INTRO.format(**information)) sys.stdout.flush() @@ -82,19 +97,14 @@ def intercepter(dtype, value, trace): pdb.pm() -def InterceptingLogger(address, _singleton=[False]): +def InterceptingLogger(_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) + sys.stderr = _IO(sys.stderr, syslog.critical) # lists as default arguments in function is normally dangerous @@ -124,29 +134,46 @@ except: # 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') + InterceptingLogger() InterceptingException(intercepter) # Messages to print +# if the key before the value has not time, syslog takes that as the source of the message FAULT = """\ -Date: {date} -VyOS image: {version} +Report Time: {date} +Image Version: VyOS {version} +Release Train: {release_train} + +Built by: {built_by} +Built on: {built_on} +Build UUID: {build_uuid} +Build Commit ID: {build_git} + +Architecture: {system_arch} +Boot via: {boot_via} +System type: {system_type} + +Hardware vendor: {hardware_vendor} +Hardware model: {hardware_model} +Hardware S/N: {hardware_serial} +Hardware UUID: {hardware_uuid} {trace} +{note} """ INTRO = """\ VyOS had an issue completing a command. -We are sorry that you encountered a problem with VyOS. +We are sorry that you encountered a problem while using 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) +When reporting problems, please include as much information as possible: +- do not obfuscate any data (feel free to contact us privately if your + business policy requires it) - and include all the information presented below """ @@ -163,6 +190,8 @@ COMMUNITY = """\ 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 +- Contact us using the online help desk https://support.vyos.io/ +- Join our community on slack where our users exchange help and advice + https://vyos.slack.com """.strip() diff --git a/python/vyos/configdict.py b/python/vyos/configdict.py index 24fe174d2..e1b704a31 100644 --- a/python/vyos/configdict.py +++ b/python/vyos/configdict.py @@ -18,7 +18,12 @@ A library for retrieving value dicts from VyOS configs in a declarative fashion. """ +from enum import Enum +from copy import deepcopy + from vyos import ConfigError +from vyos.ifconfig import Interface + def retrieve_config(path_hash, base_path, config): """ @@ -98,171 +103,338 @@ def get_ethertype(ethertype_val): raise ConfigError('invalid ethertype "{}"'.format(ethertype_val)) -def vlan_to_dict(conf): +vlan_default = { + 'address': [], + 'address_remove': [], + 'description': '', + 'dhcp_client_id': '', + 'dhcp_hostname': '', + 'dhcp_vendor_class_id': '', + 'dhcpv6_prm_only': False, + 'dhcpv6_temporary': False, + 'disable': False, + 'disable_link_detect': 1, + 'egress_qos': '', + 'egress_qos_changed': False, + 'ip_disable_arp_filter': 1, + 'ip_enable_arp_accept': 0, + 'ip_enable_arp_announce': 0, + 'ip_enable_arp_ignore': 0, + 'ip_proxy_arp': 0, + 'ipv6_autoconf': 0, + 'ipv6_eui64_prefix': [], + 'ipv6_eui64_prefix_remove': [], + 'ipv6_forwarding': 1, + 'ipv6_dup_addr_detect': 1, + 'ingress_qos': '', + 'ingress_qos_changed': False, + 'mac': '', + 'mtu': 1500, + 'vif_c': [], + 'vif_c_remove': [], + 'vrf': '' +} + +# see: https://docs.python.org/3/library/enum.html#functional-api +disable = Enum('disable','none was now both') + +def disable_state(conf, check=[3,5,7]): + """ + return if and how a particual section of the configuration is has disable'd + using "disable" including if it was disabled by one of its parent. + + check: a list of the level we should check, here 7,5 and 3 + interfaces ethernet eth1 vif-s 1 vif-c 2 disable + interfaces ethernet eth1 vif 1 disable + interfaces ethernet eth1 disable + + it returns an enum (none, was, now, both) + """ + + # save where we are in the config + current_level = conf.get_level() + + # logic to figure out if the interface (or one of it parent is disabled) + eff_disable = False + act_disable = False + + levels = check[:] + working_level = current_level[:] + + while levels: + position = len(working_level) + if not position: + break + if position not in levels: + working_level = working_level[:-1] + continue + + levels.remove(position) + conf.set_level(working_level) + working_level = working_level[:-1] + + eff_disable = eff_disable or conf.exists_effective('disable') + act_disable = act_disable or conf.exists('disable') + + conf.set_level(current_level) + + # how the disabling changed + if eff_disable and act_disable: + return disable.both + if eff_disable and not eff_disable: + return disable.was + if not eff_disable and act_disable: + return disable.now + return disable.none + + +def intf_to_dict(conf, default): """ Common used function which will extract VLAN related information from config and represent the result as Python dictionary. Function call's itself recursively if a vif-s/vif-c pair is detected. """ - vlan = { - 'id': conf.get_level()[-1], # get the '100' in 'interfaces bonding bond0 vif-s 100' - 'address': [], - 'address_remove': [], - 'description': '', - 'dhcp_client_id': '', - 'dhcp_hostname': '', - 'dhcp_vendor_class_id': '', - 'dhcpv6_prm_only': False, - 'dhcpv6_temporary': False, - 'disable': False, - 'disable_link_detect': 1, - 'egress_qos': '', - 'egress_qos_changed': False, - 'ip_disable_arp_filter': 1, - 'ip_enable_arp_accept': 0, - 'ip_enable_arp_announce': 0, - 'ip_enable_arp_ignore': 0, - 'ip_proxy_arp': 0, - 'ipv6_autoconf': 0, - 'ipv6_forwarding': 1, - 'ipv6_dup_addr_detect': 1, - 'ingress_qos': '', - 'ingress_qos_changed': False, - 'mac': '', - 'mtu': 1500, - 'vrf': '' - } + + intf = deepcopy(default) + # retrieve configured interface addresses if conf.exists('address'): - vlan['address'] = conf.return_values('address') - - # Determine interface addresses (currently effective) - to determine which - # address is no longer valid and needs to be removed from the bond - eff_addr = conf.return_effective_values('address') - act_addr = conf.return_values('address') - vlan['address_remove'] = list_diff(eff_addr, act_addr) + intf['address'] = conf.return_values('address') # retrieve interface description if conf.exists('description'): - vlan['description'] = conf.return_value('description') + intf['description'] = conf.return_value('description') # get DHCP client identifier if conf.exists('dhcp-options client-id'): - vlan['dhcp_client_id'] = conf.return_value('dhcp-options client-id') + intf['dhcp_client_id'] = conf.return_value('dhcp-options client-id') # DHCP client host name (overrides the system host name) if conf.exists('dhcp-options host-name'): - vlan['dhcp_hostname'] = conf.return_value('dhcp-options host-name') + intf['dhcp_hostname'] = conf.return_value('dhcp-options host-name') # DHCP client vendor identifier if conf.exists('dhcp-options vendor-class-id'): - vlan['dhcp_vendor_class_id'] = conf.return_value('dhcp-options vendor-class-id') + intf['dhcp_vendor_class_id'] = conf.return_value( + 'dhcp-options vendor-class-id') # DHCPv6 only acquire config parameters, no address if conf.exists('dhcpv6-options parameters-only'): - vlan['dhcpv6_prm_only'] = True + intf['dhcpv6_prm_only'] = True # DHCPv6 temporary IPv6 address if conf.exists('dhcpv6-options temporary'): - vlan['dhcpv6_temporary'] = True + intf['dhcpv6_temporary'] = True # ignore link state changes if conf.exists('disable-link-detect'): - vlan['disable_link_detect'] = 2 - - # disable VLAN interface - if conf.exists('disable'): - vlan['disable'] = True + intf['disable_link_detect'] = 2 # ARP filter configuration if conf.exists('ip disable-arp-filter'): - vlan['ip_disable_arp_filter'] = 0 + intf['ip_disable_arp_filter'] = 0 # ARP enable accept if conf.exists('ip enable-arp-accept'): - vlan['ip_enable_arp_accept'] = 1 + intf['ip_enable_arp_accept'] = 1 # ARP enable announce if conf.exists('ip enable-arp-announce'): - vlan['ip_enable_arp_announce'] = 1 + intf['ip_enable_arp_announce'] = 1 # ARP enable ignore if conf.exists('ip enable-arp-ignore'): - vlan['ip_enable_arp_ignore'] = 1 + intf['ip_enable_arp_ignore'] = 1 # Enable Proxy ARP if conf.exists('ip enable-proxy-arp'): - vlan['ip_proxy_arp'] = 1 + intf['ip_proxy_arp'] = 1 # Enable acquisition of IPv6 address using stateless autoconfig (SLAAC) if conf.exists('ipv6 address autoconf'): - vlan['ipv6_autoconf'] = 1 + intf['ipv6_autoconf'] = 1 + + # Get prefixes for IPv6 addressing based on MAC address (EUI-64) + if conf.exists('ipv6 address eui64'): + intf['ipv6_eui64_prefix'] = conf.return_values('ipv6 address eui64') # 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')) + intf['ipv6_forwarding'] = 0 # Media Access Control (MAC) address if conf.exists('mac'): - vlan['mac'] = conf.return_value('mac') + intf['mac'] = conf.return_value('mac') + + # IPv6 Duplicate Address Detection (DAD) tries + if conf.exists('ipv6 dup-addr-detect-transmits'): + intf['ipv6_dup_addr_detect'] = int( + conf.return_value('ipv6 dup-addr-detect-transmits')) # Maximum Transmission Unit (MTU) if conf.exists('mtu'): - vlan['mtu'] = int(conf.return_value('mtu')) + intf['mtu'] = int(conf.return_value('mtu')) # retrieve VRF instance if conf.exists('vrf'): - vlan['vrf'] = conf.return_value('vrf') + intf['vrf'] = conf.return_value('vrf') - # VLAN egress QoS + # egress QoS if conf.exists('egress-qos'): - vlan['egress_qos'] = conf.return_value('egress-qos') + intf['egress_qos'] = conf.return_value('egress-qos') # egress changes QoS require VLAN interface recreation if conf.return_effective_value('egress-qos'): - if vlan['egress_qos'] != conf.return_effective_value('egress-qos'): - vlan['egress_qos_changed'] = True + if intf['egress_qos'] != conf.return_effective_value('egress-qos'): + intf['egress_qos_changed'] = True - # VLAN ingress QoS + # ingress QoS if conf.exists('ingress-qos'): - vlan['ingress_qos'] = conf.return_value('ingress-qos') + intf['ingress_qos'] = conf.return_value('ingress-qos') # ingress changes QoS require VLAN interface recreation if conf.return_effective_value('ingress-qos'): - if vlan['ingress_qos'] != conf.return_effective_value('ingress-qos'): - vlan['ingress_qos_changed'] = True + if intf['ingress_qos'] != conf.return_effective_value('ingress-qos'): + intf['ingress_qos_changed'] = True - # ethertype is mandatory on vif-s nodes and only exists here! - # check if this is a vif-s node at all: - if conf.get_level()[-2] == 'vif-s': - vlan['vif_c'] = [] - vlan['vif_c_remove'] = [] - - # ethertype uses a default of 0x88A8 - tmp = '0x88A8' - if conf.exists('ethertype'): - tmp = conf.return_value('ethertype') - vlan['ethertype'] = get_ethertype(tmp) - - # get vif-c interfaces (currently effective) - to determine which vif-c + disabled = disable_state(conf) + + # Get the interface IPs + eff_addr = conf.return_effective_values('address') + act_addr = conf.return_values('address') + + # Get prefixes for IPv6 addressing based on MAC address (EUI-64) + eff_eui = conf.return_effective_values('ipv6 address eui64') + act_eui = conf.return_values('ipv6 address eui64') + + # Determine what should stay or be removed + if disabled == disable.both: + # was and is still disabled + intf['disable'] = True + intf['address'] = [] + intf['address_remove'] = [] + intf['ipv6_eui64_prefix'] = [] + intf['ipv6_eui64_prefix_remove'] = [] + elif disabled == disable.now: + # it is now disable but was not before + intf['disable'] = True + intf['address'] = [] + intf['address_remove'] = eff_addr + intf['ipv6_eui64_prefix'] = [] + intf['ipv6_eui64_prefix_remove'] = eff_eui + elif disabled == disable.was: + # it was disable but not anymore + intf['disable'] = False + intf['address'] = act_addr + intf['address_remove'] = [] + intf['ipv6_eui64_prefix'] = act_eui + intf['ipv6_eui64_prefix_remove'] = [] + else: + # normal change + intf['disable'] = False + intf['address'] = act_addr + intf['address_remove'] = list_diff(eff_addr, act_addr) + intf['ipv6_eui64_prefix'] = act_eui + intf['ipv6_eui64_prefix_remove'] = list_diff(eff_eui, act_eui) + + # Remove the default link-local address if set. + if conf.exists('ipv6 address no-default-link-local'): + intf['ipv6_eui64_prefix_remove'].append('fe80::/64') + else: + # add the link-local by default to make IPv6 work + intf['ipv6_eui64_prefix'].append('fe80::/64') + + # Find out if MAC has changed + try: + interface = Interface(intf['intf'], create=False) + if intf['mac'] and intf['mac'] != interface.get_mac(): + intf['ipv6_eui64_prefix_remove'] += intf['ipv6_eui64_prefix'] + except Exception: + # If the interface does not exists, it can not have changed + pass + + return intf, disable + + + +def add_to_dict(conf, disabled, ifdict, section, key): + """ + parse a section of vif/vif-s/vif-c and add them to the dict + follow the convention to: + * use the "key" for what to add + * use the "key" what what to remove + + conf: is the Config() already at the level we need to parse + disabled: is a disable enum so we know how to handle to data + intf: if the interface dictionary + section: is the section name to parse (vif/vif-s/vif-c) + key: is the dict key to use (vif/vifs/vifc) + """ + + if not conf.exists(section): + return ifdict + + effect = conf.list_effective_nodes(section) + active = conf.list_nodes(section) + + # the section to parse for vlan + sections = [] + + # Determine interface addresses (currently effective) - to determine which + # address is no longer valid and needs to be removed from the bond + if disabled == disable.both: + # was and is still disabled + ifdict[f'{key}_remove'] = [] + elif disabled == disable.now: + # it is now disable but was not before + ifdict[f'{key}_remove'] = effect + elif disabled == disable.was: + # it was disable but not anymore + ifdict[f'{key}_remove'] = [] + sections = active + else: + # normal change + # get 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-c') - act_intf = conf.list_nodes('vif-c') - vlan['vif_c_remove'] = list_diff(eff_intf, act_intf) - - # check if there is a Q-in-Q vlan customer interface - # and call this function recursively - if conf.exists('vif-c'): - cfg_level = conf.get_level() - # add new key (vif-c) to dictionary - for vif in conf.list_nodes('vif-c'): - # set config level to vif interface - conf.set_level(cfg_level + ['vif-c', vif]) - vlan['vif_c'].append(vlan_to_dict(conf)) + ifdict[f'{key}_remove'] = list_diff(effect, active) + sections = active + + current_level = conf.get_level() + + # add each section, the key must already exists + for s in sections: + # set config level to vif interface + conf.set_level(current_level + [section, s]) + ifdict[f'{key}'].append(vlan_to_dict(conf)) + + # re-set configuration level to leave things as found + conf.set_level(current_level) + + return ifdict + + +def vlan_to_dict(conf, default=vlan_default): + vlan, disabled = intf_to_dict(conf, default) + # get the '100' in 'interfaces bonding bond0 vif-s 100 + vlan['id'] = conf.get_level()[-1] + + current_level = conf.get_level() + + # if this is a not within vif-s node, we are done + if current_level[-2] != 'vif-s': + return vlan + + # ethertype is mandatory on vif-s nodes and only exists here! + # ethertype uses a default of 0x88A8 + tmp = '0x88A8' + if conf.exists('ethertype'): + tmp = conf.return_value('ethertype') + vlan['ethertype'] = get_ethertype(tmp) + + # check if there is a Q-in-Q vlan customer interface + # and call this function recursively + + add_to_dict(conf, disable, vlan, 'vif-c', 'vif_c') return vlan diff --git a/python/vyos/configsession.py b/python/vyos/configsession.py index aaf08e726..f2524b37e 100644 --- a/python/vyos/configsession.py +++ b/python/vyos/configsession.py @@ -181,11 +181,11 @@ class ConfigSession(object): out = self.__run_command(REMOVE_IMAGE + [name]) return out - def generate(self, cmd): - out = self.__run_command(GENERATE + cmd.split()) + def generate(self, path): + out = self.__run_command(GENERATE + path) return out - def show(self, cmd): - out = self.__run_command(SHOW + cmd.split()) + def show(self, path): + out = self.__run_command(SHOW + path) return out diff --git a/python/vyos/configtree.py b/python/vyos/configtree.py index a0b0eb3c1..d8ffaca99 100644 --- a/python/vyos/configtree.py +++ b/python/vyos/configtree.py @@ -10,7 +10,7 @@ # 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, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +# if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA import re import json @@ -200,7 +200,7 @@ class ConfigTree(object): raise ConfigTreeError() res = self.__rename(self.__config, path_str, newname_str) if (res != 0): - raise ConfigTreeError("Path [{}] doesn't exist".format(oldpath)) + raise ConfigTreeError("Path [{}] doesn't exist".format(path)) def copy(self, old_path, new_path): check_path(old_path) @@ -213,7 +213,7 @@ class ConfigTree(object): raise ConfigTreeError() res = self.__copy(self.__config, oldpath_str, newpath_str) if (res != 0): - raise ConfigTreeError("Path [{}] doesn't exist".format(oldpath)) + raise ConfigTreeError("Path [{}] doesn't exist".format(old_path)) def exists(self, path): check_path(path) diff --git a/python/vyos/debug.py b/python/vyos/debug.py index 20090fb85..1a042cbb4 100644 --- a/python/vyos/debug.py +++ b/python/vyos/debug.py @@ -41,8 +41,9 @@ def message(message, flag='', destination=sys.stdout): try: # at boot the file is created as root:vyattacfg # at runtime the file is created as user:vyattacfg - # the default permission are 644 - mask = os.umask(0o113) + # but the helper scripts are not run as this so it + # need the default permission to be 666 (an not 660) + mask = os.umask(0o111) with open(logfile, 'a') as f: f.write(_format('log', message)) @@ -133,7 +134,7 @@ def _contentenv(flag): return os.environ.get(f'VYOS_{flag.upper()}_DEBUG', '').strip() -def _contentfile(flag): +def _contentfile(flag, default=''): """ Check if debug exist for a given debug flag name @@ -153,7 +154,8 @@ def _contentfile(flag): if not os.path.isfile(flagfile): continue with open(flagfile) as f: - return f.readline().strip() + content = f.readline().strip() + return content or default return '' @@ -166,7 +168,7 @@ def _logfile(flag, default): """ # For log we return the location of the log file - log_location = _contentenv(flag) or _contentfile(flag) + log_location = _contentenv(flag) or _contentfile(flag, default) # it was not set if not log_location: @@ -177,6 +179,15 @@ def _logfile(flag, default): not log_location.startswith('/config/') and \ not log_location.startswith('/var/log/'): return default + # Do not allow to escape the folders if '..' in log_location: return default + + if not os.path.exists(log_location): + return log_location + + # this permission is unique the the config and var folder + stat = os.stat(log_location).st_mode + if stat != 0o100666: + return default return log_location diff --git a/python/vyos/ifconfig/dhcp.py b/python/vyos/ifconfig/dhcp.py index d4ff9c2cd..bf6566c07 100644 --- a/python/vyos/ifconfig/dhcp.py +++ b/python/vyos/ifconfig/dhcp.py @@ -19,28 +19,20 @@ from vyos.dicts import FixedDict from vyos.ifconfig.control import Control from vyos.template import render +config_base = r'/var/lib/dhcp/dhclient_' -class _DHCP (Control): - client_base = r'/var/lib/dhcp/dhclient_' - - def __init__(self, ifname, version, **kargs): - super().__init__(**kargs) - self.version = version - self.file = { - 'ifname': ifname, - 'conf': self.client_base + ifname + '.' + version + 'conf', - 'pid': self.client_base + ifname + '.' + version + 'pid', - 'lease': self.client_base + ifname + '.' + version + 'leases', - } - -class _DHCPv4 (_DHCP): +class _DHCPv4 (Control): def __init__(self, ifname): - super().__init__(ifname, '') + super().__init__() self.options = FixedDict(**{ 'ifname': ifname, 'hostname': '', 'client_id': '', - 'vendor_class_id': '' + 'vendor_class_id': '', + 'conf_file': config_base + f'{ifname}.conf', + 'options_file': config_base + f'{ifname}.options', + 'pid_file': config_base + f'{ifname}.pid', + 'lease_file': config_base + f'{ifname}.leases', }) # replace dhcpv4/v6 with systemd.networkd? @@ -55,25 +47,16 @@ class _DHCPv4 (_DHCP): >>> j = Interface('eth0') >>> j.dhcp.v4.set() """ - if not self.options['hostname']: # read configured system hostname. # maybe change to vyos hostd client ??? with open('/etc/hostname', 'r') as f: self.options['hostname'] = f.read().rstrip('\n') - render(self.file['conf'], 'dhcp-client/ipv4.tmpl' ,self.options) + render(self.options['options_file'], 'dhcp-client/daemon-options.tmpl', self.options) + render(self.options['conf_file'], 'dhcp-client/ipv4.tmpl', self.options) - cmd = 'start-stop-daemon' - cmd += ' --start' - cmd += ' --oknodo' - cmd += ' --quiet' - cmd += ' --pidfile {pid}' - cmd += ' --exec /sbin/dhclient' - cmd += ' --' - # now pass arguments to dhclient binary - cmd += ' -4 -nw -cf {conf} -pf {pid} -lf {lease} {ifname}' - return self._cmd(cmd.format(**self.file)) + return self._cmd('systemctl restart dhclient@{ifname}.service'.format(**self.options)) def delete(self): """ @@ -86,44 +69,29 @@ class _DHCPv4 (_DHCP): >>> j = Interface('eth0') >>> j.dhcp.v4.delete() """ - if not os.path.isfile(self.file['pid']): + if not os.path.isfile(self.options['pid_file']): self._debug_msg('No DHCP client PID found') return None - # with open(self.file['pid'], 'r') as f: - # pid = int(f.read()) - - # stop dhclient, we need to call dhclient and tell it should release the - # aquired IP address. tcpdump tells me: - # 172.16.35.103.68 > 172.16.35.254.67: [bad udp cksum 0xa0cb -> 0xb943!] BOOTP/DHCP, Request from 00:50:56:9d:11:df, length 300, xid 0x620e6946, Flags [none] (0x0000) - # Client-IP 172.16.35.103 - # Client-Ethernet-Address 00:50:56:9d:11:df - # Vendor-rfc1048 Extensions - # Magic Cookie 0x63825363 - # DHCP-Message Option 53, length 1: Release - # Server-ID Option 54, length 4: 172.16.35.254 - # Hostname Option 12, length 10: "vyos" - # - cmd = '/sbin/dhclient -cf {conf} -pf {pid} -lf {lease} -r {ifname}' - self._cmd(cmd.format(**self.file)) + self._cmd('systemctl stop dhclient@{ifname}.service'.format(**self.options)) # cleanup old config files - for name in ('conf', 'pid', 'lease'): - if os.path.isfile(self.file[name]): - os.remove(self.file[name]) + for name in ('conf_file', 'options_file', 'pid_file', 'lease_file'): + if os.path.isfile(self.options[name]): + os.remove(self.options[name]) - -class _DHCPv6 (_DHCP): +class _DHCPv6 (Control): def __init__(self, ifname): - super().__init__(ifname, 'v6') + super().__init__() self.options = FixedDict(**{ 'ifname': ifname, + 'conf_file': config_base + f'v6_{ifname}.conf', + 'options_file': config_base + f'v6_{ifname}.options', + 'pid_file': config_base + f'v6_{ifname}.pid', + 'lease_file': config_base + f'v6_{ifname}.leases', 'dhcpv6_prm_only': False, 'dhcpv6_temporary': False, }) - self.file.update({ - 'accept_ra': f'/proc/sys/net/ipv6/conf/{ifname}/accept_ra', - }) def set(self): """ @@ -134,7 +102,7 @@ class _DHCPv6 (_DHCP): >>> from vyos.ifconfig import Interface >>> j = Interface('eth0') - >>> j.set_dhcpv6() + >>> j.dhcp.v6.set() """ # better save then sorry .. should be checked in interface script @@ -143,29 +111,13 @@ class _DHCPv6 (_DHCP): raise Exception( 'DHCPv6 temporary and parameters-only options are mutually exclusive!') - render(self.file['conf'], 'dhcp-client/ipv6.tmpl', self.options) + render(self.options['options_file'], 'dhcp-client/daemon-options.tmpl', self.options) + render(self.options['conf_file'], 'dhcp-client/ipv6.tmpl', self.options) # no longer accept router announcements on this interface - self._write_sysfs(self.file['accept_ra'], 0) - - # assemble command-line to start DHCPv6 client (dhclient) - cmd = 'start-stop-daemon' - cmd += ' --start' - cmd += ' --oknodo' - cmd += ' --quiet' - cmd += ' --pidfile {pid}' - cmd += ' --exec /sbin/dhclient' - cmd += ' --' - # now pass arguments to dhclient binary - cmd += ' -6 -nw -cf {conf} -pf {pid} -lf {lease}' - # add optional arguments - if self.options['dhcpv6_prm_only']: - cmd += ' -S' - if self.options['dhcpv6_temporary']: - cmd += ' -T' - cmd += ' {ifname}' - - return self._cmd(cmd.format(**self.file)) + self._write_sysfs('/proc/sys/net/ipv6/conf/{ifname}/accept_ra'.format(**self.options), 0) + + return self._cmd('systemctl restart dhclient6@{ifname}.service'.format(**self.options)) def delete(self): """ @@ -176,33 +128,24 @@ class _DHCPv6 (_DHCP): >>> from vyos.ifconfig import Interface >>> j = Interface('eth0') - >>> j.del_dhcpv6() + >>> j.dhcp.v6.delete() """ - if not os.path.isfile(self.file['pid']): + if not os.path.isfile(self.options['pid_file']): self._debug_msg('No DHCPv6 client PID found') return None - # with open(self.file['pid'], 'r') as f: - # pid = int(f.read()) - - # stop dhclient - cmd = 'start-stop-daemon' - cmd += ' --start' - cmd += ' --oknodo' - cmd += ' --quiet' - cmd += ' --pidfile {pid}' - self._cmd(cmd.format(**self.file)) + self._cmd('systemctl stop dhclient6@{ifname}.service'.format(**self.options)) # accept router announcements on this interface - self._write_sysfs(self.options['accept_ra'], 1) + self._write_sysfs('/proc/sys/net/ipv6/conf/{ifname}/accept_ra'.format(**self.options), 1) # cleanup old config files - for name in ('conf', 'pid', 'lease'): - if os.path.isfile(self.file[name]): - os.remove(self.file[name]) + for name in ('conf_file', 'options_file', 'pid_file', 'lease_file'): + if os.path.isfile(self.options[name]): + os.remove(self.options[name]) -class DHCP (object): +class DHCP(object): def __init__(self, ifname): self.v4 = _DHCPv4(ifname) self.v6 = _DHCPv6(ifname) diff --git a/python/vyos/ifconfig/ethernet.py b/python/vyos/ifconfig/ethernet.py index 542de4f59..5b18926c9 100644 --- a/python/vyos/ifconfig/ethernet.py +++ b/python/vyos/ifconfig/ethernet.py @@ -40,6 +40,7 @@ class EthernetIf(Interface): 'bondable': True, 'broadcast': True, 'bridgeable': True, + 'eternal': '(lan|eth|eno|ens|enp|enx)[0-9]+$', } } @@ -76,10 +77,6 @@ class EthernetIf(Interface): }, }} - 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 diff --git a/python/vyos/ifconfig/geneve.py b/python/vyos/ifconfig/geneve.py index 0c1cdade9..145dc268c 100644 --- a/python/vyos/ifconfig/geneve.py +++ b/python/vyos/ifconfig/geneve.py @@ -35,6 +35,8 @@ class GeneveIf(Interface): 'vni': 0, 'remote': '', } + options = Interface.options + \ + ['vni', 'remote'] definition = { **Interface.definition, **{ diff --git a/python/vyos/ifconfig/interface.py b/python/vyos/ifconfig/interface.py index 32ce1a80c..de5ca369f 100644 --- a/python/vyos/ifconfig/interface.py +++ b/python/vyos/ifconfig/interface.py @@ -1,4 +1,4 @@ -# Copyright 2019 VyOS maintainers and contributors <maintainers@vyos.io> +# Copyright 2019-2020 VyOS maintainers and contributors <maintainers@vyos.io> # # This library is free software; you can redistribute it and/or # modify it under the terms of the GNU Lesser General Public @@ -14,6 +14,7 @@ # License along with this library. If not, see <http://www.gnu.org/licenses/>. import os +import re import json from copy import deepcopy @@ -49,7 +50,7 @@ class Interface(Control): # WireGuard to modify their display behaviour OperationalClass = Operational - options = [] + options = ['debug', 'create',] required = [] default = { 'type': '', @@ -63,6 +64,7 @@ class Interface(Control): 'bondable': False, 'broadcast': False, 'bridgeable': False, + 'eternal': '', } _command_get = { @@ -217,7 +219,7 @@ class Interface(Control): else: raise Exception('interface "{}" not found'.format(self.config['ifname'])) - # list of assigned IP addresses + # temporary list of assigned IP addresses self._addr = [] self.operational = self.OperationalClass(ifname) @@ -238,39 +240,21 @@ class Interface(Control): >>> i = Interface('eth0') >>> i.remove() """ - # stop DHCP(v6) if running - self.dhcp.v4.delete() - self.dhcp.v6.delete() # remove all assigned IP addresses from interface - this is a bit redundant # as the kernel will remove all addresses on interface deletion, but we # can not delete ALL interfaces, see below - for addr in self.get_addr(): - self.del_addr(addr) + self.flush_addrs() # --------------------------------------------------------------------- - # 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() + # Any class can define an eternal regex in its definition + # interface matching the regex will not be deleted - # 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() + eternal = self.definition['eternal'] + if not eternal: + self._delete() + elif not re.match(eternal, self.ifname): + self._delete() def _delete(self): # NOTE (Improvement): @@ -431,39 +415,28 @@ class Interface(Control): """ return self.set_interface('ipv6_autoconf', autoconf) - def set_ipv6_eui64_address(self, prefix): + def add_ipv6_eui64_address(self, prefix): """ Extended Unique Identifier (EUI), as per RFC2373, allows a host to - assign iteslf a unique IPv6 address based on a given IPv6 prefix. + assign itself a unique IPv6 address based on a given IPv6 prefix. - If prefix is passed address is assigned, if prefix is '' address is - removed from interface. + Calculate the EUI64 from the interface's MAC, then assign it + with the given prefix to the interface. """ - # if prefix is an empty string convert it to None so mac2eui64 works - # as expected - if not prefix: - prefix = None eui64 = mac2eui64(self.get_mac(), prefix) + prefixlen = prefix.split('/')[1] + self.add_addr(f'{eui64}/{prefixlen}') - if not prefix: - # if prefix is empty - thus removed - we need to walk through all - # interface IPv6 addresses and find the one with the calculated - # EUI-64 identifier. The address is then removed - for addr in self.get_addr(): - addr_wo_prefix = addr.split('/')[0] - if is_ipv6(addr_wo_prefix): - if eui64 in IPv6Address(addr_wo_prefix).exploded: - self.del_addr(addr) - - return None + def del_ipv6_eui64_address(self, prefix): + """ + Delete the address based on the interface's MAC-based EUI64 + combined with the prefix address. + """ + eui64 = mac2eui64(self.get_mac(), prefix) + prefixlen = prefix.split('/')[1] + self.del_addr(f'{eui64}/{prefixlen}') - # calculate and add EUI-64 IPv6 address - if IPv6Network(prefix): - # we also need to take the subnet length into account - prefix = prefix.split('/')[1] - eui64 = f'{eui64}/{prefix}' - self.add_addr(eui64 ) def set_ipv6_forwarding(self, forwarding): """ @@ -644,7 +617,8 @@ class Interface(Control): def add_addr(self, addr): """ Add IP(v6) address to interface. Address is only added if it is not - already assigned to that interface. + already assigned to that interface. Address format must be validated + and compressed/normalized before calling this function. addr: can be an IPv4 address, IPv6 address, dhcp or dhcpv6! IPv4: add IPv4 address to interface @@ -652,6 +626,7 @@ class Interface(Control): dhcp: start dhclient (IPv4) on interface dhcpv6: start dhclient (IPv6) on interface + Returns False if address is already assigned and wasn't re-added. Example: >>> from vyos.ifconfig import Interface >>> j = Interface('eth0') @@ -660,32 +635,44 @@ class Interface(Control): >>> j.get_addr() ['192.0.2.1/24', '2001:db8::ffff/64'] """ + # XXX: normalize/compress with ipaddress if calling functions don't? + # is subnet mask always passed, and in the same way? - # cache new IP address which is assigned to interface - self._addr.append(addr) + # we can't have both DHCP and static IPv4 addresses assigned + for a in self._addr: + if ( ( addr == 'dhcp' and a != 'dhcpv6' and is_ipv4(a) ) or + ( a == 'dhcp' and addr != 'dhcpv6' and is_ipv4(addr) ) ): + raise ConfigError(( + "Can't configure both static IPv4 and DHCP address " + "on the same interface")) - # 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") + # do not add same address twice + if addr in self._addr: + return False + # add to interface if addr == 'dhcp': + self._addr.append(addr) self.dhcp.v4.set() - elif addr == 'dhcpv6': + return True + + if addr == 'dhcpv6': + self._addr.append(addr) self.dhcp.v6.set() - else: - if not is_intf_addr_assigned(self.config['ifname'], addr): - cmd = 'ip addr add "{}" dev "{}"'.format(addr, self.config['ifname']) - return self._cmd(cmd) + return True + + if not is_intf_addr_assigned(self.ifname, addr): + self._addr.append(addr) + self._cmd(f'ip addr add "{addr}" dev "{self.ifname}"') + return True + + return False def del_addr(self, addr): """ - Delete IP(v6) address to interface. Address is only added if it is - assigned to that interface. + Delete IP(v6) address from interface. Address is only deleted if it is + assigned to that interface. Address format must be exactly the same as + was used when adding the address. addr: can be an IPv4 address, IPv6 address, dhcp or dhcpv6! IPv4: delete IPv4 address from interface @@ -693,6 +680,7 @@ class Interface(Control): dhcp: stop dhclient (IPv4) on interface dhcpv6: stop dhclient (IPv6) on interface + Returns False if address isn't already assigned and wasn't deleted. Example: >>> from vyos.ifconfig import Interface >>> j = Interface('eth0') @@ -704,11 +692,35 @@ class Interface(Control): >>> j.get_addr() ['2001:db8::ffff/64'] """ + + # remove from cache (dhcp, and dhcpv6 can not be in it) + if addr in self._addr: + self._addr.remove(addr) + + # remove from interface if addr == 'dhcp': self.dhcp.v4.delete() - elif addr == 'dhcpv6': + return True + + if addr == 'dhcpv6': self.dhcp.v6.delete() - else: - if is_intf_addr_assigned(self.config['ifname'], addr): - cmd = 'ip addr del "{}" dev "{}"'.format(addr, self.config['ifname']) - return self._cmd(cmd) + return True + + if is_intf_addr_assigned(self.ifname, addr): + self._cmd(f'ip addr del "{addr}" dev "{self.ifname}"') + return True + + return False + + def flush_addrs(self): + """ + Flush all addresses from an interface, including DHCP. + + Will raise an exception on error. + """ + # stop DHCP(v6) if running + self.dhcp.v4.delete() + self.dhcp.v6.delete() + + # flush all addresses + self._cmd(f'ip addr flush dev "{self.ifname}"') diff --git a/python/vyos/ifconfig/macvlan.py b/python/vyos/ifconfig/macvlan.py index 55b1a3e91..b5481f4a7 100644 --- a/python/vyos/ifconfig/macvlan.py +++ b/python/vyos/ifconfig/macvlan.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/>. +from copy import deepcopy from vyos.ifconfig.interface import Interface from vyos.ifconfig.vlan import VLAN @@ -27,6 +28,9 @@ class MACVLANIf(Interface): default = { 'type': 'macvlan', + 'address': '', + 'source_interface': '', + 'mode': '', } definition = { **Interface.definition, @@ -35,33 +39,32 @@ class MACVLANIf(Interface): 'prefixes': ['peth', ], }, } - options = Interface.options + ['source_interface', 'mode'] + 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) + # please do not change the order when assembling the command + cmd = 'ip link add {ifname}' + if self.config['source_interface']: + cmd += ' link {source_interface}' + cmd += ' type macvlan' + if self.config['mode']: + cmd += ' mode {mode}' + self._cmd(cmd.format(**self.config)) - @staticmethod - def get_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) + + @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. + MACVLAN interfaces require a configuration when they are added using + iproute2. This 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) + return deepcopy(cls.default) diff --git a/python/vyos/ifconfig/tunnel.py b/python/vyos/ifconfig/tunnel.py index 009a53a82..85c22b5b4 100644 --- a/python/vyos/ifconfig/tunnel.py +++ b/python/vyos/ifconfig/tunnel.py @@ -43,7 +43,7 @@ class _Tunnel(Interface): **{ 'section': 'tunnel', 'prefixes': ['tun',], - 'bridgeable': True, + 'bridgeable': False, }, } @@ -135,14 +135,21 @@ class GREIf(_Tunnel): https://git.kernel.org/pub/scm/network/iproute2/iproute2.git/tree/ip/link_gre.c """ + definition = { + **_Tunnel.definition, + **{ + 'bridgeable': True, + }, + } + 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', + options = ['local', 'remote', 'dev', 'ttl', 'tos', 'key'] + updates = ['local', 'remote', 'dev', 'ttl', 'tos', 'mtu', 'multicast', 'allmulticast'] create = 'ip tunnel add {ifname} mode {type}' @@ -160,6 +167,13 @@ class GRETapIf(_Tunnel): # no multicast, ttl or tos for gretap + definition = { + **_Tunnel.definition, + **{ + 'bridgeable': True, + }, + } + ip = [IP4, ] tunnel = IP4 @@ -189,9 +203,9 @@ class IP6GREIf(_Tunnel): default = {'type': 'ip6gre'} required = ['local', 'remote'] - options = ['local', 'remote', 'encaplimit', + options = ['local', 'remote', 'dev', 'encaplimit', 'hoplimit', 'tclass', 'flowlabel'] - updates = ['local', 'remote', 'encaplimit', + updates = ['local', 'remote', 'dev', 'encaplimit', 'hoplimit', 'tclass', 'flowlabel', 'mtu', 'multicast', 'allmulticast'] @@ -225,8 +239,8 @@ class IPIPIf(_Tunnel): default = {'type': 'ipip'} required = ['local', 'remote'] - options = ['local', 'remote', 'ttl', 'tos', 'key'] - updates = ['local', 'remote', 'ttl', 'tos', + options = ['local', 'remote', 'dev', 'ttl', 'tos', 'key'] + updates = ['local', 'remote', 'dev', 'ttl', 'tos', 'mtu', 'multicast', 'allmulticast'] create = 'ip tunnel add {ifname} mode {type}' @@ -248,9 +262,9 @@ class IPIP6If(_Tunnel): default = {'type': 'ipip6'} required = ['local', 'remote'] - options = ['local', 'remote', 'encaplimit', + options = ['local', 'remote', 'dev', 'encaplimit', 'hoplimit', 'tclass', 'flowlabel'] - updates = ['local', 'remote', 'encaplimit', + updates = ['local', 'remote', 'dev', 'encaplimit', 'hoplimit', 'tclass', 'flowlabel', 'mtu', 'multicast', 'allmulticast'] @@ -286,8 +300,8 @@ class SitIf(_Tunnel): default = {'type': 'sit'} required = ['local', 'remote'] - options = ['local', 'remote', 'ttl', 'tos', 'key'] - updates = ['local', 'remote', 'ttl', 'tos', + options = ['local', 'remote', 'dev', 'ttl', 'tos', 'key'] + updates = ['local', 'remote', 'dev', 'ttl', 'tos', 'mtu', 'multicast', 'allmulticast'] create = 'ip tunnel add {ifname} mode {type}' diff --git a/python/vyos/ifconfig/vlan.py b/python/vyos/ifconfig/vlan.py index 7b1e00d87..d68e8f6cd 100644 --- a/python/vyos/ifconfig/vlan.py +++ b/python/vyos/ifconfig/vlan.py @@ -101,26 +101,26 @@ class VLAN: >>> i.add_vlan(10) """ vlan_ifname = self.config['ifname'] + '.' + str(vlan_id) - if not os.path.exists(f'/sys/class/net/{vlan_ifname}'): - self._vlan_id = int(vlan_id) - - if ethertype: - self._ethertype = ethertype - ethertype = 'proto {}'.format(ethertype) - - # Optional ingress QOS mapping - opt_i = '' - if ingress_qos: - opt_i = 'ingress-qos-map ' + ingress_qos - # Optional egress QOS mapping - opt_e = '' - if egress_qos: - opt_e = 'egress-qos-map ' + egress_qos - - # create interface in the system - cmd = 'ip link add link {ifname} name {ifname}.{vlan} type vlan {proto} id {vlan} {opt_e} {opt_i}' \ - .format(ifname=self.config['ifname'], vlan=self._vlan_id, proto=ethertype, opt_e=opt_e, opt_i=opt_i) - self._cmd(cmd) + if os.path.exists(f'/sys/class/net/{vlan_ifname}'): + return self.__class__(vlan_ifname) + + if ethertype: + self._ethertype = ethertype + ethertype = 'proto {}'.format(ethertype) + + # Optional ingress QOS mapping + opt_i = '' + if ingress_qos: + opt_i = 'ingress-qos-map ' + ingress_qos + # Optional egress QOS mapping + opt_e = '' + if egress_qos: + opt_e = 'egress-qos-map ' + egress_qos + + # create interface in the system + cmd = 'ip link add link {ifname} name {ifname}.{vlan} type vlan {proto} id {vlan} {opt_e} {opt_i}' \ + .format(ifname=self.ifname, vlan=vlan_id, proto=ethertype, opt_e=opt_e, opt_i=opt_i) + self._cmd(cmd) # return new object mapping to the newly created interface # we can now work on this object for e.g. IP address setting diff --git a/python/vyos/ifconfig/vrrp.py b/python/vyos/ifconfig/vrrp.py index 29b10dd9e..a872725b2 100644 --- a/python/vyos/ifconfig/vrrp.py +++ b/python/vyos/ifconfig/vrrp.py @@ -109,7 +109,7 @@ class VRRP(object): return [] disabled = [] - config = json.loads(util.readfile(cls.location['vyos'])) + config = json.loads(util.read_file(cls.location['vyos'])) # add disabled groups to the list for group in config['vrrp_groups']: diff --git a/python/vyos/ifconfig/vxlan.py b/python/vyos/ifconfig/vxlan.py index f47ae17cc..f9f2e38e9 100644 --- a/python/vyos/ifconfig/vxlan.py +++ b/python/vyos/ifconfig/vxlan.py @@ -59,7 +59,8 @@ class VXLANIf(Interface): 'bridgeable': True, } } - options = ['group', 'remote', 'src_interface', 'port', 'vni', 'src_address'] + options = Interface.options + \ + ['group', 'remote', 'src_interface', 'port', 'vni', 'src_address'] mapping = { 'ifname': 'add', diff --git a/python/vyos/ifconfig/wireguard.py b/python/vyos/ifconfig/wireguard.py index ff945c9d0..fdf5d9347 100644 --- a/python/vyos/ifconfig/wireguard.py +++ b/python/vyos/ifconfig/wireguard.py @@ -165,8 +165,9 @@ class WireGuardIf(Interface): 'bridgeable': True, } } - options = ['port', 'private-key', 'pubkey', 'psk', - 'allowed-ips', 'fwmark', 'endpoint', 'keepalive'] + options = Interface.options + \ + ['port', 'private-key', 'pubkey', 'psk', + 'allowed-ips', 'fwmark', 'endpoint', 'keepalive'] """ Wireguard interface class, contains a comnfig dictionary since diff --git a/python/vyos/ifconfig/wireless.py b/python/vyos/ifconfig/wireless.py index 946ae1642..3122ac0a3 100644 --- a/python/vyos/ifconfig/wireless.py +++ b/python/vyos/ifconfig/wireless.py @@ -38,7 +38,8 @@ class WiFiIf(Interface): 'bridgeable': True, } } - options = ['phy', 'op_mode'] + options = Interface.options + \ + ['phy', 'op_mode'] def _create(self): # all interfaces will be added in monitor mode diff --git a/python/vyos/ifconfig_vlan.py b/python/vyos/ifconfig_vlan.py index 899fd17da..ee009f7f9 100644 --- a/python/vyos/ifconfig_vlan.py +++ b/python/vyos/ifconfig_vlan.py @@ -66,10 +66,18 @@ def apply_vlan_config(vlan, config): # assign/remove VRF vlan.set_vrf(config['vrf']) + # Delete old IPv6 EUI64 addresses before changing MAC + for addr in config['ipv6_eui64_prefix_remove']: + vlan.del_ipv6_eui64_address(addr) + # Change VLAN interface MAC address if config['mac']: vlan.set_mac(config['mac']) + # Add IPv6 EUI-based addresses + for addr in config['ipv6_eui64_prefix']: + vlan.add_ipv6_eui64_address(addr) + # enable/disable VLAN interface if config['disable']: vlan.set_admin_state('down') diff --git a/python/vyos/logger.py b/python/vyos/logger.py new file mode 100644 index 000000000..f7cc964d5 --- /dev/null +++ b/python/vyos/logger.py @@ -0,0 +1,143 @@ +# 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/>. + +# A wrapper class around logging to make it easier to use + +# for a syslog logger: +# from vyos.logger import syslog +# syslog.critical('message') + +# for a stderr logger: +# from vyos.logger import stderr +# stderr.critical('message') + +# for a custom logger (syslog and file): +# from vyos.logger import getLogger +# combined = getLogger(__name__, syslog=True, stream=sys.stdout, filename='/tmp/test') +# combined.critical('message') + +import sys +import logging +import logging.handlers as handlers + +TIMED = '%(asctime)s: %(message)s' +SHORT = '%(filename)s: %(message)s' +CLEAR = '%(levelname) %(asctime)s %(filename)s: %(message)s' + +_levels = { + 'CRITICAL': logging.CRITICAL, + 'ERROR': logging.CRITICAL, + 'WARNING': logging.WARNING, + 'INFO': logging.INFO, + 'DEBUG': logging.DEBUG, + 'NOTSET': logging.NOTSET, +} + +# prevent recreation of already created logger +_created = {} + +def getLogger(name=None, **kwargs): + if name in _created: + if len(kwargs) == 0: + return _created[name] + raise ValueError('a logger with the name "{name} already exists') + + logger = logging.getLogger(name) + logger.setLevel(_levels[kwargs.get('level', 'DEBUG')]) + + if 'address' in kwargs or kwargs.get('syslog', False): + logger.addHandler(_syslog(**kwargs)) + if 'stream' in kwargs: + logger.addHandler(_stream(**kwargs)) + if 'filename' in kwargs: + logger.addHandler(_file(**kwargs)) + + _created[name] = logger + return logger + + +def _syslog(**kwargs): + formating = kwargs.get('format', SHORT) + handler = handlers.SysLogHandler( + address=kwargs.get('address', '/dev/log'), + facility=kwargs.get('facility', 'syslog'), + ) + handler.setFormatter(logging.Formatter(formating)) + return handler + + +def _stream(**kwargs): + formating = kwargs.get('format', CLEAR) + handler = logging.StreamHandler( + stream=kwargs.get('stream', sys.stderr), + ) + handler.setFormatter(logging.Formatter(formating)) + return handler + + +def _file(**kwargs): + formating = kwargs.get('format', CLEAR) + handler = handlers.RotatingFileHandler( + filename=kwargs.get('filename', 1048576), + maxBytes=kwargs.get('maxBytes', 1048576), + backupCount=kwargs.get('backupCount', 3), + ) + handler.setFormatter(logging.Formatter(formating)) + return handler + + +# exported pre-built logger, please keep in mind that the names +# must be unique otherwise the logger are shared + +# a logger for stderr +stderr = getLogger( + 'VyOS Syslog', + format=SHORT, + stream=sys.stderr, + address='/dev/log' +) + +# a logger to syslog +syslog = getLogger( + 'VyOS StdErr', + format='%(message)s', + address='/dev/log' +) + + +# testing +if __name__ == '__main__': + # from vyos.logger import getLogger + formating = '%(asctime)s (%(filename)s) %(levelname)s: %(message)s' + + # syslog logger + # syslog=True if no 'address' field is provided + syslog = getLogger(__name__ + '.1', syslog=True, format=formating) + syslog.info('syslog test') + + # steam logger + stream = getLogger(__name__ + '.2', stream=sys.stdout, level='ERROR') + stream.info('steam test') + + # file logger + filelog = getLogger(__name__ + '.3', filename='/tmp/test') + filelog.info('file test') + + # create a combined logger + getLogger('VyOS', syslog=True, stream=sys.stdout, filename='/tmp/test') + + # recover the created logger from name + combined = getLogger('VyOS') + combined.info('combined test') diff --git a/python/vyos/remote.py b/python/vyos/remote.py index 1b4d3876e..3f46d979b 100644 --- a/python/vyos/remote.py +++ b/python/vyos/remote.py @@ -91,7 +91,7 @@ def get_remote_config(remote_file): ftp://<user>[:<passwd>]@<host>/<file> tftp://<host>/<file> """ - request = dict.fromkeys(['protocol', 'host', 'file', 'user', 'passwd']) + request = dict.fromkeys(['protocol', 'user', 'host', 'file']) protocols = ['scp', 'sftp', 'http', 'https', 'ftp', 'tftp'] or_protocols = '|'.join(protocols) @@ -108,11 +108,6 @@ def get_remote_config(remote_file): if user_match: request['user'] = user_match.groups()[0] request['host'] = user_match.groups()[1] - passwd_match = re.search(r'(.*):(.*)', request['user']) - if passwd_match: - # Deprectated in RFC 3986, but maintain for backward compatability. - request['user'] = passwd_match.groups()[0] - request['passwd'] = passwd_match.groups()[1] remote_file = '{0}://{1}{2}'.format(request['protocol'], request['host'], request['file']) @@ -137,7 +132,7 @@ def get_remote_config(remote_file): print('HTTP error: {0} {1}'.format(*val)) sys.exit(1) - if request['user'] and not request['passwd']: + if request['user']: curl_cmd = 'curl -# -u {0} {1}'.format(request['user'], remote_file) else: curl_cmd = 'curl {0} -# {1}'.format(redirect_opt, remote_file) diff --git a/python/vyos/template.py b/python/vyos/template.py index 6c73ce753..e4b253ed3 100644 --- a/python/vyos/template.py +++ b/python/vyos/template.py @@ -19,6 +19,7 @@ from jinja2 import Environment from jinja2 import FileSystemLoader from vyos.defaults import directories +from vyos.util import chmod, chown, makedir # reuse the same Environment to improve performance @@ -32,7 +33,7 @@ _templates_mem = { } -def render(destination, template, content, trim_blocks=False, formater=None): +def render(destination, template, content, trim_blocks=False, formater=None, permission=None, user=None, group=None): """ render a template from the template directory, it will raise on any errors destination: the file where the rendered template must be saved @@ -46,6 +47,10 @@ def render(destination, template, content, trim_blocks=False, formater=None): (recovering the load time and overhead caused by having the file out of the code) """ + # Create the directory if it does not exists + folder = os.path.dirname(destination) + makedir(folder, user, group) + # Setup a renderer for the given template # This is cached and re-used for performance if template not in _templates_mem[trim_blocks]: @@ -63,3 +68,6 @@ def render(destination, template, content, trim_blocks=False, formater=None): # Write client config file with open(destination, 'w') as f: f.write(content) + + chmod(destination, permission) + chown(destination, user, group) diff --git a/python/vyos/util.py b/python/vyos/util.py index eb78c4a26..92b6f7992 100644 --- a/python/vyos/util.py +++ b/python/vyos/util.py @@ -14,6 +14,7 @@ # License along with this library. If not, see <http://www.gnu.org/licenses/>. import os +import sys # # NOTE: Do not import full classes here, move your import to the function @@ -25,7 +26,7 @@ import os # which all have slighty different behaviour from subprocess import Popen, PIPE, STDOUT, DEVNULL def popen(command, flag='', shell=None, input=None, timeout=None, env=None, - stdout=PIPE, stderr=None, decode=None): + stdout=PIPE, stderr=PIPE, decode='utf-8'): """ popen is a wrapper helper aound subprocess.Popen with it default setting it will return a tuple (out, err) @@ -48,12 +49,14 @@ def popen(command, flag='', shell=None, input=None, timeout=None, env=None, - STDOUT, send the data to be merged with stdout - DEVNULL, discard the output decode: specify the expected text encoding (utf-8, ascii, ...) + the default is explicitely utf-8 which is python's own default usage: to get both stdout, and stderr: popen('command', stdout=PIPE, stderr=STDOUT) to discard stdout and get stderr: popen('command', stdout=DEVNUL, stderr=PIPE) """ from vyos import debug + from vyos import airbag # log if the flag is set, otherwise log if command is set if not debug.enabled(flag): flag = 'command' @@ -77,27 +80,39 @@ def popen(command, flag='', shell=None, input=None, timeout=None, env=None, stdin=stdin, stdout=stdout, stderr=stderr, env=env, shell=use_shell, ) - tmp = p.communicate(input, timeout) - out1 = b'' - out2 = b'' + + pipe = p.communicate(input, timeout) + + pipe_out = b'' if stdout == PIPE: - out1 = tmp[0] + pipe_out = pipe[0] + + pipe_err = b'' if stderr == PIPE: - out2 += tmp[1] - decoded1 = out1.decode(decode) if decode else out1.decode() - decoded2 = out2.decode(decode) if decode else out2.decode() - decoded1 = decoded1.replace('\r\n', '\n').strip() - decoded2 = decoded2.replace('\r\n', '\n').strip() - nl = '\n' if decoded1 and decoded2 else '' - decoded = decoded1 + nl + decoded2 - if decoded: - ret_msg = f"returned:\n{decoded}" - debug.message(ret_msg, flag) - return decoded, p.returncode + pipe_err = pipe[1] + + str_out = pipe_out.decode(decode).replace('\r\n', '\n').strip() + str_err = pipe_err.decode(decode).replace('\r\n', '\n').strip() + + out_msg = f"returned (out):\n{str_out}" + if str_out: + debug.message(out_msg, flag) + + if str_err: + err_msg = f"returned (err):\n{str_err}" + # this message will also be send to syslog via airbag + debug.message(err_msg, flag, destination=sys.stderr) + + # should something go wrong, report this too via airbag + airbag.noteworthy(cmd_msg) + airbag.noteworthy(out_msg) + airbag.noteworthy(err_msg) + + return str_out, p.returncode def run(command, flag='', shell=None, input=None, timeout=None, env=None, - stdout=DEVNULL, stderr=None, decode=None): + stdout=DEVNULL, stderr=PIPE, decode='utf-8'): """ A wrapper around vyos.util.popen, which discard the stdout and will return the error code of a command @@ -113,14 +128,15 @@ def run(command, flag='', shell=None, input=None, timeout=None, env=None, def cmd(command, flag='', shell=None, input=None, timeout=None, env=None, - stdout=PIPE, stderr=None, decode=None, - raising=None, message=''): + stdout=PIPE, stderr=PIPE, decode='utf-8', + raising=None, message='', expect=[0]): """ A wrapper around vyos.util.popen, which returns the stdout and will raise the error code of a command raising: specify which call should be used when raising (default is OSError) the class should only require a string as parameter + expect: a list of error codes to consider as normal """ decoded, code = popen( command, flag, @@ -129,7 +145,7 @@ def cmd(command, flag='', shell=None, input=None, timeout=None, env=None, env=env, shell=shell, decode=decode, ) - if code != 0: + if code not in expect: feedback = message + '\n' if message else '' feedback += f'failed to run command: {command}\n' feedback += f'returned: {decoded}\n' @@ -143,7 +159,7 @@ def cmd(command, flag='', shell=None, input=None, timeout=None, env=None, def call(command, flag='', shell=None, input=None, timeout=None, env=None, - stdout=PIPE, stderr=None, decode=None): + stdout=PIPE, stderr=PIPE, decode='utf-8'): """ A wrapper around vyos.util.popen, which print the stdout and will return the error code of a command @@ -155,15 +171,41 @@ def call(command, flag='', shell=None, input=None, timeout=None, env=None, env=env, shell=shell, decode=decode, ) - print(out) + if out: + print(out) return code -def read_file(path): - """ Read a file to string """ - with open(path, 'r') as f: - data = f.read().strip() - return data +def read_file(fname, defaultonfailure=None): + """ + read the content of a file, stripping any end characters (space, newlines) + should defaultonfailure be not None, it is returned on failure to read + """ + try: + """ Read a file to string """ + with open(fname, 'r') as f: + data = f.read().strip() + return data + except Exception as e: + if defaultonfailure is not None: + return defaultonfailure + raise e + + +def read_json(fname, defaultonfailure=None): + """ + read and json decode the content of a file + should defaultonfailure be not None, it is returned on failure to read + """ + import json + try: + with open(fname, 'r') as f: + data = json.load(f) + return data + except Exception as e: + if defaultonfailure is not None: + return defaultonfailure + raise e def chown(path, user, group): @@ -171,10 +213,24 @@ def chown(path, user, group): from pwd import getpwnam from grp import getgrnam - if os.path.exists(path): - uid = getpwnam(user).pw_uid - gid = getgrnam(group).gr_gid - os.chown(path, uid, gid) + if user is None or group is None: + return False + + if not os.path.exists(path): + return False + + uid = getpwnam(user).pw_uid + gid = getgrnam(group).gr_gid + os.chown(path, uid, gid) + return True + + +def chmod(path, bitmask): + if not os.path.exists(path): + return + if bitmask is None: + return + os.chmod(path, bitmask) def chmod_600(path): @@ -205,6 +261,13 @@ def chmod_755(path): os.chmod(path, bitmask) +def makedir(path, user=None, group=None): + if os.path.exists(path): + return + os.mkdir(path) + chown(path, user, group) + + def colon_separated_to_dict(data_string, uniquekeys=False): """ Converts a string containing newline-separated entries of colon-separated key-value pairs into a dict. @@ -429,21 +492,9 @@ def mac2eui64(mac, prefix=None): 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 - +def get_half_cpus(): + """ return 1/2 of the numbers of available CPUs """ + cpu = os.cpu_count() + if cpu > 1: + cpu /= 2 + return int(cpu) diff --git a/python/vyos/validate.py b/python/vyos/validate.py index 9d413ffab..446f6e4ca 100644 --- a/python/vyos/validate.py +++ b/python/vyos/validate.py @@ -86,8 +86,8 @@ def _is_intf_addr_assigned(intf, address, netmask=''): # 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'}], + # 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: @@ -240,3 +240,27 @@ def assert_mac(m): if octets[:5] == (0, 0, 94, 0, 1): raise ValueError(f'{m} is a VRRP MAC address') + +def is_bridge_member(conf, interface): + """ + Checks if passed interfaces is part of a bridge device or not. + + Returns a tuple: + None -> Interface not a bridge member + Bridge -> Interface is a member of this bridge + """ + ret_val = None + old_level = conf.get_level() + + # set config level to root + conf.set_level([]) + base = ['interfaces', 'bridge'] + for bridge in conf.list_nodes(base): + members = conf.list_nodes(base + [bridge, 'member', 'interface']) + if interface in members: + ret_val = bridge + break + + old_level = conf.set_level(old_level) + return ret_val + diff --git a/python/vyos/version.py b/python/vyos/version.py index d51a940d6..a524b36ea 100644 --- a/python/vyos/version.py +++ b/python/vyos/version.py @@ -34,9 +34,17 @@ import json import vyos.defaults +from vyos.util import read_file +from vyos.util import read_json +from vyos.util import popen +from vyos.util import run +from vyos.util import DEVNULL + + version_file = os.path.join(vyos.defaults.directories['data'], 'version.json') -def get_version_data(file=version_file): + +def get_version_data(fname=version_file): """ Get complete version data @@ -52,20 +60,50 @@ 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. """ - try: - with open(file, 'r') as f: - version_data = json.load(f) - return version_data - except FileNotFoundError: - return {} + return read_json(fname, {}) -def get_version(file=None): + +def get_version(fname=version_file): """ 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.get('version','') + return get_version_data(fname=fname).get('version', '') + + +def get_full_version_data(fname=version_file): + version_data = get_version_data(fname) + + # Get system architecture (well, kernel architecture rather) + version_data['system_arch'], _ = popen('uname -m', stderr=DEVNULL) + + # Get hypervisor name, if any + try: + hypervisor, _ = popen('hvinfo', stderr=DEVNULL) + version_data['system_type'] = f"{hypervisor} guest" + except OSError: + # hvinfo returns 1 if it cannot detect any hypervisor + version_data['system_type'] = 'bare metal' + + # Get boot type, it can be livecd, installed image, or, possible, a system installed + # via legacy "install system" mechanism + # In installed images, the squashfs image file is named after its image version, + # 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 run(""" grep -e '^overlay.*/filesystem.squashfs' /proc/mounts >/dev/null""") == 0: + boot_via = "livecd" + elif run(""" grep '^overlay /' /proc/mounts >/dev/null """) != 0: + boot_via = "legacy non-image installation" + version_data['boot_via'] = boot_via + + # Get hardware details from DMI + dmi = '/sys/class/dmi/id' + version_data['hardware_vendor'] = read_file(dmi + '/sys_vendor', 'Unknown') + version_data['hardware_model'] = read_file(dmi +'/product_name','Unknown') + + # These two assume script is run as root, normal users can't access those files + subsystem = '/sys/class/dmi/id/subsystem/id' + version_data['hardware_serial'] = read_file(subsystem + '/product_serial','Unknown') + version_data['hardware_uuid'] = read_file(subsystem + '/product_uuid', 'Unknown') + + return version_data diff --git a/schema/op-mode-definition.rnc b/schema/op-mode-definition.rnc index 9c84de0e4..cbe51e6dc 100644 --- a/schema/op-mode-definition.rnc +++ b/schema/op-mode-definition.rnc @@ -24,7 +24,7 @@ # Interface definition starts with interfaceDefinition tag that may contain node tags start = element interfaceDefinition { - node* + (node | tagNode)* } # node tag may contain node, leafNode, or tagNode tags @@ -43,7 +43,7 @@ node = element node tagNode = element tagNode { nodeNameAttr, - (properties? & children & command?) + (properties? & children? & command?) } # Leaf nodes are terminal configuration nodes that can't have children, diff --git a/schema/op-mode-definition.rng b/schema/op-mode-definition.rng index e9e7887cf..900f41e27 100644 --- a/schema/op-mode-definition.rng +++ b/schema/op-mode-definition.rng @@ -29,7 +29,10 @@ <start> <element name="interfaceDefinition"> <zeroOrMore> - <ref name="node"/> + <choice> + <ref name="node"/> + <ref name="tagNode"/> + </choice> </zeroOrMore> </element> </start> diff --git a/src/conf_mode/dhcp_relay.py b/src/conf_mode/dhcp_relay.py index ce0e01308..d24a46220 100755 --- a/src/conf_mode/dhcp_relay.py +++ b/src/conf_mode/dhcp_relay.py @@ -98,11 +98,6 @@ def generate(relay): if not relay: return None - # Create configuration directory on demand - dirname = os.path.dirname(config_file) - if not os.path.isdir(dirname): - os.mkdir(dirname) - render(config_file, 'dhcp-relay/config.tmpl', relay) return None diff --git a/src/conf_mode/dhcp_server.py b/src/conf_mode/dhcp_server.py index da01f16eb..1849ece0a 100755 --- a/src/conf_mode/dhcp_server.py +++ b/src/conf_mode/dhcp_server.py @@ -594,11 +594,6 @@ def generate(dhcp): if not dhcp or dhcp['disabled']: return None - # Create configuration directory on demand - dirname = os.path.dirname(config_file) - if not os.path.isdir(dirname): - os.mkdir(dirname) - # Please see: https://phabricator.vyos.net/T1129 for quoting of the raw parameters # we can pass to ISC DHCPd render(config_file, 'dhcp-server/dhcpd.conf.tmpl', dhcp, diff --git a/src/conf_mode/dhcpv6_relay.py b/src/conf_mode/dhcpv6_relay.py index cb5a4bbfb..ecc739063 100755 --- a/src/conf_mode/dhcpv6_relay.py +++ b/src/conf_mode/dhcpv6_relay.py @@ -84,11 +84,6 @@ def generate(relay): if relay is None: return None - # Create configuration directory on demand - dirname = os.path.dirname(config_file) - if not os.path.isdir(dirname): - os.mkdir(dirname) - render(config_file, 'dhcpv6-relay/config.tmpl', relay) return None diff --git a/src/conf_mode/dhcpv6_server.py b/src/conf_mode/dhcpv6_server.py index 94a307826..159d16401 100755 --- a/src/conf_mode/dhcpv6_server.py +++ b/src/conf_mode/dhcpv6_server.py @@ -23,7 +23,7 @@ from copy import deepcopy from vyos.config import Config from vyos.template import render from vyos.util import call -from vyos.validate import is_subnet_connected +from vyos.validate import is_subnet_connected, is_ipv6 from vyos import ConfigError config_file = r'/run/dhcp-server/dhcpdv6.conf' @@ -37,24 +37,25 @@ default_config_data = { def get_config(): dhcpv6 = deepcopy(default_config_data) conf = Config() - if not conf.exists('service dhcpv6-server'): + base = ['service', 'dhcpv6-server'] + if not conf.exists(base): return None else: - conf.set_level('service dhcpv6-server') + conf.set_level(base) # Check for global disable of DHCPv6 service - if conf.exists('disable'): + if conf.exists(['disable']): dhcpv6['disabled'] = True return dhcpv6 # Preference of this DHCPv6 server compared with others - if conf.exists('preference'): - dhcpv6['preference'] = conf.return_value('preference') + if conf.exists(['preference']): + dhcpv6['preference'] = conf.return_value(['preference']) # check for multiple, shared networks served with DHCPv6 addresses - if conf.exists('shared-network-name'): - for network in conf.list_nodes('shared-network-name'): - conf.set_level('service dhcpv6-server shared-network-name {0}'.format(network)) + if conf.exists(['shared-network-name']): + for network in conf.list_nodes(['shared-network-name']): + conf.set_level(base + ['shared-network-name', network]) config = { 'name': network, 'disabled': False, @@ -62,13 +63,13 @@ def get_config(): } # If disabled, the shared-network configuration becomes inactive - if conf.exists('disable'): + if conf.exists(['disable']): config['disabled'] = True # check for multiple subnet configurations in a shared network - if conf.exists('subnet'): - for net in conf.list_nodes('subnet'): - conf.set_level('service dhcpv6-server shared-network-name {0} subnet {1}'.format(network, net)) + if conf.exists(['subnet']): + for net in conf.list_nodes(['subnet']): + conf.set_level(base + ['shared-network-name', network, 'subnet', net]) subnet = { 'network': net, 'range6_prefix': [], @@ -94,25 +95,25 @@ def get_config(): # least one address range statement. The range statement gives the lowest and highest # IP addresses in a range. All IP addresses in the range should be in the subnet in # which the range statement is declared. - if conf.exists('address-range prefix'): - for prefix in conf.list_nodes('address-range prefix'): + if conf.exists(['address-range', 'prefix']): + for prefix in conf.list_nodes(['address-range', 'prefix']): range = { 'prefix': prefix, 'temporary': False } # Address range will be used for temporary addresses - if conf.exists('address-range prefix {0} temporary'.format(range['prefix'])): + if conf.exists(['address-range' 'prefix', prefix, 'temporary']): range['temporary'] = True # Append to subnet temporary range6 list subnet['range6_prefix'].append(range) - if conf.exists('address-range start'): - for range in conf.list_nodes('address-range start'): + if conf.exists(['address-range', 'start']): + for range in conf.list_nodes(['address-range', 'start']): range = { 'start': range, - 'stop': conf.return_value('address-range start {0} stop'.format(range)) + 'stop': conf.return_value(['address-range', 'start', range, 'stop']) } # Append to subnet range6 list @@ -120,70 +121,68 @@ def get_config(): # The domain-search option specifies a 'search list' of Domain Names to be used # by the client to locate not-fully-qualified domain names. - if conf.exists('domain-search'): - for domain in conf.return_values('domain-search'): - subnet['domain_search'].append('"' + domain + '"') + if conf.exists(['domain-search']): + subnet['domain_search'] = conf.return_values(['domain-search']) # IPv6 address valid lifetime # (at the end the address is no longer usable by the client) # (set to 30 days, the usual IPv6 default) - if conf.exists('lease-time default'): - subnet['lease_def'] = conf.return_value('lease-time default') + if conf.exists(['lease-time', 'default']): + subnet['lease_def'] = conf.return_value(['lease-time', 'default']) # Time should be the maximum length in seconds that will be assigned to a lease. # The only exception to this is that Dynamic BOOTP lease lengths, which are not # specified by the client, are not limited by this maximum. - if conf.exists('lease-time maximum'): - subnet['lease_max'] = conf.return_value('lease-time maximum') + if conf.exists(['lease-time', 'maximum']): + subnet['lease_max'] = conf.return_value(['lease-time', 'maximum']) # Time should be the minimum length in seconds that will be assigned to a lease - if conf.exists('lease-time minimum'): - subnet['lease_min'] = conf.return_value('lease-time minimum') + if conf.exists(['lease-time', 'minimum']): + subnet['lease_min'] = conf.return_value(['lease-time', 'minimum']) # Specifies a list of Domain Name System name servers available to the client. # Servers should be listed in order of preference. - if conf.exists('name-server'): - subnet['dns_server'] = conf.return_values('name-server') + if conf.exists(['name-server']): + subnet['dns_server'] = conf.return_values(['name-server']) # Ancient NIS (Network Information Service) domain name - if conf.exists('nis-domain'): - subnet['nis_domain'] = conf.return_value('nis-domain') + if conf.exists(['nis-domain']): + subnet['nis_domain'] = conf.return_value(['nis-domain']) # Ancient NIS (Network Information Service) servers - if conf.exists('nis-server'): - subnet['nis_server'] = conf.return_values('nis-server') + if conf.exists(['nis-server']): + subnet['nis_server'] = conf.return_values(['nis-server']) # Ancient NIS+ (Network Information Service) domain name - if conf.exists('nisplus-domain'): - subnet['nisp_domain'] = conf.return_value('nisplus-domain') + if conf.exists(['nisplus-domain']): + subnet['nisp_domain'] = conf.return_value(['nisplus-domain']) # Ancient NIS+ (Network Information Service) servers - if conf.exists('nisplus-server'): - subnet['nisp_server'] = conf.return_values('nisplus-server') + if conf.exists(['nisplus-server']): + subnet['nisp_server'] = conf.return_values(['nisplus-server']) # Prefix Delegation (RFC 3633) - if conf.exists('prefix-delegation'): + if conf.exists(['prefix-delegation']): print('TODO: This option is actually not implemented right now!') # Local SIP server that is to be used for all outbound SIP requests - IPv6 address - if conf.exists('sip-server-address'): - subnet['sip_address'] = conf.return_values('sip-server-address') - - # Local SIP server that is to be used for all outbound SIP requests - hostname - if conf.exists('sip-server-name'): - for hostname in conf.return_values('sip-server-name'): - subnet['sip_hostname'].append('"' + hostname + '"') + if conf.exists(['sip-server']): + for value in conf.return_values(['sip-server']): + if is_ipv6(value): + subnet['sip_address'].append(value) + else: + subnet['sip_hostname'].append(value) # List of local SNTP servers available for the client to synchronize their clocks - if conf.exists('sntp-server'): - subnet['sntp_server'] = conf.return_values('sntp-server') + if conf.exists(['sntp-server']): + subnet['sntp_server'] = conf.return_values(['sntp-server']) # # Static DHCP v6 leases # - if conf.exists('static-mapping'): - for mapping in conf.list_nodes('static-mapping'): - conf.set_level('service dhcpv6-server shared-network-name {0} subnet {1} static-mapping {2}'.format(network, net, mapping)) + if conf.exists(['static-mapping']): + for mapping in conf.list_nodes(['static-mapping']): + conf.set_level(base + ['shared-network-name', network, 'subnet', net, 'static-mapping', mapping]) mapping = { 'name': mapping, 'disabled': False, @@ -192,16 +191,16 @@ def get_config(): } # This static lease is disabled - if conf.exists('disable'): + if conf.exists(['disable']): mapping['disabled'] = True # IPv6 address used for this DHCP client - if conf.exists('ipv6-address'): - mapping['ipv6_address'] = conf.return_value('ipv6-address') + if conf.exists(['ipv6-address']): + mapping['ipv6_address'] = conf.return_value(['ipv6-address']) # This option specifies the client’s DUID identifier. DUIDs are similar but different from DHCPv4 client identifiers - if conf.exists('identifier'): - mapping['client_identifier'] = conf.return_value('identifier') + if conf.exists(['identifier']): + mapping['client_identifier'] = conf.return_value(['identifier']) # append static mapping configuration tu subnet list subnet['static_mapping'].append(mapping) @@ -209,10 +208,13 @@ def get_config(): # append subnet configuration to shared network subnet list config['subnet'].append(subnet) - # append shared network configuration to config dictionary dhcpv6['shared_network'].append(config) + # If all shared-networks are disabled, there's nothing to do. + if all(net['disabled'] for net in dhcpv6['shared_network']): + return None + return dhcpv6 def verify(dhcpv6): @@ -302,22 +304,22 @@ def verify(dhcpv6): else: subnets.append(subnet['network']) - # DHCPv6 requires at least one configured address range or one static mapping - # (FIXME: is not actually checked right now?) + # DHCPv6 requires at least one configured address range or one static mapping + # (FIXME: is not actually checked right now?) - # There must be one subnet connected to a listen interface if network is not disabled. - if not network['disabled']: - if is_subnet_connected(subnet['network']): - listen_ok = True + # There must be one subnet connected to a listen interface if network is not disabled. + if not network['disabled']: + if is_subnet_connected(subnet['network']): + listen_ok = True - # DHCPv6 subnet must not overlap. ISC DHCP also complains about overlapping - # subnets: "Warning: subnet 2001:db8::/32 overlaps subnet 2001:db8:1::/32" - net = ipaddress.ip_network(subnet['network']) - for n in subnets: - net2 = ipaddress.ip_network(n) - if (net != net2): - if net.overlaps(net2): - raise ConfigError('DHCPv6 conflicting subnet ranges: {0} overlaps {1}'.format(net, net2)) + # DHCPv6 subnet must not overlap. ISC DHCP also complains about overlapping + # subnets: "Warning: subnet 2001:db8::/32 overlaps subnet 2001:db8:1::/32" + net = ipaddress.ip_network(subnet['network']) + for n in subnets: + net2 = ipaddress.ip_network(n) + if (net != net2): + if net.overlaps(net2): + raise ConfigError('DHCPv6 conflicting subnet ranges: {0} overlaps {1}'.format(net, net2)) if not listen_ok: raise ConfigError('None of the DHCPv6 subnets are connected to a subnet6 on\n' \ @@ -331,11 +333,6 @@ def generate(dhcpv6): if not dhcpv6 or dhcpv6['disabled']: return None - # Create configuration directory on demand - dirname = os.path.dirname(config_file) - if not os.path.isdir(dirname): - os.mkdir(dirname) - render(config_file, 'dhcpv6-server/dhcpdv6.conf.tmpl', dhcpv6) return None @@ -346,6 +343,7 @@ def apply(dhcpv6): if os.path.exists(config_file): os.unlink(config_file) + else: call('systemctl restart isc-dhcp-server6.service') return None diff --git a/src/conf_mode/dns_forwarding.py b/src/conf_mode/dns_forwarding.py index 567dfa4b3..7f7417b00 100755 --- a/src/conf_mode/dns_forwarding.py +++ b/src/conf_mode/dns_forwarding.py @@ -152,10 +152,6 @@ def generate(dns): if dns is None: return None - dirname = os.path.dirname(config_file) - if not os.path.exists(dirname): - os.mkdir(dirname) - render(config_file, 'dns-forwarding/recursor.conf.tmpl', dns, trim_blocks=True) return None diff --git a/src/conf_mode/dynamic_dns.py b/src/conf_mode/dynamic_dns.py index 038f77cf9..3386324ae 100755 --- a/src/conf_mode/dynamic_dns.py +++ b/src/conf_mode/dynamic_dns.py @@ -217,10 +217,6 @@ def generate(dyndns): if dyndns['deleted']: return None - dirname = os.path.dirname(config_file) - if not os.path.exists(dirname): - os.mkdir(dirname) - render(config_file, 'dynamic-dns/ddclient.conf.tmpl', dyndns) # Config file must be accessible only by its owner diff --git a/src/conf_mode/host_name.py b/src/conf_mode/host_name.py index a669580ae..f181a7b35 100755 --- a/src/conf_mode/host_name.py +++ b/src/conf_mode/host_name.py @@ -164,10 +164,17 @@ def apply(config): if process_named_running('snmpd'): call('systemctl restart snmpd.service') - # restart pdns if it is used - ret = run('/usr/bin/rec_control --socket-dir=/run/powerdns ping') - if ret == 0: - call('systemctl restart pdns-recursor.service') + # restart pdns if it is used - we check for the control dir to not raise + # an exception on system startup + # + # File "/usr/lib/python3/dist-packages/vyos/configsession.py", line 128, in __run_command + # raise ConfigSessionError(output) + # vyos.configsession.ConfigSessionError: [ system domain-name vyos.io ] + # Fatal: Unable to generate local temporary file in directory '/run/powerdns': No such file or directory + if os.path.isdir('/run/powerdns'): + ret = run('/usr/bin/rec_control --socket-dir=/run/powerdns ping') + if ret == 0: + call('systemctl restart pdns-recursor.service') return None diff --git a/src/conf_mode/interfaces-bonding.py b/src/conf_mode/interfaces-bonding.py index fd1f218d1..a174e33e4 100755 --- a/src/conf_mode/interfaces-bonding.py +++ b/src/conf_mode/interfaces-bonding.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2019 VyOS maintainers and contributors +# Copyright (C) 2019-2020 VyOS maintainers and contributors # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 or later as @@ -20,12 +20,12 @@ from copy import deepcopy from sys import exit from netifaces import interfaces -from vyos.ifconfig import BondIf +from vyos.ifconfig import BondIf, Section from vyos.ifconfig_vlan import apply_vlan_config, verify_vlan_config -from vyos.configdict import list_diff, vlan_to_dict +from vyos.configdict import list_diff, intf_to_dict, add_to_dict from vyos.config import Config -from vyos.util import is_bridge_member -from vyos.util import call +from vyos.util import call, cmd +from vyos.validate import is_bridge_member from vyos import ConfigError default_config_data = { @@ -43,6 +43,7 @@ default_config_data = { 'disable': False, 'disable_link_detect': 1, 'hash_policy': 'layer2', + 'intf': '', 'ip_arp_cache_tmo': 30, 'ip_disable_arp_filter': 1, 'ip_enable_arp_accept': 0, @@ -51,10 +52,11 @@ default_config_data = { 'ip_proxy_arp': 0, 'ip_proxy_arp_pvlan': 0, 'ipv6_autoconf': 0, - 'ipv6_eui64_prefix': '', + 'ipv6_eui64_prefix': [], + 'ipv6_eui64_prefix_remove': [], 'ipv6_forwarding': 1, 'ipv6_dup_addr_detect': 1, - 'intf': '', + 'is_bridge_member': False, 'mac': '', 'mode': '802.3ad', 'member': [], @@ -88,6 +90,13 @@ def get_bond_mode(mode): raise ConfigError('invalid bond mode "{}"'.format(mode)) def get_config(): + # determine tagNode instance + if 'VYOS_TAGNODE_VALUE' not in os.environ: + raise ConfigError('Interface (VYOS_TAGNODE_VALUE) not specified') + + ifname = os.environ['VYOS_TAGNODE_VALUE'] + conf = Config() + # initialize kernel module if not loaded if not os.path.isfile('/sys/class/net/bonding_masters'): import syslog @@ -96,32 +105,21 @@ def get_config(): syslog.syslog(syslog.LOG_NOTICE, "failed loading bonding kernel module") raise ConfigError("failed loading bonding kernel module") - bond = deepcopy(default_config_data) - conf = Config() - - # determine tagNode instance - if 'VYOS_TAGNODE_VALUE' not in os.environ: - raise ConfigError('Interface (VYOS_TAGNODE_VALUE) not specified') - - bond['intf'] = os.environ['VYOS_TAGNODE_VALUE'] - # check if bond has been removed - cfg_base = 'interfaces bonding ' + bond['intf'] + cfg_base = 'interfaces bonding ' + ifname if not conf.exists(cfg_base): + bond = deepcopy(default_config_data) + bond['intf'] = ifname bond['deleted'] = True + # check if interface is member if a bridge + bond['is_bridge_member'] = is_bridge_member(conf, ifname) return bond # set new configuration level conf.set_level(cfg_base) - # retrieve configured interface addresses - if conf.exists('address'): - bond['address'] = conf.return_values('address') - - # get interface addresses (currently effective) - to determine which - # address is no longer valid and needs to be removed - eff_addr = conf.return_effective_values('address') - bond['address_remove'] = list_diff(eff_addr, bond['address']) + bond, disabled = intf_to_dict(conf, default_config_data) + bond['intf'] = ifname # ARP link monitoring frequency in milliseconds if conf.exists('arp-monitor interval'): @@ -131,38 +129,6 @@ def get_config(): if conf.exists('arp-monitor target'): bond['arp_mon_tgt'] = conf.return_values('arp-monitor target') - # retrieve interface description - if conf.exists('description'): - bond['description'] = conf.return_value('description') - - # get DHCP client identifier - if conf.exists('dhcp-options client-id'): - bond['dhcp_client_id'] = conf.return_value('dhcp-options client-id') - - # DHCP client host name (overrides the system host name) - if conf.exists('dhcp-options host-name'): - bond['dhcp_hostname'] = conf.return_value('dhcp-options host-name') - - # DHCP client vendor identifier - if conf.exists('dhcp-options vendor-class-id'): - bond['dhcp_vendor_class_id'] = conf.return_value('dhcp-options vendor-class-id') - - # DHCPv6 only acquire config parameters, no address - if conf.exists('dhcpv6-options parameters-only'): - bond['dhcpv6_prm_only'] = True - - # DHCPv6 temporary IPv6 address - if conf.exists('dhcpv6-options temporary'): - bond['dhcpv6_temporary'] = True - - # ignore link state changes - if conf.exists('disable-link-detect'): - bond['disable_link_detect'] = 2 - - # disable bond interface - if conf.exists('disable'): - bond['disable'] = True - # Bonding transmit hash policy if conf.exists('hash-policy'): bond['hash_policy'] = conf.return_value('hash-policy') @@ -171,50 +137,10 @@ def get_config(): if conf.exists('ip arp-cache-timeout'): bond['ip_arp_cache_tmo'] = int(conf.return_value('ip arp-cache-timeout')) - # ARP filter configuration - if conf.exists('ip disable-arp-filter'): - bond['ip_disable_arp_filter'] = 0 - - # ARP enable accept - if conf.exists('ip enable-arp-accept'): - bond['ip_enable_arp_accept'] = 1 - - # ARP enable announce - if conf.exists('ip enable-arp-announce'): - bond['ip_enable_arp_announce'] = 1 - - # ARP enable ignore - if conf.exists('ip enable-arp-ignore'): - bond['ip_enable_arp_ignore'] = 1 - - # Enable proxy-arp on this interface - if conf.exists('ip enable-proxy-arp'): - bond['ip_proxy_arp'] = 1 - # Enable private VLAN proxy ARP on this interface if conf.exists('ip proxy-arp-pvlan'): bond['ip_proxy_arp_pvlan'] = 1 - # Enable acquisition of IPv6 address using stateless autoconfig (SLAAC) - if conf.exists('ipv6 address autoconf'): - bond['ipv6_autoconf'] = 1 - - # Get prefix for IPv6 addressing based on MAC address (EUI-64) - if conf.exists('ipv6 address eui64'): - bond['ipv6_eui64_prefix'] = conf.return_value('ipv6 address eui64') - - # Disable IPv6 forwarding on this interface - if conf.exists('ipv6 disable-forwarding'): - bond['ipv6_forwarding'] = 0 - - # IPv6 Duplicate Address Detection (DAD) tries - if conf.exists('ipv6 dup-addr-detect-transmits'): - bond['ipv6_dup_addr_detect'] = int(conf.return_value('ipv6 dup-addr-detect-transmits')) - - # Media Access Control (MAC) address - if conf.exists('mac'): - bond['mac'] = conf.return_value('mac') - # Bonding mode if conf.exists('mode'): act_mode = conf.return_value('mode') @@ -224,10 +150,6 @@ def get_config(): bond['mode'] = get_bond_mode(act_mode) - # Maximum Transmission Unit (MTU) - if conf.exists('mtu'): - bond['mtu'] = int(conf.return_value('mtu')) - # determine bond member interfaces (currently configured) if conf.exists('member interface'): bond['member'] = conf.return_values('member interface') @@ -244,48 +166,18 @@ def get_config(): if conf.exists('primary'): bond['primary'] = conf.return_value('primary') - # retrieve VRF instance - if conf.exists('vrf'): - bond['vrf'] = conf.return_value('vrf') - - # get vif-s interfaces (currently effective) - to determine which vif-s - # interface is no longer present and needs to be removed - eff_intf = conf.list_effective_nodes('vif-s') - act_intf = conf.list_nodes('vif-s') - bond['vif_s_remove'] = list_diff(eff_intf, act_intf) - - if conf.exists('vif-s'): - for vif_s in conf.list_nodes('vif-s'): - # set config level to vif-s interface - conf.set_level(cfg_base + ' vif-s ' + vif_s) - bond['vif_s'].append(vlan_to_dict(conf)) - - # re-set configuration level to parse new nodes - conf.set_level(cfg_base) - # Determine vif interfaces (currently effective) - to determine which - # vif interface is no longer present and needs to be removed - eff_intf = conf.list_effective_nodes('vif') - act_intf = conf.list_nodes('vif') - bond['vif_remove'] = list_diff(eff_intf, act_intf) - - if conf.exists('vif'): - for vif in conf.list_nodes('vif'): - # set config level to vif interface - conf.set_level(cfg_base + ' vif ' + vif) - bond['vif'].append(vlan_to_dict(conf)) + add_to_dict(conf, disabled, bond, 'vif', 'vif') + add_to_dict(conf, disabled, bond, 'vif-s', 'vif_s') return bond 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)) + if bond['is_bridge_member']: + interface = bond['intf'] + bridge = bond['is_bridge_member'] + raise ConfigError(f'Interface "{interface}" can not be deleted as it belongs to bridge "{bridge}"!') return None if len (bond['arp_mon_tgt']) > 16: @@ -434,17 +326,23 @@ def apply(bond): 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']) + # Delete old IPv6 EUI64 addresses before changing MAC + for addr in bond['ipv6_eui64_prefix_remove']: + b.del_ipv6_eui64_address(addr) + # Change interface MAC address if bond['mac']: b.set_mac(bond['mac']) + # Add IPv6 EUI-based addresses + for addr in bond['ipv6_eui64_prefix']: + b.add_ipv6_eui64_address(addr) + # Maximum Transmission Unit (MTU) b.set_mtu(bond['mtu']) @@ -467,6 +365,12 @@ def apply(bond): # Add (enslave) interfaces to bond for intf in bond['member']: + # flushes only children of Interfaces class (e.g. vlan are not) + if intf in Section.interfaces(): + klass = Section.klass(intf, vlan=False) + klass(intf, create=False).flush_addrs() + # flushes also vlan interfaces + call(f'ip addr flush dev "{intf}"') b.add_port(intf) # As the bond interface is always disabled first when changing diff --git a/src/conf_mode/interfaces-bridge.py b/src/conf_mode/interfaces-bridge.py index 93c6db97e..9d638653c 100755 --- a/src/conf_mode/interfaces-bridge.py +++ b/src/conf_mode/interfaces-bridge.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2019 VyOS maintainers and contributors +# Copyright (C) 2019-2020 VyOS maintainers and contributors # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 or later as @@ -20,10 +20,11 @@ from copy import deepcopy from sys import exit from netifaces import interfaces -from vyos.ifconfig import BridgeIf +from vyos.ifconfig import BridgeIf, Section from vyos.ifconfig.stp import STP from vyos.configdict import list_diff from vyos.config import Config +from vyos.util import cmd from vyos import ConfigError default_config_data = { @@ -47,7 +48,8 @@ default_config_data = { 'ip_enable_arp_announce': 0, 'ip_enable_arp_ignore': 0, 'ipv6_autoconf': 0, - 'ipv6_eui64_prefix': '', + 'ipv6_eui64_prefix': [], + 'ipv6_eui64_prefix_remove': [], 'ipv6_forwarding': 1, 'ipv6_dup_addr_detect': 1, 'igmp_querier': 0, @@ -160,9 +162,21 @@ def get_config(): if conf.exists('ipv6 address autoconf'): bridge['ipv6_autoconf'] = 1 - # Get prefix for IPv6 addressing based on MAC address (EUI-64) + # Get prefixes for IPv6 addressing based on MAC address (EUI-64) if conf.exists('ipv6 address eui64'): - bridge['ipv6_eui64_prefix'] = conf.return_value('ipv6 address eui64') + bridge['ipv6_eui64_prefix'] = conf.return_values('ipv6 address eui64') + + # Determine currently effective EUI64 addresses - to determine which + # address is no longer valid and needs to be removed + eff_addr = conf.return_effective_values('ipv6 address eui64') + bridge['ipv6_eui64_prefix_remove'] = list_diff(eff_addr, bridge['ipv6_eui64_prefix']) + + # Remove the default link-local address if set. + if conf.exists('ipv6 address no-default-link-local'): + bridge['ipv6_eui64_prefix_remove'].append('fe80::/64') + else: + # add the link-local by default to make IPv6 work + bridge['ipv6_eui64_prefix'].append('fe80::/64') # Disable IPv6 forwarding on this interface if conf.exists('ipv6 disable-forwarding'): @@ -176,6 +190,12 @@ def get_config(): if conf.exists('mac'): bridge['mac'] = conf.return_value('mac') + # Find out if MAC has changed - if so, we need to delete all IPv6 EUI64 addresses + # before re-adding them + if ( bridge['mac'] and bridge['intf'] in Section.interfaces(section='bridge') + and bridge['mac'] != BridgeIf(bridge['intf'], create=False).get_mac() ): + bridge['ipv6_eui64_prefix_remove'] += bridge['ipv6_eui64_prefix'] + # Interval at which neighbor bridges are removed if conf.exists('max-age'): bridge['max_age'] = int(conf.return_value('max-age')) @@ -283,8 +303,6 @@ def apply(bridge): 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 @@ -318,9 +336,10 @@ def apply(bridge): # assign/remove VRF br.set_vrf(bridge['vrf']) - # Change interface MAC address - if bridge['mac']: - br.set_mac(bridge['mac']) + # Delete old IPv6 EUI64 addresses before changing MAC + # (adding members to a fresh bridge changes its MAC too) + for addr in bridge['ipv6_eui64_prefix_remove']: + br.del_ipv6_eui64_address(addr) # remove interface from bridge for intf in bridge['member_remove']: @@ -328,8 +347,24 @@ def apply(bridge): # add interfaces to bridge for member in bridge['member']: + # flushes address of only children of Interfaces class + # (e.g. vlan are not) + if member['name'] in Section.interfaces(): + klass = Section.klass(member['name'], vlan=False) + klass(member['name'], create=False).flush_addrs() + # flushes all interfaces + cmd(f'ip addr flush dev "{member["name"]}"') br.add_port(member['name']) + # Change interface MAC address + if bridge['mac']: + br.set_mac(bridge['mac']) + + # Add IPv6 EUI-based addresses (must be done after adding the + # 1st bridge member or setting its MAC) + for addr in bridge['ipv6_eui64_prefix']: + br.add_ipv6_eui64_address(addr) + # up/down interface if bridge['disable']: br.set_admin_state('down') diff --git a/src/conf_mode/interfaces-dummy.py b/src/conf_mode/interfaces-dummy.py index a256103af..23eaa4ecb 100755 --- a/src/conf_mode/interfaces-dummy.py +++ b/src/conf_mode/interfaces-dummy.py @@ -23,7 +23,7 @@ from netifaces import interfaces from vyos.ifconfig import DummyIf from vyos.configdict import list_diff from vyos.config import Config -from vyos.util import is_bridge_member +from vyos.validate import is_bridge_member from vyos import ConfigError default_config_data = { @@ -33,6 +33,7 @@ default_config_data = { 'description': '', 'disable': False, 'intf': '', + 'is_bridge_member': False, 'vrf': '' } @@ -49,6 +50,8 @@ def get_config(): # Check if interface has been removed if not conf.exists('interfaces dummy ' + dummy['intf']): dummy['deleted'] = True + # check if interface is member if a bridge + dummy['is_bridge_member'] = is_bridge_member(conf, dummy['intf']) return dummy # set new configuration level @@ -80,13 +83,11 @@ def get_config(): 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)) + if dummy['is_bridge_member']: + interface = dummy['intf'] + bridge = dummy['is_bridge_member'] + raise ConfigError(f'Interface "{interface}" can not be deleted as it belongs to bridge "{bridge}"!') + return None vrf_name = dummy['vrf'] diff --git a/src/conf_mode/interfaces-ethernet.py b/src/conf_mode/interfaces-ethernet.py index 5a977d797..3ddd394d7 100755 --- a/src/conf_mode/interfaces-ethernet.py +++ b/src/conf_mode/interfaces-ethernet.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2019 VyOS maintainers and contributors +# Copyright (C) 2019-2020 VyOS maintainers and contributors # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 or later as @@ -20,9 +20,9 @@ from sys import exit from copy import deepcopy from netifaces import interfaces -from vyos.ifconfig import EthernetIf +from vyos.ifconfig import EthernetIf, Section from vyos.ifconfig_vlan import apply_vlan_config, verify_vlan_config -from vyos.configdict import list_diff, vlan_to_dict +from vyos.configdict import list_diff, intf_to_dict, add_to_dict from vyos.config import Config from vyos import ConfigError @@ -49,7 +49,8 @@ default_config_data = { 'ip_proxy_arp': 0, 'ip_proxy_arp_pvlan': 0, 'ipv6_autoconf': 0, - 'ipv6_eui64_prefix': '', + 'ipv6_eui64_prefix': [], + 'ipv6_eui64_prefix_remove': [], 'ipv6_forwarding': 1, 'ipv6_dup_addr_detect': 1, 'intf': '', @@ -69,18 +70,18 @@ default_config_data = { } def get_config(): - eth = 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') - eth['intf'] = os.environ['VYOS_TAGNODE_VALUE'] + ifname = os.environ['VYOS_TAGNODE_VALUE'] + conf = Config() # check if ethernet interface has been removed - cfg_base = ['interfaces', 'ethernet', eth['intf']] + cfg_base = ['interfaces', 'ethernet', ifname] if not conf.exists(cfg_base): + eth = deepcopy(default_config_data) + eth['intf'] = ifname eth['deleted'] = True # we can not bail out early as ethernet interface can not be removed # Kernel will complain with: RTNETLINK answers: Operation not supported. @@ -90,42 +91,8 @@ def get_config(): # set new configuration level conf.set_level(cfg_base) - # retrieve configured interface addresses - if conf.exists('address'): - eth['address'] = conf.return_values('address') - - # get interface addresses (currently effective) - to determine which - # address is no longer valid and needs to be removed - eff_addr = conf.return_effective_values('address') - eth['address_remove'] = list_diff(eff_addr, eth['address']) - - # retrieve interface description - if conf.exists('description'): - eth['description'] = conf.return_value('description') - - # get DHCP client identifier - if conf.exists('dhcp-options client-id'): - eth['dhcp_client_id'] = conf.return_value('dhcp-options client-id') - - # DHCP client host name (overrides the system host name) - if conf.exists('dhcp-options host-name'): - eth['dhcp_hostname'] = conf.return_value('dhcp-options host-name') - - # DHCP client vendor identifier - if conf.exists('dhcp-options vendor-class-id'): - eth['dhcp_vendor_class_id'] = conf.return_value('dhcp-options vendor-class-id') - - # DHCPv6 only acquire config parameters, no address - if conf.exists('dhcpv6-options parameters-only'): - eth['dhcpv6_prm_only'] = True - - # DHCPv6 temporary IPv6 address - if conf.exists('dhcpv6-options temporary'): - eth['dhcpv6_temporary'] = True - - # ignore link state changes - if conf.exists('disable-link-detect'): - eth['disable_link_detect'] = 2 + eth, disabled = intf_to_dict(conf, default_config_data) + eth['intf'] = ifname # disable ethernet flow control (pause frames) if conf.exists('disable-flow-control'): @@ -135,10 +102,6 @@ def get_config(): if conf.exists('hw-id'): eth['hw_id'] = conf.return_value('hw-id') - # disable interface - if conf.exists('disable'): - eth['disable'] = True - # interface duplex if conf.exists('duplex'): eth['duplex'] = conf.return_value('duplex') @@ -147,54 +110,10 @@ def get_config(): if conf.exists('ip arp-cache-timeout'): eth['ip_arp_cache_tmo'] = int(conf.return_value('ip arp-cache-timeout')) - # ARP filter configuration - if conf.exists('ip disable-arp-filter'): - eth['ip_disable_arp_filter'] = 0 - - # ARP enable accept - if conf.exists('ip enable-arp-accept'): - eth['ip_enable_arp_accept'] = 1 - - # ARP enable announce - if conf.exists('ip enable-arp-announce'): - eth['ip_enable_arp_announce'] = 1 - - # ARP enable ignore - if conf.exists('ip enable-arp-ignore'): - eth['ip_enable_arp_ignore'] = 1 - - # Enable proxy-arp on this interface - if conf.exists('ip enable-proxy-arp'): - eth['ip_proxy_arp'] = 1 - # Enable private VLAN proxy ARP on this interface if conf.exists('ip proxy-arp-pvlan'): eth['ip_proxy_arp_pvlan'] = 1 - # Enable acquisition of IPv6 address using stateless autoconfig (SLAAC) - if conf.exists('ipv6 address autoconf'): - eth['ipv6_autoconf'] = 1 - - # Get prefix for IPv6 addressing based on MAC address (EUI-64) - if conf.exists('ipv6 address eui64'): - eth['ipv6_eui64_prefix'] = conf.return_value('ipv6 address eui64') - - # Disable IPv6 forwarding on this interface - if conf.exists('ipv6 disable-forwarding'): - eth['ipv6_forwarding'] = 0 - - # IPv6 Duplicate Address Detection (DAD) tries - if conf.exists('ipv6 dup-addr-detect-transmits'): - eth['ipv6_dup_addr_detect'] = int(conf.return_value('ipv6 dup-addr-detect-transmits')) - - # Media Access Control (MAC) address - if conf.exists('mac'): - eth['mac'] = conf.return_value('mac') - - # Maximum Transmission Unit (MTU) - if conf.exists('mtu'): - eth['mtu'] = int(conf.return_value('mtu')) - # GRO (generic receive offload) if conf.exists('offload-options generic-receive'): eth['offload_gro'] = conf.return_value('offload-options generic-receive') @@ -219,37 +138,8 @@ 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 - # interface is no longer present and needs to be removed - eff_intf = conf.list_effective_nodes('vif-s') - act_intf = conf.list_nodes('vif-s') - eth['vif_s_remove'] = list_diff(eff_intf, act_intf) - - if conf.exists('vif-s'): - for vif_s in conf.list_nodes('vif-s'): - # set config level to vif-s interface - conf.set_level(cfg_base + ['vif-s', vif_s]) - eth['vif_s'].append(vlan_to_dict(conf)) - - # re-set configuration level to parse new nodes - conf.set_level(cfg_base) - # Determine vif interfaces (currently effective) - to determine which - # vif interface is no longer present and needs to be removed - eff_intf = conf.list_effective_nodes('vif') - act_intf = conf.list_nodes('vif') - eth['vif_remove'] = list_diff(eff_intf, act_intf) - - if conf.exists('vif'): - for vif in conf.list_nodes('vif'): - # set config level to vif interface - conf.set_level(cfg_base + ['vif', vif]) - eth['vif'].append(vlan_to_dict(conf)) + add_to_dict(conf, disabled, eth, 'vif', 'vif') + add_to_dict(conf, disabled, eth, 'vif-s', 'vif_s') return eth @@ -336,13 +226,15 @@ def apply(eth): 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']) + # Delete old IPv6 EUI64 addresses before changing MAC + for addr in eth['ipv6_eui64_prefix_remove']: + e.del_ipv6_eui64_address(addr) + # Change interface MAC address - re-set to real hardware address (hw-id) # if custom mac is removed if eth['mac']: @@ -350,6 +242,10 @@ def apply(eth): elif eth['hw_id']: e.set_mac(eth['hw_id']) + # Add IPv6 EUI-based addresses + for addr in eth['ipv6_eui64_prefix']: + e.add_ipv6_eui64_address(addr) + # Maximum Transmission Unit (MTU) e.set_mtu(eth['mtu']) diff --git a/src/conf_mode/interfaces-geneve.py b/src/conf_mode/interfaces-geneve.py index e47473d76..708a64474 100755 --- a/src/conf_mode/interfaces-geneve.py +++ b/src/conf_mode/interfaces-geneve.py @@ -22,7 +22,7 @@ from netifaces import interfaces from vyos.config import Config from vyos.ifconfig import GeneveIf -from vyos.util import is_bridge_member +from vyos.validate import is_bridge_member from vyos import ConfigError default_config_data = { @@ -33,6 +33,7 @@ default_config_data = { 'intf': '', 'ip_arp_cache_tmo': 30, 'ip_proxy_arp': 0, + 'is_bridge_member': False, 'mtu': 1500, 'remote': '', 'vni': '' @@ -51,6 +52,8 @@ def get_config(): # Check if interface has been removed if not conf.exists('interfaces geneve ' + geneve['intf']): geneve['deleted'] = True + # check if interface is member if a bridge + geneve['is_bridge_member'] = is_bridge_member(conf, geneve['intf']) return geneve # set new configuration level @@ -93,13 +96,11 @@ def get_config(): def verify(geneve): if geneve['deleted']: - 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)) + if geneve['is_bridge_member']: + interface = geneve['intf'] + bridge = geneve['is_bridge_member'] + raise ConfigError(f'Interface "{interface}" can not be deleted as it belongs to bridge "{bridge}"!') + return None if not geneve['remote']: diff --git a/src/conf_mode/interfaces-l2tpv3.py b/src/conf_mode/interfaces-l2tpv3.py index 11ba9acdd..33cf62f70 100755 --- a/src/conf_mode/interfaces-l2tpv3.py +++ b/src/conf_mode/interfaces-l2tpv3.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2019 VyOS maintainers and contributors +# Copyright (C) 2019-2020 VyOS maintainers and contributors # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 or later as @@ -18,13 +18,13 @@ import os from sys import exit from copy import deepcopy +from netifaces import interfaces 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 +from vyos.validate import is_bridge_member, is_addr_assigned default_config_data = { 'address': [], @@ -36,9 +36,10 @@ default_config_data = { 'local_port': 5000, 'intf': '', 'ipv6_autoconf': 0, - 'ipv6_eui64_prefix': '', + 'ipv6_eui64_prefix': [], 'ipv6_forwarding': 1, 'ipv6_dup_addr_detect': 1, + 'is_bridge_member': False, 'mtu': 1488, 'peer_session_id': '', 'peer_tunnel_id': '', @@ -68,15 +69,16 @@ def get_config(): # Check if interface has been removed if not conf.exists('interfaces l2tpv3 ' + l2tpv3['intf']): l2tpv3['deleted'] = True - # to delete the l2tpv3 interface we need to current - # tunnel_id and session_id - if conf.exists_effective('interfaces l2tpv3 {} tunnel-id'.format(l2tpv3['intf'])): - l2tpv3['tunnel_id'] = conf.return_effective_value( - 'interfaces l2tpv3 {} tunnel-id'.format(l2tpv3['intf'])) + interface = l2tpv3['intf'] + # check if interface is member if a bridge + l2tpv3['is_bridge_member'] = is_bridge_member(conf, interface) - if conf.exists_effective('interfaces l2tpv3 {} session-id'.format(l2tpv3['intf'])): - l2tpv3['session_id'] = conf.return_effective_value( - 'interfaces l2tpv3 {} session-id'.format(l2tpv3['intf'])) + # to delete the l2tpv3 interface we need the current tunnel_id and session_id + if conf.exists_effective(f'interfaces l2tpv3 {interface} tunnel-id'): + l2tpv3['tunnel_id'] = conf.return_effective_value(f'interfaces l2tpv3 {interface} tunnel-id') + + if conf.exists_effective(f'interfaces l2tpv3 {interface} session-id'): + l2tpv3['session_id'] = conf.return_effective_value(f'interfaces l2tpv3 {interface} session-id') return l2tpv3 @@ -111,9 +113,14 @@ def get_config(): if conf.exists('ipv6 address autoconf'): l2tpv3['ipv6_autoconf'] = 1 - # Get prefix for IPv6 addressing based on MAC address (EUI-64) + # Get prefixes for IPv6 addressing based on MAC address (EUI-64) if conf.exists('ipv6 address eui64'): - l2tpv3['ipv6_eui64_prefix'] = conf.return_value('ipv6 address eui64') + l2tpv3['ipv6_eui64_prefix'] = conf.return_values('ipv6 address eui64') + + # Remove the default link-local address if set. + if not conf.exists('ipv6 address no-default-link-local'): + # add the link-local by default to make IPv6 work + l2tpv3['ipv6_eui64_prefix'].append('fe80::/64') # Disable IPv6 forwarding on this interface if conf.exists('ipv6 disable-forwarding'): @@ -158,17 +165,19 @@ def verify(l2tpv3): interface = l2tpv3['intf'] if l2tpv3['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)) + if l2tpv3['is_bridge_member']: + interface = l2tpv3['intf'] + bridge = l2tpv3['is_bridge_member'] + raise ConfigError(f'Interface "{interface}" can not be deleted as it belongs to bridge "{bridge}"!') + return None if not l2tpv3['local_address']: raise ConfigError(f'Must configure the l2tpv3 local-ip for {interface}') + if not is_addr_assigned(l2tpv3['local_address']): + raise ConfigError(f'Must use a configured IP on l2tpv3 local-ip for {interface}') + if not l2tpv3['remote_address']: raise ConfigError(f'Must configure the l2tpv3 remote-ip for {interface}') @@ -224,8 +233,6 @@ def apply(l2tpv3): 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 @@ -237,6 +244,10 @@ def apply(l2tpv3): for addr in l2tpv3['address']: l.add_addr(addr) + # IPv6 EUI-based addresses + for addr in l2tpv3['ipv6_eui64_prefix']: + l.add_ipv6_eui64_address(addr) + # As the interface is always disabled first when changing parameters # we will only re-enable the interface if it is not administratively # disabled diff --git a/src/conf_mode/interfaces-openvpn.py b/src/conf_mode/interfaces-openvpn.py index 836deb64b..029bc1d69 100755 --- a/src/conf_mode/interfaces-openvpn.py +++ b/src/conf_mode/interfaces-openvpn.py @@ -25,12 +25,12 @@ from time import sleep from shutil import rmtree from vyos.config import Config +from vyos.configdict import list_diff from vyos.ifconfig import VTunIf -from vyos.util import call, is_bridge_member, chown, chmod_600, chmod_755 -from vyos.validate import is_addr_assigned -from vyos import ConfigError from vyos.template import render - +from vyos.util import call, chown, chmod_600, chmod_755 +from vyos.validate import is_addr_assigned, is_bridge_member, is_ipv4 +from vyos import ConfigError user = 'openvpn' group = 'openvpn' @@ -39,6 +39,7 @@ default_config_data = { 'address': [], 'auth_user': '', 'auth_pass': '', + 'auth_user_pass_file': '', 'auth': False, 'bridge_member': [], 'compress_lzo': False, @@ -50,11 +51,13 @@ default_config_data = { 'hash': '', 'intf': '', 'ipv6_autoconf': 0, - 'ipv6_eui64_prefix': '', + 'ipv6_eui64_prefix': [], + 'ipv6_eui64_prefix_remove': [], 'ipv6_forwarding': 1, 'ipv6_dup_addr_detect': 1, 'ipv6_local_address': [], 'ipv6_remote_address': [], + 'is_bridge_member': False, 'ping_restart': '60', 'ping_interval': '10', 'local_address': [], @@ -66,6 +69,7 @@ default_config_data = { 'options': [], 'persistent_tunnel': False, 'protocol': 'udp', + 'protocol_real': '', 'redirect_gateway': '', 'remote_address': [], 'remote_host': [], @@ -193,10 +197,13 @@ def get_config(): raise ConfigError('Interface (VYOS_TAGNODE_VALUE) not specified') openvpn['intf'] = os.environ['VYOS_TAGNODE_VALUE'] + openvpn['auth_user_pass_file'] = f"/run/openvpn/{openvpn['intf']}.pw" # Check if interface instance has been removed if not conf.exists('interfaces openvpn ' + openvpn['intf']): openvpn['deleted'] = True + # check if interface is member if a bridge + openvpn['is_bridge_member'] = is_bridge_member(conf, openvpn['intf']) return openvpn # Check if we belong to any bridge interface @@ -309,9 +316,21 @@ def get_config(): if conf.exists('ipv6 address autoconf'): openvpn['ipv6_autoconf'] = 1 - # Get prefix for IPv6 addressing based on MAC address (EUI-64) + # Get prefixes for IPv6 addressing based on MAC address (EUI-64) if conf.exists('ipv6 address eui64'): - openvpn['ipv6_eui64_prefix'] = conf.return_value('ipv6 address eui64') + openvpn['ipv6_eui64_prefix'] = conf.return_values('ipv6 address eui64') + + # Determine currently effective EUI64 addresses - to determine which + # address is no longer valid and needs to be removed + eff_addr = conf.return_effective_values('ipv6 address eui64') + openvpn['ipv6_eui64_prefix_remove'] = list_diff(eff_addr, openvpn['ipv6_eui64_prefix']) + + # Remove the default link-local address if set. + if conf.exists('ipv6 address no-default-link-local'): + openvpn['ipv6_eui64_prefix_remove'].append('fe80::/64') + else: + # add the link-local by default to make IPv6 work + openvpn['ipv6_eui64_prefix'].append('fe80::/64') # Disable IPv6 forwarding on this interface if conf.exists('ipv6 disable-forwarding'): @@ -553,6 +572,23 @@ def get_config(): if openvpn['mode'] == 'server' and not openvpn['server_topology']: openvpn['server_topology'] = 'net30' + # Convert protocol to real protocol used by openvpn. + # To make openvpn listen on both IPv4 and IPv6 we must use *6 protocols + # (https://community.openvpn.net/openvpn/ticket/360), unless local is IPv4 + # in which case it must use the standard protocols. + # Note: this will break openvpn if IPv6 is disabled on the system. + # This currently isn't supported, a check can be added in the future. + if openvpn['protocol'] == 'tcp-active': + openvpn['protocol_real'] = 'tcp6-client' + elif openvpn['protocol'] == 'tcp-passive': + openvpn['protocol_real'] = 'tcp6-server' + else: + openvpn['protocol_real'] = 'udp6' + + if is_ipv4(openvpn['local_host']): + # takes out the '6' + openvpn['protocol_real'] = openvpn['protocol_real'][:3] + openvpn['protocol_real'][4:] + # Set defaults where necessary. # If any of the input parameters are wrong, # this will return False and no defaults will be set. @@ -598,13 +634,11 @@ def get_config(): 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)) + if openvpn['is_bridge_member']: + interface = openvpn['intf'] + bridge = openvpn['is_bridge_member'] + raise ConfigError(f'Interface "{interface}" can not be deleted as it belongs to bridge "{bridge}"!') + return None @@ -917,18 +951,18 @@ def verify(openvpn): return None def generate(openvpn): - if openvpn['deleted'] or openvpn['disable']: - return None - interface = openvpn['intf'] directory = os.path.dirname(get_config_name(interface)) - # we can't know in advance which clients have been, - # remove all client configs + # we can't know in advance which clients have been removed, + # thus all client configs will be removed and re-added on demand ccd_dir = os.path.join(directory, 'ccd', interface) if os.path.isdir(ccd_dir): rmtree(ccd_dir, ignore_errors=True) + if openvpn['deleted'] or openvpn['disable']: + return None + # create config directory on demand directories = [] directories.append(f'{directory}/status') @@ -944,17 +978,16 @@ def generate(openvpn): fix_permissions.append(openvpn['tls_key']) # Generate User/Password authentication file - user_auth_file = f'/tmp/openvpn-{interface}-pw' if openvpn['auth']: - with open(user_auth_file, 'w') as f: + with open(openvpn['auth_user_pass_file'], 'w') as f: f.write('{}\n{}'.format(openvpn['auth_user'], openvpn['auth_pass'])) # also change permission on auth file - fix_permissions.append(user_auth_file) + fix_permissions.append(openvpn['auth_user_pass_file']) else: # delete old auth file if present - if os.path.isfile(user_auth_file): - os.remove(user_auth_file) + if os.path.isfile(openvpn['auth_user_pass_file']): + os.remove(openvpn['auth_user_pass_file']) # Generate client specific configuration for client in openvpn['client']: @@ -980,15 +1013,14 @@ def apply(openvpn): # Do some cleanup when OpenVPN is disabled/deleted if openvpn['deleted'] or openvpn['disable']: - # cleanup old configuration file - if os.path.isfile(get_config_name(interface)): - os.remove(get_config_name(interface)) + # cleanup old configuration files + cleanup = [] + cleanup.append(get_config_name(interface)) + cleanup.append(openvpn['auth_user_pass_file']) - # cleanup client config dir - directory = os.path.dirname(get_config_name(interface)) - ccd_dir = os.path.join(directory, 'ccd', interface) - if os.path.isdir(ccd_dir): - rmtree(ccd_dir, ignore_errors=True) + for file in cleanup: + if os.path.isfile(file): + os.unlink(file) return None @@ -1025,13 +1057,24 @@ def apply(openvpn): 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']) + # IPv6 EUI-based addresses - only in TAP mode (TUN's have no MAC) + # If MAC has changed, old EUI64 addresses won't get deleted, + # but this isn't easy to solve, so leave them. + # This is even more difficult as openvpn uses a random MAC for the + # initial interface creation, unless set by 'lladdr'. + # NOTE: right now the interface is always deleted. For future + # compatibility when tap's are not deleted, leave the del_ in + if openvpn['mode'] == 'tap': + for addr in openvpn['ipv6_eui64_prefix_remove']: + o.del_ipv6_eui64_address(addr) + for addr in openvpn['ipv6_eui64_prefix']: + o.add_ipv6_eui64_address(addr) + except: pass diff --git a/src/conf_mode/interfaces-pppoe.py b/src/conf_mode/interfaces-pppoe.py index f942b7d2f..e72540f66 100755 --- a/src/conf_mode/interfaces-pppoe.py +++ b/src/conf_mode/interfaces-pppoe.py @@ -173,12 +173,6 @@ def generate(pppoe): 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(f'systemctl stop ppp@{intf}.service') @@ -189,27 +183,23 @@ def generate(pppoe): os.unlink(file) else: + # generated script must be executable + # Create PPP configuration files render(config_pppoe, 'pppoe/peer.tmpl', - pppoe, trim_blocks=True) + pppoe, trim_blocks=True, permission=0o755) # Create script for ip-pre-up.d render(script_pppoe_pre_up, 'pppoe/ip-pre-up.script.tmpl', - pppoe, trim_blocks=True) + pppoe, trim_blocks=True, permission=0o755) # Create script for ip-up.d render(script_pppoe_ip_up, 'pppoe/ip-up.script.tmpl', - pppoe, trim_blocks=True) + pppoe, trim_blocks=True, permission=0o755) # Create script for ip-down.d render(script_pppoe_ip_down, 'pppoe/ip-down.script.tmpl', - pppoe, trim_blocks=True) + pppoe, trim_blocks=True, permission=0o755) # Create script for ipv6-up.d render(script_pppoe_ipv6_up, 'pppoe/ipv6-up.script.tmpl', - pppoe, trim_blocks=True) - - # make generated script file executable - chmod_755(script_pppoe_pre_up) - chmod_755(script_pppoe_ip_up) - chmod_755(script_pppoe_ip_down) - chmod_755(script_pppoe_ipv6_up) + pppoe, trim_blocks=True, permission=0o755) return None diff --git a/src/conf_mode/interfaces-pseudo-ethernet.py b/src/conf_mode/interfaces-pseudo-ethernet.py index 655006146..f0f893b44 100755 --- a/src/conf_mode/interfaces-pseudo-ethernet.py +++ b/src/conf_mode/interfaces-pseudo-ethernet.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2019 VyOS maintainers and contributors +# Copyright (C) 2019-2020 VyOS maintainers and contributors # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 or later as @@ -20,11 +20,11 @@ from copy import deepcopy from sys import exit from netifaces import interfaces -from vyos.ifconfig import MACVLANIf -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.configdict import list_diff, vlan_to_dict, intf_to_dict, add_to_dict +from vyos.ifconfig import MACVLANIf, Section +from vyos.ifconfig_vlan import apply_vlan_config, verify_vlan_config +from vyos.validate import is_bridge_member from vyos import ConfigError default_config_data = { @@ -39,6 +39,7 @@ default_config_data = { 'dhcpv6_temporary': False, 'disable': False, 'disable_link_detect': 1, + 'intf': '', 'ip_arp_cache_tmo': 30, 'ip_disable_arp_filter': 1, 'ip_enable_arp_accept': 0, @@ -47,10 +48,11 @@ default_config_data = { 'ip_proxy_arp': 0, 'ip_proxy_arp_pvlan': 0, 'ipv6_autoconf': 0, - 'ipv6_eui64_prefix': '', + 'ipv6_eui64_prefix': [], + 'ipv6_eui64_prefix_remove': [], 'ipv6_forwarding': 1, 'ipv6_dup_addr_detect': 1, - 'intf': '', + 'is_bridge_member': False, 'source_interface': '', 'source_interface_changed': False, 'mac': '', @@ -63,109 +65,36 @@ default_config_data = { } def get_config(): - peth = deepcopy(default_config_data) - conf = Config() - # determine tagNode instance if 'VYOS_TAGNODE_VALUE' not in os.environ: raise ConfigError('Interface (VYOS_TAGNODE_VALUE) not specified') - peth['intf'] = os.environ['VYOS_TAGNODE_VALUE'] - cfg_base = ['interfaces', 'pseudo-ethernet', peth['intf']] + ifname = os.environ['VYOS_TAGNODE_VALUE'] + conf = Config() # Check if interface has been removed + cfg_base = ['interfaces', 'pseudo-ethernet', ifname] if not conf.exists(cfg_base): + peth = deepcopy(default_config_data) peth['deleted'] = True + # check if interface is member if a bridge + peth['is_bridge_member'] = is_bridge_member(conf, ifname) return peth # set new configuration level conf.set_level(cfg_base) - # retrieve configured interface addresses - if conf.exists(['address']): - peth['address'] = conf.return_values(['address']) - - # get interface addresses (currently effective) - to determine which - # address is no longer valid and needs to be removed - eff_addr = conf.return_effective_values(['address']) - peth['address_remove'] = list_diff(eff_addr, peth['address']) - - # retrieve interface description - if conf.exists(['description']): - peth['description'] = conf.return_value(['description']) - - # get DHCP client identifier - if conf.exists(['dhcp-options', 'client-id']): - peth['dhcp_client_id'] = conf.return_value(['dhcp-options', 'client-id']) - - # DHCP client host name (overrides the system host name) - if conf.exists(['dhcp-options', 'host-name']): - peth['dhcp_hostname'] = conf.return_value(['dhcp-options', 'host-name']) - - # DHCP client vendor identifier - if conf.exists(['dhcp-options', 'vendor-class-id']): - peth['dhcp_vendor_class_id'] = conf.return_value(['dhcp-options', 'vendor-class-id']) - - # DHCPv6 only acquire config parameters, no address - if conf.exists(['dhcpv6-options parameters-only']): - peth['dhcpv6_prm_only'] = True - - # DHCPv6 temporary IPv6 address - if conf.exists(['dhcpv6-options temporary']): - peth['dhcpv6_temporary'] = True - - # disable interface - if conf.exists(['disable']): - peth['disable'] = True - - # ignore link state changes - if conf.exists(['disable-link-detect']): - peth['disable_link_detect'] = 2 + peth, disabled = intf_to_dict(conf, default_config_data) + peth['intf'] = ifname # ARP cache entry timeout in seconds if conf.exists(['ip', 'arp-cache-timeout']): peth['ip_arp_cache_tmo'] = int(conf.return_value(['ip', 'arp-cache-timeout'])) - # ARP filter configuration - if conf.exists(['ip', 'disable-arp-filter']): - peth['ip_disable_arp_filter'] = 0 - - # ARP enable accept - if conf.exists(['ip', 'enable-arp-accept']): - peth['ip_enable_arp_accept'] = 1 - - # ARP enable announce - if conf.exists(['ip', 'enable-arp-announce']): - peth['ip_enable_arp_announce'] = 1 - - # ARP enable ignore - if conf.exists(['ip', 'enable-arp-ignore']): - peth['ip_enable_arp_ignore'] = 1 - - # Enable proxy-arp on this interface - if conf.exists(['ip', 'enable-proxy-arp']): - peth['ip_proxy_arp'] = 1 - # Enable private VLAN proxy ARP on this interface if conf.exists(['ip', 'proxy-arp-pvlan']): peth['ip_proxy_arp_pvlan'] = 1 - # Enable acquisition of IPv6 address using stateless autoconfig (SLAAC) - if conf.exists('ipv6 address autoconf'): - peth['ipv6_autoconf'] = 1 - - # Get prefix for IPv6 addressing based on MAC address (EUI-64) - if conf.exists('ipv6 address eui64'): - peth['ipv6_eui64_prefix'] = conf.return_value('ipv6 address eui64') - - # Disable IPv6 forwarding on this interface - if conf.exists('ipv6 disable-forwarding'): - peth['ipv6_forwarding'] = 0 - - # IPv6 Duplicate Address Detection (DAD) tries - if conf.exists('ipv6 dup-addr-detect-transmits'): - peth['ipv6_dup_addr_detect'] = int(conf.return_value('ipv6 dup-addr-detect-transmits')) - # Physical interface if conf.exists(['source-interface']): peth['source_interface'] = conf.return_value(['source-interface']) @@ -173,65 +102,29 @@ def get_config(): if tmp != peth['source_interface']: peth['source_interface_changed'] = True - # Media Access Control (MAC) address - if conf.exists(['mac']): - peth['mac'] = conf.return_value(['mac']) - # MACvlan mode if conf.exists(['mode']): peth['mode'] = conf.return_value(['mode']) - # retrieve VRF instance - if conf.exists('vrf'): - peth['vrf'] = conf.return_value('vrf') - - # re-set configuration level to parse new nodes - conf.set_level(cfg_base) - # get vif-s interfaces (currently effective) - to determine which vif-s - # interface is no longer present and needs to be removed - eff_intf = conf.list_effective_nodes('vif-s') - act_intf = conf.list_nodes('vif-s') - peth['vif_s_remove'] = list_diff(eff_intf, act_intf) - - if conf.exists('vif-s'): - for vif_s in conf.list_nodes('vif-s'): - # set config level to vif-s interface - conf.set_level(cfg_base + ['vif-s', vif_s]) - peth['vif_s'].append(vlan_to_dict(conf)) - - # re-set configuration level to parse new nodes - conf.set_level(cfg_base) - # Determine vif interfaces (currently effective) - to determine which - # vif interface is no longer present and needs to be removed - eff_intf = conf.list_effective_nodes('vif') - act_intf = conf.list_nodes('vif') - peth['vif_remove'] = list_diff(eff_intf, act_intf) - - if conf.exists('vif'): - for vif in conf.list_nodes('vif'): - # set config level to vif interface - conf.set_level(cfg_base + ['vif', vif]) - peth['vif'].append(vlan_to_dict(conf)) - + add_to_dict(conf, disabled, peth, 'vif', 'vif') + add_to_dict(conf, disabled, peth, 'vif-s', 'vif_s') return peth def verify(peth): if peth['deleted']: - 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)) + if peth['is_bridge_member']: + interface = peth['intf'] + bridge = peth['is_bridge_member'] + raise ConfigError(f'Interface "{interface}" can not be deleted as it belongs to bridge "{bridge}"!') + return None if not peth['source_interface']: - raise ConfigError('Link device must be set for virtual ethernet {}'.format(peth['intf'])) + raise ConfigError('source-interface 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') + raise ConfigError('Pseudo-ethernet source-interface does not exist') vrf_name = peth['vrf'] if vrf_name and vrf_name not in interfaces(): @@ -245,38 +138,29 @@ def generate(peth): return None def apply(peth): - - p = '' if peth['deleted']: # delete interface - p = MACVLANIf(peth['intf']) - p.remove() + MACVLANIf(peth['intf']).remove() return None - elif peth['source_interface_changed']: - # Check if MACVLAN interface already exists. Parameters like the - # underlaying source-interface device can not be changed on the fly - # and the interface needs to be recreated from the bottom. - # - # 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']) - p.remove() - - # MACVLAN interface needs to be created on-block instead of passing a ton - # of arguments, I just use a dict that is managed by vyos.ifconfig - conf = deepcopy(MACVLANIf.get_config()) - - # Assign MACVLAN instance configuration parameters to config dict - 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'], **conf) - else: - p = MACVLANIf(peth['intf']) + # Check if MACVLAN interface already exists. Parameters like the underlaying + # source-interface device can not be changed on the fly and the interface + # needs to be recreated from the bottom. + if peth['intf'] in interfaces(): + if peth['source_interface_changed']: + MACVLANIf(peth['intf']).remove() + + # MACVLAN interface needs to be created on-block instead of passing a ton + # of arguments, I just use a dict that is managed by vyos.ifconfig + conf = deepcopy(MACVLANIf.get_config()) + + # Assign MACVLAN instance configuration parameters to config dict + 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'], **conf) # update interface description used e.g. within SNMP p.set_alias(peth['description']) @@ -314,8 +198,6 @@ def apply(peth): 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 @@ -324,10 +206,18 @@ def apply(peth): # assign/remove VRF p.set_vrf(peth['vrf']) + # Delete old IPv6 EUI64 addresses before changing MAC + for addr in peth['ipv6_eui64_prefix_remove']: + p.del_ipv6_eui64_address(addr) + # Change interface MAC address if peth['mac']: p.set_mac(peth['mac']) + # Add IPv6 EUI-based addresses + for addr in peth['ipv6_eui64_prefix']: + p.add_ipv6_eui64_address(addr) + # Change interface mode p.set_mode(peth['mode']) diff --git a/src/conf_mode/interfaces-tunnel.py b/src/conf_mode/interfaces-tunnel.py index 06c2ea29b..fc084814a 100755 --- a/src/conf_mode/interfaces-tunnel.py +++ b/src/conf_mode/interfaces-tunnel.py @@ -25,7 +25,7 @@ from vyos.config import Config from vyos.ifconfig import Interface, GREIf, GRETapIf, IPIPIf, IP6GREIf, IPIP6If, IP6IP6If, SitIf, Sit6RDIf from vyos.ifconfig.afi import IP4, IP6 from vyos.configdict import list_diff -from vyos.validate import is_ipv4, is_ipv6 +from vyos.validate import is_ipv4, is_ipv6, is_bridge_member from vyos import ConfigError from vyos.dicts import FixedDict @@ -255,7 +255,9 @@ default_config_data = { 'ipv6_forwarding': 1, 'ipv6_dad_transmits': 1, # internal + 'interfaces': [], 'tunnel': {}, + 'bridge': '', # the following names are exactly matching the name # for the ip command and must not be changed 'ifname': '', @@ -264,6 +266,7 @@ default_config_data = { 'mtu': '1476', 'local': '', 'remote': '', + 'dev': '', 'multicast': 'disable', 'allmulticast': 'disable', 'ttl': '255', @@ -285,6 +288,7 @@ mapping = { 'local': ('local-ip', False, None), 'remote': ('remote-ip', False, None), 'multicast': ('multicast', False, None), + 'dev': ('source-interface', False, None), 'ttl': ('parameters ip ttl', False, None), 'tos': ('parameters ip tos', False, None), 'key': ('parameters ip key', False, None), @@ -405,6 +409,10 @@ def get_config(): ct = conf.get_config_dict()['tunnel'] options['tunnel'] = {} + # check for bridges + options['bridge'] = is_bridge_member(conf, ifname) + options['interfaces'] = interfaces() + for name in ct: tunnel = ct[name] encap = tunnel.get('encapsulation', '') @@ -429,6 +437,11 @@ def verify(conf): if changes['section'] == 'delete': if ifname in options['nhrp']: raise ConfigError(f'Can not delete interface tunnel {iftype} {ifname}, it is used by nhrp') + + bridge = options['bridge'] + if bridge: + raise ConfigError(f'Interface "{ifname}" can not be deleted as it belongs to bridge "{bridge}"!') + # done, bail out early return None @@ -448,7 +461,7 @@ def verify(conf): # what are the tunnel options we can set / modified / deleted kls = get_class(options) - valid = kls.updates + ['alias', 'addresses-add', 'addresses-del', 'vrf'] + valid = kls.updates + ['alias', 'addresses-add', 'addresses-del', 'vrf', 'state'] if changes['section'] == 'create': valid.extend(['type',]) @@ -474,6 +487,7 @@ def verify(conf): afi_remote = get_afi(tun_remote) tun_ismgre = iftype == 'gre' and not options['remote'] tun_is6rd = iftype == 'sit' and options['6rd-prefix'] + tun_dev = options['dev'] # incompatible options @@ -483,6 +497,9 @@ def verify(conf): if tun_local and options['dhcp-interface']: raise ConfigError(f'Must configure only one of local-ip or dhcp-interface for tunnel {iftype} {ifname}') + if tun_dev and iftype in ('gre-bridge', 'sit'): + raise ConfigError(f'source interface can not be used with {iftype} {ifname}') + # tunnel endpoint if afi_local != afi_remote: @@ -510,9 +527,14 @@ def verify(conf): # vrf check vrf = options['vrf'] - if vrf and vrf not in interfaces(): + if vrf and vrf not in options['interfaces']: raise ConfigError(f'VRF "{vrf}" does not exist') + # source-interface check + + if tun_dev and tun_dev not in options['interfaces']: + raise ConfigError(f'device "{dev}" does not exist') + # tunnel encapsulation check convert = { diff --git a/src/conf_mode/interfaces-vxlan.py b/src/conf_mode/interfaces-vxlan.py index 6639a9b0d..74eae4281 100755 --- a/src/conf_mode/interfaces-vxlan.py +++ b/src/conf_mode/interfaces-vxlan.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2019 VyOS maintainers and contributors +# Copyright (C) 2019-2020 VyOS maintainers and contributors # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 or later as @@ -22,7 +22,7 @@ from netifaces import interfaces from vyos.config import Config from vyos.ifconfig import VXLANIf, Interface -from vyos.util import is_bridge_member +from vyos.validate import is_bridge_member from vyos import ConfigError default_config_data = { @@ -39,9 +39,10 @@ default_config_data = { 'ip_enable_arp_ignore': 0, 'ip_proxy_arp': 0, 'ipv6_autoconf': 0, - 'ipv6_eui64_prefix': '', + 'ipv6_eui64_prefix': [], 'ipv6_forwarding': 1, 'ipv6_dup_addr_detect': 1, + 'is_bridge_member': False, 'source_address': '', 'source_interface': '', 'mtu': 1450, @@ -64,6 +65,8 @@ def get_config(): # Check if interface has been removed if not conf.exists('interfaces vxlan ' + vxlan['intf']): vxlan['deleted'] = True + # check if interface is member if a bridge + vxlan['is_bridge_member'] = is_bridge_member(conf, vxlan['intf']) return vxlan # set new configuration level @@ -113,9 +116,14 @@ def get_config(): if conf.exists('ipv6 address autoconf'): vxlan['ipv6_autoconf'] = 1 - # Get prefix for IPv6 addressing based on MAC address (EUI-64) + # Get prefixes for IPv6 addressing based on MAC address (EUI-64) if conf.exists('ipv6 address eui64'): - vxlan['ipv6_eui64_prefix'] = conf.return_value('ipv6 address eui64') + vxlan['ipv6_eui64_prefix'] = conf.return_values('ipv6 address eui64') + + # Remove the default link-local address if set. + if not conf.exists('ipv6 address no-default-link-local'): + # add the link-local by default to make IPv6 work + vxlan['ipv6_eui64_prefix'].append('fe80::/64') # Disable IPv6 forwarding on this interface if conf.exists('ipv6 disable-forwarding'): @@ -154,13 +162,11 @@ def get_config(): def verify(vxlan): if vxlan['deleted']: - 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)) + if vxlan['is_bridge_member']: + interface = vxlan['intf'] + bridge = vxlan['is_bridge_member'] + raise ConfigError(f'Interface "{interface}" can not be deleted as it belongs to bridge "{bridge}"!') + return None if vxlan['mtu'] < 1500: @@ -237,8 +243,6 @@ def apply(vxlan): 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 @@ -250,7 +254,11 @@ def apply(vxlan): for addr in vxlan['address']: v.add_addr(addr) - # As the bond interface is always disabled first when changing + # IPv6 EUI-based addresses + for addr in vxlan['ipv6_eui64_prefix']: + v.add_ipv6_eui64_address(addr) + + # As the VXLAN interface is always disabled first when changing # parameters we will only re-enable the interface if it is not # administratively disabled if not vxlan['disable']: diff --git a/src/conf_mode/interfaces-wireguard.py b/src/conf_mode/interfaces-wireguard.py index 8bf81c747..01f84260d 100755 --- a/src/conf_mode/interfaces-wireguard.py +++ b/src/conf_mode/interfaces-wireguard.py @@ -24,8 +24,8 @@ from netifaces import interfaces 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.util import chown, chmod_750, call +from vyos.validate import is_bridge_member from vyos import ConfigError kdir = r'/config/auth/wireguard' @@ -35,10 +35,11 @@ default_config_data = { 'address': [], 'address_remove': [], 'description': '', - 'lport': None, + 'listen_port': '', 'deleted': False, 'disable': False, - 'fwmark': 0x00, + 'is_bridge_member': False, + 'fwmark': 0, 'mtu': 1420, 'peer': [], 'peer_remove': [], # stores public keys of peers to remove @@ -80,6 +81,8 @@ def get_config(): # Check if interface has been removed if not conf.exists(base + [wg['intf']]): wg['deleted'] = True + # check if interface is member if a bridge + wg['is_bridge_member'] = is_bridge_member(conf, wg['intf']) return wg conf.set_level(base + [wg['intf']]) @@ -103,7 +106,7 @@ def get_config(): # local port to listen on if conf.exists(['port']): - wg['lport'] = conf.return_value(['port']) + wg['listen_port'] = conf.return_value(['port']) # fwmark value if conf.exists(['fwmark']): @@ -189,12 +192,11 @@ def verify(wg): interface = wg['intf'] 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)) + if wg['is_bridge_member']: + interface = wg['intf'] + bridge = wg['is_bridge_member'] + raise ConfigError(f'Interface "{interface}" can not be deleted as it belongs to bridge "{bridge}"!') + return None vrf_name = wg['vrf'] @@ -220,6 +222,12 @@ def verify(wg): if not peer['pubkey']: raise ConfigError(f'Peer public-key required for peer "{peer_name}"!') + if peer['address'] and not peer['port']: + raise ConfigError(f'Peer "{peer_name}" port must be defined if address is defined!') + + if not peer['address'] and peer['port']: + raise ConfigError(f'Peer "{peer_name}" address must be defined if port is defined!') + def apply(wg): # init wg class @@ -261,8 +269,8 @@ def apply(wg): # peer allowed-ips w.config['allowed-ips'] = peer['allowed-ips'] # local listen port - if wg['lport']: - w.config['port'] = wg['lport'] + if wg['listen_port']: + w.config['port'] = wg['listen_port'] # fwmark if c['fwmark']: w.config['fwmark'] = wg['fwmark'] diff --git a/src/conf_mode/interfaces-wireless.py b/src/conf_mode/interfaces-wireless.py index 498c24df0..148a7f6e0 100755 --- a/src/conf_mode/interfaces-wireless.py +++ b/src/conf_mode/interfaces-wireless.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2019 VyOS maintainers and contributors +# Copyright (C) 2019-2020 VyOS maintainers and contributors # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 or later as @@ -25,12 +25,12 @@ from netaddr import EUI, mac_unix_expanded from vyos.config import Config from vyos.configdict import list_diff, vlan_to_dict -from vyos.ifconfig import WiFiIf +from vyos.ifconfig import WiFiIf, Section from vyos.ifconfig_vlan import apply_vlan_config, verify_vlan_config -from vyos.util import chown, is_bridge_member, call -from vyos import ConfigError from vyos.template import render - +from vyos.util import chown, call +from vyos.validate import is_bridge_member +from vyos import ConfigError default_config_data = { 'address': [], @@ -88,9 +88,11 @@ default_config_data = { 'ip_enable_arp_announce': 0, 'ip_enable_arp_ignore': 0, 'ipv6_autoconf': 0, - 'ipv6_eui64_prefix': '', + 'ipv6_eui64_prefix': [], + 'ipv6_eui64_prefix_remove': [], 'ipv6_forwarding': 1, 'ipv6_dup_addr_detect': 1, + 'is_bridge_member': False, 'mac' : '', 'max_stations' : '', 'mgmt_frame_protection' : 'disabled', @@ -136,6 +138,8 @@ def get_config(): cfg_base = 'interfaces wireless ' + wifi['intf'] if not conf.exists(cfg_base): wifi['deleted'] = True + # check if interface is member if a bridge + wifi['is_bridge_member'] = is_bridge_member(conf, wifi['intf']) # we can not bail out early as wireless interface can not be removed # Kernel will complain with: RTNETLINK answers: Operation not supported. # Thus we need to remove individual settings @@ -365,9 +369,21 @@ def get_config(): if conf.exists('ipv6 address autoconf'): wifi['ipv6_autoconf'] = 1 - # Get prefix for IPv6 addressing based on MAC address (EUI-64) + # Get prefixes for IPv6 addressing based on MAC address (EUI-64) if conf.exists('ipv6 address eui64'): - wifi['ipv6_eui64_prefix'] = conf.return_value('ipv6 address eui64') + wifi['ipv6_eui64_prefix'] = conf.return_values('ipv6 address eui64') + + # Determine currently effective EUI64 addresses - to determine which + # address is no longer valid and needs to be removed + eff_addr = conf.return_effective_values('ipv6 address eui64') + wifi['ipv6_eui64_prefix_remove'] = list_diff(eff_addr, wifi['ipv6_eui64_prefix']) + + # Remove the default link-local address if set. + if conf.exists('ipv6 address no-default-link-local'): + wifi['ipv6_eui64_prefix_remove'].append('fe80::/64') + else: + # add the link-local by default to make IPv6 work + wifi['ipv6_eui64_prefix'].append('fe80::/64') # ARP enable ignore if conf.exists('ip enable-arp-ignore'): @@ -389,6 +405,12 @@ def get_config(): if conf.exists('mac'): wifi['mac'] = conf.return_value('mac') + # Find out if MAC has changed - if so, we need to delete all IPv6 EUI64 addresses + # before re-adding them + if ( wifi['mac'] and wifi['intf'] in Section.interfaces(section='wireless') + and wifi['mac'] != WiFiIf(wifi['intf'], create=False).get_mac() ): + wifi['ipv6_eui64_prefix_remove'] += wifi['ipv6_eui64_prefix'] + # Maximum number of wireless radio stations if conf.exists('max-stations'): wifi['max_stations'] = conf.return_value('max-stations') @@ -442,6 +464,10 @@ def get_config(): wifi['sec_wpa_cipher'].append('CCMP') wifi['sec_wpa_cipher'].append('TKIP') + # WPA Group Cipher suite + if conf.exists('security wpa group-cipher'): + wifi['sec_wpa_group_cipher'] = conf.return_values('security wpa group-cipher') + # WPA personal shared pass phrase if conf.exists('security wpa passphrase'): wifi['sec_wpa_passphrase'] = conf.return_value('security wpa passphrase') @@ -524,15 +550,12 @@ 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['is_bridge_member']: + interface = wifi['intf'] + bridge = wifi['is_bridge_member'] + raise ConfigError(f'Interface "{interface}" can not be deleted as it belongs to bridge "{bridge}"!') + return None if wifi['op_mode'] != 'monitor' and not wifi['ssid']: raise ConfigError('SSID must be set for {}'.format(wifi['intf'])) @@ -692,6 +715,10 @@ def apply(wifi): # ignore link state changes w.set_link_detect(wifi['disable_link_detect']) + # Delete old IPv6 EUI64 addresses before changing MAC + for addr in wifi['ipv6_eui64_prefix_remove']: + w.del_ipv6_eui64_address(addr) + # Change interface MAC address - re-set to real hardware address (hw-id) # if custom mac is removed if wifi['mac']: @@ -699,6 +726,10 @@ def apply(wifi): elif wifi['hw_id']: w.set_mac(wifi['hw_id']) + # Add IPv6 EUI-based addresses + for addr in wifi['ipv6_eui64_prefix']: + w.add_ipv6_eui64_address(addr) + # configure ARP filter configuration w.set_arp_filter(wifi['ip_disable_arp_filter']) # configure ARP accept @@ -709,8 +740,6 @@ def apply(wifi): w.set_arp_ignore(wifi['ip_enable_arp_ignore']) # 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 diff --git a/src/conf_mode/interfaces-wirelessmodem.py b/src/conf_mode/interfaces-wirelessmodem.py index da1855cd9..a3a2a2648 100755 --- a/src/conf_mode/interfaces-wirelessmodem.py +++ b/src/conf_mode/interfaces-wirelessmodem.py @@ -21,14 +21,10 @@ from copy import deepcopy from netifaces import interfaces from vyos.config import Config -from vyos.util import chown -from vyos.util import chmod_755 -from vyos.util import is_bridge_member -from vyos.util import cmd -from vyos.util import call -from vyos import ConfigError from vyos.template import render - +from vyos.util import chown, chmod_755, cmd, call +from vyos.validate import is_bridge_member +from vyos import ConfigError default_config_data = { 'address': [], @@ -44,6 +40,7 @@ default_config_data = { 'metric': '10', 'mtu': '1500', 'name_server': True, + 'is_bridge_member': False, 'intf': '', 'vrf': '' } @@ -70,6 +67,8 @@ def get_config(): # Check if interface has been removed if not conf.exists('interfaces wirelessmodem ' + wwan['intf']): wwan['deleted'] = True + # check if interface is member if a bridge + wwan['is_bridge_member'] = is_bridge_member(conf, wwan['intf']) return wwan # set new configuration level @@ -119,13 +118,11 @@ def get_config(): 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)) + if wwan['is_bridge_member']: + interface = wwan['intf'] + bridge = wwan['is_bridge_member'] + raise ConfigError(f'Interface "{interface}" can not be deleted as it belongs to bridge "{bridge}"!') + return None if not wwan['apn']: @@ -155,12 +152,6 @@ def generate(wwan): config_files = [config_wwan, config_wwan_chat, script_wwan_pre_up, script_wwan_ip_up, script_wwan_ip_down] - # Ensure directories for config files exist - otherwise create them on demand - for file in config_files: - dirname = os.path.dirname(file) - if not os.path.isdir(dirname): - os.mkdir(dirname) - # Always hang-up WWAN connection prior generating new configuration file cmd(f'systemctl stop ppp@{intf}.service') @@ -175,17 +166,18 @@ def generate(wwan): render(config_wwan, 'wwan/peer.tmpl', wwan) # Create PPP chat script render(config_wwan_chat, 'wwan/chat.tmpl', wwan) + + # generated script file must be executable + # Create script for ip-pre-up.d - render(script_wwan_pre_up, 'wwan/ip-pre-up.script.tmpl', wwan) + render(script_wwan_pre_up, 'wwan/ip-pre-up.script.tmpl', + wwan, permission=0o755) # Create script for ip-up.d - render(script_wwan_ip_up, 'wwan/ip-up.script.tmpl', wwan) + render(script_wwan_ip_up, 'wwan/ip-up.script.tmpl', + wwan, permission=0o755) # Create script for ip-down.d - render(script_wwan_ip_down, 'wwan/ip-down.script.tmpl', wwan) - - # make generated script file executable - chmod_755(script_wwan_pre_up) - chmod_755(script_wwan_ip_up) - chmod_755(script_wwan_ip_down) + render(script_wwan_ip_down, 'wwan/ip-down.script.tmpl', + wwan, permission=0o755) return None diff --git a/src/conf_mode/ipsec-settings.py b/src/conf_mode/ipsec-settings.py index 4fffa11ee..3398bcdf2 100755 --- a/src/conf_mode/ipsec-settings.py +++ b/src/conf_mode/ipsec-settings.py @@ -28,10 +28,10 @@ from vyos.template import render ra_conn_name = "remote-access" charon_conf_file = "/etc/strongswan.d/charon.conf" -ipsec_secrets_flie = "/etc/ipsec.secrets" +ipsec_secrets_file = "/etc/ipsec.secrets" ipsec_ra_conn_dir = "/etc/ipsec.d/tunnels/" ipsec_ra_conn_file = ipsec_ra_conn_dir + ra_conn_name -ipsec_conf_flie = "/etc/ipsec.conf" +ipsec_conf_file = "/etc/ipsec.conf" ca_cert_path = "/etc/ipsec.d/cacerts" server_cert_path = "/etc/ipsec.d/certs" server_key_path = "/etc/ipsec.d/private" @@ -96,6 +96,24 @@ def get_config(): return data +def write_ipsec_secrets(c): + if c.get("ipsec_l2tp_auth_mode") == "pre-shared-secret": + secret_txt = "{0}\n{1} %any : PSK \"{2}\"\n{3}\n".format(delim_ipsec_l2tp_begin, c['outside_addr'], c['ipsec_l2tp_secret'], delim_ipsec_l2tp_end) + elif data.get("ipsec_l2tp_auth_mode") == "x509": + secret_txt = "{0}\n: RSA {1}\n{2}\n".format(delim_ipsec_l2tp_begin, c['server_key_file_copied'], delim_ipsec_l2tp_end) + + old_umask = os.umask(0o077) + with open(ipsec_secrets_file, 'a+') as f: + f.write(secret_txt) + os.umask(old_umask) + +def write_ipsec_conf(c): + ipsec_confg_txt = "{0}\ninclude {1}\n{2}\n".format(delim_ipsec_l2tp_begin, ipsec_ra_conn_file, delim_ipsec_l2tp_end) + + old_umask = os.umask(0o077) + with open(ipsec_conf_file, 'a+') as f: + f.write(ipsec_confg_txt) + os.umask(old_umask) ### Remove config from file by delimiter def remove_confs(delim_begin, delim_end, conf_file): @@ -150,11 +168,12 @@ def generate(data): render(charon_conf_file, 'ipsec/charon.tmpl', data, trim_blocks=True) if data["ipsec_l2tp"]: - remove_confs(delim_ipsec_l2tp_begin, delim_ipsec_l2tp_end, ipsec_conf_flie) - - old_umask = os.umask(0o077) - render(ipsec_secrets_flie, 'ipsec/ipsec.secrets.tmpl', c, trim_blocks=True) - os.umask(old_umask) + remove_confs(delim_ipsec_l2tp_begin, delim_ipsec_l2tp_end, ipsec_secrets_file) + # old_umask = os.umask(0o077) + # render(ipsec_secrets_file, 'ipsec/ipsec.secrets.tmpl', data, trim_blocks=True) + # os.umask(old_umask) + ## Use this method while IPSec CLI handler won't be overwritten to python + write_ipsec_secrets(data) old_umask = os.umask(0o077) @@ -162,18 +181,21 @@ def generate(data): if not os.path.exists(ipsec_ra_conn_dir): os.makedirs(ipsec_ra_conn_dir) - render(ipsec_ra_conn_file, 'ipsec/remote-access.tmpl', c, trim_blocks=True) + render(ipsec_ra_conn_file, 'ipsec/remote-access.tmpl', data, trim_blocks=True) os.umask(old_umask) - old_umask = os.umask(0o077) - render(ipsec_conf_flie, 'ipsec/ipsec.conf.tmpl', c, trim_blocks=True) - os.umask(old_umask) + remove_confs(delim_ipsec_l2tp_begin, delim_ipsec_l2tp_end, ipsec_conf_file) + # old_umask = os.umask(0o077) + # render(ipsec_conf_file, 'ipsec/ipsec.conf.tmpl', data, trim_blocks=True) + # os.umask(old_umask) + ## Use this method while IPSec CLI handler won't be overwritten to python + write_ipsec_conf(data) else: if os.path.exists(ipsec_ra_conn_file): remove_confs(delim_ipsec_l2tp_begin, delim_ipsec_l2tp_end, ipsec_ra_conn_file) - remove_confs(delim_ipsec_l2tp_begin, delim_ipsec_l2tp_end, ipsec_secrets_flie) - remove_confs(delim_ipsec_l2tp_begin, delim_ipsec_l2tp_end, ipsec_conf_flie) + remove_confs(delim_ipsec_l2tp_begin, delim_ipsec_l2tp_end, ipsec_secrets_file) + remove_confs(delim_ipsec_l2tp_begin, delim_ipsec_l2tp_end, ipsec_conf_file) def restart_ipsec(): call('ipsec restart >&/dev/null') diff --git a/src/conf_mode/protocols_static_multicast.py b/src/conf_mode/protocols_static_multicast.py new file mode 100755 index 000000000..411a130ec --- /dev/null +++ b/src/conf_mode/protocols_static_multicast.py @@ -0,0 +1,115 @@ +#!/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 sys import exit + +from vyos import ConfigError +from vyos.config import Config +from vyos.util import call +from vyos.template import render + + +config_file = r'/tmp/static_mcast.frr' + +# Get configuration for static multicast route +def get_config(): + conf = Config() + mroute = { + 'old_mroute' : {}, + 'mroute' : {} + } + + base_path = "protocols static multicast" + + if not (conf.exists(base_path) or conf.exists_effective(base_path)): + return None + + conf.set_level(base_path) + + # Get multicast effective routes + for route in conf.list_effective_nodes('route'): + mroute['old_mroute'][route] = {} + for next_hop in conf.list_effective_nodes('route {0} next-hop'.format(route)): + mroute['old_mroute'][route].update({ + next_hop : conf.return_value('route {0} next-hop {1} distance'.format(route, next_hop)) + }) + + # Get multicast effective interface-routes + for route in conf.list_effective_nodes('interface-route'): + if not route in mroute['old_mroute']: + mroute['old_mroute'][route] = {} + for next_hop in conf.list_effective_nodes('interface-route {0} next-hop-interface'.format(route)): + mroute['old_mroute'][route].update({ + next_hop : conf.return_value('interface-route {0} next-hop-interface {1} distance'.format(route, next_hop)) + }) + + # Get multicast routes + for route in conf.list_nodes('route'): + mroute['mroute'][route] = {} + for next_hop in conf.list_nodes('route {0} next-hop'.format(route)): + mroute['mroute'][route].update({ + next_hop : conf.return_value('route {0} next-hop {1} distance'.format(route, next_hop)) + }) + + # Get multicast interface-routes + for route in conf.list_nodes('interface-route'): + if not route in mroute['mroute']: + mroute['mroute'][route] = {} + for next_hop in conf.list_nodes('interface-route {0} next-hop-interface'.format(route)): + mroute['mroute'][route].update({ + next_hop : conf.return_value('interface-route {0} next-hop-interface {1} distance'.format(route, next_hop)) + }) + + return mroute + +def verify(mroute): + if mroute is None: + return None + + for route in mroute['mroute']: + route = route.split('/') + if IPv4Address(route[0]) < IPv4Address('224.0.0.0'): + raise ConfigError(route + " not a multicast network") + +def generate(mroute): + if mroute is None: + return None + + render(config_file, 'frr-mcast/static_mcast.frr.tmpl', mroute) + return None + +def apply(mroute): + if mroute is None: + return None + + if os.path.exists(config_file): + call("sudo vtysh -d staticd -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 index 236480854..8bc35bb45 100755 --- a/src/conf_mode/salt-minion.py +++ b/src/conf_mode/salt-minion.py @@ -17,117 +17,102 @@ import os from copy import deepcopy -from pwd import getpwnam from socket import gethostname from sys import exit from urllib3 import PoolManager from vyos.config import Config -from vyos import ConfigError -from vyos.util import call from vyos.template import render - +from vyos.util import call, chown +from vyos import ConfigError config_file = r'/etc/salt/minion' +master_keyfile = r'/opt/vyatta/etc/config/salt/pki/minion/master_sign.pub' default_config_data = { - 'hash_type': 'sha256', - 'log_file': '/var/log/salt/minion', + 'hash': 'sha256', 'log_level': 'warning', 'master' : 'salt', 'user': 'minion', + 'group': 'vyattacfg', 'salt_id': gethostname(), 'mine_interval': '60', - 'verify_master_pubkey_sign': 'false' + 'verify_master_pubkey_sign': 'false', + 'master_key': '' } def get_config(): salt = deepcopy(default_config_data) conf = Config() - if not conf.exists('service salt-minion'): + base = ['service', 'salt-minion'] + + if not conf.exists(base): return None else: - conf.set_level('service salt-minion') - - if conf.exists('hash_type'): - salt['hash_type'] = conf.return_value('hash_type') - - if conf.exists('log_file'): - salt['log_file'] = conf.return_value('log_file') + conf.set_level(base) - if conf.exists('log_level'): - salt['log_level'] = conf.return_value('log_level') + if conf.exists(['hash']): + salt['hash'] = conf.return_value(['hash']) - if conf.exists('master'): - master = conf.return_values('master') - salt['master'] = master + if conf.exists(['master']): + salt['master'] = conf.return_values(['master']) - if conf.exists('id'): - salt['salt_id'] = conf.return_value('id') + if conf.exists(['id']): + salt['salt_id'] = conf.return_value(['id']) - if conf.exists('user'): - salt['user'] = conf.return_value('user') + if conf.exists(['user']): + salt['user'] = conf.return_value(['user']) - if conf.exists('mine_interval'): - salt['mine_interval'] = conf.return_value('mine_interval') + if conf.exists(['interval']): + salt['interval'] = conf.return_value(['interval']) - salt['master-key'] = None - if conf.exists('master-key'): - salt['master-key'] = conf.return_value('master-key') + if conf.exists(['master-key']): + salt['master_key'] = conf.return_value(['master-key']) salt['verify_master_pubkey_sign'] = 'true' return salt -def generate(salt): - paths = ['/etc/salt/','/var/run/salt','/opt/vyatta/etc/config/salt/'] - directory = '/opt/vyatta/etc/config/salt/pki/minion' - uid = getpwnam(salt['user']).pw_uid - http = PoolManager() +def verify(salt): + return None - if salt is None: +def generate(salt): + if not salt: return None - if not os.path.exists(directory): - os.makedirs(directory) - - render(config_file, 'salt-minion/minion.tmpl', salt) - - path = "/etc/salt/" - for path in paths: - for root, dirs, files in os.walk(path): - for usgr in dirs: - os.chown(os.path.join(root, usgr), uid, 100) - for usgr in files: - os.chown(os.path.join(root, usgr), uid, 100) + render(config_file, 'salt-minion/minion.tmpl', salt, + user=salt['user'], group=salt['group']) - if not os.path.exists('/opt/vyatta/etc/config/salt/pki/minion/master_sign.pub'): - if not salt['master-key'] is None: - r = http.request('GET', salt['master-key'], preload_content=False) + if not os.path.exists(master_keyfile): + if salt['master_key']: + req = PoolManager().request('GET', salt['master_key'], preload_content=False) - with open('/opt/vyatta/etc/config/salt/pki/minion/master_sign.pub', 'wb') as out: + with open(master_keyfile, 'wb') as f: while True: - data = r.read(1024) + data = req.read(1024) if not data: break - out.write(data) + f.write(data) - r.release_conn() + req.release_conn() + chown(master_keyfile, salt['user'], salt['group']) return None def apply(salt): - if salt is not None: - call("sudo systemctl restart salt-minion") + if not salt: + # Salt removed from running config + call('systemctl stop salt-minion.service') + if os.path.exists(config_file): + os.unlink(config_file) else: - # Salt access is removed in the commit - call("sudo systemctl stop salt-minion") - os.unlink(config_file) + call('systemctl restart salt-minion.service') return None if __name__ == '__main__': try: c = get_config() + verify(c) generate(c) apply(c) except ConfigError as e: diff --git a/src/conf_mode/service-ipoe.py b/src/conf_mode/service-ipoe.py deleted file mode 100755 index 76aa80a10..000000000 --- a/src/conf_mode/service-ipoe.py +++ /dev/null @@ -1,284 +0,0 @@ -#!/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 -import re - -from sys import exit -from time import sleep - -from stat import S_IRUSR, S_IWUSR, S_IRGRP -from vyos.config import Config -from vyos import ConfigError -from vyos.util import call -from vyos.template import render - - -ipoe_conf = '/run/accel-pppd/ipoe.conf' -ipoe_chap_secrets = '/run/accel-pppd/ipoe.chap-secrets' - - -def _get_cpu(): - cpu_cnt = 1 - if os.cpu_count() == 1: - cpu_cnt = 1 - else: - cpu_cnt = int(os.cpu_count()/2) - return cpu_cnt - - -def get_config(): - c = Config() - if not c.exists(['service', 'ipoe-server']): - return None - - config_data = { - 'chap_secrets_file' : ipoe_chap_secrets - } - - c.set_level(['service', 'ipoe-server']) - config_data['interfaces'] = {} - for intfc in c.list_nodes(['interface']): - config_data['interfaces'][intfc] = { - 'mode': 'L2', - 'shared': '1', - # may need a conifg option, can be dhcpv4 or up for unclassified pkts - 'sess_start': 'dhcpv4', - 'range': None, - 'ifcfg': '1', - 'vlan_mon': [] - } - config_data['dns'] = { - 'server1': None, - 'server2': None - } - config_data['dnsv6'] = { - 'server1': None, - 'server2': None, - 'server3': None - } - config_data['ipv6'] = { - 'prfx': [], - 'pd': [], - } - config_data['auth'] = { - 'auth_if': {}, - 'mech': 'noauth', - 'radius': {}, - 'radsettings': { - 'dae-server': {} - } - } - - if c.exists(['interface', intfc, 'network-mode']): - config_data['interfaces'][intfc]['mode'] = c.return_value( - ['interface', intfc, 'network-mode']) - if c.return_value(['interface', intfc, 'network']) == 'vlan': - config_data['interfaces'][intfc]['shared'] = '0' - if c.exists(['interface', intfc, 'vlan-id']): - config_data['interfaces'][intfc]['vlan_mon'] += c.return_values( - ['interface', intfc, 'vlan-id']) - if c.exists(['interface', intfc, 'vlan-range']): - config_data['interfaces'][intfc]['vlan_mon'] += c.return_values( - ['interface', intfc, 'vlan-range']) - if c.exists(['interface', intfc, 'client-subnet']): - config_data['interfaces'][intfc]['range'] = c.return_value( - ['interface', intfc, 'client-subnet']) - if c.exists(['dns-server', 'server-1']): - config_data['dns']['server1'] = c.return_value( - ['dns-server', 'server-1']) - if c.exists(['dns-server', 'server-2']): - config_data['dns']['server2'] = c.return_value( - ['dns-server', 'server-2']) - if c.exists(['dnsv6-server', 'server-1']): - config_data['dnsv6']['server1'] = c.return_value( - ['dnsv6-server', 'server-1']) - if c.exists(['dnsv6-server', 'server-2']): - config_data['dnsv6']['server2'] = c.return_value( - ['dnsv6-server', 'server-2']) - if c.exists(['dnsv6-server', 'server-3']): - config_data['dnsv6']['server3'] = c.return_value( - ['dnsv6-server', 'server-3']) - if not c.exists(['authentication', 'mode', 'noauth']): - config_data['auth']['mech'] = c.return_value( - ['authentication', 'mode']) - if c.exists(['authentication', 'mode', 'local']): - for auth_int in c.list_nodes(['authentication', 'interface']): - for mac in c.list_nodes(['authentication', 'interface', auth_int, 'mac-address']): - config_data['auth']['auth_if'][auth_int] = {} - if c.exists(['authentication', 'interface', auth_int, 'mac-address', mac, 'rate-limit']): - config_data['auth']['auth_if'][auth_int][mac] = {} - config_data['auth']['auth_if'][auth_int][mac]['up'] = c.return_value( - ['authentication', 'interface', auth_int, 'mac-address', mac, 'rate-limit upload']) - config_data['auth']['auth_if'][auth_int][mac]['down'] = c.return_value( - ['authentication', 'interface', auth_int, 'mac-address', 'mac', 'rate-limit download']) - else: - config_data['auth']['auth_if'][auth_int][mac] = {} - config_data['auth']['auth_if'][auth_int][mac]['up'] = None - config_data['auth']['auth_if'][auth_int][mac]['down'] = None - # client vlan-id - if c.exists(['authentication', 'interface', auth_int, 'mac-address', mac, 'vlan-id']): - config_data['auth']['auth_if'][auth_int][mac]['vlan'] = c.return_value( - ['authentication', 'interface', auth_int, 'mac-address', mac, 'vlan-id']) - if c.exists(['authentication', 'mode', 'radius']): - for rsrv in c.list_nodes(['authentication', 'radius-server']): - config_data['auth']['radius'][rsrv] = {} - if c.exists(['authentication', 'radius-server', rsrv, 'secret']): - config_data['auth']['radius'][rsrv]['secret'] = c.return_value( - ['authentication', 'radius-server', rsrv, 'secret']) - else: - config_data['auth']['radius'][rsrv]['secret'] = None - if c.exists(['authentication', 'radius-server', rsrv, 'fail-time']): - config_data['auth']['radius'][rsrv]['fail-time'] = c.return_value( - ['authentication', 'radius-server', rsrv, 'fail-time']) - else: - config_data['auth']['radius'][rsrv]['fail-time'] = '0' - if c.exists(['authentication', 'radius-server', rsrv, 'req-limit']): - config_data['auth']['radius'][rsrv]['req-limit'] = c.return_value( - ['authentication', 'radius-server', rsrv, 'req-limit']) - else: - config_data['auth']['radius'][rsrv]['req-limit'] = '0' - if c.exists(['authentication', 'radius-settings']): - if c.exists(['authentication', 'radius-settings', 'timeout']): - config_data['auth']['radsettings']['timeout'] = c.return_value( - ['authentication', 'radius-settings', 'timeout']) - if c.exists(['authentication', 'radius-settings', 'nas-ip-address']): - config_data['auth']['radsettings']['nas-ip-address'] = c.return_value( - ['authentication', 'radius-settings', 'nas-ip-address']) - if c.exists(['authentication', 'radius-settings', 'nas-identifier']): - config_data['auth']['radsettings']['nas-identifier'] = c.return_value( - ['authentication', 'radius-settings', 'nas-identifier']) - if c.exists(['authentication', 'radius-settings', 'max-try']): - config_data['auth']['radsettings']['max-try'] = c.return_value( - ['authentication', 'radius-settings', 'max-try']) - if c.exists(['authentication', 'radius-settings', 'acct-timeout']): - config_data['auth']['radsettings']['acct-timeout'] = c.return_value( - ['authentication', 'radius-settings', 'acct-timeout']) - if c.exists(['authentication', 'radius-settings', 'dae-server', 'ip-address']): - config_data['auth']['radsettings']['dae-server']['ip-address'] = c.return_value( - ['authentication', 'radius-settings', 'dae-server', 'ip-address']) - if c.exists(['authentication', 'radius-settings', 'dae-server', 'port']): - config_data['auth']['radsettings']['dae-server']['port'] = c.return_value( - ['authentication', 'radius-settings', 'dae-server', 'port']) - if c.exists(['authentication', 'radius-settings', 'dae-server', 'secret']): - config_data['auth']['radsettings']['dae-server']['secret'] = c.return_value( - ['authentication', 'radius-settings', 'dae-server', 'secret']) - - if c.exists(['client-ipv6-pool', 'prefix']): - config_data['ipv6']['prfx'] = c.return_values( - ['client-ipv6-pool', 'prefix']) - if c.exists(['client-ipv6-pool', 'delegate-prefix']): - config_data['ipv6']['pd'] = c.return_values( - ['client-ipv6-pool', 'delegate-prefix']) - - return config_data - - -def generate(ipoe): - if not ipoe: - return None - - dirname = os.path.dirname(ipoe_conf) - if not os.path.exists(dirname): - os.mkdir(dirname) - - ipoe['thread_cnt'] = _get_cpu() - render(ipoe_conf, 'ipoe-server/ipoe.config.tmpl', ipoe, trim_blocks=True) - - if ipoe['auth']['mech'] == 'local': - render(ipoe_chap_secrets, 'ipoe-server/chap-secrets.tmpl', ipoe) - os.chmod(ipoe_chap_secrets, S_IRUSR | S_IWUSR | S_IRGRP) - - else: - if os.path.exists(ipoe_chap_secrets): - os.unlink(ipoe_chap_secrets) - - return None - - -def verify(c): - if c == None or not c: - return None - - if not c['interfaces']: - raise ConfigError("service ipoe-server interface requires a value") - - for intfc in c['interfaces']: - if not c['interfaces'][intfc]['range']: - raise ConfigError("service ipoe-server interface " + - intfc + " client-subnet needs a value") - - if c['auth']['mech'] == 'radius': - if not c['auth']['radius']: - raise ConfigError( - "service ipoe-server authentication radius-server requires a value for authentication mode radius") - else: - for radsrv in c['auth']['radius']: - if not c['auth']['radius'][radsrv]['secret']: - raise ConfigError( - "service ipoe-server authentication radius-server " + radsrv + " secret requires a value") - - if c['auth']['radsettings']['dae-server']: - try: - if c['auth']['radsettings']['dae-server']['ip-address']: - pass - except: - raise ConfigError( - "service ipoe-server authentication radius-settings dae-server ip-address value required") - try: - if c['auth']['radsettings']['dae-server']['secret']: - pass - except: - raise ConfigError( - "service ipoe-server authentication radius-settings dae-server secret value required") - try: - if c['auth']['radsettings']['dae-server']['port']: - pass - except: - raise ConfigError( - "service ipoe-server authentication radius-settings dae-server port value required") - - if len(c['ipv6']['pd']) != 0 and len(c['ipv6']['prfx']) == 0: - raise ConfigError( - "service ipoe-server client-ipv6-pool prefix needs a value") - - return c - - -def apply(ipoe): - if ipoe == None: - call('systemctl stop accel-ppp@ipoe.service') - - if os.path.exists(ipoe_conf): - os.unlink(ipoe_conf) - - if os.path.exists(ipoe_chap_secrets): - os.unlink(ipoe_chap_secrets) - - return None - - call('systemctl restart accel-ppp@ipoe.service') - - -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/service-pppoe.py b/src/conf_mode/service-pppoe.py deleted file mode 100755 index a96249199..000000000 --- a/src/conf_mode/service-pppoe.py +++ /dev/null @@ -1,428 +0,0 @@ -#!/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 -import re - -from socket import socket, AF_INET, SOCK_STREAM -from sys import exit -from time import sleep - -from vyos.config import Config -from vyos import ConfigError -from vyos.util import run -from vyos.template import render - - -pidfile = r'/var/run/accel_pppoe.pid' -pppoe_cnf_dir = r'/etc/accel-ppp/pppoe' -chap_secrets = pppoe_cnf_dir + '/chap-secrets' -pppoe_conf = pppoe_cnf_dir + '/pppoe.config' -# accel-pppd -d -c /etc/accel-ppp/pppoe/pppoe.config -p -# /var/run/accel_pppoe.pid - -# config path creation -if not os.path.exists(pppoe_cnf_dir): - os.makedirs(pppoe_cnf_dir) - -# -# 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(AF_INET, SOCK_STREAM) - while True: - try: - s.connect(("127.0.0.1", 2001)) - break - except ConnectionRefusedError: - sleep(0.5) - cnt += 1 - if cnt == 100: - raise("failed to start pppoe server") - - -def _accel_cmd(command): - return run(f'/usr/bin/accel-cmd {command}') - - -def get_config(): - c = Config() - if not c.exists('service pppoe-server'): - return None - - config_data = { - 'concentrator': 'vyos-ac', - 'authentication': { - 'local-users': { - }, - 'mode': 'local', - 'radiussrv': {}, - 'radiusopt': {} - }, - 'client_ip_pool': '', - 'client_ip_subnets': [], - 'client_ipv6_pool': {}, - 'interface': {}, - 'ppp_gw': '', - 'svc_name': [], - 'dns': [], - 'dnsv6': [], - 'wins': [], - 'mtu': '1492', - 'ppp_options': {}, - 'limits': {}, - 'snmp': 'disable', - 'sesscrtl': 'replace', - 'pado_delay': '' - } - - c.set_level(['service', 'pppoe-server']) - # general options - if c.exists(['access-concentrator']): - config_data['concentrator'] = c.return_value(['access-concentrator']) - if c.exists(['service-name']): - config_data['svc_name'] = c.return_values(['service-name']) - if c.exists(['interface']): - for intfc in c.list_nodes(['interface']): - config_data['interface'][intfc] = {'vlans': []} - if c.exists(['interface', intfc, 'vlan-id']): - config_data['interface'][intfc]['vlans'] += c.return_values( - ['interface', intfc, 'vlan-id']) - if c.exists(['interface', intfc, 'vlan-range']): - config_data['interface'][intfc]['vlans'] += c.return_values( - ['interface', intfc, 'vlan-range']) - if c.exists(['local-ip']): - config_data['ppp_gw'] = c.return_value(['local-ip']) - if c.exists(['dns-servers']): - if c.return_value(['dns-servers', 'server-1']): - config_data['dns'].append( - c.return_value(['dns-servers', 'server-1'])) - if c.return_value(['dns-servers', 'server-2']): - config_data['dns'].append( - c.return_value(['dns-servers', 'server-2'])) - if c.exists(['dnsv6-servers']): - if c.return_value(['dnsv6-servers', 'server-1']): - config_data['dnsv6'].append( - c.return_value(['dnsv6-servers', 'server-1'])) - if c.return_value(['dnsv6-servers', 'server-2']): - config_data['dnsv6'].append( - c.return_value(['dnsv6-servers', 'server-2'])) - if c.return_value(['dnsv6-servers', 'server-3']): - config_data['dnsv6'].append( - c.return_value(['dnsv6-servers', 'server-3'])) - if c.exists(['wins-servers']): - if c.return_value(['wins-servers', 'server-1']): - config_data['wins'].append( - c.return_value(['wins-servers', 'server-1'])) - if c.return_value(['wins-servers', 'server-2']): - config_data['wins'].append( - c.return_value(['wins-servers', 'server-2'])) - if c.exists(['client-ip-pool']): - if c.exists(['client-ip-pool', 'start']): - config_data['client_ip_pool'] = c.return_value( - ['client-ip-pool start']) - if c.exists(['client-ip-pool stop']): - config_data['client_ip_pool'] += '-' + re.search( - '[0-9]+$', c.return_value(['client-ip-pool', 'stop'])).group(0) - else: - raise ConfigError('client ip pool stop required') - 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']) - if c.exists(['client-ipv6-pool', 'delegate-prefix']): - config_data['client_ipv6_pool']['delegate-prefix'] = c.return_values( - ['client-ipv6-pool', 'delegate-prefix']) - if c.exists(['limits']): - if c.exists(['limits', 'burst']): - config_data['limits']['burst'] = str( - c.return_value(['limits', 'burst'])) - if c.exists(['limits', 'timeout']): - config_data['limits']['timeout'] = str( - c.return_value(['limits', 'timeout'])) - if c.exists(['limits', 'connection-limit']): - config_data['limits']['conn-limit'] = str( - c.return_value(['limits', 'connection-limit'])) - if c.exists(['snmp']): - config_data['snmp'] = 'enable' - if c.exists(['snmp', 'master-agent']): - config_data['snmp'] = 'enable-ma' - - # authentication mode local - if not c.exists(['authentication', 'mode']): - raise ConfigError('pppoe-server authentication mode required') - - 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': 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']) - - # 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, 'secret']), - 'fail-time': ftime, - 'req-limit': reql - } - } - ) - - # 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'])) - } - } - ) - # 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-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(['mtu']): - config_data['mtu'] = c.return_value(['mtu']) - - # ppp_options - ppp_options = {} - if c.exists(['ppp-options']): - if c.exists(['ppp-options', 'ccp']): - ppp_options['ccp'] = c.return_value(['ppp-options', 'ccp']) - if c.exists(['ppp-options', 'min-mtu']): - ppp_options['min-mtu'] = c.return_value(['ppp-options', 'min-mtu']) - if c.exists(['ppp-options', 'mru']): - ppp_options['mru'] = c.return_value(['ppp-options', 'mru']) - if c.exists(['ppp-options', 'mppe deny']): - ppp_options['mppe'] = 'deny' - if c.exists(['ppp-options', 'mppe', 'require']): - ppp_options['mppe'] = 'require' - if c.exists(['ppp-options', 'mppe', 'prefer']): - ppp_options['mppe'] = 'prefer' - 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 c.exists(['ppp-options', 'ipv4']): - ppp_options['ipv4'] = c.return_value(['ppp-options', 'ipv4']) - if c.exists(['ppp-options', 'ipv6']): - ppp_options['ipv6'] = c.return_value(['ppp-options', 'ipv6']) - if c.exists(['ppp-options', 'ipv6-accept-peer-intf-id']): - ppp_options['ipv6-accept-peer-intf-id'] = 1 - if c.exists(['ppp-options', 'ipv6-intf-id']): - ppp_options['ipv6-intf-id'] = c.return_value( - ['ppp-options', 'ipv6-intf-id']) - if c.exists(['ppp-options', 'ipv6-peer-intf-id']): - ppp_options['ipv6-peer-intf-id'] = c.return_value( - ['ppp-options', 'ipv6-peer-intf-id']) - if c.exists(['ppp-options', 'lcp-echo-timeout']): - ppp_options['lcp-echo-timeout'] = c.return_value( - ['ppp-options', 'lcp-echo-timeout']) - - if len(ppp_options) != 0: - config_data['ppp_options'] = ppp_options - - if c.exists(['session-control']): - config_data['sesscrtl'] = c.return_value(['session-control']) - - if c.exists(['pado-delay']): - config_data['pado_delay'] = '0' - a = {} - for id in c.list_nodes(['pado-delay']): - if not c.return_value(['pado-delay', id, 'sessions']): - a[id] = 0 - else: - a[id] = c.return_value(['pado-delay', id, 'sessions']) - - for k in sorted(a.keys()): - if k != sorted(a.keys())[-1]: - config_data['pado_delay'] += ",{0}:{1}".format(k, a[k]) - else: - config_data['pado_delay'] += ",{0}:{1}".format('-1', a[k]) - - 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( - 'pppoe-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 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') - - # local ippool and gateway settings config checks - - if c['client_ip_subnets'] or c['client_ip_pool']: - if not c['ppp_gw']: - raise ConfigError('pppoe-server local-ip required') - - if c['ppp_gw'] and not c['client_ip_subnets'] and not c['client_ip_pool']: - print ("Warning: No pppoe client IPv4 pool defined") - - -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) - - render(pppoe_conf, 'pppoe-server/pppoe.config.tmpl', c, trim_blocks=True) - - if c['authentication']['local-users']: - old_umask = os.umask(0o077) - render(chap_secrets, 'pppoe-server/chap-secrets.tmpl', c, trim_blocks=True) - os.umask(old_umask) - - 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 = 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') - - -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/service_ipoe-server.py b/src/conf_mode/service_ipoe-server.py new file mode 100755 index 000000000..b53692d37 --- /dev/null +++ b/src/conf_mode/service_ipoe-server.py @@ -0,0 +1,300 @@ +#!/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 +import re + +from copy import deepcopy +from stat import S_IRUSR, S_IWUSR, S_IRGRP +from sys import exit + +from vyos.config import Config +from vyos.template import render +from vyos.util import call, get_half_cpus +from vyos.validate import is_ipv4 +from vyos import ConfigError + +ipoe_conf = '/run/accel-pppd/ipoe.conf' +ipoe_chap_secrets = '/run/accel-pppd/ipoe.chap-secrets' + +default_config_data = { + 'auth_mode': 'local', + 'auth_interfaces': [], + 'chap_secrets_file': ipoe_chap_secrets, # used in Jinja2 template + 'interfaces': [], + 'dnsv4': [], + 'dnsv6': [], + 'client_ipv6_pool': [], + 'client_ipv6_delegate_prefix': [], + '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': '', + 'thread_cnt': get_half_cpus() +} + +def get_config(): + conf = Config() + base_path = ['service', 'ipoe-server'] + if not conf.exists(base_path): + return None + + conf.set_level(base_path) + ipoe = deepcopy(default_config_data) + + for interface in conf.list_nodes(['interface']): + tmp = { + 'mode': 'L2', + 'name': interface, + 'shared': '1', + # may need a config option, can be dhcpv4 or up for unclassified pkts + 'sess_start': 'dhcpv4', + 'range': None, + 'ifcfg': '1', + 'vlan_mon': [] + } + + conf.set_level(base_path + ['interface', interface]) + + if conf.exists(['network-mode']): + tmp['mode'] = conf.return_value(['network-mode']) + + if conf.exists(['network']): + mode = conf.return_value(['network']) + if mode == 'vlan': + tmp['shared'] = '0' + + if conf.exists(['vlan-id']): + tmp['vlan_mon'] += conf.return_values(['vlan-id']) + + if conf.exists(['vlan-range']): + tmp['vlan_mon'] += conf.return_values(['vlan-range']) + + if conf.exists(['client-subnet']): + tmp['range'] = conf.return_value(['client-subnet']) + + ipoe['interfaces'].append(tmp) + + conf.set_level(base_path) + + if conf.exists(['name-server']): + for name_server in conf.return_values(['name-server']): + if is_ipv4(name_server): + ipoe['dnsv4'].append(name_server) + else: + ipoe['dnsv6'].append(name_server) + + if conf.exists(['authentication', 'mode']): + ipoe['auth_mode'] = conf.return_value(['authentication', 'mode']) + + if conf.exists(['authentication', 'interface']): + for interface in conf.list_nodes(['authentication', 'interface']): + tmp = { + 'name': interface, + 'mac': [] + } + for client in conf.list_nodes(base_path + ['authentication', 'interface', interface, 'mac-address']): + mac = { + 'address': mac, + 'rate_download': '', + 'rate_upload': '', + 'vlan_id': '' + } + conf.set_level(base_path + ['authentication', 'interface', interface, 'mac-address', client]) + + if conf.exists(['rate-limit', 'download']): + mac['rate_download'] = conf.return_value(['rate-limit', 'download']) + + if conf.exists(['rate-limit', 'upload']): + mac['rate_upload'] = conf.return_value(['rate-limit', 'upload']) + + if conf.exists(['vlan-id']): + mac['vlan'] = conf.return_value(['vlan-id']) + + tmp['mac'].append(mac) + + ipoe['auth_interfaces'].append(tmp) + + # + # authentication mode radius servers and settings + if conf.exists(['authentication', 'mode', 'radius']): + for server in conf.list_nodes(['authentication', 'radius', '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']): + ipoe['radius_server'].append(radius) + + # + # advanced radius-setting + conf.set_level(base_path + ['authentication', 'radius']) + if conf.exists(['acct-timeout']): + ipoe['radius_acct_tmo'] = conf.return_value(['acct-timeout']) + + if conf.exists(['max-try']): + ipoe['radius_max_try'] = conf.return_value(['max-try']) + + if conf.exists(['timeout']): + ipoe['radius_timeout'] = conf.return_value(['timeout']) + + if conf.exists(['nas-identifier']): + ipoe['radius_nas_id'] = conf.return_value(['nas-identifier']) + + if conf.exists(['nas-ip-address']): + ipoe['radius_nas_ip'] = conf.return_value(['nas-ip-address']) + + if conf.exists(['source-address']): + ipoe['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']) + + ipoe['radius_dynamic_author'] = dae + + + conf.set_level(base_path) + if conf.exists(['client-ipv6-pool', 'prefix']): + for prefix in conf.list_nodes(['client-ipv6-pool', 'prefix']): + tmp = { + 'prefix': prefix, + 'mask': '64' + } + + if conf.exists(['client-ipv6-pool', 'prefix', prefix, 'mask']): + tmp['mask'] = conf.return_value(['client-ipv6-pool', 'prefix', prefix, 'mask']) + + ipoe['client_ipv6_pool'].append(tmp) + + + if conf.exists(['client-ipv6-pool', 'delegate']): + for prefix in conf.list_nodes(['client-ipv6-pool', 'delegate']): + tmp = { + 'prefix': prefix, + 'mask': '' + } + + if conf.exists(['client-ipv6-pool', 'delegate', prefix, 'delegation-prefix']): + tmp['mask'] = conf.return_value(['client-ipv6-pool', 'delegate', prefix, 'delegation-prefix']) + + ipoe['client_ipv6_delegate_prefix'].append(tmp) + + return ipoe + + +def verify(ipoe): + if not ipoe: + return None + + if not ipoe['interfaces']: + raise ConfigError('No IPoE interface configured') + + for interface in ipoe['interfaces']: + if not interface['range']: + raise ConfigError(f'No IPoE client subnet defined on interface "{ interface }"') + + if len(ipoe['dnsv4']) > 2: + raise ConfigError('Not more then two IPv4 DNS name-servers can be configured') + + if len(ipoe['dnsv6']) > 3: + raise ConfigError('Not more then three IPv6 DNS name-servers can be configured') + + if ipoe['auth_mode'] == 'radius': + if len(ipoe['radius_server']) == 0: + raise ConfigError('RADIUS authentication requires at least one server') + + for radius in ipoe['radius_server']: + if not radius['key']: + server = radius['server'] + raise ConfigError(f'Missing RADIUS secret key for server "{ server }"') + + if ipoe['client_ipv6_delegate_prefix'] and not ipoe['client_ipv6_pool']: + raise ConfigError('IPoE IPv6 deletate-prefix requires IPv6 prefix to be configured!') + + return None + + +def generate(ipoe): + if not ipoe: + return None + + render(ipoe_conf, 'accel-ppp/ipoe.config.tmpl', ipoe, trim_blocks=True) + + if ipoe['auth_mode'] == 'local': + render(ipoe_chap_secrets, 'accel-ppp/chap-secrets.ipoe.tmpl', ipoe) + os.chmod(ipoe_chap_secrets, S_IRUSR | S_IWUSR | S_IRGRP) + + else: + if os.path.exists(ipoe_chap_secrets): + os.unlink(ipoe_chap_secrets) + + return None + + +def apply(ipoe): + if ipoe == None: + call('systemctl stop accel-ppp@ipoe.service') + for file in [ipoe_conf, ipoe_chap_secrets]: + if os.path.exists(file): + os.unlink(file) + + return None + + call('systemctl restart accel-ppp@ipoe.service') + +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/service_pppoe-server.py b/src/conf_mode/service_pppoe-server.py new file mode 100755 index 000000000..e05b0ab2a --- /dev/null +++ b/src/conf_mode/service_pppoe-server.py @@ -0,0 +1,463 @@ +#!/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 +import re + +from copy import deepcopy +from stat import S_IRUSR, S_IWUSR, S_IRGRP +from sys import exit + +from vyos.config import Config +from vyos.template import render +from vyos.util import call, get_half_cpus +from vyos.validate import is_ipv4 +from vyos import ConfigError + +pppoe_conf = r'/run/accel-pppd/pppoe.conf' +pppoe_chap_secrets = r'/run/accel-pppd/pppoe.chap-secrets' + +default_config_data = { + 'auth_mode': 'local', + 'auth_proto': ['auth_mschap_v2', 'auth_mschap_v1', 'auth_chap_md5', 'auth_pap'], + 'chap_secrets_file': pppoe_chap_secrets, # used in Jinja2 template + 'client_ip_pool': '', + 'client_ip_subnets': [], + 'client_ipv6_pool': [], + 'client_ipv6_delegate_prefix': [], + 'concentrator': 'vyos-ac', + 'interfaces': [], + 'local_users' : [], + + 'svc_name': [], + 'dnsv4': [], + 'dnsv6': [], + 'wins': [], + 'mtu': '1492', + + 'limits_burst': '', + 'limits_connections': '', + 'limits_timeout': '', + + 'pado_delay': '', + 'ppp_ccp': False, + 'ppp_gw': '', + 'ppp_ipv4': '', + 'ppp_ipv6': '', + 'ppp_ipv6_accept_peer_intf_id': False, + 'ppp_ipv6_intf_id': '', + 'ppp_ipv6_peer_intf_id': '', + 'ppp_echo_failure': '3', + 'ppp_echo_interval': '30', + 'ppp_echo_timeout': '0', + 'ppp_min_mtu': '', + 'ppp_mppe': 'prefer', + 'ppp_mru': '', + + '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': '', + 'sesscrtl': 'replace', + 'snmp': False, + 'thread_cnt': get_half_cpus() +} + +def get_config(): + conf = Config() + base_path = ['service', 'pppoe-server'] + if not conf.exists(base_path): + return None + + conf.set_level(base_path) + pppoe = deepcopy(default_config_data) + + # general options + if conf.exists(['access-concentrator']): + pppoe['concentrator'] = conf.return_value(['access-concentrator']) + + if conf.exists(['service-name']): + pppoe['svc_name'] = conf.return_values(['service-name']) + + if conf.exists(['interface']): + for interface in conf.list_nodes(['interface']): + conf.set_level(base_path + ['interface', interface]) + tmp = { + 'name': interface, + 'vlans': [] + } + + if conf.exists(['vlan-id']): + tmp['vlans'] += conf.return_values(['vlan-id']) + + if conf.exists(['vlan-range']): + tmp['vlans'] += conf.return_values(['vlan-range']) + + pppoe['interfaces'].append(tmp) + + conf.set_level(base_path) + + if conf.exists(['local-ip']): + pppoe['ppp_gw'] = conf.return_value(['local-ip']) + + if conf.exists(['name-server']): + for name_server in conf.return_values(['name-server']): + if is_ipv4(name_server): + pppoe['dnsv4'].append(name_server) + else: + pppoe['dnsv6'].append(name_server) + + if conf.exists(['wins-server']): + pppoe['wins'] = conf.return_values(['wins-server']) + + + if conf.exists(['client-ip-pool']): + if conf.exists(['client-ip-pool', 'start']) and conf.exists(['client-ip-pool', 'stop']): + start = conf.return_value(['client-ip-pool', 'start']) + stop = conf.return_value(['client-ip-pool', 'stop']) + pppoe['client_ip_pool'] = start + '-' + re.search('[0-9]+$', stop).group(0) + + if conf.exists(['client-ip-pool', 'subnet']): + pppoe['client_ip_subnets'] = conf.return_values(['client-ip-pool', 'subnet']) + + + if conf.exists(['client-ipv6-pool', 'prefix']): + for prefix in conf.list_nodes(['client-ipv6-pool', 'prefix']): + tmp = { + 'prefix': prefix, + 'mask': '64' + } + + if conf.exists(['client-ipv6-pool', 'prefix', prefix, 'mask']): + tmp['mask'] = conf.return_value(['client-ipv6-pool', 'prefix', prefix, 'mask']) + + pppoe['client_ipv6_pool'].append(tmp) + + + if conf.exists(['client-ipv6-pool', 'delegate']): + for prefix in conf.list_nodes(['client-ipv6-pool', 'delegate']): + tmp = { + 'prefix': prefix, + 'mask': '' + } + + if conf.exists(['client-ipv6-pool', 'delegate', prefix, 'delegation-prefix']): + tmp['mask'] = conf.return_value(['client-ipv6-pool', 'delegate', prefix, 'delegation-prefix']) + + pppoe['client_ipv6_delegate_prefix'].append(tmp) + + + if conf.exists(['limits']): + if conf.exists(['limits', 'burst']): + pppoe['limits_burst'] = conf.return_value(['limits', 'burst']) + + if conf.exists(['limits', 'connection-limit']): + pppoe['limits_connections'] = conf.return_value(['limits', 'connection-limit']) + + if conf.exists(['limits', 'timeout']): + pppoe['limits_timeout'] = conf.return_value(['limits', 'timeout']) + + + if conf.exists(['snmp']): + pppoe['snmp'] = True + + if conf.exists(['snmp', 'master-agent']): + pppoe['snmp'] = 'enable-ma' + + # authentication mode local + if conf.exists(['authentication', 'mode']): + pppoe['auth_mode'] = conf.return_value(['authentication', 'mode']) + + 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']) + + pppoe['local_users'].append(user) + + conf.set_level(base_path) + + if conf.exists(['authentication', 'protocols']): + auth_mods = { + 'mschap-v2': 'auth_mschap_v2', + 'mschap': 'auth_mschap_v1', + 'chap': 'auth_chap_md5', + 'pap': 'auth_pap' + } + + pppoe['auth_proto'] = [] + for proto in conf.return_values(['authentication', 'protocols']): + pppoe['auth_proto'].append(auth_mods[proto]) + + # + # authentication mode radius servers and settings + if conf.exists(['authentication', 'mode', 'radius']): + + for server in conf.list_nodes(['authentication', 'radius', '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']): + pppoe['radius_server'].append(radius) + + # + # advanced radius-setting + conf.set_level(base_path + ['authentication', 'radius']) + + if conf.exists(['acct-timeout']): + pppoe['radius_acct_tmo'] = conf.return_value(['acct-timeout']) + + if conf.exists(['max-try']): + pppoe['radius_max_try'] = conf.return_value(['max-try']) + + if conf.exists(['timeout']): + pppoe['radius_timeout'] = conf.return_value(['timeout']) + + if conf.exists(['nas-identifier']): + pppoe['radius_nas_id'] = conf.return_value(['nas-identifier']) + + if conf.exists(['nas-ip-address']): + pppoe['radius_nas_ip'] = conf.return_value(['nas-ip-address']) + + if conf.exists(['source-address']): + pppoe['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']) + + pppoe['radius_dynamic_author'] = dae + + # RADIUS based rate-limiter + if conf.exists(['rate-limit', 'enable']): + pppoe['radius_shaper_attr'] = 'Filter-Id' + c_attr = ['rate-limit', 'enable', 'attribute'] + if conf.exists(c_attr): + pppoe['radius_shaper_attr'] = conf.return_value(c_attr) + + c_vendor = ['rate-limit', 'enable', 'vendor'] + if conf.exists(c_vendor): + pppoe['radius_shaper_vendor'] = conf.return_value(c_vendor) + + # re-set config level + conf.set_level(base_path) + + if conf.exists(['mtu']): + pppoe['mtu'] = conf.return_value(['mtu']) + + if conf.exists(['session-control']): + pppoe['sesscrtl'] = conf.return_value(['session-control']) + + # ppp_options + if conf.exists(['ppp-options']): + conf.set_level(base_path + ['ppp-options']) + + if conf.exists(['ccp']): + pppoe['ppp_ccp'] = True + + if conf.exists(['ipv4']): + pppoe['ppp_ipv4'] = conf.return_value(['ipv4']) + + if conf.exists(['ipv6']): + pppoe['ppp_ipv6'] = conf.return_value(['ipv6']) + + if conf.exists(['ipv6-accept-peer-intf-id']): + pppoe['ppp_ipv6_peer_intf_id'] = True + + if conf.exists(['ipv6-intf-id']): + pppoe['ppp_ipv6_intf_id'] = conf.return_value(['ipv6-intf-id']) + + if conf.exists(['ipv6-peer-intf-id']): + pppoe['ppp_ipv6_peer_intf_id'] = conf.return_value(['ipv6-peer-intf-id']) + + if conf.exists(['lcp-echo-failure']): + pppoe['ppp_echo_failure'] = conf.return_value(['lcp-echo-failure']) + + if conf.exists(['lcp-echo-failure']): + pppoe['ppp_echo_interval'] = conf.return_value(['lcp-echo-failure']) + + if conf.exists(['lcp-echo-timeout']): + pppoe['ppp_echo_timeout'] = conf.return_value(['lcp-echo-timeout']) + + if conf.exists(['min-mtu']): + pppoe['ppp_min_mtu'] = conf.return_value(['min-mtu']) + + if conf.exists(['mppe']): + pppoe['ppp_mppe'] = conf.return_value(['mppe']) + + if conf.exists(['mru']): + pppoe['ppp_mru'] = conf.return_value(['mru']) + + if conf.exists(['pado-delay']): + pppoe['pado_delay'] = '0' + a = {} + for id in conf.list_nodes(['pado-delay']): + if not conf.return_value(['pado-delay', id, 'sessions']): + a[id] = 0 + else: + a[id] = conf.return_value(['pado-delay', id, 'sessions']) + + for k in sorted(a.keys()): + if k != sorted(a.keys())[-1]: + pppoe['pado_delay'] += ",{0}:{1}".format(k, a[k]) + else: + pppoe['pado_delay'] += ",{0}:{1}".format('-1', a[k]) + + return pppoe + + +def verify(pppoe): + if not pppoe: + return None + + # vertify auth settings + if pppoe['auth_mode'] == 'local': + if not pppoe['local_users']: + raise ConfigError('PPPoE local auth mode requires local users to be configured!') + + for user in pppoe['local_users']: + username = user['name'] + if not user['password']: + raise ConfigError(f'Password required for local user "{username}"') + + # 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 local user "{username}"') + + if user['download'] and not user['upload']: + raise ConfigError(f'Upload speed value required for local user "{username}"') + + elif pppoe['auth_mode'] == 'radius': + if len(pppoe['radius_server']) == 0: + raise ConfigError('RADIUS authentication requires at least one server') + + for radius in pppoe['radius_server']: + if not radius['key']: + server = radius['server'] + raise ConfigError(f'Missing RADIUS secret key for server "{ server }"') + + if len(pppoe['wins']) > 2: + raise ConfigError('Not more then two IPv4 WINS name-servers can be configured') + + if len(pppoe['dnsv4']) > 2: + raise ConfigError('Not more then two IPv4 DNS name-servers can be configured') + + if len(pppoe['dnsv6']) > 3: + raise ConfigError('Not more then three IPv6 DNS name-servers can be configured') + + # local ippool and gateway settings config checks + if pppoe['client_ip_subnets'] or pppoe['client_ip_pool']: + if not pppoe['ppp_gw']: + raise ConfigError('PPPoE server requires local IP to be configured') + + if pppoe['ppp_gw'] and not pppoe['client_ip_subnets'] and not pppoe['client_ip_pool']: + print("Warning: No PPPoE client pool defined") + + return None + + +def generate(pppoe): + if not pppoe: + return None + + render(pppoe_conf, 'accel-ppp/pppoe.config.tmpl', pppoe, trim_blocks=True) + + if pppoe['local_users']: + render(pppoe_chap_secrets, 'accel-ppp/chap-secrets.tmpl', pppoe, trim_blocks=True) + os.chmod(pppoe_chap_secrets, S_IRUSR | S_IWUSR | S_IRGRP) + else: + if os.path.exists(pppoe_chap_secrets): + os.unlink(pppoe_chap_secrets) + + return None + + +def apply(pppoe): + if not pppoe: + call('systemctl stop accel-ppp@pppoe.service') + for file in [pppoe_conf, pppoe_chap_secrets]: + if os.path.exists(file): + os.unlink(file) + + return None + + call('systemctl restart accel-ppp@pppoe.service') + +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/service-router-advert.py b/src/conf_mode/service_router-advert.py index 620f3eacf..620f3eacf 100755 --- a/src/conf_mode/service-router-advert.py +++ b/src/conf_mode/service_router-advert.py diff --git a/src/conf_mode/system-login.py b/src/conf_mode/system-login.py index 91e2b369f..09c5422eb 100755 --- a/src/conf_mode/system-login.py +++ b/src/conf_mode/system-login.py @@ -17,6 +17,7 @@ import os from crypt import crypt, METHOD_SHA512 +from netifaces import interfaces from psutil import users from pwd import getpwall, getpwnam from stat import S_IRUSR, S_IWUSR, S_IRWXU, S_IRGRP, S_IXGRP @@ -39,6 +40,7 @@ default_config_data = { 'del_users': [], 'radius_server': [], 'radius_source_address': '', + 'radius_vrf': '' } def get_local_users(): @@ -127,6 +129,10 @@ def get_config(): if conf.exists(['source-address']): login['radius_source_address'] = conf.return_value(['source-address']) + # retrieve VRF instance + if conf.exists(['vrf']): + login['radius_vrf'] = conf.return_value(['vrf']) + # Read in all RADIUS servers and store to list for server in conf.list_nodes(['server']): server_cfg = { @@ -193,6 +199,9 @@ def verify(login): if fail: raise ConfigError('At least one RADIUS server must be active.') + vrf_name = login['radius_vrf'] + if vrf_name and vrf_name not in interfaces(): + raise ConfigError(f'VRF "{vrf_name}" does not exist') return None @@ -217,7 +226,7 @@ def generate(login): # env=env) if len(login['radius_server']) > 0: - render(radius_config_file, 'system-login/pam_radius_auth.conf.tmpl', login) + render(radius_config_file, 'system-login/pam_radius_auth.conf.tmpl', login, trim_blocks=True) uid = getpwnam('root').pw_uid gid = getpwnam('root').pw_gid diff --git a/src/conf_mode/vpn-pptp.py b/src/conf_mode/vpn-pptp.py deleted file mode 100755 index 15b80f984..000000000 --- a/src/conf_mode/vpn-pptp.py +++ /dev/null @@ -1,257 +0,0 @@ -#!/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 -import re - -from socket import socket, AF_INET, SOCK_STREAM -from sys import exit -from time import sleep - -from vyos.config import Config -from vyos import ConfigError -from vyos.util import run -from vyos.template import render - - -pidfile = r'/var/run/accel_pptp.pid' -pptp_cnf_dir = r'/etc/accel-ppp/pptp' -chap_secrets = pptp_cnf_dir + '/chap-secrets' -pptp_conf = pptp_cnf_dir + '/pptp.config' - -# config path creation -if not os.path.exists(pptp_cnf_dir): - os.makedirs(pptp_cnf_dir) - -def _chk_con(): - cnt = 0 - s = socket(AF_INET, SOCK_STREAM) - while True: - try: - s.connect(("127.0.0.1", 2003)) - break - except ConnectionRefusedError: - sleep(0.5) - cnt += 1 - if cnt == 100: - raise("failed to start pptp server") - break - - -def _accel_cmd(command): - return run('/usr/bin/accel-cmd -p 2003 {command}') - -### -# inline helper functions end -### - - -def get_config(): - c = Config() - if not c.exists(['vpn', 'pptp', 'remote-access']): - return None - - c.set_level(['vpn', 'pptp', 'remote-access']) - config_data = { - 'authentication': { - 'mode': 'local', - 'local-users': { - }, - 'radiussrv': {}, - 'auth_proto': 'auth_mschap_v2', - 'mppe': 'require' - }, - 'outside_addr': '', - 'dns': [], - 'wins': [], - 'client_ip_pool': '', - 'mtu': '1436', - } - - ### 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(['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': '' - } - } - ) - - 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']) - - # 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 not c.return_value(['authentication', 'radius', 'server', rsrv, 'fail-time']): - ftime = '0' - else: - ftime = c.return_value( - ['authentication', 'radius', 'server', rsrv, 'fail-time']) - if not c.return_value(['authentication', 'radius-server', rsrv, 'req-limit']): - reql = '0' - else: - reql = 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 - } - } - ) - - if c.exists(['client-ip-pool']): - if c.exists(['client-ip-pool', 'start']): - config_data['client_ip_pool'] = c.return_value( - ['client-ip-pool', 'start']) - if c.exists(['client-ip-pool', 'stop']): - config_data['client_ip_pool'] += '-' + \ - re.search( - '[0-9]+$', c.return_value(['client-ip-pool', 'stop'])).group(0) - if c.exists(['mtu']): - config_data['mtu'] = c.return_value(['mtu']) - - # gateway address - if c.exists(['gateway-address']): - config_data['gw_ip'] = c.return_value(['gateway-address']) - else: - config_data['gw_ip'] = re.sub( - '[0-9]+$', '1', config_data['client_ip_pool']) - - if c.exists(['authentication', 'require']): - if c.return_value(['authentication', 'require']) == 'pap': - config_data['authentication']['auth_proto'] = 'auth_pap' - if c.return_value(['authentication', 'require']) == 'chap': - config_data['authentication']['auth_proto'] = 'auth_chap_md5' - if c.return_value(['authentication', 'require']) == 'mschap': - config_data['authentication']['auth_proto'] = 'auth_mschap_v1' - if c.return_value(['authentication', 'require']) == 'mschap-v2': - config_data['authentication']['auth_proto'] = 'auth_mschap_v2' - - if c.exists(['authentication', 'mppe']): - config_data['authentication']['mppe'] = c.return_value( - ['authentication', 'mppe']) - - return config_data - - -def verify(c): - if c == None: - return None - - if c['authentication']['mode'] == 'local': - if not c['authentication']['local-users']: - raise ConfigError( - 'pptp-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') - - -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) - - render(pptp_conf, 'pptp/pptp.config.tmpl', c, trim_blocks=True) - - if c['authentication']['local-users']: - old_umask = os.umask(0o077) - render(chap_secrets, 'pptp/chap-secrets.tmpl', c, trim_blocks=True) - 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 = 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) - 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) - exit(1) diff --git a/src/conf_mode/vpn_l2tp.py b/src/conf_mode/vpn_l2tp.py index a8b183bef..f312f2a17 100755 --- a/src/conf_mode/vpn_l2tp.py +++ b/src/conf_mode/vpn_l2tp.py @@ -25,7 +25,7 @@ from time import sleep from ipaddress import ip_network from vyos.config import Config -from vyos.util import call +from vyos.util import call, get_half_cpus from vyos.validate import is_ipv4 from vyos import ConfigError from vyos.template import render @@ -65,7 +65,7 @@ default_config_data = { 'radius_dynamic_author': '', 'wins': [], 'ip6_column': [], - 'thread_cnt': 1 + 'thread_cnt': get_half_cpus() } def get_config(): @@ -77,10 +77,6 @@ def get_config(): conf.set_level(base_path) l2tp = deepcopy(default_config_data) - cpu = os.cpu_count() - if cpu > 1: - l2tp['thread_cnt'] = int(cpu/2) - ### general options ### if conf.exists(['name-server']): for name_server in conf.return_values(['name-server']): @@ -252,7 +248,7 @@ def get_config(): 'mask': '' } - if conf.exists(['client-ipv6-pool', 'delegate', prefix, 'mask']): + if conf.exists(['client-ipv6-pool', 'delegate', prefix, 'delegation-prefix']): tmp['mask'] = conf.return_value(['client-ipv6-pool', 'delegate', prefix, 'delegation-prefix']) l2tp['client_ipv6_delegate_prefix'].append(tmp) @@ -313,7 +309,7 @@ def verify(l2tp): for radius in l2tp['radius_server']: if not radius['key']: - raise ConfigError(f"Missing RADIUS secret for server {{ radius['key'] }}") + raise ConfigError(f"Missing RADIUS secret for server { radius['key'] }") # check for the existence of a client ip pool if not (l2tp['client_ip_pool'] or l2tp['client_ip_subnets']): @@ -344,14 +340,10 @@ def generate(l2tp): if not l2tp: return None - dirname = os.path.dirname(l2tp_conf) - if not os.path.exists(dirname): - os.mkdir(dirname) - - render(l2tp_conf, 'l2tp/l2tp.config.tmpl', c, trim_blocks=True) + render(l2tp_conf, 'accel-ppp/l2tp.config.tmpl', l2tp, trim_blocks=True) if l2tp['auth_mode'] == 'local': - render(l2tp_chap_secrets, 'l2tp/chap-secrets.tmpl', l2tp) + render(l2tp_chap_secrets, 'accel-ppp/chap-secrets.tmpl', l2tp) os.chmod(l2tp_chap_secrets, S_IRUSR | S_IWUSR | S_IRGRP) else: @@ -364,12 +356,9 @@ def generate(l2tp): def apply(l2tp): if not l2tp: call('systemctl stop accel-ppp@l2tp.service') - - if os.path.exists(l2tp_conf): - os.unlink(l2tp_conf) - - if os.path.exists(l2tp_chap_secrets): - os.unlink(l2tp_chap_secrets) + for file in [l2tp_chap_secrets, l2tp_conf]: + if os.path.exists(file): + os.unlink(file) return None diff --git a/src/conf_mode/vpn_pptp.py b/src/conf_mode/vpn_pptp.py new file mode 100755 index 000000000..085c9c2c6 --- /dev/null +++ b/src/conf_mode/vpn_pptp.py @@ -0,0 +1,279 @@ +#!/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 +import re + +from copy import deepcopy +from stat import S_IRUSR, S_IWUSR, S_IRGRP +from sys import exit + +from vyos.config import Config +from vyos.template import render +from vyos.util import call, get_half_cpus +from vyos import ConfigError + +pptp_conf = '/run/accel-pppd/pptp.conf' +pptp_chap_secrets = '/run/accel-pppd/pptp.chap-secrets' + +default_pptp = { + 'auth_mode' : 'local', + 'local_users' : [], + 'radius_server' : [], + 'radius_acct_tmo' : '30', + 'radius_max_try' : '3', + 'radius_timeout' : '30', + 'radius_nas_id' : '', + 'radius_nas_ip' : '', + 'radius_source_address' : '', + 'radius_shaper_attr' : '', + 'radius_shaper_vendor': '', + 'radius_dynamic_author' : '', + 'chap_secrets_file': pptp_chap_secrets, # used in Jinja2 template + 'outside_addr': '', + 'dnsv4': [], + 'wins': [], + 'client_ip_pool': '', + 'mtu': '1436', + 'auth_proto' : ['auth_mschap_v2'], + 'ppp_mppe' : 'prefer', + 'thread_cnt': get_half_cpus() +} + +def get_config(): + conf = Config() + base_path = ['vpn', 'pptp', 'remote-access'] + if not conf.exists(base_path): + return None + + pptp = deepcopy(default_pptp) + conf.set_level(base_path) + + if conf.exists(['name-server']): + pptp['dnsv4'] = conf.return_values(['name-server']) + + if conf.exists(['wins-server']): + pptp['wins'] = conf.return_values(['wins-server']) + + if conf.exists(['outside-address']): + pptp['outside_addr'] = conf.return_value(['outside-address']) + + if conf.exists(['authentication', 'mode']): + pptp['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' : '*', + } + + 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 not conf.exists(['disable']): + pptp['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']): + pptp['radius_server'].append(radius) + + # + # advanced radius-setting + conf.set_level(base_path + ['authentication', 'radius']) + + if conf.exists(['acct-timeout']): + pptp['radius_acct_tmo'] = conf.return_value(['acct-timeout']) + + if conf.exists(['max-try']): + pptp['radius_max_try'] = conf.return_value(['max-try']) + + if conf.exists(['timeout']): + pptp['radius_timeout'] = conf.return_value(['timeout']) + + if conf.exists(['nas-identifier']): + pptp['radius_nas_id'] = conf.return_value(['nas-identifier']) + + if conf.exists(['nas-ip-address']): + pptp['radius_nas_ip'] = conf.return_value(['nas-ip-address']) + + if conf.exists(['source-address']): + pptp['radius_source_address'] = conf.return_value(['source-address']) + + # Dynamic Authorization Extensions (DOA)/Change Of Authentication (COA) + if conf.exists(['dae-server']): + dae = { + 'port' : '', + 'server' : '', + 'key' : '' + } + + if conf.exists(['dynamic-author', 'ip-address']): + dae['server'] = conf.return_value(['dynamic-author', 'ip-address']) + + 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']) + + pptp['radius_dynamic_author'] = dae + + if conf.exists(['rate-limit', 'enable']): + pptp['radius_shaper_attr'] = 'Filter-Id' + c_attr = ['rate-limit', 'enable', 'attribute'] + if conf.exists(c_attr): + pptp['radius_shaper_attr'] = conf.return_value(c_attr) + + c_vendor = ['rate-limit', 'enable', 'vendor'] + if conf.exists(c_vendor): + pptp['radius_shaper_vendor'] = conf.return_value(c_vendor) + + conf.set_level(base_path) + if conf.exists(['client-ip-pool']): + if conf.exists(['client-ip-pool', 'start']) and conf.exists(['client-ip-pool', 'stop']): + start = conf.return_value(['client-ip-pool', 'start']) + stop = conf.return_value(['client-ip-pool', 'stop']) + pptp['client_ip_pool'] = start + '-' + re.search('[0-9]+$', stop).group(0) + + if conf.exists(['mtu']): + pptp['mtu'] = conf.return_value(['mtu']) + + # gateway address + if conf.exists(['gateway-address']): + pptp['gw_ip'] = conf.return_value(['gateway-address']) + else: + # calculate gw-ip-address + if conf.exists(['client-ip-pool', 'start']): + # use start ip as gw-ip-address + pptp['gateway_address'] = conf.return_value(['client-ip-pool', 'start']) + + if conf.exists(['authentication', 'require']): + # clear default list content, now populate with actual CLI values + pptp['auth_proto'] = [] + auth_mods = { + 'pap': 'auth_pap', + 'chap': 'auth_chap_md5', + 'mschap': 'auth_mschap_v1', + 'mschap-v2': 'auth_mschap_v2' + } + + for proto in conf.return_values(['authentication', 'require']): + pptp['auth_proto'].append(auth_mods[proto]) + + if conf.exists(['authentication', 'mppe']): + pptp['ppp_mppe'] = conf.return_value(['authentication', 'mppe']) + + return pptp + + +def verify(pptp): + if not pptp: + return None + + if pptp['auth_mode'] == 'local': + if not pptp['local_users']: + raise ConfigError('PPTP local auth mode requires local users to be configured!') + + for user in pptp['local_users']: + username = user['name'] + if not user['password']: + raise ConfigError(f'Password required for local user "{username}"') + + elif pptp['auth_mode'] == 'radius': + if len(pptp['radius_server']) == 0: + raise ConfigError('RADIUS authentication requires at least one server') + + for radius in pptp['radius_server']: + if not radius['key']: + server = radius['server'] + raise ConfigError(f'Missing RADIUS secret key for server "{ server }"') + + if len(pptp['dnsv4']) > 2: + raise ConfigError('Not more then two IPv4 DNS name-servers can be configured') + + if len(pptp['wins']) > 2: + raise ConfigError('Not more then two IPv4 WINS name-servers can be configured') + + +def generate(pptp): + if not pptp: + return None + + render(pptp_conf, 'accel-ppp/pptp.config.tmpl', pptp, trim_blocks=True) + + if pptp['local_users']: + render(pptp_chap_secrets, 'accel-ppp/chap-secrets.tmpl', pptp, trim_blocks=True) + os.chmod(pptp_chap_secrets, S_IRUSR | S_IWUSR | S_IRGRP) + else: + if os.path.exists(pptp_chap_secrets): + os.unlink(pptp_chap_secrets) + + +def apply(pptp): + if not pptp: + call('systemctl stop accel-ppp@pptp.service') + for file in [pptp_conf, pptp_chap_secrets]: + if os.path.exists(file): + os.unlink(file) + + return None + + call('systemctl restart accel-ppp@pptp.service') + +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/vpn_sstp.py b/src/conf_mode/vpn_sstp.py index 438731972..d250cd3b0 100755 --- a/src/conf_mode/vpn_sstp.py +++ b/src/conf_mode/vpn_sstp.py @@ -23,7 +23,7 @@ from stat import S_IRUSR, S_IWUSR, S_IRGRP from vyos.config import Config from vyos import ConfigError -from vyos.util import call, run +from vyos.util import call, run, get_half_cpus from vyos.template import render @@ -56,7 +56,7 @@ default_config_data = { 'ppp_echo_failure' : '', 'ppp_echo_interval' : '', 'ppp_echo_timeout' : '', - 'thread_cnt' : 1 + 'thread_cnt' : get_half_cpus() } def get_config(): @@ -68,10 +68,6 @@ def get_config(): conf.set_level(base_path) - cpu = os.cpu_count() - if cpu > 1: - sstp['thread_cnt'] = int(cpu/2) - if conf.exists(['authentication', 'mode']): sstp['auth_mode'] = conf.return_value(['authentication', 'mode']) @@ -259,21 +255,22 @@ def verify(sstp): raise ConfigError('SSTP local auth mode requires local users to be configured!') for user in sstp['local_users']: + username = user['name'] if not user['password']: - raise ConfigError(f"Password required for user {user['name']}") + raise ConfigError(f'Password required for local user "{username}"') # 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']}") + raise ConfigError(f'Download speed value required for local user "{username}"') if user['download'] and not user['upload']: - raise ConfigError(f"Upload speed value required for user {user['name']}") + raise ConfigError(f'Upload speed value required for local user "{username}"') if not sstp['client_ip_pool']: - raise ConfigError("Client IP subnet required") + raise ConfigError('Client IP subnet required') if not sstp['client_gateway']: - raise ConfigError("Client gateway IP address required") + raise ConfigError('Client gateway IP address required') if len(sstp['dnsv4']) > 2: raise ConfigError('Not more then two IPv4 DNS name-servers can be configured') @@ -282,35 +279,35 @@ def verify(sstp): 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") + file = sstp['ssl_ca'] + raise ConfigError(f'SSL CA certificate file "{file}" does not exist') if not os.path.exists(sstp['ssl_cert']): - raise ConfigError(f"SSL cert file {sstp['ssl_cert']} does not exist") + file = sstp['ssl_cert'] + raise ConfigError(f'SSL public key file "{file}" does not exist') if not os.path.exists(sstp['ssl_key']): - raise ConfigError(f"SSL key file {sstp['ssl_key']} does not exist") + file = sstp['ssl_key'] + raise ConfigError(f'SSL private key file "{file}" does not exist') if sstp['auth_mode'] == 'radius': if len(sstp['radius_server']) == 0: - raise ConfigError("RADIUS authentication requires at least one server") + 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'] }}") + server = radius['server'] + raise ConfigError(f'Missing RADIUS secret key for server "{ server }"') def generate(sstp): if not sstp: return None - dirname = os.path.dirname(sstp_conf) - if not os.path.exists(dirname): - os.mkdir(dirname) - # accel-cmd reload doesn't work so any change results in a restart of the daemon - render(sstp_conf, 'sstp/sstp.config.tmpl', sstp, trim_blocks=True) + render(sstp_conf, 'accel-ppp/sstp.config.tmpl', sstp, trim_blocks=True) if sstp['local_users']: - render(sstp_chap_secrets, 'sstp/chap-secrets.tmpl', sstp, trim_blocks=True) + render(sstp_chap_secrets, 'accel-ppp/chap-secrets.tmpl', sstp, trim_blocks=True) os.chmod(sstp_chap_secrets, S_IRUSR | S_IWUSR | S_IRGRP) else: if os.path.exists(sstp_chap_secrets): @@ -321,12 +318,9 @@ def generate(sstp): def apply(sstp): if not sstp: call('systemctl stop accel-ppp@sstp.service') - - if os.path.exists(sstp_conf): - os.unlink(sstp_conf) - - if os.path.exists(sstp_chap_secrets): - os.unlink(sstp_chap_secrets) + for file in [sstp_chap_secrets, sstp_conf]: + if os.path.exists(file): + os.unlink(file) return None diff --git a/src/etc/dhcp/dhclient-enter-hooks.d/03-vyos-ipwrapper b/src/etc/dhcp/dhclient-enter-hooks.d/03-vyos-ipwrapper index 59f92703c..f1167fcd2 100644 --- a/src/etc/dhcp/dhclient-enter-hooks.d/03-vyos-ipwrapper +++ b/src/etc/dhcp/dhclient-enter-hooks.d/03-vyos-ipwrapper @@ -15,8 +15,11 @@ function frr_alive () { # convert ip route command to vtysh function iptovtysh () { # prepare variables for vtysh command - VTYSH_DISTANCE="210" - VTYSH_TAG="210" + local VTYSH_DISTANCE="210" + local VTYSH_TAG="210" + local VTYSH_NETADDR="" + local VTYSH_GATEWAY="" + local VTYSH_DEV="" # convert default route to 0.0.0.0/0 if [ "$4" == "default" ] ; then VTYSH_NETADDR="0.0.0.0/0" @@ -74,3 +77,4 @@ function ip () { fi fi } + diff --git a/src/etc/dhcp/dhclient-exit-hooks.d/01-vyos-cleanup b/src/etc/dhcp/dhclient-exit-hooks.d/01-vyos-cleanup index ce846f6c3..88a4d9db9 100644 --- a/src/etc/dhcp/dhclient-exit-hooks.d/01-vyos-cleanup +++ b/src/etc/dhcp/dhclient-exit-hooks.d/01-vyos-cleanup @@ -1,12 +1,74 @@ +# NOTE: here we use 'ip' wrapper, therefore a route will be actually deleted via /usr/sbin/ip or vtysh, according to the system state + if [[ $reason =~ (EXPIRE|FAIL|RELEASE|STOP) ]]; then # delete dynamic nameservers from a configuration if lease was deleted logmsg info "Deleting nameservers with tag \"dhcp-${interface}\" via vyos-hostsd-client" vyos-hostsd-client --delete-name-servers --tag dhcp-${interface} - # try to delete default ip route (NOTE: here we use 'ip' wrapper, therefore a route will be actually deleted via /usr/sbin/ip or vtysh, according to the system state) + # try to delete default ip route for router in $old_routers; do logmsg info "Deleting default route: via $router dev ${interface}" ip -4 route del default via $router dev ${interface} done + # delete rfc3442 routes + if [ -n "$old_rfc3442_classless_static_routes" ]; then + set -- $old_rfc3442_classless_static_routes + while [ $# -gt 0 ]; do + net_length=$1 + via_arg='' + case $net_length in + 32|31|30|29|28|27|26|25) + if [ $# -lt 9 ]; then + return 1 + fi + net_address="${2}.${3}.${4}.${5}" + gateway="${6}.${7}.${8}.${9}" + shift 9 + ;; + 24|23|22|21|20|19|18|17) + if [ $# -lt 8 ]; then + return 1 + fi + net_address="${2}.${3}.${4}.0" + gateway="${5}.${6}.${7}.${8}" + shift 8 + ;; + 16|15|14|13|12|11|10|9) + if [ $# -lt 7 ]; then + return 1 + fi + net_address="${2}.${3}.0.0" + gateway="${4}.${5}.${6}.${7}" + shift 7 + ;; + 8|7|6|5|4|3|2|1) + if [ $# -lt 6 ]; then + return 1 + fi + net_address="${2}.0.0.0" + gateway="${3}.${4}.${5}.${6}" + shift 6 + ;; + 0) # default route + if [ $# -lt 5 ]; then + return 1 + fi + net_address="0.0.0.0" + gateway="${2}.${3}.${4}.${5}" + shift 5 + ;; + *) # error + return 1 + ;; + esac + # take care of link-local routes + if [ "${gateway}" != '0.0.0.0' ]; then + via_arg="via ${gateway}" + fi + # delete route (ip detects host routes automatically) + ip -4 route del "${net_address}/${net_length}" \ + ${via_arg} dev "${interface}" >/dev/null 2>&1 + done + fi fi if [[ $reason =~ (EXPIRE6|RELEASE6|STOP6) ]]; then diff --git a/src/etc/dhcp/dhclient-exit-hooks.d/02-vyos-dhcp-renew-rfc3442 b/src/etc/dhcp/dhclient-exit-hooks.d/02-vyos-dhcp-renew-rfc3442 new file mode 100644 index 000000000..9202fe72d --- /dev/null +++ b/src/etc/dhcp/dhclient-exit-hooks.d/02-vyos-dhcp-renew-rfc3442 @@ -0,0 +1,148 @@ +# support for RFC3442 routes in DHCP RENEW + +function convert_to_cidr () { + cidr="" + set -- $1 + while [ $# -gt 0 ]; do + net_length=$1 + + case $net_length in + 32|31|30|29|28|27|26|25) + if [ $# -lt 9 ]; then + return 1 + fi + net_address="${2}.${3}.${4}.${5}" + gateway="${6}.${7}.${8}.${9}" + shift 9 + ;; + 24|23|22|21|20|19|18|17) + if [ $# -lt 8 ]; then + return 1 + fi + net_address="${2}.${3}.${4}.0" + gateway="${5}.${6}.${7}.${8}" + shift 8 + ;; + 16|15|14|13|12|11|10|9) + if [ $# -lt 7 ]; then + return 1 + fi + net_address="${2}.${3}.0.0" + gateway="${4}.${5}.${6}.${7}" + shift 7 + ;; + 8|7|6|5|4|3|2|1) + if [ $# -lt 6 ]; then + return 1 + fi + net_address="${2}.0.0.0" + gateway="${3}.${4}.${5}.${6}" + shift 6 + ;; + 0) # default route + if [ $# -lt 5 ]; then + return 1 + fi + net_address="0.0.0.0" + gateway="${2}.${3}.${4}.${5}" + shift 5 + ;; + *) # error + return 1 + ;; + esac + + cidr+="${net_address}/${net_length}:${gateway} " + done +} + +# main script starts here + +RUN="yes" + +if [ "$RUN" = "yes" ]; then + convert_to_cidr "$old_rfc3442_classless_static_routes" + old_cidr=$cidr + convert_to_cidr "$new_rfc3442_classless_static_routes" + new_cidr=$cidr + + if [ "$reason" = "RENEW" ]; then + if [ "$new_rfc3442_classless_static_routes" != "$old_rfc3442_classless_static_routes" ]; then + logmsg info "RFC3442 route change detected, old_routes: $old_rfc3442_classless_static_routes" + logmsg info "RFC3442 route change detected, new_routes: $new_rfc3442_classless_static_routes" + if [ -z "$new_rfc3442_classless_static_routes" ]; then + # delete all routes from the old_rfc3442_classless_static_routes + for route in $old_cidr; do + network=$(printf "${route}" | awk -F ":" '{print $1}') + gateway=$(printf "${route}" | awk -F ":" '{print $2}') + # take care of link-local routes + if [ "${gateway}" != '0.0.0.0' ]; then + via_arg="via ${gateway}" + else + via_arg="" + fi + ip -4 route del "${network}" "${via_arg}" dev "${interface}" >/dev/null 2>&1 + done + elif [ -z "$old_rfc3442_classless_static_routes" ]; then + # add all routes from the new_rfc3442_classless_static_routes + for route in $new_cidr; do + network=$(printf "${route}" | awk -F ":" '{print $1}') + gateway=$(printf "${route}" | awk -F ":" '{print $2}') + # take care of link-local routes + if [ "${gateway}" != '0.0.0.0' ]; then + via_arg="via ${gateway}" + else + via_arg="" + fi + ip -4 route add "${network}" "${via_arg}" dev "${interface}" >/dev/null 2>&1 + done + else + # update routes + # delete old + for old_route in $old_cidr; do + match="false" + for new_route in $new_cidr; do + if [[ "$old_route" == "$new_route" ]]; then + match="true" + break + fi + done + if [[ "$match" == "false" ]]; then + # delete old_route + network=$(printf "${old_route}" | awk -F ":" '{print $1}') + gateway=$(printf "${old_route}" | awk -F ":" '{print $2}') + # take care of link-local routes + if [ "${gateway}" != '0.0.0.0' ]; then + via_arg="via ${gateway}" + else + via_arg="" + fi + ip -4 route del "${network}" "${via_arg}" dev "${interface}" >/dev/null 2>&1 + fi + done + # add new + for new_route in $new_cidr; do + match="false" + for old_route in $old_cidr; do + if [[ "$new_route" == "$old_route" ]]; then + match="true" + break + fi + done + if [[ "$match" == "false" ]]; then + # add new_route + network=$(printf "${new_route}" | awk -F ":" '{print $1}') + gateway=$(printf "${new_route}" | awk -F ":" '{print $2}') + # take care of link-local routes + if [ "${gateway}" != '0.0.0.0' ]; then + via_arg="via ${gateway}" + else + via_arg="" + fi + ip -4 route add "${network}" "${via_arg}" dev "${interface}" >/dev/null 2>&1 + fi + done + fi + fi + fi +fi diff --git a/src/etc/sysctl.d/31-vyos-addr_gen_mode.conf b/src/etc/sysctl.d/31-vyos-addr_gen_mode.conf new file mode 100644 index 000000000..07a0d1584 --- /dev/null +++ b/src/etc/sysctl.d/31-vyos-addr_gen_mode.conf @@ -0,0 +1,14 @@ +### Added by vyos-1x ### +# +# addr_gen_mode - INTEGER +# Defines how link-local and autoconf addresses are generated. +# +# 0: generate address based on EUI64 (default) +# 1: do no generate a link-local address, use EUI64 for addresses generated +# from autoconf +# 2: generate stable privacy addresses, using the secret from +# stable_secret (RFC7217) +# 3: generate stable privacy addresses, using a random secret if unset +# +net.ipv6.conf.all.addr_gen_mode = 1 +net.ipv6.conf.default.addr_gen_mode = 1 diff --git a/src/migration-scripts/dhcpv6-server/0-to-1 b/src/migration-scripts/dhcpv6-server/0-to-1 new file mode 100755 index 000000000..6f1150da1 --- /dev/null +++ b/src/migration-scripts/dhcpv6-server/0-to-1 @@ -0,0 +1,61 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2020 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +# combine both sip-server-address and sip-server-name nodes to common sip-server + +from sys import argv, exit +from vyos.configtree import ConfigTree + +if (len(argv) < 1): + print("Must specify file name!") + exit(1) + +file_name = argv[1] + +with open(file_name, 'r') as f: + config_file = f.read() + +config = ConfigTree(config_file) +base = ['service', 'dhcpv6-server', 'shared-network-name'] +if not config.exists(base): + # Nothing to do + exit(0) +else: + # we need to run this for every configured network + for network in config.list_nodes(base): + for subnet in config.list_nodes(base + [network, 'subnet']): + sip_server = [] + + # Do we have 'sip-server-address' configured? + if config.exists(base + [network, 'subnet', subnet, 'sip-server-address']): + sip_server += config.return_values(base + [network, 'subnet', subnet, 'sip-server-address']) + config.delete(base + [network, 'subnet', subnet, 'sip-server-address']) + + # Do we have 'sip-server-name' configured? + if config.exists(base + [network, 'subnet', subnet, 'sip-server-name']): + sip_server += config.return_values(base + [network, 'subnet', subnet, 'sip-server-name']) + config.delete(base + [network, 'subnet', subnet, 'sip-server-name']) + + # Write new CLI value for sip-server + for server in sip_server: + config.set(base + [network, 'subnet', subnet, 'sip-server'], value=server, replace=False) + + try: + with open(file_name, 'w') as f: + f.write(config.to_string()) + except OSError as e: + print("Failed to save the modified config: {}".format(e)) + exit(1) diff --git a/src/migration-scripts/dns-forwarding/1-to-2 b/src/migration-scripts/dns-forwarding/1-to-2 index 9a50b6aa3..8c4f4b5c7 100755 --- a/src/migration-scripts/dns-forwarding/1-to-2 +++ b/src/migration-scripts/dns-forwarding/1-to-2 @@ -20,17 +20,16 @@ # listen-address nodes instead. This is required as PowerDNS can only listen # on interface addresses and not on interface names. -import sys - from ipaddress import ip_interface +from sys import argv, exit from vyos.ifconfig import Interface from vyos.configtree import ConfigTree -if (len(sys.argv) < 1): +if (len(argv) < 1): print("Must specify file name!") - sys.exit(1) + exit(1) -file_name = sys.argv[1] +file_name = argv[1] with open(file_name, 'r') as f: config_file = f.read() @@ -40,45 +39,45 @@ config = ConfigTree(config_file) 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 + exit(0) - if config.exists(base + ['listen-on']): - listen_intf = config.return_values(base + ['listen-on']) - # Delete node with abandoned command - config.delete(base + ['listen-on']) +if config.exists(base + ['listen-on']): + listen_intf = config.return_values(base + ['listen-on']) + # Delete node with abandoned command + config.delete(base + ['listen-on']) - # retrieve interface addresses for every configured listen-on interface - listen_addr = [] - for intf in listen_intf: - # we need to treat vif and vif-s interfaces differently, - # both "real interfaces" use dots for vlan identifiers - those - # need to be exchanged with vif and vif-s identifiers - if intf.count('.') == 1: - # this is a regular VLAN interface - intf = intf.split('.')[0] + ' vif ' + intf.split('.')[1] - elif intf.count('.') == 2: - # this is a QinQ VLAN interface - intf = intf.split('.')[0] + ' vif-s ' + intf.split('.')[1] + ' vif-c ' + intf.split('.')[2] + # retrieve interface addresses for every configured listen-on interface + listen_addr = [] + for intf in listen_intf: + # we need to evaluate the interface section before manipulating the 'intf' variable + section = Interface.section(intf) + if not section: + raise ValueError(f'Invalid interface name {intf}') - section = Interface.section(intf) - if not section: - raise ValueError(f'Invalid interface name {intf}') - path = ['interfaces', section, intf, 'address'] + # we need to treat vif and vif-s interfaces differently, + # both "real interfaces" use dots for vlan identifiers - those + # need to be exchanged with vif and vif-s identifiers + if intf.count('.') == 1: + # this is a regular VLAN interface + intf = intf.split('.')[0] + ' vif ' + intf.split('.')[1] + elif intf.count('.') == 2: + # this is a QinQ VLAN interface + intf = intf.split('.')[0] + ' vif-s ' + intf.split('.')[1] + ' vif-c ' + intf.split('.')[2] - # retrieve corresponding interface addresses in CIDR format - # those need to be converted in pure IP addresses without network information - for addr in config.return_values(path): - listen_addr.append( ip_interface(addr).ip ) + # retrieve corresponding interface addresses in CIDR format + # those need to be converted in pure IP addresses without network information + path = ['interfaces', section, intf, 'address'] + for addr in config.return_values(path): + listen_addr.append( ip_interface(addr).ip ) - for addr in listen_addr: - config.set(base + ['listen-address'], value=addr, replace=False) + for addr in listen_addr: + config.set(base + ['listen-address'], value=addr, replace=False) try: with open(file_name, 'w') as f: f.write(config.to_string()) except OSError as e: print("Failed to save the modified config: {}".format(e)) - sys.exit(1) + exit(1) + +exit(0) diff --git a/src/migration-scripts/ipoe-server/0-to-1 b/src/migration-scripts/ipoe-server/0-to-1 new file mode 100755 index 000000000..f328ebced --- /dev/null +++ b/src/migration-scripts/ipoe-server/0-to-1 @@ -0,0 +1,133 @@ +#!/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 primary/secondary identifier from nameserver +# - Unifi RADIUS configuration by placing it all under "authentication radius" node + +import os +import sys + +from sys import argv, exit +from vyos.configtree import ConfigTree + +if (len(argv) < 1): + print("Must specify file name!") + exit(1) + +file_name = argv[1] + +with open(file_name, 'r') as f: + config_file = f.read() + +config = ConfigTree(config_file) +base = ['service', 'ipoe-server'] +if not config.exists(base): + # Nothing to do + exit(0) +else: + + # Migrate IPv4 DNS servers + dns_base = base + ['dns-server'] + if config.exists(dns_base): + for server in ['server-1', 'server-2']: + if config.exists(dns_base + [server]): + dns = config.return_value(dns_base + [server]) + config.set(base + ['name-server'], value=dns, replace=False) + + config.delete(dns_base) + + # Migrate IPv6 DNS servers + dns_base = base + ['dnsv6-server'] + if config.exists(dns_base): + for server in ['server-1', 'server-2', 'server-3']: + if config.exists(dns_base + [server]): + dns = config.return_value(dns_base + [server]) + config.set(base + ['name-server'], value=dns, replace=False) + + config.delete(dns_base) + + # Migrate radius-settings node to RADIUS and use this as base for the + # later migration of the RADIUS servers - this will save a lot of code + radius_settings = base + ['authentication', 'radius-settings'] + if config.exists(radius_settings): + config.rename(radius_settings, 'radius') + + # Migrate RADIUS dynamic author / change of authorisation server + dae_old = base + ['authentication', 'radius', 'dae-server'] + if config.exists(dae_old): + config.rename(dae_old, 'dynamic-author') + dae_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 = base + ['authentication', 'radius-server'] + if config.exists(radius_server): + new_base = base + ['authentication', 'radius', 'server'] + config.set(new_base) + config.set_tag(new_base) + for server in config.list_nodes(radius_server): + old_base = radius_server + [server] + config.copy(old_base, new_base + [server]) + + # migrate key + if config.exists(new_base + [server, 'secret']): + config.rename(new_base + [server, 'secret'], 'key') + + # remove old req-limit node + if config.exists(new_base + [server, 'req-limit']): + config.delete(new_base + [server, 'req-limit']) + + config.delete(radius_server) + + # Migrate IPv6 prefixes + ipv6_base = base + ['client-ipv6-pool'] + if config.exists(ipv6_base + ['prefix']): + prefix_old = config.return_values(ipv6_base + ['prefix']) + # delete old prefix CLI nodes + config.delete(ipv6_base + ['prefix']) + # create ned prefix tag node + config.set(ipv6_base + ['prefix']) + config.set_tag(ipv6_base + ['prefix']) + + for p in prefix_old: + prefix = p.split(',')[0] + mask = p.split(',')[1] + config.set(ipv6_base + ['prefix', prefix, 'mask'], value=mask) + + if config.exists(ipv6_base + ['delegate-prefix']): + prefix_old = config.return_values(ipv6_base + ['delegate-prefix']) + # delete old delegate prefix CLI nodes + config.delete(ipv6_base + ['delegate-prefix']) + # create ned delegation tag node + config.set(ipv6_base + ['delegate']) + config.set_tag(ipv6_base + ['delegate']) + + for p in prefix_old: + prefix = p.split(',')[0] + mask = p.split(',')[1] + config.set(ipv6_base + ['delegate', prefix, 'delegation-prefix'], value=mask) + + 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/l2tp/2-to-3 b/src/migration-scripts/l2tp/2-to-3 index bd0839e03..3472ee3ed 100755 --- a/src/migration-scripts/l2tp/2-to-3 +++ b/src/migration-scripts/l2tp/2-to-3 @@ -95,13 +95,13 @@ else: # delete old delegate prefix CLI nodes config.delete(ipv6_base + ['delegate-prefix']) # create ned delegation tag node - config.set(ipv6_base + ['delegate ']) - config.set_tag(ipv6_base + ['delegate ']) + config.set(ipv6_base + ['delegate']) + config.set_tag(ipv6_base + ['delegate']) for p in prefix_old: prefix = p.split(',')[0] mask = p.split(',')[1] - config.set(ipv6_base + ['delegate', prefix, 'mask'], value=mask) + config.set(ipv6_base + ['delegate', prefix, 'delegate-prefix'], value=mask) try: with open(file_name, 'w') as f: diff --git a/src/migration-scripts/pppoe-server/2-to-3 b/src/migration-scripts/pppoe-server/2-to-3 new file mode 100755 index 000000000..fa6ef02da --- /dev/null +++ b/src/migration-scripts/pppoe-server/2-to-3 @@ -0,0 +1,142 @@ +#!/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 primary/secondary identifier from nameserver + +import os +import sys + +from sys import argv, exit +from vyos.configtree import ConfigTree + +if (len(argv) < 1): + print("Must specify file name!") + exit(1) + +file_name = argv[1] + +with open(file_name, 'r') as f: + config_file = f.read() + +config = ConfigTree(config_file) +base = ['service', 'pppoe-server'] +if not config.exists(base): + # Nothing to do + exit(0) +else: + + # Migrate IPv4 DNS servers + dns_base = base + ['dns-servers'] + if config.exists(dns_base): + for server in ['server-1', 'server-2']: + if config.exists(dns_base + [server]): + dns = config.return_value(dns_base + [server]) + config.set(base + ['name-server'], value=dns, replace=False) + + config.delete(dns_base) + + # Migrate IPv6 DNS servers + dns_base = base + ['dnsv6-servers'] + if config.exists(dns_base): + for server in ['server-1', 'server-2', 'server-3']: + if config.exists(dns_base + [server]): + dns = config.return_value(dns_base + [server]) + config.set(base + ['name-server'], value=dns, replace=False) + + config.delete(dns_base) + + # Migrate IPv4 WINS servers + wins_base = base + ['wins-servers'] + if config.exists(wins_base): + for server in ['server-1', 'server-2']: + if config.exists(wins_base + [server]): + wins = config.return_value(wins_base + [server]) + config.set(base + ['wins-server'], value=wins, replace=False) + + config.delete(wins_base) + + # Migrate radius-settings node to RADIUS and use this as base for the + # later migration of the RADIUS servers - this will save a lot of code + radius_settings = base + ['authentication', 'radius-settings'] + if config.exists(radius_settings): + config.rename(radius_settings, 'radius') + + # Migrate RADIUS dynamic author / change of authorisation server + dae_old = base + ['authentication', 'radius', 'dae-server'] + if config.exists(dae_old): + config.rename(dae_old, 'dynamic-author') + dae_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 = base + ['authentication', 'radius-server'] + if config.exists(radius_server): + new_base = base + ['authentication', 'radius', 'server'] + config.set(new_base) + config.set_tag(new_base) + for server in config.list_nodes(radius_server): + old_base = radius_server + [server] + config.copy(old_base, new_base + [server]) + + # migrate key + if config.exists(new_base + [server, 'secret']): + config.rename(new_base + [server, 'secret'], 'key') + + # remove old req-limit node + if config.exists(new_base + [server, 'req-limit']): + config.delete(new_base + [server, 'req-limit']) + + config.delete(radius_server) + + # Migrate IPv6 prefixes + ipv6_base = base + ['client-ipv6-pool'] + if config.exists(ipv6_base + ['prefix']): + prefix_old = config.return_values(ipv6_base + ['prefix']) + # delete old prefix CLI nodes + config.delete(ipv6_base + ['prefix']) + # create ned prefix tag node + config.set(ipv6_base + ['prefix']) + config.set_tag(ipv6_base + ['prefix']) + + for p in prefix_old: + prefix = p.split(',')[0] + mask = p.split(',')[1] + config.set(ipv6_base + ['prefix', prefix, 'mask'], value=mask) + + if config.exists(ipv6_base + ['delegate-prefix']): + prefix_old = config.return_values(ipv6_base + ['delegate-prefix']) + # delete old delegate prefix CLI nodes + config.delete(ipv6_base + ['delegate-prefix']) + # create ned delegation tag node + config.set(ipv6_base + ['delegate']) + config.set_tag(ipv6_base + ['delegate']) + + for p in prefix_old: + prefix = p.split(',')[0] + mask = p.split(',')[1] + config.set(ipv6_base + ['delegate', prefix, 'delegation-prefix'], value=mask) + + 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/pptp/1-to-2 b/src/migration-scripts/pptp/1-to-2 new file mode 100755 index 000000000..a13cc3a4f --- /dev/null +++ b/src/migration-scripts/pptp/1-to-2 @@ -0,0 +1,71 @@ +#!/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 dns-servers node to common name-servers +# - remove radios req-limit node + +from sys import argv, exit + +from vyos.configtree import ConfigTree + +if (len(argv) < 1): + print("Must specify file name!") + exit(1) + +file_name = argv[1] + +with open(file_name, 'r') as f: + config_file = f.read() + +config = ConfigTree(config_file) +base = ['vpn', 'pptp', 'remote-access'] +if not config.exists(base): + # Nothing to do + exit(0) +else: + # Migrate IPv4 DNS servers + dns_base = base + ['dns-servers'] + if config.exists(dns_base): + for server in ['server-1', 'server-2']: + if config.exists(dns_base + [server]): + dns = config.return_value(dns_base + [server]) + config.set(base + ['name-server'], value=dns, replace=False) + + config.delete(dns_base) + + # Migrate IPv4 WINS servers + wins_base = base + ['wins-servers'] + if config.exists(wins_base): + for server in ['server-1', 'server-2']: + if config.exists(wins_base + [server]): + wins = config.return_value(wins_base + [server]) + config.set(base + ['wins-server'], value=wins, replace=False) + + config.delete(wins_base) + + # Remove RADIUS server req-limit node + radius_base = base + ['authentication', 'radius'] + if config.exists(radius_base): + for server in config.list_nodes(radius_base + ['server']): + if config.exists(radius_base + ['server', server, 'req-limit']): + config.delete(radius_base + ['server', server, 'req-limit']) + + try: + with open(file_name, 'w') as f: + f.write(config.to_string()) + except OSError as e: + print("Failed to save the modified config: {}".format(e)) + exit(1) diff --git a/src/migration-scripts/salt/0-to-1 b/src/migration-scripts/salt/0-to-1 new file mode 100755 index 000000000..79053c056 --- /dev/null +++ b/src/migration-scripts/salt/0-to-1 @@ -0,0 +1,58 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2020 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +# Delete log_file, log_level and user nodes +# rename hash_type to hash +# rename mine_interval to interval + +from sys import argv,exit + +from vyos.configtree import ConfigTree + +if (len(argv) < 1): + print("Must specify file name!") + exit(1) + +file_name = argv[1] + +with open(file_name, 'r') as f: + config_file = f.read() + +config = ConfigTree(config_file) + +base = ['service', 'salt-minion'] +if not config.exists(base): + # Nothing to do + exit(0) +else: + + # delete nodes which are now populated with sane defaults + for node in ['log_file', 'log_level', 'user']: + if config.exists(base + [node]): + config.delete(base + [node]) + + if config.exists(base + ['hash_type']): + config.rename(base + ['hash_type'], 'hash') + + if config.exists(base + ['mine_interval']): + config.rename(base + ['mine_interval'], 'interval') + + try: + with open(file_name, 'w') as f: + f.write(config.to_string()) + except OSError as e: + print("Failed to save the modified config: {}".format(e)) + exit(1) diff --git a/src/op_mode/dns_forwarding_statistics.py b/src/op_mode/dns_forwarding_statistics.py index 8ae92beb7..1fb61d263 100755 --- a/src/op_mode/dns_forwarding_statistics.py +++ b/src/op_mode/dns_forwarding_statistics.py @@ -4,7 +4,7 @@ import jinja2 from sys import exit from vyos.config import Config -from vyos.config import cmd +from vyos.util import cmd PDNS_CMD='/usr/bin/rec_control --socket-dir=/run/powerdns' diff --git a/src/op_mode/powerctrl.py b/src/op_mode/powerctrl.py index 4ab91384b..69af427ec 100755 --- a/src/op_mode/powerctrl.py +++ b/src/op_mode/powerctrl.py @@ -15,168 +15,179 @@ # along with this program. If not, see <http://www.gnu.org/licenses/>. import os -import sys -import argparse import re +from argparse import ArgumentParser from datetime import datetime, timedelta, time as type_time, date as type_date -from vyos.util import ask_yes_no -from vyos.util import cmd -from vyos.util import call -from vyos.util import run -from vyos.util import STDOUT +from sys import exit +from time import time + +from vyos.util import ask_yes_no, cmd, call, run, STDOUT systemd_sched_file = "/run/systemd/shutdown/scheduled" -def parse_time(s): - try: - if re.match(r'^\d{1,2}$', s): - return datetime.strptime(s, "%M").time() - else: - return datetime.strptime(s, "%H:%M").time() - except ValueError: - return None +def utc2local(datetime): + now = time() + offs = datetime.fromtimestamp(now) - datetime.utcfromtimestamp(now) + return datetime + offs -def parse_date(s): - for fmt in ["%d%m%Y", "%d/%m/%Y", "%d.%m.%Y", "%d:%m:%Y", "%Y-%m-%d"]: +def parse_time(s): try: - return datetime.strptime(s, fmt).date() + if re.match(r'^\d{1,2}$', s): + return datetime.strptime(s, "%M").time() + else: + return datetime.strptime(s, "%H:%M").time() except ValueError: - continue - # If nothing matched... - return None + return None + + +def parse_date(s): + for fmt in ["%d%m%Y", "%d/%m/%Y", "%d.%m.%Y", "%d:%m:%Y", "%Y-%m-%d"]: + try: + return datetime.strptime(s, fmt).date() + except ValueError: + continue + # If nothing matched... + return None + def get_shutdown_status(): - if os.path.exists(systemd_sched_file): - # Get scheduled from systemd file - with open(systemd_sched_file, 'r') as f: - data = f.read().rstrip('\n') - r_data = {} - for line in data.splitlines(): - tmp_split = line.split("=") - if tmp_split[0] == "USEC": - # Convert USEC to human readable format - r_data['DATETIME'] = datetime.utcfromtimestamp(int(tmp_split[1])/1000000).strftime('%Y-%m-%d %H:%M:%S') - else: - r_data[tmp_split[0]] = tmp_split[1] - return r_data - return None + if os.path.exists(systemd_sched_file): + # Get scheduled from systemd file + with open(systemd_sched_file, 'r') as f: + data = f.read().rstrip('\n') + r_data = {} + for line in data.splitlines(): + tmp_split = line.split("=") + if tmp_split[0] == "USEC": + # Convert USEC to human readable format + r_data['DATETIME'] = datetime.utcfromtimestamp( + int(tmp_split[1])/1000000).strftime('%Y-%m-%d %H:%M:%S') + else: + r_data[tmp_split[0]] = tmp_split[1] + return r_data + return None + def check_shutdown(): - output = get_shutdown_status() - if output and 'MODE' in output: - if output['MODE'] == 'reboot': - print("Reboot is scheduled", output['DATETIME']) - elif output['MODE'] == 'poweroff': - print("Poweroff is scheduled", output['DATETIME']) - else: - print("Reboot or poweroff is not scheduled") + output = get_shutdown_status() + if output and 'MODE' in output: + dt = datetime.strptime(output['DATETIME'], '%Y-%m-%d %H:%M:%S') + if output['MODE'] == 'reboot': + print("Reboot is scheduled", utc2local(dt)) + elif output['MODE'] == 'poweroff': + print("Poweroff is scheduled", utc2local(dt)) + else: + print("Reboot or poweroff is not scheduled") + def cancel_shutdown(): - output = get_shutdown_status() - if output and 'MODE' in output: - timenow = datetime.now().strftime('%Y-%m-%d %H:%M:%S') - try: - cmd('/sbin/shutdown -c --no-wall') - except OSError as e: - sys.exit("Could not cancel a reboot or poweroff: %s" % e) - message = "Scheduled %s has been cancelled %s" % (output['MODE'], timenow) - run(f'wall {message}') - else: - print("Reboot or poweroff is not scheduled") - -def execute_shutdown(time, reboot = True, ask=True): - if not ask: - action = "reboot" if reboot else "poweroff" - if not ask_yes_no("Are you sure you want to %s this system?" % action): - sys.exit(0) - - action = "-r" if reboot else "-P" - - if len(time) == 0: - ### T870 legacy reboot job support - chk_vyatta_based_reboots() - ### - - out = cmd(f'/sbin/shutdown {action} now', stderr=STDOUT) - print(out.split(",",1)[0]) - return - elif len(time) == 1: - # Assume the argument is just time - ts = parse_time(time[0]) - if ts: - cmd(f'/sbin/shutdown {action} {time[0]}', stderr=STDOUT) + output = get_shutdown_status() + if output and 'MODE' in output: + timenow = datetime.now().strftime('%Y-%m-%d %H:%M:%S') + try: + run('/sbin/shutdown -c --no-wall') + except OSError as e: + exit("Could not cancel a reboot or poweroff: %s" % e) + + message = 'Scheduled {} has been cancelled {}'.format(output['MODE'], timenow) + run(f'wall {message} > /dev/null 2>&1') else: - sys.exit("Invalid time \"{0}\". The valid format is HH:MM".format(time[0])) - elif len(time) == 2: - # Assume it's date and time - ts = parse_time(time[0]) - ds = parse_date(time[1]) - if ts and ds: - t = datetime.combine(ds, ts) - td = t - datetime.now() - t2 = 1 + int(td.total_seconds())//60 # Get total minutes - cmd('/sbin/shutdown {action} {t2}', stderr=STDOUT) + print("Reboot or poweroff is not scheduled") + + +def execute_shutdown(time, reboot=True, ask=True): + if not ask: + action = "reboot" if reboot else "poweroff" + if not ask_yes_no("Are you sure you want to %s this system?" % action): + exit(0) + + action = "-r" if reboot else "-P" + + if len(time) == 0: + # T870 legacy reboot job support + chk_vyatta_based_reboots() + ### + + out = cmd(f'/sbin/shutdown {action} now', stderr=STDOUT) + print(out.split(",", 1)[0]) + return + elif len(time) == 1: + # Assume the argument is just time + ts = parse_time(time[0]) + if ts: + cmd(f'/sbin/shutdown {action} {time[0]}', stderr=STDOUT) + else: + exit("Invalid time \"{0}\". The valid format is HH:MM".format(time[0])) + elif len(time) == 2: + # Assume it's date and time + ts = parse_time(time[0]) + ds = parse_date(time[1]) + if ts and ds: + t = datetime.combine(ds, ts) + td = t - datetime.now() + t2 = 1 + int(td.total_seconds())//60 # Get total minutes + cmd('/sbin/shutdown {action} {t2}', stderr=STDOUT) + else: + if not ts: + exit("Invalid time \"{0}\". The valid format is HH:MM".format(time[0])) + else: + exit("Invalid time \"{0}\". A valid format is YYYY-MM-DD [HH:MM]".format(time[1])) else: - if not ts: - sys.exit("Invalid time \"{0}\". The valid format is HH:MM".format(time[0])) - else: - sys.exit("Invalid time \"{0}\". A valid format is YYYY-MM-DD [HH:MM]".format(time[1])) - else: - sys.exit("Could not decode date and time. Valids formats are HH:MM or YYYY-MM-DD HH:MM") - check_shutdown() + exit("Could not decode date and time. Valids formats are HH:MM or YYYY-MM-DD HH:MM") + check_shutdown() + def chk_vyatta_based_reboots(): - ### T870 commit-confirm is still using the vyatta code base, once gone, the code below can be removed - ### legacy scheduled reboot s are using at and store the is as /var/run/<name>.job - ### name is the node of scheduled the job, commit-confirm checks for that + # T870 commit-confirm is still using the vyatta code base, once gone, the code below can be removed + # legacy scheduled reboot s are using at and store the is as /var/run/<name>.job + # name is the node of scheduled the job, commit-confirm checks for that + + f = r'/var/run/confirm.job' + if os.path.exists(f): + jid = open(f).read().strip() + if jid != 0: + call(f'sudo atrm {jid}') + os.remove(f) - f = r'/var/run/confirm.job' - if os.path.exists(f): - jid = open(f).read().strip() - if jid != 0: - call(f'sudo atrm {jid}') - os.remove(f) def main(): - parser = argparse.ArgumentParser() - parser.add_argument("--yes", "-y", - help="Do not ask for confirmation", - action="store_true", - dest="yes") - action = parser.add_mutually_exclusive_group(required=True) - action.add_argument("--reboot", "-r", - help="Reboot the system", - nargs="*", - metavar="Minutes|HH:MM") - - action.add_argument("--poweroff", "-p", - help="Poweroff the system", - nargs="*", - metavar="Minutes|HH:MM") - - action.add_argument("--cancel", "-c", - help="Cancel pending shutdown", - action="store_true") - - action.add_argument("--check", - help="Check pending chutdown", - action="store_true") - args = parser.parse_args() - - try: - if args.reboot is not None: - execute_shutdown(args.reboot, reboot=True, ask=args.yes) - if args.poweroff is not None: - execute_shutdown(args.poweroff, reboot=False,ask=args.yes) - if args.cancel: - cancel_shutdown() - if args.check: - check_shutdown() - except KeyboardInterrupt: - sys.exit("Interrupted") + parser = ArgumentParser() + parser.add_argument("--yes", "-y", + help="Do not ask for confirmation", + action="store_true", + dest="yes") + action = parser.add_mutually_exclusive_group(required=True) + action.add_argument("--reboot", "-r", + help="Reboot the system", + nargs="*", + metavar="Minutes|HH:MM") + + action.add_argument("--poweroff", "-p", + help="Poweroff the system", + nargs="*", + metavar="Minutes|HH:MM") + + action.add_argument("--cancel", "-c", + help="Cancel pending shutdown", + action="store_true") + + action.add_argument("--check", + help="Check pending chutdown", + action="store_true") + args = parser.parse_args() + try: + if args.reboot is not None: + execute_shutdown(args.reboot, reboot=True, ask=args.yes) + if args.poweroff is not None: + execute_shutdown(args.poweroff, reboot=False, ask=args.yes) + if args.cancel: + cancel_shutdown() + if args.check: + check_shutdown() + except KeyboardInterrupt: + exit("Interrupted") if __name__ == "__main__": - main() - + main() diff --git a/src/op_mode/show_dhcp.py b/src/op_mode/show_dhcp.py index c49e604b7..f9577e57e 100755 --- a/src/op_mode/show_dhcp.py +++ b/src/op_mode/show_dhcp.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,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 json -import argparse -import ipaddress -import tabulate -import sys -import collections -import os +# TODO: merge with show_dhcpv6.py + +from json import dumps +from argparse import ArgumentParser +from ipaddress import ip_address +from tabulate import tabulate +from sys import exit +from collections import OrderedDict from datetime import datetime from isc_dhcp_leases import Lease, IscDhcpLeases @@ -33,7 +33,7 @@ from vyos.util import call lease_file = "/config/dhcpd.leases" pool_key = "shared-networkname" -lease_display_fields = collections.OrderedDict() +lease_display_fields = OrderedDict() lease_display_fields['ip'] = 'IP address' lease_display_fields['hardware_address'] = 'Hardware address' lease_display_fields['state'] = 'State' @@ -102,7 +102,7 @@ def get_lease_data(lease): return data -def get_leases(leases, state, pool=None, sort='ip'): +def get_leases(config, leases, state, pool=None, sort='ip'): # get leases from file leases = IscDhcpLeases(lease_file).get() @@ -116,7 +116,7 @@ def get_leases(leases, state, pool=None, sort='ip'): leases = list(filter(lambda x: in_pool(x, pool), leases)) else: print("Pool {0} does not exist.".format(pool)) - sys.exit(0) + exit(0) # should maybe filter all state=active by lease.valid here? @@ -134,7 +134,7 @@ def get_leases(leases, state, pool=None, sort='ip'): # apply output/display sort if sort == 'ip': - leases = sorted(leases, key = lambda lease: int(ipaddress.ip_address(lease['ip']))) + leases = sorted(leases, key = lambda lease: int(ip_address(lease['ip']))) else: leases = sorted(leases, key = lambda lease: lease[sort]) @@ -148,7 +148,7 @@ def show_leases(leases): lease_list_params.append(l[k]) lease_list.append(lease_list_params) - output = tabulate.tabulate(lease_list, lease_display_fields.values()) + output = tabulate(lease_list, lease_display_fields.values()) print(output) @@ -161,18 +161,18 @@ def get_pool_size(config, pool): start = config.return_effective_value("service dhcp-server shared-network-name {0} subnet {1} range {2} start".format(pool, s, r)) stop = config.return_effective_value("service dhcp-server shared-network-name {0} subnet {1} range {2} stop".format(pool, s, r)) - size += int(ipaddress.ip_address(stop)) - int(ipaddress.ip_address(start)) + size += int(ip_address(stop)) - int(ip_address(start)) return size def show_pool_stats(stats): headers = ["Pool", "Size", "Leases", "Available", "Usage"] - output = tabulate.tabulate(stats, headers) + output = tabulate(stats, headers) print(output) if __name__ == '__main__': - parser = argparse.ArgumentParser() + parser = ArgumentParser() group = parser.add_mutually_exclusive_group() group.add_argument("-l", "--leases", action="store_true", help="Show DHCP leases") @@ -180,27 +180,50 @@ if __name__ == '__main__': group.add_argument("--allowed", type=str, choices=["pool", "sort", "state"], help="Show allowed values for argument") parser.add_argument("-p", "--pool", type=str, help="Show lease for specific pool") - parser.add_argument("-S", "--sort", type=str, choices=lease_display_fields.keys(), default='ip', help="Sort by") - parser.add_argument("-t", "--state", type=str, nargs="+", choices=lease_valid_states, default="active", help="Lease state to show (can specify multiple with spaces)") + parser.add_argument("-S", "--sort", type=str, default='ip', help="Sort by") + parser.add_argument("-t", "--state", type=str, nargs="+", default=["active"], help="Lease state to show (can specify multiple with spaces)") parser.add_argument("-j", "--json", action="store_true", default=False, help="Produce JSON output") args = parser.parse_args() + conf = Config() + + if args.allowed == 'pool': + if conf.exists_effective('service dhcp-server'): + print(' '.join(conf.list_effective_nodes("service dhcp-server shared-network-name"))) + exit(0) + elif args.allowed == 'sort': + print(' '.join(lease_display_fields.keys())) + exit(0) + elif args.allowed == 'state': + print(' '.join(lease_valid_states)) + exit(0) + elif args.allowed: + parser.print_help() + exit(1) + + if args.sort not in lease_display_fields.keys(): + print(f'Invalid sort key, choose from: {list(lease_display_fields.keys())}') + exit(0) + + if not set(args.state) < set(lease_valid_states): + print(f'Invalid lease state, choose from: {lease_valid_states}') + exit(0) + # Do nothing if service is not configured - config = Config() - if not config.exists_effective('service dhcp-server'): + if not conf.exists_effective('service dhcp-server'): print("DHCP service is not configured.") - sys.exit(0) + exit(0) # if dhcp server is down, inactive leases may still be shown as active, so warn the user. if call('systemctl -q is-active isc-dhcp-server.service') != 0: print("WARNING: DHCP server is configured but not started. Data may be stale.") if args.leases: - leases = get_leases(lease_file, args.state, args.pool, args.sort) + leases = get_leases(conf, lease_file, args.state, args.pool, args.sort) if args.json: - print(json.dumps(leases, indent=4)) + print(dumps(leases, indent=4)) else: show_leases(leases) @@ -211,18 +234,15 @@ if __name__ == '__main__': if args.pool: pools = [args.pool] else: - pools = config.list_effective_nodes("service dhcp-server shared-network-name") + pools = conf.list_effective_nodes("service dhcp-server shared-network-name") # Get pool usage stats stats = [] for p in pools: - size = get_pool_size(config, p) - leases = len(get_leases(lease_file, state='active', pool=p)) + size = get_pool_size(conf, p) + leases = len(get_leases(conf, lease_file, state='active', pool=p)) - if size != 0: - use_percentage = round(leases / size * 100) - else: - use_percentage = 0 + use_percentage = round(leases / size * 100) if size != 0 else 0 if args.json: pool_stats = {"pool": p, "size": size, "leases": leases, @@ -234,15 +254,10 @@ if __name__ == '__main__': # Print stats if args.json: - print(json.dumps(stats, indent=4)) + print(dumps(stats, indent=4)) else: show_pool_stats(stats) - elif args.allowed == 'pool': - print(' '.join(config.list_effective_nodes("service dhcp-server shared-network-name"))) - elif args.allowed == 'sort': - print(' '.join(lease_display_fields.keys())) - elif args.allowed == 'state': - print(' '.join(lease_valid_states)) else: parser.print_help() + exit(1) diff --git a/src/op_mode/show_dhcpv6.py b/src/op_mode/show_dhcpv6.py index d686defc0..ac211fb0a 100755 --- a/src/op_mode/show_dhcpv6.py +++ b/src/op_mode/show_dhcpv6.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,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 json -import argparse -import ipaddress -import tabulate -import sys -import collections -import os +# TODO: merge with show_dhcp.py + +from json import dumps +from argparse import ArgumentParser +from ipaddress import ip_address +from tabulate import tabulate +from sys import exit +from collections import OrderedDict from datetime import datetime from isc_dhcp_leases import Lease, IscDhcpLeases @@ -32,7 +32,7 @@ from vyos.util import call lease_file = "/config/dhcpdv6.leases" pool_key = "shared-networkname" -lease_display_fields = collections.OrderedDict() +lease_display_fields = OrderedDict() lease_display_fields['ip'] = 'IPv6 address' lease_display_fields['state'] = 'State' lease_display_fields['last_comm'] = 'Last communication' @@ -108,7 +108,7 @@ def get_lease_data(lease): return data -def get_leases(leases, state, pool=None, sort='ip'): +def get_leases(config, leases, state, pool=None, sort='ip'): leases = IscDhcpLeases(lease_file).get() # filter leases by state @@ -121,7 +121,7 @@ def get_leases(leases, state, pool=None, sort='ip'): leases = list(filter(lambda x: in_pool(x, pool), leases)) else: print("Pool {0} does not exist.".format(pool)) - sys.exit(0) + exit(0) # should maybe filter all state=active by lease.valid here? @@ -139,7 +139,7 @@ def get_leases(leases, state, pool=None, sort='ip'): # apply output/display sort if sort == 'ip': - leases = sorted(leases, key = lambda k: int(ipaddress.ip_address(k['ip']))) + leases = sorted(leases, key = lambda k: int(ip_address(k['ip']))) else: leases = sorted(leases, key = lambda k: k[sort]) @@ -153,12 +153,12 @@ def show_leases(leases): lease_list_params.append(l[k]) lease_list.append(lease_list_params) - output = tabulate.tabulate(lease_list, lease_display_fields.values()) + output = tabulate(lease_list, lease_display_fields.values()) print(output) if __name__ == '__main__': - parser = argparse.ArgumentParser() + parser = ArgumentParser() group = parser.add_mutually_exclusive_group() group.add_argument("-l", "--leases", action="store_true", help="Show DHCPv6 leases") @@ -166,36 +166,54 @@ if __name__ == '__main__': group.add_argument("--allowed", type=str, choices=["pool", "sort", "state"], help="Show allowed values for argument") parser.add_argument("-p", "--pool", type=str, help="Show lease for specific pool") - parser.add_argument("-S", "--sort", type=str, choices=lease_display_fields.keys(), default='ip', help="Sort by") - parser.add_argument("-t", "--state", type=str, nargs="+", choices=lease_valid_states, default="active", help="Lease state to show (can specify multiple with spaces)") + parser.add_argument("-S", "--sort", type=str, default='ip', help="Sort by") + parser.add_argument("-t", "--state", type=str, nargs="+", default=["active"], help="Lease state to show (can specify multiple with spaces)") parser.add_argument("-j", "--json", action="store_true", default=False, help="Produce JSON output") args = parser.parse_args() + conf = Config() + + if args.allowed == 'pool': + if conf.exists_effective('service dhcpv6-server'): + print(' '.join(conf.list_effective_nodes("service dhcpv6-server shared-network-name"))) + exit(0) + elif args.allowed == 'sort': + print(' '.join(lease_display_fields.keys())) + exit(0) + elif args.allowed == 'state': + print(' '.join(lease_valid_states)) + exit(0) + elif args.allowed: + parser.print_help() + exit(1) + + if args.sort not in lease_display_fields.keys(): + print(f'Invalid sort key, choose from: {list(lease_display_fields.keys())}') + exit(0) + + if not set(args.state) < set(lease_valid_states): + print(f'Invalid lease state, choose from: {lease_valid_states}') + exit(0) + # Do nothing if service is not configured - c = Config() - if not c.exists_effective('service dhcpv6-server'): + if not conf.exists_effective('service dhcpv6-server'): print("DHCPv6 service is not configured") - sys.exit(0) + exit(0) # if dhcp server is down, inactive leases may still be shown as active, so warn the user. if call('systemctl -q is-active isc-dhcp-server6.service') != 0: print("WARNING: DHCPv6 server is configured but not started. Data may be stale.") if args.leases: - leases = get_leases(lease_file, args.state, args.pool, args.sort) + leases = get_leases(conf, lease_file, args.state, args.pool, args.sort) if args.json: - print(json.dumps(leases, indent=4)) + print(dumps(leases, indent=4)) else: show_leases(leases) elif args.statistics: print("DHCPv6 statistics option is not available") - elif args.allowed == 'pool': - print(' '.join(c.list_effective_nodes("service dhcpv6-server shared-network-name"))) - elif args.allowed == 'sort': - print(' '.join(lease_display_fields.keys())) - elif args.allowed == 'state': - print(' '.join(lease_valid_states)) else: parser.print_help() + exit(1) diff --git a/src/op_mode/show_interfaces.py b/src/op_mode/show_interfaces.py index 8b6690b7d..7041c7e16 100755 --- a/src/op_mode/show_interfaces.py +++ b/src/op_mode/show_interfaces.py @@ -18,6 +18,7 @@ import os import re import sys +import glob import datetime import argparse import netifaces @@ -146,9 +147,20 @@ def run_allowed(**kwarg): sys.stdout.write(' '.join(Section.interfaces())) +def pppoe(ifname): + out = cmd(f'ps -C pppd -f') + if ifname in out: + return 'C' + elif ifname in [_.split('/')[-1] for _ in glob.glob('/etc/ppp/peers/pppoe*')]: + return 'D' + return '' + + @register('show') def run_show_intf(ifnames, iftypes, vif, vrrp): + handled = [] for interface in filtered_interfaces(ifnames, iftypes, vif, vrrp): + handled.append(interface.ifname) cache = interface.operational.load_counters() out = cmd(f'ip addr show {interface.ifname}') @@ -173,6 +185,17 @@ def run_show_intf(ifnames, iftypes, vif, vrrp): print() print(interface.operational.formated_stats()) + for ifname in ifnames: + if ifname not in handled and ifname.startswith('pppoe'): + state = pppoe(ifname) + if not state: + continue + string = { + 'C': 'Coming up', + 'D': 'Link down', + }[state] + print('{}: {}'.format(ifname, string)) + @register('show-brief') def run_show_intf_brief(ifnames, iftypes, vif, vrrp): @@ -183,7 +206,10 @@ def run_show_intf_brief(ifnames, iftypes, vif, vrrp): print(format1 % ("Interface", "IP Address", "S/L", "Description")) print(format1 % ("---------", "----------", "---", "-----------")) + handled = [] for interface in filtered_interfaces(ifnames, iftypes, vif, vrrp): + handled.append(interface.ifname) + oper_state = interface.operational.get_state() admin_state = interface.get_admin_state() @@ -206,6 +232,17 @@ def run_show_intf_brief(ifnames, iftypes, vif, vrrp): print(format2 % (i, a)) print(format1 % ('', '', '/'.join(s+l), d)) + for ifname in ifnames: + if ifname not in handled and ifname.startswith('pppoe'): + state = pppoe(ifname) + if not state: + continue + string = { + 'C': 'u/D', + 'D': 'A/D', + }[state] + print(format1 % (ifname, '', string, '')) + @register('show-count') def run_show_counters(ifnames, iftypes, vif, vrrp): diff --git a/src/op_mode/version.py b/src/op_mode/version.py index 2791f5e5c..c335eefce 100755 --- a/src/op_mode/version.py +++ b/src/op_mode/version.py @@ -25,14 +25,13 @@ import sys import argparse import json -import pystache - import vyos.version import vyos.limericks from vyos.util import cmd from vyos.util import call from vyos.util import run +from vyos.util import read_file from vyos.util import DEVNULL @@ -41,90 +40,42 @@ parser.add_argument("-a", "--all", action="store_true", help="Include individual parser.add_argument("-f", "--funny", action="store_true", help="Add something funny to the output") parser.add_argument("-j", "--json", action="store_true", help="Produce JSON output") -def read_file(name): - try: - with open (name, "r") as f: - data = f.read() - return data.strip() - except: - # This works since we only read /sys/class/* stuff - # with this function - return "Unknown" version_output_tmpl = """ -Version: VyOS {{version}} -Release Train: {{release_train}} +Version: VyOS {version} +Release Train: {release_train} -Built by: {{built_by}} -Built on: {{built_on}} -Build UUID: {{build_uuid}} -Build Commit ID: {{build_git}} +Built by: {built_by} +Built on: {built_on} +Build UUID: {build_uuid} +Build Commit ID: {build_git} -Architecture: {{system_arch}} -Boot via: {{boot_via}} -System type: {{system_type}} +Architecture: {system_arch} +Boot via: {boot_via} +System type: {system_type} -Hardware vendor: {{hardware_vendor}} -Hardware model: {{hardware_model}} -Hardware S/N: {{hardware_serial}} -Hardware UUID: {{hardware_uuid}} +Hardware vendor: {hardware_vendor} +Hardware model: {hardware_model} +Hardware S/N: {hardware_serial} +Hardware UUID: {hardware_uuid} Copyright: VyOS maintainers and contributors - """ if __name__ == '__main__': args = parser.parse_args() - version_data = vyos.version.get_version_data() - - # Get system architecture (well, kernel architecture rather) - version_data['system_arch'] = cmd('uname -m') - - - # Get hypervisor name, if any - system_type = "bare metal" - try: - hypervisor = cmd('hvinfo',stderr=DEVNULL) - system_type = "{0} guest".format(hypervisor) - except OSError: - # hvinfo returns 1 if it cannot detect any hypervisor - pass - version_data['system_type'] = system_type - - - # Get boot type, it can be livecd, installed image, or, possible, a system installed - # via legacy "install system" mechanism - # In installed images, the squashfs image file is named after its image version, - # 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 run(""" grep -e '^overlay.*/filesystem.squashfs' /proc/mounts >/dev/null""") == 0: - boot_via = "livecd" - elif run(""" grep '^overlay /' /proc/mounts >/dev/null """) != 0: - boot_via = "legacy non-image installation" - version_data['boot_via'] = boot_via - - - # Get hardware details from DMI - version_data['hardware_vendor'] = read_file('/sys/class/dmi/id/sys_vendor') - version_data['hardware_model'] = read_file('/sys/class/dmi/id/product_name') - - # These two assume script is run as root, normal users can't access those files - version_data['hardware_serial'] = read_file('/sys/class/dmi/id/subsystem/id/product_serial') - version_data['hardware_uuid'] = read_file('/sys/class/dmi/id/subsystem/id/product_uuid') - + version_data = vyos.version.get_full_version_data() if args.json: print(json.dumps(version_data)) sys.exit(0) - else: - output = pystache.render(version_output_tmpl, version_data).strip() - print(output) - if args.all: - print("Package versions:") - call("dpkg -l") + print(version_output_tmpl.format(**version_data).strip()) + + if args.all: + print("Package versions:") + call("dpkg -l") - if args.funny: - print(vyos.limericks.get_random()) + if args.funny: + print(vyos.limericks.get_random()) diff --git a/src/op_mode/vrrp.py b/src/op_mode/vrrp.py index 923cfa4d4..e024d7f63 100755 --- a/src/op_mode/vrrp.py +++ b/src/op_mode/vrrp.py @@ -21,7 +21,6 @@ import argparse import json import tabulate -import vyos.keepalived import vyos.util from vyos.ifconfig.vrrp import VRRP diff --git a/src/op_mode/wireguard.py b/src/op_mode/wireguard.py index 297ba599d..15bf63e81 100755 --- a/src/op_mode/wireguard.py +++ b/src/op_mode/wireguard.py @@ -147,8 +147,12 @@ if __name__ == '__main__': if args.listkdir: list_key_dirs() if args.showinterface: - intf = WireGuardIf(args.showinterface, create=False, debug=False) - print(intf.operational.show_interface()) + try: + intf = WireGuardIf(args.showinterface, create=False, debug=False) + print(intf.operational.show_interface()) + # the interface does not exists + except Exception: + pass if args.delkdir: if args.location: del_key_dir(args.location) diff --git a/src/services/vyos-http-api-server b/src/services/vyos-http-api-server index b5ad8b159..c36cbd640 100755 --- a/src/services/vyos-http-api-server +++ b/src/services/vyos-http-api-server @@ -22,6 +22,7 @@ import grp import json import traceback import threading +import signal import vyos.config @@ -317,11 +318,20 @@ def generate_op(): command = json.loads(command) try: - cmd = command['cmd'] - res = session.generate(cmd) + op = command['op'] + path = command['path'] except KeyError: - return error(400, "Missing required field \"cmd\"") - except VyOSError as e: + return error(400, "Missing required field. \"op\" and \"path\" fields are required") + + if not isinstance(path, list): + return error(400, "Malformed command: \"path\" field must be a list of strings") + + try: + if op == 'generate': + res = session.generate(path) + else: + return error(400, "\"{0}\" is not a valid operation".format(op)) + except ConfigSessionError as e: return error(400, str(e)) except Exception as e: print(traceback.format_exc(), file=sys.stderr) @@ -338,11 +348,20 @@ def show_op(): command = json.loads(command) try: - cmd = command['cmd'] - res = session.show(cmd) + op = command['op'] + path = command['path'] except KeyError: - return error(400, "Missing required field \"cmd\"") - except VyOSError as e: + return error(400, "Missing required field. \"op\" and \"path\" fields are required") + + if not isinstance(path, list): + return error(400, "Malformed command: \"path\" field must be a list of strings") + + try: + if op == 'show': + res = session.show(path) + else: + return error(400, "\"{0}\" is not a valid operation".format(op)) + except ConfigSessionError as e: return error(400, str(e)) except Exception as e: print(traceback.format_exc(), file=sys.stderr) @@ -350,6 +369,8 @@ def show_op(): return success(res) +def shutdown(): + raise KeyboardInterrupt if __name__ == '__main__': # systemd's user and group options don't work, do it by hand here, @@ -372,4 +393,9 @@ if __name__ == '__main__': app.config['vyos_keys'] = server_config['api_keys'] app.config['vyos_debug'] = server_config['debug'] + def sig_handler(signum, frame): + shutdown() + + signal.signal(signal.SIGTERM, sig_handler) + bottle.run(app, host=server_config["listen_address"], port=server_config["port"], debug=True) diff --git a/src/systemd/dhclient6@.service b/src/systemd/dhclient6@.service new file mode 100644 index 000000000..fd69e4d48 --- /dev/null +++ b/src/systemd/dhclient6@.service @@ -0,0 +1,18 @@ +[Unit] +Description=DHCPv6 client on %i +Documentation=man:dhclient(8) +ConditionPathExists=/var/lib/dhcp/dhclient_v6_%i.conf +ConditionPathExists=/var/lib/dhcp/dhclient_v6_%i.options +After=vyos-router.service + +[Service] +WorkingDirectory=/var/lib/dhcp +Type=exec +EnvironmentFile=-/var/lib/dhcp/dhclient_v6_%i.options +PIDFile=/var/lib/dhcp/dhclient_v6_%i.pid +ExecStart=/sbin/dhclient -6 $DHCLIENT_OPTS +ExecStop=/sbin/dhclient -6 $DHCLIENT_OPTS -r +Restart=always + +[Install] +WantedBy=multi-user.target diff --git a/src/systemd/dhclient@.service b/src/systemd/dhclient@.service new file mode 100644 index 000000000..2ced1038a --- /dev/null +++ b/src/systemd/dhclient@.service @@ -0,0 +1,18 @@ +[Unit] +Description=DHCP client on %i +Documentation=man:dhclient(8) +ConditionPathExists=/var/lib/dhcp/dhclient_%i.conf +ConditionPathExists=/var/lib/dhcp/dhclient_%i.options +After=vyos-router.service + +[Service] +WorkingDirectory=/var/lib/dhcp +Type=exec +EnvironmentFile=-/var/lib/dhcp/dhclient_%i.options +PIDFile=/var/lib/dhcp/dhclient_%i.pid +ExecStart=/sbin/dhclient -4 $DHCLIENT_OPTS +ExecStop=/sbin/dhclient -4 $DHCLIENT_OPTS -r +Restart=always + +[Install] +WantedBy=multi-user.target diff --git a/src/systemd/isc-dhcp-server6.service b/src/systemd/isc-dhcp-server6.service index 743f16840..27bebc57f 100644 --- a/src/systemd/isc-dhcp-server6.service +++ b/src/systemd/isc-dhcp-server6.service @@ -2,7 +2,7 @@ Description=ISC DHCP IPv6 server Documentation=man:dhcpd(8) RequiresMountsFor=/run -ConditionPathExists=/run/dhcp-server/dhcpd.conf +ConditionPathExists=/run/dhcp-server/dhcpdv6.conf After=vyos-router.service [Service] diff --git a/src/validators/numeric b/src/validators/numeric index 0a2d83d14..2cd5178b9 100755 --- a/src/validators/numeric +++ b/src/validators/numeric @@ -19,7 +19,6 @@ import sys import argparse -import re parser = argparse.ArgumentParser() parser.add_argument("-f", "--float", action="store_true", help="Accept floating point values") @@ -50,8 +49,9 @@ if args.range: valid = False for r in args.range: try: - lower, upper = re.match(r'(\d+)\s*\-\s*(\d+)', r).groups() - lower, upper = int(lower), int(upper) + list = r.split('-') + lower = int(list[0]) + upper = int(list[1]) except: print("{0} is not a valid number range",format(args.range), file=sys.stderr) sys.exit(1) |