diff options
133 files changed, 2894 insertions, 427 deletions
diff --git a/.github/reviewers.yml b/.github/reviewers.yml index a6f0a3785..a1647d20d 100644 --- a/.github/reviewers.yml +++ b/.github/reviewers.yml @@ -1,8 +1,3 @@ --- "**/*": - - dmbaturin - - sarthurdev - - zdc - - jestabro - - sever-sever - - c-po + - team: reviewers diff --git a/.github/workflows/auto-author-assign.yml b/.github/workflows/auto-author-assign.yml index a769145f8..13bfd9bb1 100644 --- a/.github/workflows/auto-author-assign.yml +++ b/.github/workflows/auto-author-assign.yml @@ -23,5 +23,5 @@ jobs: - name: Request review based on files changes and/or groups the author belongs to uses: shufo/auto-assign-reviewer-by-files@v1.1.4 with: - token: ${{ secrets.GITHUB_TOKEN }} + token: ${{ secrets.PR_ACTION_ASSIGN_REVIEWERS }} config: .github/reviewers.yml @@ -1,4 +1,4 @@ -# vyos-1x: VyOS 1.2.0+ Configuration Scripts and Data +# vyos-1x: VyOS command definitions, configuration scripts, and data [![Coverage](https://sonarcloud.io/api/project_badges/measure?project=vyos%3Avyos-1x&metric=coverage)](https://sonarcloud.io/component_measures?id=vyos%3Avyos-1x&metric=coverage) [![FOSSA Status](https://app.fossa.io/api/projects/git%2Bgithub.com%2Fvyos%2Fvyos-1x.svg?type=shield)](https://app.fossa.io/projects/git%2Bgithub.com%2Fvyos%2Fvyos-1x?ref=badge_shield) @@ -36,7 +36,7 @@ src ## Interface/command definitions Raw `node.def` files for the old backend are no longer written by hand or -generated by custom sciprts. They are all now produced from a unified XML format +generated by custom scripts. They are all now produced from a unified XML format that supports a strict subset of the old backend features. In particular, it intentionally does not support embedded shell scripts, default values, and value "types", instead delegating those tasks to external scripts. @@ -54,8 +54,7 @@ The guidelines in a nutshell: generating taret config, see our [documentation](https://docs.vyos.io/en/latest/contributing/development.html#python) for the common structure -* Use the `get_config_dict()` API as much as possible when retrieving values from - the CLI +* Use the `get_config_dict()` API as much as possible when retrieving values from the CLI * Use a template processor when the format is more complex than just one line (Jinja2 and pystache are acceptable options) diff --git a/data/config-mode-dependencies.json b/data/config-mode-dependencies.json new file mode 100644 index 000000000..9e943ba2c --- /dev/null +++ b/data/config-mode-dependencies.json @@ -0,0 +1,12 @@ +{ + "firewall": {"group_resync": ["nat", "policy-route"]}, + "http_api": {"https": ["https"]}, + "pki": { + "ethernet": ["interfaces-ethernet"], + "openvpn": ["interfaces-openvpn"], + "https": ["https"], + "ipsec": ["vpn_ipsec"], + "openconnect": ["vpn_openconnect"], + "sstp": ["vpn_sstp"] + } +} diff --git a/data/configd-include.json b/data/configd-include.json index 5a4912e30..648655a8b 100644 --- a/data/configd-include.json +++ b/data/configd-include.json @@ -28,6 +28,7 @@ "interfaces-openvpn.py", "interfaces-pppoe.py", "interfaces-pseudo-ethernet.py", +"interfaces-sstpc.py", "interfaces-tunnel.py", "interfaces-vti.py", "interfaces-vxlan.py", diff --git a/data/op-mode-standardized.json b/data/op-mode-standardized.json index 1509975b4..a69cf55e9 100644 --- a/data/op-mode-standardized.json +++ b/data/op-mode-standardized.json @@ -12,6 +12,7 @@ "nat.py", "neighbor.py", "openconnect.py", +"openvpn.py", "route.py", "system.py", "ipsec.py", diff --git a/data/templates/accel-ppp/l2tp.config.j2 b/data/templates/accel-ppp/l2tp.config.j2 index 9eeaf7622..3d1e835a9 100644 --- a/data/templates/accel-ppp/l2tp.config.j2 +++ b/data/templates/accel-ppp/l2tp.config.j2 @@ -88,6 +88,9 @@ verbose=1 {% for r in radius_server %} server={{ r.server }},{{ r.key }},auth-port={{ r.port }},acct-port={{ r.acct_port }},req-limit=0,fail-time={{ r.fail_time }} {% endfor %} +{% if radius_dynamic_author.server is vyos_defined %} +dae-server={{ radius_dynamic_author.server }}:{{ radius_dynamic_author.port }},{{ radius_dynamic_author.key }} +{% endif %} {% if radius_acct_inter_jitter %} acct-interim-jitter={{ radius_acct_inter_jitter }} {% endif %} @@ -118,8 +121,10 @@ lcp-echo-failure={{ ppp_echo_failure }} {% if ccp_disable %} ccp=0 {% endif %} -{% if client_ipv6_pool %} -ipv6=allow +{% if ppp_ipv6 is vyos_defined %} +ipv6={{ ppp_ipv6 }} +{% else %} +{{ 'ipv6=allow' if client_ipv6_pool_configured else '' }} {% endif %} diff --git a/data/templates/firewall/nftables-defines.j2 b/data/templates/firewall/nftables-defines.j2 index dd06dee28..0a7e79edd 100644 --- a/data/templates/firewall/nftables-defines.j2 +++ b/data/templates/firewall/nftables-defines.j2 @@ -85,5 +85,18 @@ } {% endfor %} {% endif %} +{% if group.interface_group is vyos_defined %} +{% for group_name, group_conf in group.interface_group.items() %} +{% set includes = group_conf.include if group_conf.include is vyos_defined else [] %} + set I_{{ group_name }} { + type ifname + flags interval + auto-merge +{% if group_conf.interface is vyos_defined or includes %} + elements = { {{ group_conf.interface | nft_nested_group(includes, group.interface_group, 'interface') | join(",") }} } +{% endif %} + } +{% endfor %} +{% endif %} {% endif %} {% endmacro %} diff --git a/data/templates/frr/bgpd.frr.j2 b/data/templates/frr/bgpd.frr.j2 index e8d135c78..5febd7c66 100644 --- a/data/templates/frr/bgpd.frr.j2 +++ b/data/templates/frr/bgpd.frr.j2 @@ -517,6 +517,9 @@ router bgp {{ system_as }} {{ 'vrf ' ~ vrf if vrf is vyos_defined }} {% if parameters.network_import_check is vyos_defined %} bgp network import-check {% endif %} +{% if parameters.route_reflector_allow_outbound_policy is vyos_defined %} +bgp route-reflector allow-outbound-policy +{% endif %} {% if parameters.no_client_to_client_reflection is vyos_defined %} no bgp client-to-client reflection {% endif %} diff --git a/data/templates/frr/ospfd.frr.j2 b/data/templates/frr/ospfd.frr.j2 index 2a8afefbc..882ec8f97 100644 --- a/data/templates/frr/ospfd.frr.j2 +++ b/data/templates/frr/ospfd.frr.j2 @@ -161,6 +161,12 @@ router ospf {{ 'vrf ' ~ vrf if vrf is vyos_defined }} {% if parameters.abr_type is vyos_defined %} ospf abr-type {{ parameters.abr_type }} {% endif %} +{% if parameters.opaque_lsa is vyos_defined %} + ospf opaque-lsa +{% endif %} +{% if parameters.rfc1583_compatibility is vyos_defined %} + ospf rfc1583compatibility +{% endif %} {% if parameters.router_id is vyos_defined %} ospf router-id {{ parameters.router_id }} {% endif %} diff --git a/data/templates/ipsec/swanctl/peer.j2 b/data/templates/ipsec/swanctl/peer.j2 index d097a04fc..837fa263c 100644 --- a/data/templates/ipsec/swanctl/peer.j2 +++ b/data/templates/ipsec/swanctl/peer.j2 @@ -124,7 +124,7 @@ {% endif %} {% elif tunnel_esp.mode == 'transport' %} local_ts = {{ peer_conf.local_address }}{{ local_suffix }} - remote_ts = {{ peer }}{{ remote_suffix }} + remote_ts = {{ peer_conf.remote_address | join(",") }}{{ remote_suffix }} {% endif %} ipcomp = {{ 'yes' if tunnel_esp.compression is vyos_defined else 'no' }} mode = {{ tunnel_esp.mode }} diff --git a/data/templates/protocols/systemd_vyos_failover_service.j2 b/data/templates/protocols/systemd_vyos_failover_service.j2 new file mode 100644 index 000000000..e6501e0f5 --- /dev/null +++ b/data/templates/protocols/systemd_vyos_failover_service.j2 @@ -0,0 +1,11 @@ +[Unit] +Description=Failover route service +After=vyos-router.service + +[Service] +Type=simple +Restart=always +ExecStart=/usr/bin/python3 /usr/libexec/vyos/vyos-failover.py --config /run/vyos-failover.conf + +[Install] +WantedBy=multi-user.target diff --git a/data/templates/router-advert/radvd.conf.j2 b/data/templates/router-advert/radvd.conf.j2 index a464795ad..f4b384958 100644 --- a/data/templates/router-advert/radvd.conf.j2 +++ b/data/templates/router-advert/radvd.conf.j2 @@ -43,6 +43,13 @@ interface {{ iface }} { }; {% endfor %} {% endif %} +{% if iface_config.source_address is vyos_defined %} + AdvRASrcAddress { +{% for source_address in iface_config.source_address %} + {{ source_address }} +{% endfor %} + }; +{% endif %} {% if iface_config.prefix is vyos_defined %} {% for prefix, prefix_options in iface_config.prefix.items() %} prefix {{ prefix }} { diff --git a/data/templates/snmp/etc.snmpd.conf.j2 b/data/templates/snmp/etc.snmpd.conf.j2 index d7dc0ba5d..47bf6878f 100644 --- a/data/templates/snmp/etc.snmpd.conf.j2 +++ b/data/templates/snmp/etc.snmpd.conf.j2 @@ -69,13 +69,14 @@ agentaddress unix:/run/snmpd.socket{{ ',' ~ options | join(',') if options is vy {% for network in comm_config.network %} {% if network | is_ipv4 %} {{ comm_config.authorization }}community {{ comm }} {{ network }} -{% elif client | is_ipv6 %} +{% elif network | is_ipv6 %} {{ comm_config.authorization }}community6 {{ comm }} {{ network }} {% endif %} {% endfor %} {% endif %} {% if comm_config.client is not vyos_defined and comm_config.network is not vyos_defined %} {{ comm_config.authorization }}community {{ comm }} +{{ comm_config.authorization }}community6 {{ comm }} {% endif %} {% endfor %} {% endif %} diff --git a/data/templates/squid/sg_acl.conf.j2 b/data/templates/squid/sg_acl.conf.j2 index ce72b173a..78297a2b8 100644 --- a/data/templates/squid/sg_acl.conf.j2 +++ b/data/templates/squid/sg_acl.conf.j2 @@ -1,6 +1,5 @@ ### generated by service_webproxy.py ### dbhome {{ squidguard_db_dir }} - dest {{ category }}-{{ rule }} { {% if list_type == 'domains' %} domainlist {{ category }}/domains diff --git a/data/templates/squid/squid.conf.j2 b/data/templates/squid/squid.conf.j2 index 5781c883f..b953c8b18 100644 --- a/data/templates/squid/squid.conf.j2 +++ b/data/templates/squid/squid.conf.j2 @@ -24,7 +24,12 @@ acl Safe_ports port {{ port }} {% endfor %} {% endif %} acl CONNECT method CONNECT - +{% if domain_block is vyos_defined %} +{% for domain in domain_block %} +acl BLOCKDOMAIN dstdomain {{ domain }} +{% endfor %} +http_access deny BLOCKDOMAIN +{% endif %} {% if authentication is vyos_defined %} {% if authentication.children is vyos_defined %} auth_param basic children {{ authentication.children }} diff --git a/data/templates/squid/squidGuard.conf.j2 b/data/templates/squid/squidGuard.conf.j2 index 1bc4c984f..a93f878df 100644 --- a/data/templates/squid/squidGuard.conf.j2 +++ b/data/templates/squid/squidGuard.conf.j2 @@ -1,10 +1,16 @@ ### generated by service_webproxy.py ### -{% macro sg_rule(category, log, db_dir) %} +{% macro sg_rule(category, rule, log, db_dir) %} +{% set domains = db_dir + '/' + category + '/domains' %} +{% set urls = db_dir + '/' + category + '/urls' %} {% set expressions = db_dir + '/' + category + '/expressions' %} -dest {{ category }}-default { +dest {{ category }}-{{ rule }}{ +{% if domains | is_file %} domainlist {{ category }}/domains +{% endif %} +{% if urls | is_file %} urllist {{ category }}/urls +{% endif %} {% if expressions | is_file %} expressionlist {{ category }}/expressions {% endif %} @@ -17,8 +23,9 @@ dest {{ category }}-default { {% if url_filtering is vyos_defined and url_filtering.disable is not vyos_defined %} {% if url_filtering.squidguard is vyos_defined %} {% set sg_config = url_filtering.squidguard %} -{% set acl = namespace(value='local-ok-default') %} +{% set acl = namespace(value='') %} {% set acl.value = acl.value + ' !in-addr' if sg_config.allow_ipaddr_url is not defined else acl.value %} +{% set ruleacls = {} %} dbhome {{ squidguard_db_dir }} logdir /var/log/squid @@ -38,24 +45,28 @@ dest local-ok-default { domainlist local-ok-default/domains } {% endif %} + {% if sg_config.local_ok_url is vyos_defined %} {% set acl.value = acl.value + ' local-ok-url-default' %} dest local-ok-url-default { urllist local-ok-url-default/urls } {% endif %} + {% if sg_config.local_block is vyos_defined %} {% set acl.value = acl.value + ' !local-block-default' %} dest local-block-default { domainlist local-block-default/domains } {% endif %} + {% if sg_config.local_block_url is vyos_defined %} {% set acl.value = acl.value + ' !local-block-url-default' %} dest local-block-url-default { urllist local-block-url-default/urls } {% endif %} + {% if sg_config.local_block_keyword is vyos_defined %} {% set acl.value = acl.value + ' !local-block-keyword-default' %} dest local-block-keyword-default { @@ -65,16 +76,100 @@ dest local-block-keyword-default { {% if sg_config.block_category is vyos_defined %} {% for category in sg_config.block_category %} -{{ sg_rule(category, sg_config.log, squidguard_db_dir) }} +{{ sg_rule(category, 'default', sg_config.log, squidguard_db_dir) }} {% set acl.value = acl.value + ' !' + category + '-default' %} {% endfor %} {% endif %} {% if sg_config.allow_category is vyos_defined %} {% for category in sg_config.allow_category %} -{{ sg_rule(category, False, squidguard_db_dir) }} +{{ sg_rule(category, 'default', False, squidguard_db_dir) }} {% set acl.value = acl.value + ' ' + category + '-default' %} {% endfor %} {% endif %} + + +{% if sg_config.rule is vyos_defined %} +{% for rule, rule_config in sg_config.rule.items() %} +{% if rule_config.local_ok is vyos_defined %} +{% if rule in ruleacls %} +{% set _dummy = ruleacls.update({rule: ruleacls[rule] + ' local-ok-' + rule}) %} +{% else %} +{% set _dummy = ruleacls.update({rule:'local-ok-' + rule}) %} +{% endif %} +dest local-ok-{{ rule }} { + domainlist local-ok-{{ rule }}/domains +} +{% endif %} + +{% if rule_config.local_ok_url is vyos_defined %} +{% if rule in ruleacls %} +{% set _dummy = ruleacls.update({rule: ruleacls[rule] + ' local-ok-url-' + rule}) %} +{% else %} +{% set _dummy = ruleacls.update({rule:'local-ok-url-' + rule}) %} +{% endif %} +dest local-ok-url-{{ rule }} { + urllist local-ok-url-{{ rule }}/urls +} +{% endif %} + +{% if rule_config.local_block is vyos_defined %} +{% if rule in ruleacls %} +{% set _dummy = ruleacls.update({rule: ruleacls[rule] + ' !local-block-' + rule}) %} +{% else %} +{% set _dummy = ruleacls.update({rule:'!local-block-' + rule}) %} +{% endif %} +dest local-block-{{ rule }} { + domainlist local-block-{{ rule }}/domains +} +{% endif %} + +{% if rule_config.local_block_url is vyos_defined %} +{% if rule in ruleacls %} +{% set _dummy = ruleacls.update({rule: ruleacls[rule] + ' !local-block-url-' + rule}) %} +{% else %} +{% set _dummy = ruleacls.update({rule:'!ocal-block-url-' + rule}) %} +{% endif %} +dest local-block-url-{{ rule }} { + urllist local-block-url-{{ rule }}/urls +} +{% endif %} + +{% if rule_config.local_block_keyword is vyos_defined %} +{% if rule in ruleacls %} +{% set _dummy = ruleacls.update({rule: ruleacls[rule] + ' !local-block-keyword-' + rule}) %} +{% else %} +{% set _dummy = ruleacls.update({rule:'!local-block-keyword-' + rule}) %} +{% endif %} +dest local-block-keyword-{{ rule }} { + expressionlist local-block-keyword-{{ rule }}/expressions +} +{% endif %} + +{% if rule_config.block_category is vyos_defined %} +{% for b_category in rule_config.block_category %} +{% if rule in ruleacls %} +{% set _dummy = ruleacls.update({rule: ruleacls[rule] + ' !' + b_category + '-' + rule}) %} +{% else %} +{% set _dummy = ruleacls.update({rule:'!' + b_category + '-' + rule}) %} +{% endif %} +{{ sg_rule(b_category, rule, sg_config.log, squidguard_db_dir) }} +{% endfor %} +{% endif %} + +{% if rule_config.allow_category is vyos_defined %} +{% for a_category in rule_config.allow_category %} +{% if rule in ruleacls %} +{% set _dummy = ruleacls.update({rule: ruleacls[rule] + ' ' + a_category + '-' + rule}) %} +{% else %} +{% set _dummy = ruleacls.update({rule:a_category + '-' + rule}) %} +{% endif %} +{{ sg_rule(a_category, rule, sg_config.log, squidguard_db_dir) }} +{% endfor %} +{% endif %} +{% endfor %} +{% endif %} + + {% if sg_config.source_group is vyos_defined %} {% for sgroup, sg_config in sg_config.source_group.items() %} {% if sg_config.address is vyos_defined %} @@ -83,28 +178,15 @@ src {{ sgroup }} { ip {{ address }} {% endfor %} } - {% endif %} {% endfor %} {% endif %} -{% if sg_config.rule is vyos_defined %} -{% for rule, rule_config in sg_config.rule.items() %} -{% for b_category in rule_config.block_category %} -dest {{ b_category }} { - domainlist {{ b_category }}/domains - urllist {{ b_category }}/urls -} -{% endfor %} -{% endfor %} -{% endif %} acl { {% if sg_config.rule is vyos_defined %} {% for rule, rule_config in sg_config.rule.items() %} {{ rule_config.source_group }} { -{% for b_category in rule_config.block_category %} - pass local-ok-1 !in-addr !{{ b_category }} all -{% endfor %} + pass {{ ruleacls[rule] }} {{ 'none' if rule_config.default_action is vyos_defined('block') else 'any' }} } {% endfor %} {% endif %} @@ -113,7 +195,7 @@ acl { {% if sg_config.enable_safe_search is vyos_defined %} rewrite safesearch {% endif %} - pass {{ acl.value }} {{ 'none' if sg_config.default_action is vyos_defined('block') else 'allow' }} + pass {{ acl.value }} {{ 'none' if sg_config.default_action is vyos_defined('block') else 'any' }} redirect 302:http://{{ sg_config.redirect_url }} {% if sg_config.log is vyos_defined %} log blacklist.log diff --git a/data/templates/sstp-client/peer.j2 b/data/templates/sstp-client/peer.j2 new file mode 100644 index 000000000..1127d0564 --- /dev/null +++ b/data/templates/sstp-client/peer.j2 @@ -0,0 +1,46 @@ +### Autogenerated by interfaces-sstpc.py ### +{{ '# ' ~ description if description is vyos_defined else '' }} + +# Require peer to provide the local IP address if it is not +# specified explicitly in the config file. +noipdefault + +# Don't show the password in logfiles: +hide-password + +remotename {{ ifname }} +linkname {{ ifname }} +ipparam {{ ifname }} +ifname {{ ifname }} +pty "sstpc --ipparam {{ ifname }} --nolaunchpppd {{ server }}:{{ port }} --ca-cert {{ ca_file_path }}" + +# Override any connect script that may have been set in /etc/ppp/options. +connect /bin/true + +# Don't try to authenticate the remote node +noauth + +# We won't want EAP +refuse-eap + +# Don't try to proxy ARP for the remote endpoint. User can set proxy +# arp entries up manually if they wish. More importantly, having +# the "proxyarp" parameter set disables the "defaultroute" option. +noproxyarp + +# Unlimited connection attempts +maxfail 0 + +plugin sstp-pppd-plugin.so +sstp-sock /var/run/sstpc/sstpc-{{ ifname }} + +persist +debug + +{% if authentication is vyos_defined %} +{{ 'user "' + authentication.user + '"' if authentication.user is vyos_defined }} +{{ 'password "' + authentication.password + '"' if authentication.password is vyos_defined }} +{% endif %} + +{{ "usepeerdns" if no_peer_dns is not vyos_defined }} + diff --git a/debian/control b/debian/control index 66ac3c6f7..7e69003ff 100644 --- a/debian/control +++ b/debian/control @@ -154,6 +154,7 @@ Depends: squidguard, sshguard, ssl-cert, + sstp-client, strongswan (>= 5.9), strongswan-swanctl (>= 5.9), stunnel4, diff --git a/interface-definitions/container.xml.in b/interface-definitions/container.xml.in index f84c94a40..d50039665 100644 --- a/interface-definitions/container.xml.in +++ b/interface-definitions/container.xml.in @@ -272,6 +272,10 @@ <tagNode name="network"> <properties> <help>Network name</help> + <constraint> + <regex>[-_a-zA-Z0-9]{1,11}</regex> + </constraint> + <constraintErrorMessage>Network name cannot be longer than 11 characters</constraintErrorMessage> </properties> <children> <leafNode name="description"> diff --git a/interface-definitions/dns-domain-name.xml.in b/interface-definitions/dns-domain-name.xml.in index 70b2fb271..9aca38735 100644 --- a/interface-definitions/dns-domain-name.xml.in +++ b/interface-definitions/dns-domain-name.xml.in @@ -25,7 +25,7 @@ <constraint> <validator name="ipv4-address"/> <validator name="ipv6-address"/> - <validator name="interface-name"/> + #include <include/constraint/interface-name.xml.in> </constraint> </properties> </leafNode> diff --git a/interface-definitions/dns-dynamic.xml.in b/interface-definitions/dns-dynamic.xml.in index e41ba7f60..a39e412b2 100644 --- a/interface-definitions/dns-dynamic.xml.in +++ b/interface-definitions/dns-dynamic.xml.in @@ -237,19 +237,7 @@ <constraintErrorMessage>Please choose from the list of allowed protocols</constraintErrorMessage> </properties> </leafNode> - <leafNode name="server"> - <properties> - <help>Server to send DDNS update to</help> - <valueHelp> - <format>IPv4</format> - <description>IP address of DDNS server</description> - </valueHelp> - <valueHelp> - <format>FQDN</format> - <description>Hostname of DDNS server</description> - </valueHelp> - </properties> - </leafNode> + #include <include/server-ipv4-fqdn.xml.i> <leafNode name="zone"> <properties> <help>DNS zone to update (only available with CloudFlare)</help> diff --git a/interface-definitions/firewall.xml.in b/interface-definitions/firewall.xml.in index 2ebce79e5..c964abb41 100644 --- a/interface-definitions/firewall.xml.in +++ b/interface-definitions/firewall.xml.in @@ -134,6 +134,35 @@ #include <include/generic-description.xml.i> </children> </tagNode> + <tagNode name="interface-group"> + <properties> + <help>Firewall interface-group</help> + <constraint> + <regex>[a-zA-Z0-9][\w\-\.]*</regex> + </constraint> + </properties> + <children> + <leafNode name="interface"> + <properties> + <help>Interface-group member</help> + <completionHelp> + <script>${vyos_completion_dir}/list_interfaces.py</script> + </completionHelp> + <multi/> + </properties> + </leafNode> + <leafNode name="include"> + <properties> + <help>Include another interface-group</help> + <completionHelp> + <path>firewall group interface-group</path> + </completionHelp> + <multi/> + </properties> + </leafNode> + #include <include/generic-description.xml.i> + </children> + </tagNode> <tagNode name="ipv6-address-group"> <properties> <help>Firewall ipv6-address-group</help> @@ -412,6 +441,7 @@ #include <include/firewall/geoip.xml.i> #include <include/firewall/source-destination-group-ipv6.xml.i> #include <include/firewall/port.xml.i> + #include <include/firewall/address-mask-ipv6.xml.i> </children> </node> <node name="source"> @@ -424,6 +454,7 @@ #include <include/firewall/geoip.xml.i> #include <include/firewall/source-destination-group-ipv6.xml.i> #include <include/firewall/port.xml.i> + #include <include/firewall/address-mask-ipv6.xml.i> </children> </node> #include <include/firewall/common-rule.xml.i> @@ -578,6 +609,7 @@ #include <include/firewall/geoip.xml.i> #include <include/firewall/source-destination-group.xml.i> #include <include/firewall/port.xml.i> + #include <include/firewall/address-mask.xml.i> </children> </node> <node name="source"> @@ -590,6 +622,7 @@ #include <include/firewall/geoip.xml.i> #include <include/firewall/source-destination-group.xml.i> #include <include/firewall/port.xml.i> + #include <include/firewall/address-mask.xml.i> </children> </node> #include <include/firewall/common-rule.xml.i> diff --git a/interface-definitions/high-availability.xml.in b/interface-definitions/high-availability.xml.in index 0631acdda..784e51151 100644 --- a/interface-definitions/high-availability.xml.in +++ b/interface-definitions/high-availability.xml.in @@ -199,7 +199,7 @@ <description>Interface name</description> </valueHelp> <constraint> - <validator name="interface-name"/> + #include <include/constraint/interface-name.xml.in> </constraint> <multi/> </properties> diff --git a/interface-definitions/include/bgp/afi-rd.xml.i b/interface-definitions/include/bgp/afi-rd.xml.i index 767502094..beb1447df 100644 --- a/interface-definitions/include/bgp/afi-rd.xml.i +++ b/interface-definitions/include/bgp/afi-rd.xml.i @@ -17,7 +17,7 @@ <description>Route Distinguisher, (x.x.x.x:yyy|xxxx:yyyy)</description> </valueHelp> <constraint> - <regex>((25[0-5]|2[0-4][0-9]|[1][0-9][0-9]|[1-9][0-9]|[0-9]?)(\.(25[0-5]|2[0-4][0-9]|[1][0-9][0-9]|[1-9][0-9]|[0-9]?)){3}|[0-9]{1,10}):[0-9]{1,5}</regex> + <validator name="bgp-rd-rt" argument="--route-distinguisher"/> </constraint> </properties> </leafNode> diff --git a/interface-definitions/include/bgp/neighbor-update-source.xml.i b/interface-definitions/include/bgp/neighbor-update-source.xml.i index 37faf2cce..60c127e8f 100644 --- a/interface-definitions/include/bgp/neighbor-update-source.xml.i +++ b/interface-definitions/include/bgp/neighbor-update-source.xml.i @@ -22,7 +22,7 @@ <constraint> <validator name="ipv4-address"/> <validator name="ipv6-address"/> - <validator name="interface-name"/> + #include <include/constraint/interface-name.xml.in> </constraint> </properties> </leafNode> diff --git a/interface-definitions/include/bgp/protocol-common-config.xml.i b/interface-definitions/include/bgp/protocol-common-config.xml.i index 70176144d..366630f78 100644 --- a/interface-definitions/include/bgp/protocol-common-config.xml.i +++ b/interface-definitions/include/bgp/protocol-common-config.xml.i @@ -926,7 +926,7 @@ <constraint> <validator name="ipv4-address"/> <validator name="ipv6-address"/> - <validator name="interface-name"/> + #include <include/constraint/interface-name.xml.in> </constraint> </properties> <children> @@ -1431,6 +1431,12 @@ <valueless/> </properties> </leafNode> + <leafNode name="route-reflector-allow-outbound-policy"> + <properties> + <help>Route reflector client allow policy outbound</help> + <valueless/> + </properties> + </leafNode> <leafNode name="no-client-to-client-reflection"> <properties> <help>Disable client to client route reflection</help> diff --git a/interface-definitions/include/certificate-ca.xml.i b/interface-definitions/include/certificate-ca.xml.i index b97378658..3cde2a48d 100644 --- a/interface-definitions/include/certificate-ca.xml.i +++ b/interface-definitions/include/certificate-ca.xml.i @@ -7,7 +7,7 @@ <description>File in /config/auth directory</description> </valueHelp> <constraint> - <validator name="file-exists" argument="--directory /config/auth"/> + <validator name="file-path" argument="--strict --parent-dir /config/auth"/> </constraint> </properties> </leafNode> diff --git a/interface-definitions/include/certificate-key.xml.i b/interface-definitions/include/certificate-key.xml.i index 1db9dd069..2c4d81fbb 100644 --- a/interface-definitions/include/certificate-key.xml.i +++ b/interface-definitions/include/certificate-key.xml.i @@ -7,7 +7,7 @@ <description>File in /config/auth directory</description> </valueHelp> <constraint> - <validator name="file-exists" argument="--directory /config/auth"/> + <validator name="file-path" argument="--strict --parent-dir /config/auth"/> </constraint> </properties> </leafNode> diff --git a/interface-definitions/include/certificate.xml.i b/interface-definitions/include/certificate.xml.i index fb5be45cc..6a5b2936c 100644 --- a/interface-definitions/include/certificate.xml.i +++ b/interface-definitions/include/certificate.xml.i @@ -7,7 +7,7 @@ <description>File in /config/auth directory</description> </valueHelp> <constraint> - <validator name="file-exists" argument="--directory /config/auth"/> + <validator name="file-path" argument="--strict --parent-dir /config/auth"/> </constraint> </properties> </leafNode> diff --git a/interface-definitions/include/constraint/interface-name.xml.in b/interface-definitions/include/constraint/interface-name.xml.in new file mode 100644 index 000000000..2d1f7b757 --- /dev/null +++ b/interface-definitions/include/constraint/interface-name.xml.in @@ -0,0 +1,4 @@ +<!-- include start from constraint/interface-name.xml.in --> +<regex>(bond|br|dum|en|ersp|eth|gnv|lan|l2tp|l2tpeth|macsec|peth|ppp|pppoe|pptp|sstp|tun|veth|vti|vtun|vxlan|wg|wlan|wwan)[0-9]+(.\d+)?|lo</regex> +<validator name="file-path --lookup-path /sys/class/net --directory"/> +<!-- include end --> diff --git a/interface-definitions/include/dhcp-interface.xml.i b/interface-definitions/include/dhcp-interface.xml.i index 939b45f15..f5107ba2b 100644 --- a/interface-definitions/include/dhcp-interface.xml.i +++ b/interface-definitions/include/dhcp-interface.xml.i @@ -9,7 +9,7 @@ <description>DHCP interface name</description> </valueHelp> <constraint> - <validator name="interface-name"/> + #include <include/constraint/interface-name.xml.in> </constraint> </properties> </leafNode> diff --git a/interface-definitions/include/firewall/address-mask-ipv6.xml.i b/interface-definitions/include/firewall/address-mask-ipv6.xml.i new file mode 100644 index 000000000..8c0483209 --- /dev/null +++ b/interface-definitions/include/firewall/address-mask-ipv6.xml.i @@ -0,0 +1,14 @@ +<!-- include start from firewall/address-mask-ipv6.xml.i --> +<leafNode name="address-mask"> + <properties> + <help>IP mask</help> + <valueHelp> + <format>ipv6</format> + <description>IP mask to apply</description> + </valueHelp> + <constraint> + <validator name="ipv6"/> + </constraint> + </properties> +</leafNode> +<!-- include end --> diff --git a/interface-definitions/include/firewall/address-mask.xml.i b/interface-definitions/include/firewall/address-mask.xml.i new file mode 100644 index 000000000..7f6f17d1e --- /dev/null +++ b/interface-definitions/include/firewall/address-mask.xml.i @@ -0,0 +1,14 @@ +<!-- include start from firewall/address-mask.xml.i --> +<leafNode name="address-mask"> + <properties> + <help>IP mask</help> + <valueHelp> + <format>ipv4</format> + <description>IPv4 mask to apply</description> + </valueHelp> + <constraint> + <validator name="ipv4-address"/> + </constraint> + </properties> +</leafNode> +<!-- include end --> diff --git a/interface-definitions/include/firewall/common-rule.xml.i b/interface-definitions/include/firewall/common-rule.xml.i index 75ad427f9..75acefd96 100644 --- a/interface-definitions/include/firewall/common-rule.xml.i +++ b/interface-definitions/include/firewall/common-rule.xml.i @@ -26,14 +26,22 @@ </leafNode> </children> </node> -<leafNode name="inbound-interface"> +<node name="inbound-interface"> <properties> <help>Match inbound-interface</help> - <completionHelp> - <script>${vyos_completion_dir}/list_interfaces.py</script> - </completionHelp> </properties> -</leafNode> + <children> + #include <include/firewall/match-interface.xml.i> + </children> +</node> +<node name="outbound-interface"> + <properties> + <help>Match outbound-interface</help> + </properties> + <children> + #include <include/firewall/match-interface.xml.i> + </children> +</node> <node name="ipsec"> <properties> <help>Inbound IPsec packets</help> @@ -130,14 +138,6 @@ </leafNode> </children> </node> -<leafNode name="outbound-interface"> - <properties> - <help>Match outbound-interface</help> - <completionHelp> - <script>${vyos_completion_dir}/list_interfaces.py</script> - </completionHelp> - </properties> -</leafNode> <leafNode name="protocol"> <properties> <help>Protocol to match (protocol name, number, or "all")</help> diff --git a/interface-definitions/include/firewall/icmpv6-type-name.xml.i b/interface-definitions/include/firewall/icmpv6-type-name.xml.i index a2e68abfb..e17a20e17 100644 --- a/interface-definitions/include/firewall/icmpv6-type-name.xml.i +++ b/interface-definitions/include/firewall/icmpv6-type-name.xml.i @@ -3,7 +3,7 @@ <properties> <help>ICMPv6 type-name</help> <completionHelp> - <list>destination-unreachable packet-too-big time-exceeded echo-request echo-reply mld-listener-query mld-listener-report mld-listener-reduction nd-router-solicit nd-router-advert nd-neighbor-solicit nd-neighbor-advert nd-redirect parameter-problem router-renumbering</list> + <list>destination-unreachable packet-too-big time-exceeded echo-request echo-reply mld-listener-query mld-listener-report mld-listener-reduction nd-router-solicit nd-router-advert nd-neighbor-solicit nd-neighbor-advert nd-redirect parameter-problem router-renumbering ind-neighbor-solicit ind-neighbor-advert mld2-listener-report</list> </completionHelp> <valueHelp> <format>destination-unreachable</format> @@ -65,8 +65,20 @@ <format>router-renumbering</format> <description>ICMPv6 type 138: router-renumbering</description> </valueHelp> + <valueHelp> + <format>ind-neighbor-solicit</format> + <description>ICMPv6 type 141: ind-neighbor-solicit</description> + </valueHelp> + <valueHelp> + <format>ind-neighbor-advert</format> + <description>ICMPv6 type 142: ind-neighbor-advert</description> + </valueHelp> + <valueHelp> + <format>mld2-listener-report</format> + <description>ICMPv6 type 143: mld2-listener-report</description> + </valueHelp> <constraint> - <regex>(destination-unreachable|packet-too-big|time-exceeded|echo-request|echo-reply|mld-listener-query|mld-listener-report|mld-listener-reduction|nd-router-solicit|nd-router-advert|nd-neighbor-solicit|nd-neighbor-advert|nd-redirect|parameter-problem|router-renumbering)</regex> + <regex>(destination-unreachable|packet-too-big|time-exceeded|echo-request|echo-reply|mld-listener-query|mld-listener-report|mld-listener-reduction|nd-router-solicit|nd-router-advert|nd-neighbor-solicit|nd-neighbor-advert|nd-redirect|parameter-problem|router-renumbering|ind-neighbor-solicit|ind-neighbor-advert|mld2-listener-report)</regex> </constraint> </properties> </leafNode> diff --git a/interface-definitions/include/firewall/match-interface.xml.i b/interface-definitions/include/firewall/match-interface.xml.i new file mode 100644 index 000000000..675a87574 --- /dev/null +++ b/interface-definitions/include/firewall/match-interface.xml.i @@ -0,0 +1,18 @@ +<!-- include start from firewall/match-interface.xml.i --> +<leafNode name="interface-name"> + <properties> + <help>Match interface</help> + <completionHelp> + <script>${vyos_completion_dir}/list_interfaces.py</script> + </completionHelp> + </properties> +</leafNode> +<leafNode name="interface-group"> + <properties> + <help>Match interface-group</help> + <completionHelp> + <path>firewall group interface-group</path> + </completionHelp> + </properties> +</leafNode> +<!-- include end -->
\ No newline at end of file diff --git a/interface-definitions/include/generic-interface-broadcast.xml.i b/interface-definitions/include/generic-interface-broadcast.xml.i index 6f76dde1a..af35a888b 100644 --- a/interface-definitions/include/generic-interface-broadcast.xml.i +++ b/interface-definitions/include/generic-interface-broadcast.xml.i @@ -10,7 +10,7 @@ <description>Interface name</description> </valueHelp> <constraint> - <validator name="interface-name"/> + #include <include/constraint/interface-name.xml.in> </constraint> </properties> </leafNode> diff --git a/interface-definitions/include/generic-interface-multi-broadcast.xml.i b/interface-definitions/include/generic-interface-multi-broadcast.xml.i index 00638f3b7..1ae38fb43 100644 --- a/interface-definitions/include/generic-interface-multi-broadcast.xml.i +++ b/interface-definitions/include/generic-interface-multi-broadcast.xml.i @@ -10,7 +10,7 @@ <description>Interface name</description> </valueHelp> <constraint> - <validator name="interface-name"/> + #include <include/constraint/interface-name.xml.in> </constraint> <multi/> </properties> diff --git a/interface-definitions/include/generic-interface-multi.xml.i b/interface-definitions/include/generic-interface-multi.xml.i index 65aae28ae..16916ff54 100644 --- a/interface-definitions/include/generic-interface-multi.xml.i +++ b/interface-definitions/include/generic-interface-multi.xml.i @@ -10,7 +10,7 @@ <description>Interface name</description> </valueHelp> <constraint> - <validator name="interface-name"/> + #include <include/constraint/interface-name.xml.in> </constraint> <multi/> </properties> diff --git a/interface-definitions/include/generic-interface.xml.i b/interface-definitions/include/generic-interface.xml.i index 8b4cf1d65..36ddee417 100644 --- a/interface-definitions/include/generic-interface.xml.i +++ b/interface-definitions/include/generic-interface.xml.i @@ -10,7 +10,7 @@ <description>Interface name</description> </valueHelp> <constraint> - <validator name="interface-name"/> + #include <include/constraint/interface-name.xml.in> </constraint> </properties> </leafNode> diff --git a/interface-definitions/include/interface/no-peer-dns.xml.i b/interface-definitions/include/interface/no-peer-dns.xml.i new file mode 100644 index 000000000..d663f04c1 --- /dev/null +++ b/interface-definitions/include/interface/no-peer-dns.xml.i @@ -0,0 +1,8 @@ +<!-- include start from interface/no-peer-dns.xml.i --> +<leafNode name="no-peer-dns"> + <properties> + <help>Do not use DNS servers provided by the peer</help> + <valueless/> + </properties> +</leafNode> +<!-- include end --> diff --git a/interface-definitions/include/interface/redirect.xml.i b/interface-definitions/include/interface/redirect.xml.i index 3be9ee16b..8df8957ac 100644 --- a/interface-definitions/include/interface/redirect.xml.i +++ b/interface-definitions/include/interface/redirect.xml.i @@ -10,7 +10,7 @@ <description>Interface name</description> </valueHelp> <constraint> - <validator name="interface-name"/> + #include <include/constraint/interface-name.xml.in> </constraint> </properties> </leafNode> diff --git a/interface-definitions/include/listen-address-single.xml.i b/interface-definitions/include/listen-address-single.xml.i index b5841cabb..30293b338 100644 --- a/interface-definitions/include/listen-address-single.xml.i +++ b/interface-definitions/include/listen-address-single.xml.i @@ -1,3 +1,4 @@ +<!-- include start from listen-address-single.xml.i --> <leafNode name="listen-address"> <properties> <help>Local IP addresses to listen on</help> diff --git a/interface-definitions/include/ospf/protocol-common-config.xml.i b/interface-definitions/include/ospf/protocol-common-config.xml.i index 0615063af..06609c10e 100644 --- a/interface-definitions/include/ospf/protocol-common-config.xml.i +++ b/interface-definitions/include/ospf/protocol-common-config.xml.i @@ -358,7 +358,7 @@ <description>Interface name</description> </valueHelp> <constraint> - <validator name="interface-name"/> + #include <include/constraint/interface-name.xml.in> </constraint> </properties> <children> diff --git a/interface-definitions/include/ospfv3/protocol-common-config.xml.i b/interface-definitions/include/ospfv3/protocol-common-config.xml.i index 630534eea..c0aab912d 100644 --- a/interface-definitions/include/ospfv3/protocol-common-config.xml.i +++ b/interface-definitions/include/ospfv3/protocol-common-config.xml.i @@ -118,7 +118,7 @@ <description>Interface used for routing information exchange</description> </valueHelp> <constraint> - <validator name="interface-name"/> + #include <include/constraint/interface-name.xml.in> </constraint> </properties> <children> diff --git a/interface-definitions/include/rip/interface.xml.i b/interface-definitions/include/rip/interface.xml.i index baeceac1c..e0792cdc1 100644 --- a/interface-definitions/include/rip/interface.xml.i +++ b/interface-definitions/include/rip/interface.xml.i @@ -10,7 +10,7 @@ <description>Interface name</description> </valueHelp> <constraint> - <validator name="interface-name"/> + #include <include/constraint/interface-name.xml.in> </constraint> </properties> <children> diff --git a/interface-definitions/include/routing-passive-interface.xml.i b/interface-definitions/include/routing-passive-interface.xml.i index 095b683de..fe229aebe 100644 --- a/interface-definitions/include/routing-passive-interface.xml.i +++ b/interface-definitions/include/routing-passive-interface.xml.i @@ -16,7 +16,7 @@ </valueHelp> <constraint> <regex>(default)</regex> - <validator name="interface-name"/> + #include <include/constraint/interface-name.xml.in> </constraint> <multi/> </properties> diff --git a/interface-definitions/include/server-ipv4-fqdn.xml.i b/interface-definitions/include/server-ipv4-fqdn.xml.i new file mode 100644 index 000000000..7bab9812c --- /dev/null +++ b/interface-definitions/include/server-ipv4-fqdn.xml.i @@ -0,0 +1,15 @@ +<!-- include start from server-ipv4-fqdn.xml.i --> +<leafNode name="server"> + <properties> + <help>Remote server to connect to</help> + <valueHelp> + <format>ipv4</format> + <description>Server IPv4 address</description> + </valueHelp> + <valueHelp> + <format>hostname</format> + <description>Server hostname/FQDN</description> + </valueHelp> + </properties> +</leafNode> +<!-- include end --> diff --git a/interface-definitions/include/source-interface.xml.i b/interface-definitions/include/source-interface.xml.i index a9c2a0f9d..4c1fddb57 100644 --- a/interface-definitions/include/source-interface.xml.i +++ b/interface-definitions/include/source-interface.xml.i @@ -10,7 +10,7 @@ <script>${vyos_completion_dir}/list_interfaces.py</script> </completionHelp> <constraint> - <validator name="interface-name"/> + #include <include/constraint/interface-name.xml.in> </constraint> </properties> </leafNode> diff --git a/interface-definitions/include/static/static-route-interface.xml.i b/interface-definitions/include/static/static-route-interface.xml.i index ed4f455e5..cc7a92612 100644 --- a/interface-definitions/include/static/static-route-interface.xml.i +++ b/interface-definitions/include/static/static-route-interface.xml.i @@ -10,7 +10,7 @@ <description>Gateway interface name</description> </valueHelp> <constraint> - <validator name="interface-name"/> + #include <include/constraint/interface-name.xml.in> </constraint> </properties> </leafNode> diff --git a/interface-definitions/include/static/static-route.xml.i b/interface-definitions/include/static/static-route.xml.i index 04ee999c7..aeb2044c9 100644 --- a/interface-definitions/include/static/static-route.xml.i +++ b/interface-definitions/include/static/static-route.xml.i @@ -26,7 +26,7 @@ <description>Gateway interface name</description> </valueHelp> <constraint> - <validator name="interface-name"/> + #include <include/constraint/interface-name.xml.in> </constraint> </properties> <children> diff --git a/interface-definitions/include/static/static-route6.xml.i b/interface-definitions/include/static/static-route6.xml.i index 6131ac7fe..d5e7a25bc 100644 --- a/interface-definitions/include/static/static-route6.xml.i +++ b/interface-definitions/include/static/static-route6.xml.i @@ -25,7 +25,7 @@ <description>Gateway interface name</description> </valueHelp> <constraint> - <validator name="interface-name"/> + #include <include/constraint/interface-name.xml.in> </constraint> </properties> <children> diff --git a/interface-definitions/include/version/firewall-version.xml.i b/interface-definitions/include/version/firewall-version.xml.i index 065925319..bc04f8d51 100644 --- a/interface-definitions/include/version/firewall-version.xml.i +++ b/interface-definitions/include/version/firewall-version.xml.i @@ -1,3 +1,3 @@ <!-- include start from include/version/firewall-version.xml.i --> -<syntaxVersion component='firewall' version='8'></syntaxVersion> +<syntaxVersion component='firewall' version='9'></syntaxVersion> <!-- include end --> diff --git a/interface-definitions/interfaces-bonding.xml.in b/interface-definitions/interfaces-bonding.xml.in index 96e0e5d89..a8a558348 100644 --- a/interface-definitions/interfaces-bonding.xml.in +++ b/interface-definitions/interfaces-bonding.xml.in @@ -199,7 +199,7 @@ <description>Interface name</description> </valueHelp> <constraint> - <validator name="interface-name"/> + #include <include/constraint/interface-name.xml.in> </constraint> <multi/> </properties> @@ -218,7 +218,7 @@ <description>Interface name</description> </valueHelp> <constraint> - <validator name="interface-name"/> + #include <include/constraint/interface-name.xml.in> </constraint> </properties> </leafNode> diff --git a/interface-definitions/interfaces-pppoe.xml.in b/interface-definitions/interfaces-pppoe.xml.in index 719060fa9..35c4889ea 100644 --- a/interface-definitions/interfaces-pppoe.xml.in +++ b/interface-definitions/interfaces-pppoe.xml.in @@ -82,12 +82,7 @@ <leafNode name="mtu"> <defaultValue>1492</defaultValue> </leafNode> - <leafNode name="no-peer-dns"> - <properties> - <help>Do not use DNS servers provided by the peer</help> - <valueless/> - </properties> - </leafNode> + #include <include/interface/no-peer-dns.xml.i> <leafNode name="remote-address"> <properties> <help>IPv4 address of remote end of the PPPoE link</help> diff --git a/interface-definitions/interfaces-sstpc.xml.in b/interface-definitions/interfaces-sstpc.xml.in new file mode 100644 index 000000000..30b55a9fa --- /dev/null +++ b/interface-definitions/interfaces-sstpc.xml.in @@ -0,0 +1,47 @@ +<?xml version="1.0"?> +<interfaceDefinition> + <node name="interfaces"> + <children> + <tagNode name="sstpc" owner="${vyos_conf_scripts_dir}/interfaces-sstpc.py"> + <properties> + <help>Secure Socket Tunneling Protocol (SSTP) client Interface</help> + <priority>460</priority> + <constraint> + <regex>sstpc[0-9]+</regex> + </constraint> + <constraintErrorMessage>Secure Socket Tunneling Protocol interface must be named sstpcN</constraintErrorMessage> + <valueHelp> + <format>sstpcN</format> + <description>Secure Socket Tunneling Protocol interface name</description> + </valueHelp> + </properties> + <children> + #include <include/interface/description.xml.i> + #include <include/interface/disable.xml.i> + #include <include/interface/authentication.xml.i> + #include <include/interface/no-default-route.xml.i> + #include <include/interface/default-route-distance.xml.i> + #include <include/interface/no-peer-dns.xml.i> + #include <include/interface/mtu-68-1500.xml.i> + <leafNode name="mtu"> + <defaultValue>1452</defaultValue> + </leafNode> + #include <include/server-ipv4-fqdn.xml.i> + #include <include/port-number.xml.i> + <leafNode name="port"> + <defaultValue>443</defaultValue> + </leafNode> + <node name="ssl"> + <properties> + <help>Secure Sockets Layer (SSL) configuration</help> + </properties> + <children> + #include <include/pki/ca-certificate.xml.i> + </children> + </node> + #include <include/interface/vrf.xml.i> + </children> + </tagNode> + </children> + </node> +</interfaceDefinition> diff --git a/interface-definitions/interfaces-virtual-ethernet.xml.in b/interface-definitions/interfaces-virtual-ethernet.xml.in new file mode 100644 index 000000000..8059ec33b --- /dev/null +++ b/interface-definitions/interfaces-virtual-ethernet.xml.in @@ -0,0 +1,45 @@ +<?xml version="1.0"?> +<interfaceDefinition> + <node name="interfaces"> + <children> + <tagNode name="virtual-ethernet" owner="${vyos_conf_scripts_dir}/interfaces-virtual-ethernet.py"> + <properties> + <help>Virtual Ethernet (veth) Interface</help> + <priority>300</priority> + <constraint> + <regex>veth[0-9]+</regex> + </constraint> + <constraintErrorMessage>Virutal Ethernet interface must be named vethN</constraintErrorMessage> + <valueHelp> + <format>vethN</format> + <description>Virtual Ethernet interface name</description> + </valueHelp> + </properties> + <children> + #include <include/interface/address-ipv4-ipv6-dhcp.xml.i> + #include <include/interface/description.xml.i> + #include <include/interface/dhcp-options.xml.i> + #include <include/interface/dhcpv6-options.xml.i> + #include <include/interface/disable.xml.i> + #include <include/interface/vrf.xml.i> + <leafNode name="peer-name"> + <properties> + <help>Virtual ethernet peer interface name</help> + <completionHelp> + <path>interfaces virtual-ethernet</path> + </completionHelp> + <valueHelp> + <format>txt</format> + <description>Name of peer interface</description> + </valueHelp> + <constraint> + <regex>veth[0-9]+</regex> + </constraint> + <constraintErrorMessage>Virutal Ethernet interface must be named vethN</constraintErrorMessage> + </properties> + </leafNode> + </children> + </tagNode> + </children> + </node> +</interfaceDefinition> diff --git a/interface-definitions/protocols-failover.xml.in b/interface-definitions/protocols-failover.xml.in new file mode 100644 index 000000000..900c76eab --- /dev/null +++ b/interface-definitions/protocols-failover.xml.in @@ -0,0 +1,114 @@ +<?xml version="1.0"?> +<interfaceDefinition> + <node name="protocols"> + <children> + <node name="failover" owner="${vyos_conf_scripts_dir}/protocols_failover.py"> + <properties> + <help>Failover Routing</help> + <priority>490</priority> + </properties> + <children> + <tagNode name="route"> + <properties> + <help>Failover IPv4 route</help> + <valueHelp> + <format>ipv4net</format> + <description>IPv4 failover route</description> + </valueHelp> + <constraint> + <validator name="ipv4-prefix"/> + </constraint> + </properties> + <children> + <tagNode name="next-hop"> + <properties> + <help>Next-hop IPv4 router address</help> + <valueHelp> + <format>ipv4</format> + <description>Next-hop router address</description> + </valueHelp> + <constraint> + <validator name="ipv4-address"/> + </constraint> + </properties> + <children> + <node name="check"> + <properties> + <help>Check target options</help> + </properties> + <children> + #include <include/port-number.xml.i> + <leafNode name="target"> + <properties> + <help>Check target address</help> + <valueHelp> + <format>ipv4</format> + <description>Address to check</description> + </valueHelp> + <constraint> + <validator name="ipv4-address"/> + </constraint> + </properties> + </leafNode> + <leafNode name="timeout"> + <properties> + <help>Timeout between checks</help> + <valueHelp> + <format>u32:1-300</format> + <description>Timeout in seconds between checks</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 1-255"/> + </constraint> + </properties> + <defaultValue>10</defaultValue> + </leafNode> + <leafNode name="type"> + <properties> + <help>Check type</help> + <completionHelp> + <list>arp icmp tcp</list> + </completionHelp> + <valueHelp> + <format>arp</format> + <description>Check target by ARP</description> + </valueHelp> + <valueHelp> + <format>icmp</format> + <description>Check target by ICMP</description> + </valueHelp> + <valueHelp> + <format>tcp</format> + <description>Check target by TCP</description> + </valueHelp> + <constraint> + <regex>(arp|icmp|tcp)</regex> + </constraint> + </properties> + <defaultValue>icmp</defaultValue> + </leafNode> + </children> + </node> + #include <include/static/static-route-interface.xml.i> + <leafNode name="metric"> + <properties> + <help>Route metric for this gateway</help> + <valueHelp> + <format>u32:1-255</format> + <description>Route metric</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 1-255"/> + </constraint> + </properties> + <defaultValue>1</defaultValue> + </leafNode> + </children> + </tagNode> + </children> + </tagNode> + </children> + </node> + </children> + </node> +</interfaceDefinition> diff --git a/interface-definitions/protocols-rip.xml.in b/interface-definitions/protocols-rip.xml.in index 2195b0316..33aae5015 100644 --- a/interface-definitions/protocols-rip.xml.in +++ b/interface-definitions/protocols-rip.xml.in @@ -39,7 +39,7 @@ <script>${vyos_completion_dir}/list_interfaces.py</script> </completionHelp> <constraint> - <validator name="interface-name"/> + #include <include/constraint/interface-name.xml.in> </constraint> </properties> <children> diff --git a/interface-definitions/protocols-ripng.xml.in b/interface-definitions/protocols-ripng.xml.in index d7e4b2514..cd35dbf53 100644 --- a/interface-definitions/protocols-ripng.xml.in +++ b/interface-definitions/protocols-ripng.xml.in @@ -40,7 +40,7 @@ <script>${vyos_completion_dir}/list_interfaces.py</script> </completionHelp> <constraint> - <validator name="interface-name"/> + #include <include/constraint/interface-name.xml.in> </constraint> </properties> <children> diff --git a/interface-definitions/protocols-rpki.xml.in b/interface-definitions/protocols-rpki.xml.in index 4535d3990..0098cacb6 100644 --- a/interface-definitions/protocols-rpki.xml.in +++ b/interface-definitions/protocols-rpki.xml.in @@ -51,7 +51,7 @@ <properties> <help>RPKI SSH known hosts file</help> <constraint> - <validator name="file-exists"/> + <validator name="file-path"/> </constraint> </properties> </leafNode> @@ -59,7 +59,7 @@ <properties> <help>RPKI SSH private key file</help> <constraint> - <validator name="file-exists"/> + <validator name="file-path"/> </constraint> </properties> </leafNode> @@ -67,7 +67,7 @@ <properties> <help>RPKI SSH public key file path</help> <constraint> - <validator name="file-exists"/> + <validator name="file-path"/> </constraint> </properties> </leafNode> diff --git a/interface-definitions/protocols-static-arp.xml.in b/interface-definitions/protocols-static-arp.xml.in index 8b1b3b5e1..52caf435a 100644 --- a/interface-definitions/protocols-static-arp.xml.in +++ b/interface-definitions/protocols-static-arp.xml.in @@ -20,7 +20,7 @@ <description>Interface name</description> </valueHelp> <constraint> - <validator name="interface-name"/> + #include <include/constraint/interface-name.xml.in> </constraint> </properties> <children> diff --git a/interface-definitions/qos.xml.in b/interface-definitions/qos.xml.in index e2dbcbeef..dc807781e 100644 --- a/interface-definitions/qos.xml.in +++ b/interface-definitions/qos.xml.in @@ -16,7 +16,7 @@ <description>Interface name</description> </valueHelp> <constraint> - <validator name="interface-name"/> + #include <include/constraint/interface-name.xml.in> </constraint> </properties> <children> diff --git a/interface-definitions/service-router-advert.xml.in b/interface-definitions/service-router-advert.xml.in index 87ec512d6..8b7364a8c 100644 --- a/interface-definitions/service-router-advert.xml.in +++ b/interface-definitions/service-router-advert.xml.in @@ -305,6 +305,19 @@ </leafNode> </children> </tagNode> + <leafNode name="source-address"> + <properties> + <help>Use IPv6 address as source address. Useful with VRRP.</help> + <valueHelp> + <format>ipv6</format> + <description>IPv6 address to be advertized (must be configured on interface)</description> + </valueHelp> + <constraint> + <validator name="ipv6-address"/> + </constraint> + <multi/> + </properties> + </leafNode> <leafNode name="reachable-time"> <properties> <help>Time, in milliseconds, that a node assumes a neighbor is reachable after having received a reachability confirmation</help> diff --git a/interface-definitions/service-upnp.xml.in b/interface-definitions/service-upnp.xml.in index ec23d87df..79d8ae42e 100644 --- a/interface-definitions/service-upnp.xml.in +++ b/interface-definitions/service-upnp.xml.in @@ -24,7 +24,7 @@ <script>${vyos_completion_dir}/list_interfaces.py</script> </completionHelp> <constraint> - <validator name="interface-name" /> + #include <include/constraint/interface-name.xml.in> </constraint> </properties> </leafNode> @@ -119,7 +119,7 @@ </valueHelp> <multi/> <constraint> - <validator name="interface-name" /> + #include <include/constraint/interface-name.xml.in> <validator name="ipv4-address"/> <validator name="ipv4-prefix"/> <validator name="ipv6-address"/> diff --git a/interface-definitions/vpn-l2tp.xml.in b/interface-definitions/vpn-l2tp.xml.in index cb5900e0d..06ca4ece5 100644 --- a/interface-definitions/vpn-l2tp.xml.in +++ b/interface-definitions/vpn-l2tp.xml.in @@ -230,6 +230,7 @@ <properties> <help>Port for Dynamic Authorization Extension server (DM/CoA)</help> </properties> + <defaultValue>1700</defaultValue> </leafNode> <leafNode name="secret"> <properties> diff --git a/op-mode-definitions/connect.xml.in b/op-mode-definitions/connect.xml.in index d0c93195c..116cd6231 100644 --- a/op-mode-definitions/connect.xml.in +++ b/op-mode-definitions/connect.xml.in @@ -20,6 +20,7 @@ <help>Bring up a connection-oriented network interface</help> <completionHelp> <path>interfaces pppoe</path> + <path>interfaces sstpc</path> <path>interfaces wwan</path> </completionHelp> </properties> diff --git a/op-mode-definitions/container.xml.in b/op-mode-definitions/container.xml.in index 97a087ce2..786bd66d3 100644 --- a/op-mode-definitions/container.xml.in +++ b/op-mode-definitions/container.xml.in @@ -69,7 +69,7 @@ <list><filename></list> </completionHelp> </properties> - <command>sudo podman build --layers --force-rm --tag "$4" $6</command> + <command>sudo podman build --net host --layers --force-rm --tag "$4" $6</command> </tagNode> </children> </tagNode> diff --git a/op-mode-definitions/disconnect.xml.in b/op-mode-definitions/disconnect.xml.in index 4415c0ed2..843998c4f 100644 --- a/op-mode-definitions/disconnect.xml.in +++ b/op-mode-definitions/disconnect.xml.in @@ -10,6 +10,7 @@ <help>Take down a connection-oriented network interface</help> <completionHelp> <path>interfaces pppoe</path> + <path>interfaces sstpc</path> <path>interfaces wwan</path> </completionHelp> </properties> diff --git a/op-mode-definitions/generate-ipsec-debug-archive.xml.in b/op-mode-definitions/generate-ipsec-debug-archive.xml.in index f268d5ae5..a9ce113d1 100644 --- a/op-mode-definitions/generate-ipsec-debug-archive.xml.in +++ b/op-mode-definitions/generate-ipsec-debug-archive.xml.in @@ -8,7 +8,7 @@ <properties> <help>Generate IPSec debug-archive</help> </properties> - <command>${vyos_op_scripts_dir}/generate_ipsec_debug_archive.sh</command> + <command>sudo ${vyos_op_scripts_dir}/generate_ipsec_debug_archive.py</command> </node> </children> </node> diff --git a/op-mode-definitions/generate-system-login-user.xml.in b/op-mode-definitions/generate-system-login-user.xml.in new file mode 100755 index 000000000..d0519b6bd --- /dev/null +++ b/op-mode-definitions/generate-system-login-user.xml.in @@ -0,0 +1,90 @@ +<?xml version="1.0"?>
+<interfaceDefinition>
+ <node name="generate">
+ <children>
+ <node name="system">
+ <properties>
+ <help>Generate system related parameters</help>
+ </properties>
+ <children>
+ <node name="login">
+ <properties>
+ <help>Generate system login related parameters</help>
+ </properties>
+ <children>
+ <tagNode name="username">
+ <properties>
+ <help>Username used for authentication</help>
+ <completionHelp>
+ <list><username></list>
+ </completionHelp>
+ </properties>
+ <children>
+ <node name="otp-key">
+ <properties>
+ <help>Generate OpenConnect OTP token</help>
+ </properties>
+ <children>
+ <node name="hotp-time">
+ <properties>
+ <help>HOTP time-based token</help>
+ </properties>
+ <command>sudo ${vyos_op_scripts_dir}/generate_system_login_user.py --username "$5"</command>
+ <children>
+ <tagNode name="rate-limit">
+ <properties>
+ <help>Duration of single time interval</help>
+ </properties>
+ <command>sudo ${vyos_op_scripts_dir}/generate_system_login_user.py --username "$5" --rate_limit "$9"</command>
+ <children>
+ <tagNode name="rate-time">
+ <properties>
+ <help>The number of digits in the one-time password</help>
+ </properties>
+ <command>sudo ${vyos_op_scripts_dir}/generate_system_login_user.py --username "$5" --rate_limit "$9" --rate_time "${11}" </command>
+ <children>
+ <tagNode name="window-size">
+ <properties>
+ <help>The number of digits in the one-time password</help>
+ </properties>
+ <command>sudo ${vyos_op_scripts_dir}/generate_system_login_user.py --username "$5" --rate_limit "$9" --rate_time "${11}" --window_size "${13}"</command>
+ </tagNode>
+ </children>
+ </tagNode>
+ </children>
+ </tagNode>
+ <tagNode name="window-size">
+ <properties>
+ <help>The number of digits in the one-time password</help>
+ </properties>
+ <command>sudo ${vyos_op_scripts_dir}/generate_system_login_user.py --username "$5" --window_size "${9}"</command>
+ <children>
+ <tagNode name="rate-limit">
+ <properties>
+ <help>Duration of single time interval</help>
+ </properties>
+ <command>sudo ${vyos_op_scripts_dir}/generate_system_login_user.py --username "$5" --rate_limit "${11}" --window_size "${9}"</command>
+ <children>
+ <tagNode name="rate-time">
+ <properties>
+ <help>Duration of single time interval</help>
+ </properties>
+ <command>sudo ${vyos_op_scripts_dir}/generate_system_login_user.py --username "$5" --rate_limit "${11}" --rate_time "${13}" --window_size "${9}"</command>
+ </tagNode>
+ </children>
+ </tagNode>
+ </children>
+ </tagNode>
+ </children>
+ </node>
+ </children>
+ </node>
+ </children>
+ </tagNode>
+ </children>
+ </node>
+ </children>
+ </node>
+ </children>
+ </node>
+</interfaceDefinition>
diff --git a/op-mode-definitions/monitor-log.xml.in b/op-mode-definitions/monitor-log.xml.in index dccdfaf9a..1b1f53dc2 100644 --- a/op-mode-definitions/monitor-log.xml.in +++ b/op-mode-definitions/monitor-log.xml.in @@ -224,6 +224,23 @@ </properties> <command>journalctl --no-hostname --boot --follow --unit ssh.service</command> </leafNode> + <node name="sstpc"> + <properties> + <help>Monitor last lines of SSTP client log</help> + </properties> + <command>journalctl --no-hostname --boot --follow --unit "ppp@sstpc*.service"</command> + <children> + <tagNode name="interface"> + <properties> + <help>Monitor last lines of SSTP client log for specific interface</help> + <completionHelp> + <script>${vyos_completion_dir}/list_interfaces.py -t sstpc</script> + </completionHelp> + </properties> + <command>journalctl --no-hostname --boot --follow --unit "ppp@$5.service"</command> + </tagNode> + </children> + </node> <node name="vpn"> <properties> <help>Show log for Virtual Private Network (VPN)</help> diff --git a/op-mode-definitions/openvpn.xml.in b/op-mode-definitions/openvpn.xml.in index 301688271..aec09fa48 100644 --- a/op-mode-definitions/openvpn.xml.in +++ b/op-mode-definitions/openvpn.xml.in @@ -23,7 +23,7 @@ <script>sudo ${vyos_completion_dir}/list_interfaces.py --type openvpn</script> </completionHelp> </properties> - <command>sudo ${vyos_op_scripts_dir}/reset_openvpn.py $4</command> + <command>sudo ${vyos_op_scripts_dir}/openvpn.py reset --interface $4</command> </tagNode> </children> </node> @@ -109,19 +109,19 @@ <properties> <help>Show tunnel status for OpenVPN client interfaces</help> </properties> - <command>sudo ${vyos_op_scripts_dir}/show_openvpn.py --mode=client</command> + <command>sudo ${vyos_op_scripts_dir}/openvpn.py show --mode client</command> </leafNode> <leafNode name="server"> <properties> <help>Show tunnel status for OpenVPN server interfaces</help> </properties> - <command>sudo ${vyos_op_scripts_dir}/show_openvpn.py --mode=server</command> + <command>sudo ${vyos_op_scripts_dir}/openvpn.py show --mode server</command> </leafNode> <leafNode name="site-to-site"> <properties> <help>Show tunnel status for OpenVPN site-to-site interfaces</help> </properties> - <command>sudo ${vyos_op_scripts_dir}/show_openvpn.py --mode=site-to-site</command> + <command>sudo ${vyos_op_scripts_dir}/openvpn.py show --mode site-to-site</command> </leafNode> </children> </node> diff --git a/op-mode-definitions/show-acceleration.xml.in b/op-mode-definitions/show-acceleration.xml.in index d0dcea2d6..6fd3babf5 100644 --- a/op-mode-definitions/show-acceleration.xml.in +++ b/op-mode-definitions/show-acceleration.xml.in @@ -29,13 +29,13 @@ <properties> <help>Intel QAT flows</help> </properties> - <command>${vyos_op_scripts_dir}/show_acceleration.py --flow --dev $6</command> + <command>sudo ${vyos_op_scripts_dir}/show_acceleration.py --flow --dev $6</command> </node> <node name="config"> <properties> <help>Intel QAT configuration</help> </properties> - <command>${vyos_op_scripts_dir}/show_acceleration.py --conf --dev $6</command> + <command>sudo ${vyos_op_scripts_dir}/show_acceleration.py --conf --dev $6</command> </node> </children> </tagNode> @@ -43,16 +43,16 @@ <properties> <help>Intel QAT status</help> </properties> - <command>${vyos_op_scripts_dir}/show_acceleration.py --status</command> + <command>sudo ${vyos_op_scripts_dir}/show_acceleration.py --status</command> </node> <node name="interrupts"> <properties> <help>Intel QAT interrupts</help> </properties> - <command>${vyos_op_scripts_dir}/show_acceleration.py --interrupts</command> + <command>sudo ${vyos_op_scripts_dir}/show_acceleration.py --interrupts</command> </node> </children> - <command>${vyos_op_scripts_dir}/show_acceleration.py --hw</command> + <command>sudo ${vyos_op_scripts_dir}/show_acceleration.py --hw</command> </node> </children> </node> diff --git a/op-mode-definitions/show-interfaces-sstpc.xml.in b/op-mode-definitions/show-interfaces-sstpc.xml.in new file mode 100644 index 000000000..e66d3a0ac --- /dev/null +++ b/op-mode-definitions/show-interfaces-sstpc.xml.in @@ -0,0 +1,51 @@ +<?xml version="1.0"?> +<interfaceDefinition> + <node name="show"> + <children> + <node name="interfaces"> + <children> + <tagNode name="sstpc"> + <properties> + <help>Show specified SSTP client interface information</help> + <completionHelp> + <path>interfaces sstpc</path> + </completionHelp> + </properties> + <command>${vyos_op_scripts_dir}/show_interfaces.py --intf="$4"</command> + <children> + <leafNode name="log"> + <properties> + <help>Show specified SSTP client interface log</help> + </properties> + <command>journalctl --no-hostname --boot --follow --unit "ppp@$4".service</command> + </leafNode> + <leafNode name="statistics"> + <properties> + <help>Show specified SSTP client interface statistics</help> + <completionHelp> + <path>interfaces sstpc</path> + </completionHelp> + </properties> + <command>if [ -d "/sys/class/net/$4" ]; then /usr/sbin/pppstats "$4"; fi</command> + </leafNode> + </children> + </tagNode> + <node name="sstpc"> + <properties> + <help>Show SSTP client interface information</help> + </properties> + <command>${vyos_op_scripts_dir}/show_interfaces.py --intf-type=sstpc --action=show-brief</command> + <children> + <leafNode name="detail"> + <properties> + <help>Show detailed SSTP client interface information</help> + </properties> + <command>${vyos_op_scripts_dir}/show_interfaces.py --intf-type=sstpc --action=show</command> + </leafNode> + </children> + </node> + </children> + </node> + </children> + </node> +</interfaceDefinition> diff --git a/op-mode-definitions/show-interfaces-virtual-ethernet.xml.in b/op-mode-definitions/show-interfaces-virtual-ethernet.xml.in new file mode 100644 index 000000000..c70f1e3d1 --- /dev/null +++ b/op-mode-definitions/show-interfaces-virtual-ethernet.xml.in @@ -0,0 +1,42 @@ +<?xml version="1.0"?> +<interfaceDefinition> + <node name="show"> + <children> + <node name="interfaces"> + <children> + <tagNode name="virtual-ethernet"> + <properties> + <help>Show specified virtual-ethernet interface information</help> + <completionHelp> + <path>interfaces virtual-ethernet</path> + </completionHelp> + </properties> + <command>${vyos_op_scripts_dir}/show_interfaces.py --intf="$4"</command> + <children> + <leafNode name="brief"> + <properties> + <help>Show summary of the specified virtual-ethernet interface information</help> + </properties> + <command>${vyos_op_scripts_dir}/show_interfaces.py --intf="$4" --action=show-brief</command> + </leafNode> + </children> + </tagNode> + <node name="virtual-ethernet"> + <properties> + <help>Show virtual-ethernet interface information</help> + </properties> + <command>${vyos_op_scripts_dir}/show_interfaces.py --intf-type=virtual-ethernet --action=show-brief</command> + <children> + <leafNode name="detail"> + <properties> + <help>Show detailed virtual-ethernet interface information</help> + </properties> + <command>${vyos_op_scripts_dir}/show_interfaces.py --intf-type=virtual-ethernet --action=show</command> + </leafNode> + </children> + </node> + </children> + </node> + </children> + </node> +</interfaceDefinition> diff --git a/op-mode-definitions/show-log.xml.in b/op-mode-definitions/show-log.xml.in index 404de1913..64a54015b 100644 --- a/op-mode-definitions/show-log.xml.in +++ b/op-mode-definitions/show-log.xml.in @@ -356,6 +356,23 @@ </properties> <command>journalctl --no-hostname --boot --unit ssh.service</command> </leafNode> + <node name="sstpc"> + <properties> + <help>Show log for SSTP client</help> + </properties> + <command>journalctl --no-hostname --boot --unit "ppp@sstpc*.service"</command> + <children> + <tagNode name="interface"> + <properties> + <help>Show SSTP client log on specific interface</help> + <completionHelp> + <script>${vyos_completion_dir}/list_interfaces.py -t sstpc</script> + </completionHelp> + </properties> + <command>journalctl --no-hostname --boot --unit "ppp@$5.service"</command> + </tagNode> + </children> + </node> <tagNode name="tail"> <properties> <help>Show last n changes to messages</help> diff --git a/op-mode-definitions/show-raid.xml.in b/op-mode-definitions/show-raid.xml.in index 8bf394552..2ae3fad6a 100644 --- a/op-mode-definitions/show-raid.xml.in +++ b/op-mode-definitions/show-raid.xml.in @@ -9,7 +9,7 @@ <script>${vyos_completion_dir}/list_raidset.sh</script> </completionHelp> </properties> - <command>${vyos_op_scripts_dir}/show_raid.sh $3</command> + <command>sudo ${vyos_op_scripts_dir}/show_raid.sh $3</command> </tagNode> </children> </node> diff --git a/op-mode-definitions/vpn-ipsec.xml.in b/op-mode-definitions/vpn-ipsec.xml.in index f1af65fcb..803ce4cc2 100644 --- a/op-mode-definitions/vpn-ipsec.xml.in +++ b/op-mode-definitions/vpn-ipsec.xml.in @@ -137,6 +137,12 @@ <help>Show Internet Protocol Security (IPsec) information</help> </properties> <children> + <node name="connections"> + <properties> + <help>Show VPN connections</help> + </properties> + <command>sudo ${vyos_op_scripts_dir}/ipsec.py show_connections</command> + </node> <node name="policy"> <properties> <help>Show the in-kernel crypto policies</help> diff --git a/python/vyos/configdep.py b/python/vyos/configdep.py index e6b82ca93..d4b2cc78f 100644 --- a/python/vyos/configdep.py +++ b/python/vyos/configdep.py @@ -14,11 +14,21 @@ # along with this library. If not, see <http://www.gnu.org/licenses/>. import os +import json +import typing from inspect import stack from vyos.util import load_as_module +from vyos.defaults import directories +from vyos.configsource import VyOSError +from vyos import ConfigError -dependents = {} +# https://peps.python.org/pep-0484/#forward-references +# for type 'Config' +if typing.TYPE_CHECKING: + from vyos.config import Config + +dependent_func: dict[str, list[typing.Callable]] = {} def canon_name(name: str) -> str: return os.path.splitext(name)[0].replace('-', '_') @@ -30,9 +40,22 @@ def canon_name_of_path(path: str) -> str: def caller_name() -> str: return stack()[-1].filename -def run_config_mode_script(script: str, config): - from vyos.defaults import directories +def read_dependency_dict() -> dict: + path = os.path.join(directories['data'], + 'config-mode-dependencies.json') + with open(path) as f: + d = json.load(f) + return d + +def get_dependency_dict(config: 'Config') -> dict: + if hasattr(config, 'cached_dependency_dict'): + d = getattr(config, 'cached_dependency_dict') + else: + d = read_dependency_dict() + setattr(config, 'cached_dependency_dict', d) + return d +def run_config_mode_script(script: str, config: 'Config'): path = os.path.join(directories['conf_mode'], script) name = canon_name(script) mod = load_as_module(name, path) @@ -46,20 +69,27 @@ def run_config_mode_script(script: str, config): except (VyOSError, ConfigError) as e: raise ConfigError(repr(e)) -def def_closure(script: str, config): +def def_closure(target: str, config: 'Config', + tagnode: typing.Optional[str] = None) -> typing.Callable: + script = target + '.py' def func_impl(): + if tagnode: + os.environ['VYOS_TAGNODE_VALUE'] = tagnode run_config_mode_script(script, config) return func_impl -def set_dependent(target: str, config): +def set_dependents(case: str, config: 'Config', + tagnode: typing.Optional[str] = None): + d = get_dependency_dict(config) k = canon_name_of_path(caller_name()) - l = dependents.setdefault(k, []) - func = def_closure(target, config) - l.append(func) + l = dependent_func.setdefault(k, []) + for target in d[k][case]: + func = def_closure(target, config, tagnode) + l.append(func) def call_dependents(): k = canon_name_of_path(caller_name()) - l = dependents.get(k, []) + l = dependent_func.get(k, []) while l: f = l.pop(0) f() diff --git a/python/vyos/configverify.py b/python/vyos/configverify.py index afa0c5b33..8e0ce701e 100644 --- a/python/vyos/configverify.py +++ b/python/vyos/configverify.py @@ -388,8 +388,10 @@ def verify_accel_ppp_base_service(config, local_users=True): """ # vertify auth settings if local_users and dict_search('authentication.mode', config) == 'local': - if dict_search(f'authentication.local_users', config) == None: - raise ConfigError('Authentication mode local requires local users to be configured!') + if (dict_search(f'authentication.local_users', config) is None or + dict_search(f'authentication.local_users', config) == {}): + raise ConfigError( + 'Authentication mode local requires local users to be configured!') for user in dict_search('authentication.local_users.username', config): user_config = config['authentication']['local_users']['username'][user] diff --git a/python/vyos/firewall.py b/python/vyos/firewall.py index 59ec4948f..429c44802 100644 --- a/python/vyos/firewall.py +++ b/python/vyos/firewall.py @@ -113,12 +113,19 @@ def parse_rule(rule_conf, fw_name, rule_id, ip_name): if side in rule_conf: prefix = side[0] side_conf = rule_conf[side] + address_mask = side_conf.get('address_mask', None) if 'address' in side_conf: suffix = side_conf['address'] - if suffix[0] == '!': - suffix = f'!= {suffix[1:]}' - output.append(f'{ip_name} {prefix}addr {suffix}') + operator = '' + exclude = suffix[0] == '!' + if exclude: + operator = '!= ' + suffix = suffix[1:] + if address_mask: + operator = '!=' if exclude else '==' + operator = f'& {address_mask} {operator} ' + output.append(f'{ip_name} {prefix}addr {operator}{suffix}') if 'fqdn' in side_conf: fqdn = side_conf['fqdn'] @@ -168,9 +175,13 @@ def parse_rule(rule_conf, fw_name, rule_id, ip_name): if 'address_group' in group: group_name = group['address_group'] operator = '' - if group_name[0] == '!': + exclude = group_name[0] == "!" + if exclude: operator = '!=' group_name = group_name[1:] + if address_mask: + operator = '!=' if exclude else '==' + operator = f'& {address_mask} {operator}' output.append(f'{ip_name} {prefix}addr {operator} @A{def_suffix}_{group_name}') # Generate firewall group domain-group elif 'domain_group' in group: @@ -225,12 +236,20 @@ def parse_rule(rule_conf, fw_name, rule_id, ip_name): output.append(f'ip6 hoplimit {operator} {value}') if 'inbound_interface' in rule_conf: - iiface = rule_conf['inbound_interface'] - output.append(f'iifname {iiface}') + if 'interface_name' in rule_conf['inbound_interface']: + iiface = rule_conf['inbound_interface']['interface_name'] + output.append(f'iifname {{{iiface}}}') + else: + iiface = rule_conf['inbound_interface']['interface_group'] + output.append(f'iifname @I_{iiface}') if 'outbound_interface' in rule_conf: - oiface = rule_conf['outbound_interface'] - output.append(f'oifname {oiface}') + if 'interface_name' in rule_conf['outbound_interface']: + oiface = rule_conf['outbound_interface']['interface_name'] + output.append(f'oifname {{{oiface}}}') + else: + oiface = rule_conf['outbound_interface']['interface_group'] + output.append(f'oifname @I_{oiface}') if 'ttl' in rule_conf: operators = {'eq': '==', 'gt': '>', 'lt': '<'} diff --git a/python/vyos/frr.py b/python/vyos/frr.py index 0ffd5cba9..ccb132dd5 100644 --- a/python/vyos/frr.py +++ b/python/vyos/frr.py @@ -477,7 +477,7 @@ class FRRConfig: # for the listed FRR issues above pass if count >= count_max: - raise ConfigurationNotValid(f'Config commit retry counter ({count_max}) exceeded') + raise ConfigurationNotValid(f'Config commit retry counter ({count_max}) exceeded for {daemon} dameon!') # Save configuration to /run/frr/config/frr.conf save_configuration() diff --git a/python/vyos/ifconfig/__init__.py b/python/vyos/ifconfig/__init__.py index a37615c8f..206b2bba1 100644 --- a/python/vyos/ifconfig/__init__.py +++ b/python/vyos/ifconfig/__init__.py @@ -1,4 +1,4 @@ -# Copyright 2019-2021 VyOS maintainers and contributors <maintainers@vyos.io> +# Copyright 2019-2022 VyOS maintainers and contributors <maintainers@vyos.io> # # This library is free software; you can redistribute it and/or # modify it under the terms of the GNU Lesser General Public @@ -36,4 +36,6 @@ from vyos.ifconfig.tunnel import TunnelIf from vyos.ifconfig.wireless import WiFiIf from vyos.ifconfig.l2tpv3 import L2TPv3If from vyos.ifconfig.macsec import MACsecIf +from vyos.ifconfig.veth import VethIf from vyos.ifconfig.wwan import WWANIf +from vyos.ifconfig.sstpc import SSTPCIf diff --git a/python/vyos/ifconfig/sstpc.py b/python/vyos/ifconfig/sstpc.py new file mode 100644 index 000000000..50fc6ee6b --- /dev/null +++ b/python/vyos/ifconfig/sstpc.py @@ -0,0 +1,40 @@ +# Copyright 2022 VyOS maintainers and contributors <maintainers@vyos.io> +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library. If not, see <http://www.gnu.org/licenses/>. + +from vyos.ifconfig.interface import Interface + +@Interface.register +class SSTPCIf(Interface): + iftype = 'sstpc' + definition = { + **Interface.definition, + **{ + 'section': 'sstpc', + 'prefixes': ['sstpc', ], + 'eternal': 'sstpc[0-9]+$', + }, + } + + def _create(self): + # we can not create this interface as it is managed outside + pass + + def _delete(self): + # we can not create this interface as it is managed outside + pass + + def get_mac(self): + """ Get a synthetic MAC address. """ + return self.get_mac_synthetic() diff --git a/python/vyos/ifconfig/veth.py b/python/vyos/ifconfig/veth.py new file mode 100644 index 000000000..aafbf226a --- /dev/null +++ b/python/vyos/ifconfig/veth.py @@ -0,0 +1,54 @@ +# Copyright 2022 VyOS maintainers and contributors <maintainers@vyos.io> +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library. If not, see <http://www.gnu.org/licenses/>. + +from vyos.ifconfig.interface import Interface + + +@Interface.register +class VethIf(Interface): + """ + Abstraction of a Linux veth interface + """ + iftype = 'veth' + definition = { + **Interface.definition, + **{ + 'section': 'virtual-ethernet', + 'prefixes': ['veth', ], + 'bridgeable': True, + }, + } + + def _create(self): + """ + Create veth interface in OS kernel. Interface is administrative + down by default. + """ + # check before create, as we have 2 veth interfaces in our CLI + # interface virtual-ethernet veth0 peer-name 'veth1' + # interface virtual-ethernet veth1 peer-name 'veth0' + # + # but iproute2 creates the pair with one command: + # ip link add vet0 type veth peer name veth1 + if self.exists(self.config['peer_name']): + return + + # create virtual-ethernet interface + cmd = 'ip link add {ifname} type {type}'.format(**self.config) + cmd += f' peer name {self.config["peer_name"]}' + self._cmd(cmd) + + # interface is always A/D down. It needs to be enabled explicitly + self.set_admin_state('down') diff --git a/python/vyos/opmode.py b/python/vyos/opmode.py index 9dba8d30f..5ff768859 100644 --- a/python/vyos/opmode.py +++ b/python/vyos/opmode.py @@ -51,6 +51,12 @@ class IncorrectValue(Error): """ pass +class CommitInProgress(Error): + """ Requested operation is valid, but not possible at the time due + to a commit being in progress. + """ + pass + class InternalError(Error): """ Any situation when VyOS detects that it could not perform an operation correctly due to logic errors in its own code diff --git a/python/vyos/util.py b/python/vyos/util.py index 9ebe69b6c..6a828c0ac 100644 --- a/python/vyos/util.py +++ b/python/vyos/util.py @@ -539,13 +539,16 @@ def seconds_to_human(s, separator=""): return result -def bytes_to_human(bytes, initial_exponent=0): +def bytes_to_human(bytes, initial_exponent=0, precision=2): """ Converts a value in bytes to a human-readable size string like 640 KB The initial_exponent parameter is the exponent of 2, e.g. 10 (1024) for kilobytes, 20 (1024 * 1024) for megabytes. """ + if bytes == 0: + return "0 B" + from math import log2 bytes = bytes * (2**initial_exponent) @@ -571,7 +574,7 @@ def bytes_to_human(bytes, initial_exponent=0): # Add a new case when the first machine with petabyte RAM # hits the market. - size_string = "{0:.2f} {1}".format(value, suffix) + size_string = "{0:.{1}f} {2}".format(value, precision, suffix) return size_string def human_to_bytes(value): diff --git a/scripts/check-pr-title-and-commit-messages.py b/scripts/check-pr-title-and-commit-messages.py index 3317745d6..9801b7456 100755 --- a/scripts/check-pr-title-and-commit-messages.py +++ b/scripts/check-pr-title-and-commit-messages.py @@ -7,7 +7,7 @@ import requests from pprint import pprint # Use the same regex for PR title and commit messages for now -title_regex = r'^(([a-zA-Z.]+:\s)?)T\d+:\s+[^\s]+.*' +title_regex = r'^(([a-zA-Z\-_.]+:\s)?)T\d+:\s+[^\s]+.*' commit_regex = title_regex def check_pr_title(title): diff --git a/smoketest/scripts/cli/test_dependency_graph.py b/smoketest/scripts/cli/test_dependency_graph.py new file mode 100755 index 000000000..45a40acc4 --- /dev/null +++ b/smoketest/scripts/cli/test_dependency_graph.py @@ -0,0 +1,54 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2022 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +import json +import unittest +from graphlib import TopologicalSorter, CycleError + +DEP_FILE = '/usr/share/vyos/config-mode-dependencies.json' + +def graph_from_dict(d): + g = {} + for k in list(d): + g[k] = set() + # add the dependencies for every sub-case; should there be cases + # that are mutally exclusive in the future, the graphs will be + # distinguished + for el in list(d[k]): + g[k] |= set(d[k][el]) + return g + +class TestDependencyGraph(unittest.TestCase): + def setUp(self): + with open(DEP_FILE) as f: + dd = json.load(f) + self.dependency_graph = graph_from_dict(dd) + + def test_cycles(self): + ts = TopologicalSorter(self.dependency_graph) + out = None + try: + # get node iterator + order = ts.static_order() + # try iteration + _ = [*order] + except CycleError as e: + out = e.args + + self.assertIsNone(out) + +if __name__ == '__main__': + unittest.main(verbosity=2) diff --git a/smoketest/scripts/cli/test_firewall.py b/smoketest/scripts/cli/test_firewall.py index e172e086d..9b28eb81b 100755 --- a/smoketest/scripts/cli/test_firewall.py +++ b/smoketest/scripts/cli/test_firewall.py @@ -124,6 +124,8 @@ class TestFirewall(VyOSUnitTestSHIM.TestCase): self.cli_set(['firewall', 'group', 'port-group', 'smoketest_port', 'port', '123']) self.cli_set(['firewall', 'group', 'domain-group', 'smoketest_domain', 'address', 'example.com']) self.cli_set(['firewall', 'group', 'domain-group', 'smoketest_domain', 'address', 'example.org']) + self.cli_set(['firewall', 'group', 'interface-group', 'smoketest_interface', 'interface', 'eth0']) + self.cli_set(['firewall', 'group', 'interface-group', 'smoketest_interface', 'interface', 'vtun0']) self.cli_set(['firewall', 'name', 'smoketest', 'rule', '1', 'action', 'accept']) self.cli_set(['firewall', 'name', 'smoketest', 'rule', '1', 'source', 'group', 'network-group', 'smoketest_network']) @@ -134,6 +136,8 @@ class TestFirewall(VyOSUnitTestSHIM.TestCase): self.cli_set(['firewall', 'name', 'smoketest', 'rule', '2', 'source', 'group', 'mac-group', 'smoketest_mac']) self.cli_set(['firewall', 'name', 'smoketest', 'rule', '3', 'action', 'accept']) self.cli_set(['firewall', 'name', 'smoketest', 'rule', '3', 'source', 'group', 'domain-group', 'smoketest_domain']) + self.cli_set(['firewall', 'name', 'smoketest', 'rule', '4', 'action', 'accept']) + self.cli_set(['firewall', 'name', 'smoketest', 'rule', '4', 'outbound-interface', 'interface-group', 'smoketest_interface']) self.cli_set(['firewall', 'interface', 'eth0', 'in', 'name', 'smoketest']) @@ -151,7 +155,8 @@ class TestFirewall(VyOSUnitTestSHIM.TestCase): ['set D_smoketest_domain'], ['elements = { 192.0.2.5, 192.0.2.8,'], ['192.0.2.10, 192.0.2.11 }'], - ['ip saddr @D_smoketest_domain', 'return'] + ['ip saddr @D_smoketest_domain', 'return'], + ['oifname @I_smoketest_interface', 'return'] ] self.verify_nftables(nftables_search, 'ip vyos_filter') @@ -225,10 +230,10 @@ class TestFirewall(VyOSUnitTestSHIM.TestCase): self.cli_set(['firewall', 'name', name, 'rule', '5', 'protocol', 'tcp']) self.cli_set(['firewall', 'name', name, 'rule', '5', 'tcp', 'flags', 'syn']) self.cli_set(['firewall', 'name', name, 'rule', '5', 'tcp', 'mss', mss_range]) - self.cli_set(['firewall', 'name', name, 'rule', '5', 'inbound-interface', interface]) + self.cli_set(['firewall', 'name', name, 'rule', '5', 'inbound-interface', 'interface-name', interface]) self.cli_set(['firewall', 'name', name, 'rule', '6', 'action', 'return']) self.cli_set(['firewall', 'name', name, 'rule', '6', 'protocol', 'gre']) - self.cli_set(['firewall', 'name', name, 'rule', '6', 'outbound-interface', interface]) + self.cli_set(['firewall', 'name', name, 'rule', '6', 'outbound-interface', 'interface-name', interface]) self.cli_set(['firewall', 'interface', interface, 'in', 'name', name]) @@ -290,6 +295,40 @@ class TestFirewall(VyOSUnitTestSHIM.TestCase): self.verify_nftables(nftables_search, 'ip vyos_filter') + def test_ipv4_mask(self): + name = 'smoketest-mask' + interface = 'eth0' + + self.cli_set(['firewall', 'group', 'address-group', 'mask_group', 'address', '1.1.1.1']) + + self.cli_set(['firewall', 'name', name, 'default-action', 'drop']) + self.cli_set(['firewall', 'name', name, 'enable-default-log']) + + self.cli_set(['firewall', 'name', name, 'rule', '1', 'action', 'drop']) + self.cli_set(['firewall', 'name', name, 'rule', '1', 'destination', 'address', '0.0.1.2']) + self.cli_set(['firewall', 'name', name, 'rule', '1', 'destination', 'address-mask', '0.0.255.255']) + + self.cli_set(['firewall', 'name', name, 'rule', '2', 'action', 'accept']) + self.cli_set(['firewall', 'name', name, 'rule', '2', 'source', 'address', '!0.0.3.4']) + self.cli_set(['firewall', 'name', name, 'rule', '2', 'source', 'address-mask', '0.0.255.255']) + + self.cli_set(['firewall', 'name', name, 'rule', '3', 'action', 'drop']) + self.cli_set(['firewall', 'name', name, 'rule', '3', 'source', 'group', 'address-group', 'mask_group']) + self.cli_set(['firewall', 'name', name, 'rule', '3', 'source', 'address-mask', '0.0.255.255']) + + self.cli_set(['firewall', 'interface', interface, 'in', 'name', name]) + + self.cli_commit() + + nftables_search = [ + [f'daddr & 0.0.255.255 == 0.0.1.2'], + [f'saddr & 0.0.255.255 != 0.0.3.4'], + [f'saddr & 0.0.255.255 == @A_mask_group'] + ] + + self.verify_nftables(nftables_search, 'ip vyos_filter') + + def test_ipv6_basic_rules(self): name = 'v6-smoketest' interface = 'eth0' @@ -306,11 +345,11 @@ class TestFirewall(VyOSUnitTestSHIM.TestCase): self.cli_set(['firewall', 'ipv6-name', name, 'rule', '2', 'action', 'reject']) self.cli_set(['firewall', 'ipv6-name', name, 'rule', '2', 'protocol', 'tcp_udp']) self.cli_set(['firewall', 'ipv6-name', name, 'rule', '2', 'destination', 'port', '8888']) - self.cli_set(['firewall', 'ipv6-name', name, 'rule', '2', 'inbound-interface', interface]) + self.cli_set(['firewall', 'ipv6-name', name, 'rule', '2', 'inbound-interface', 'interface-name', interface]) self.cli_set(['firewall', 'ipv6-name', name, 'rule', '3', 'action', 'return']) self.cli_set(['firewall', 'ipv6-name', name, 'rule', '3', 'protocol', 'gre']) - self.cli_set(['firewall', 'ipv6-name', name, 'rule', '3', 'outbound-interface', interface]) + self.cli_set(['firewall', 'ipv6-name', name, 'rule', '3', 'outbound-interface', 'interface-name', interface]) self.cli_set(['firewall', 'interface', interface, 'in', 'ipv6-name', name]) @@ -369,6 +408,39 @@ class TestFirewall(VyOSUnitTestSHIM.TestCase): self.verify_nftables(nftables_search, 'ip6 vyos_filter') + def test_ipv6_mask(self): + name = 'v6-smoketest-mask' + interface = 'eth0' + + self.cli_set(['firewall', 'group', 'ipv6-address-group', 'mask_group', 'address', '::beef']) + + self.cli_set(['firewall', 'ipv6-name', name, 'default-action', 'drop']) + self.cli_set(['firewall', 'ipv6-name', name, 'enable-default-log']) + + self.cli_set(['firewall', 'ipv6-name', name, 'rule', '1', 'action', 'drop']) + self.cli_set(['firewall', 'ipv6-name', name, 'rule', '1', 'destination', 'address', '::1111:2222:3333:4444']) + self.cli_set(['firewall', 'ipv6-name', name, 'rule', '1', 'destination', 'address-mask', '::ffff:ffff:ffff:ffff']) + + self.cli_set(['firewall', 'ipv6-name', name, 'rule', '2', 'action', 'accept']) + self.cli_set(['firewall', 'ipv6-name', name, 'rule', '2', 'source', 'address', '!::aaaa:bbbb:cccc:dddd']) + self.cli_set(['firewall', 'ipv6-name', name, 'rule', '2', 'source', 'address-mask', '::ffff:ffff:ffff:ffff']) + + self.cli_set(['firewall', 'ipv6-name', name, 'rule', '3', 'action', 'drop']) + self.cli_set(['firewall', 'ipv6-name', name, 'rule', '3', 'source', 'group', 'address-group', 'mask_group']) + self.cli_set(['firewall', 'ipv6-name', name, 'rule', '3', 'source', 'address-mask', '::ffff:ffff:ffff:ffff']) + + self.cli_set(['firewall', 'interface', interface, 'in', 'ipv6-name', name]) + + self.cli_commit() + + nftables_search = [ + ['daddr & ::ffff:ffff:ffff:ffff == ::1111:2222:3333:4444'], + ['saddr & ::ffff:ffff:ffff:ffff != ::aaaa:bbbb:cccc:dddd'], + ['saddr & ::ffff:ffff:ffff:ffff == @A6_mask_group'] + ] + + self.verify_nftables(nftables_search, 'ip6 vyos_filter') + def test_state_policy(self): self.cli_set(['firewall', 'state-policy', 'established', 'action', 'accept']) self.cli_set(['firewall', 'state-policy', 'related', 'action', 'accept']) diff --git a/src/validators/interface-name b/smoketest/scripts/cli/test_interfaces_virtual_ethernet.py index 105815eee..4732342fc 100755 --- a/src/validators/interface-name +++ b/smoketest/scripts/cli/test_interfaces_virtual_ethernet.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2021 VyOS maintainers and contributors +# Copyright (C) 2022 VyOS maintainers and contributors # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 or later as @@ -15,20 +15,25 @@ # along with this program. If not, see <http://www.gnu.org/licenses/>. import os -import re +import unittest -from sys import argv -from sys import exit +from vyos.ifconfig import Section +from base_interfaces_test import BasicInterfaceTest -pattern = '^(bond|br|dum|en|ersp|eth|gnv|lan|l2tp|l2tpeth|macsec|peth|ppp|pppoe|pptp|sstp|tun|vti|vtun|vxlan|wg|wlan|wwan)[0-9]+(.\d+)?|lo$' +class VEthInterfaceTest(BasicInterfaceTest.TestCase): + @classmethod + def setUpClass(cls): + cls._test_dhcp = True + cls._base_path = ['interfaces', 'virtual-ethernet'] -if __name__ == '__main__': - if len(argv) != 2: - exit(1) - interface = argv[1] + cls._options = { + 'veth0': ['peer-name veth1'], + 'veth1': ['peer-name veth0'], + } + + cls._interfaces = list(cls._options) + # call base-classes classmethod + super(VEthInterfaceTest, cls).setUpClass() - if re.match(pattern, interface): - exit(0) - if os.path.exists(f'/sys/class/net/{interface}'): - exit(0) - exit(1) +if __name__ == '__main__': + unittest.main(verbosity=2) diff --git a/smoketest/scripts/cli/test_pki.py b/smoketest/scripts/cli/test_pki.py index cba5ffdde..b18b0b039 100755 --- a/smoketest/scripts/cli/test_pki.py +++ b/smoketest/scripts/cli/test_pki.py @@ -246,5 +246,27 @@ class TestPKI(VyOSUnitTestSHIM.TestCase): self.cli_delete(['service', 'https', 'certificates', 'certificate']) + def test_certificate_eapol_update(self): + self.cli_set(base_path + ['certificate', 'smoketest', 'certificate', valid_ca_cert.replace('\n','')]) + self.cli_set(base_path + ['certificate', 'smoketest', 'private', 'key', valid_ca_private_key.replace('\n','')]) + self.cli_commit() + + self.cli_set(['interfaces', 'ethernet', 'eth1', 'eapol', 'certificate', 'smoketest']) + self.cli_commit() + + cert_data = None + + with open('/run/wpa_supplicant/eth1_cert.pem') as f: + cert_data = f.read() + + self.cli_set(base_path + ['certificate', 'smoketest', 'certificate', valid_update_cert.replace('\n','')]) + self.cli_set(base_path + ['certificate', 'smoketest', 'private', 'key', valid_update_private_key.replace('\n','')]) + self.cli_commit() + + with open('/run/wpa_supplicant/eth1_cert.pem') as f: + self.assertNotEqual(cert_data, f.read()) + + self.cli_delete(['interfaces', 'ethernet', 'eth1', 'eapol']) + if __name__ == '__main__': unittest.main(verbosity=2) diff --git a/smoketest/scripts/cli/test_protocols_bgp.py b/smoketest/scripts/cli/test_protocols_bgp.py index d2dad8c1a..debc8270c 100755 --- a/smoketest/scripts/cli/test_protocols_bgp.py +++ b/smoketest/scripts/cli/test_protocols_bgp.py @@ -294,6 +294,7 @@ class TestProtocolsBGP(VyOSUnitTestSHIM.TestCase): self.cli_set(base_path + ['parameters', 'minimum-holdtime', min_hold_time]) self.cli_set(base_path + ['parameters', 'no-suppress-duplicates']) self.cli_set(base_path + ['parameters', 'reject-as-sets']) + self.cli_set(base_path + ['parameters', 'route-reflector-allow-outbound-policy']) self.cli_set(base_path + ['parameters', 'shutdown']) self.cli_set(base_path + ['parameters', 'suppress-fib-pending']) @@ -322,6 +323,7 @@ class TestProtocolsBGP(VyOSUnitTestSHIM.TestCase): self.assertIn(f' bgp bestpath peer-type multipath-relax', frrconfig) self.assertIn(f' bgp minimum-holdtime {min_hold_time}', frrconfig) self.assertIn(f' bgp reject-as-sets', frrconfig) + self.assertIn(f' bgp route-reflector allow-outbound-policy', frrconfig) self.assertIn(f' bgp shutdown', frrconfig) self.assertIn(f' bgp suppress-fib-pending', frrconfig) self.assertNotIn(f'bgp ebgp-requires-policy', frrconfig) diff --git a/smoketest/scripts/cli/test_protocols_ospf.py b/smoketest/scripts/cli/test_protocols_ospf.py index 51c947537..339713bf6 100755 --- a/smoketest/scripts/cli/test_protocols_ospf.py +++ b/smoketest/scripts/cli/test_protocols_ospf.py @@ -70,6 +70,8 @@ class TestProtocolsOSPF(VyOSUnitTestSHIM.TestCase): self.cli_set(base_path + ['auto-cost', 'reference-bandwidth', bandwidth]) self.cli_set(base_path + ['parameters', 'router-id', router_id]) self.cli_set(base_path + ['parameters', 'abr-type', abr_type]) + self.cli_set(base_path + ['parameters', 'opaque-lsa']) + self.cli_set(base_path + ['parameters', 'rfc1583-compatibility']) self.cli_set(base_path + ['log-adjacency-changes', 'detail']) self.cli_set(base_path + ['default-metric', metric]) @@ -79,10 +81,12 @@ class TestProtocolsOSPF(VyOSUnitTestSHIM.TestCase): # Verify FRR ospfd configuration frrconfig = self.getFRRconfig('router ospf') self.assertIn(f'router ospf', frrconfig) + self.assertIn(f' compatible rfc1583', frrconfig) self.assertIn(f' auto-cost reference-bandwidth {bandwidth}', frrconfig) self.assertIn(f' ospf router-id {router_id}', frrconfig) self.assertIn(f' ospf abr-type {abr_type}', frrconfig) self.assertIn(f' timers throttle spf 200 1000 10000', frrconfig) # defaults + self.assertIn(f' capability opaque', frrconfig) self.assertIn(f' default-metric {metric}', frrconfig) diff --git a/src/conf_mode/firewall.py b/src/conf_mode/firewall.py index 9fee20358..20cf1ead1 100755 --- a/src/conf_mode/firewall.py +++ b/src/conf_mode/firewall.py @@ -26,7 +26,7 @@ from vyos.config import Config from vyos.configdict import dict_merge from vyos.configdict import node_changed from vyos.configdiff import get_config_diff, Diff -from vyos.configdep import set_dependent, call_dependents +from vyos.configdep import set_dependents, call_dependents # from vyos.configverify import verify_interface_exists from vyos.firewall import fqdn_config_parse from vyos.firewall import geoip_update @@ -65,7 +65,8 @@ valid_groups = [ 'address_group', 'domain_group', 'network_group', - 'port_group' + 'port_group', + 'interface_group' ] nested_group_types = [ @@ -162,11 +163,8 @@ def get_config(config=None): firewall['group_resync'] = bool('group' in firewall or node_changed(conf, base + ['group'])) if firewall['group_resync']: - # Update nat as firewall groups were updated - set_dependent(nat_conf_script, conf) - # Update policy route as firewall groups were updated - set_dependent(policy_route_conf_script, conf) - + # Update nat and policy-route as firewall groups were updated + set_dependents('group_resync', conf) if 'config_trap' in firewall and firewall['config_trap'] == 'enable': diff = get_config_diff(conf) @@ -279,6 +277,8 @@ def verify_nested_group(group_name, group, groups, seen): if 'include' not in group: return + seen.append(group_name) + for g in group['include']: if g not in groups: raise ConfigError(f'Nested group "{g}" does not exist') @@ -286,8 +286,6 @@ def verify_nested_group(group_name, group, groups, seen): if g in seen: raise ConfigError(f'Group "{group_name}" has a circular reference') - seen.append(g) - if 'include' in groups[g]: verify_nested_group(g, groups[g], groups, seen) diff --git a/src/conf_mode/http-api.py b/src/conf_mode/http-api.py index be80613c6..6328294c1 100755 --- a/src/conf_mode/http-api.py +++ b/src/conf_mode/http-api.py @@ -25,6 +25,7 @@ import vyos.defaults from vyos.config import Config from vyos.configdict import dict_merge +from vyos.configdep import set_dependents, call_dependents from vyos.template import render from vyos.util import cmd from vyos.util import call @@ -61,6 +62,11 @@ def get_config(config=None): else: conf = Config() + # reset on creation/deletion of 'api' node + https_base = ['service', 'https'] + if conf.exists(https_base): + set_dependents("https", conf) + base = ['service', 'https', 'api'] if not conf.exists(base): return None @@ -132,7 +138,7 @@ def apply(http_api): # Let uvicorn settle before restarting Nginx sleep(1) - cmd(f'{vyos_conf_scripts_dir}/https.py', raising=ConfigError) + call_dependents() if __name__ == '__main__': try: diff --git a/src/conf_mode/interfaces-bonding.py b/src/conf_mode/interfaces-bonding.py index 21cf204fc..9936620c8 100755 --- a/src/conf_mode/interfaces-bonding.py +++ b/src/conf_mode/interfaces-bonding.py @@ -21,6 +21,7 @@ from netifaces import interfaces from vyos.config import Config from vyos.configdict import get_interface_dict +from vyos.configdict import is_node_changed from vyos.configdict import leaf_node_changed from vyos.configdict import is_member from vyos.configdict import is_source_interface @@ -81,10 +82,10 @@ def get_config(config=None): if 'mode' in bond: bond['mode'] = get_bond_mode(bond['mode']) - tmp = leaf_node_changed(conf, base + [ifname, 'mode']) + tmp = is_node_changed(conf, base + [ifname, 'mode']) if tmp: bond['shutdown_required'] = {} - tmp = leaf_node_changed(conf, base + [ifname, 'lacp-rate']) + tmp = is_node_changed(conf, base + [ifname, 'lacp-rate']) if tmp: bond['shutdown_required'] = {} # determine which members have been removed @@ -116,7 +117,7 @@ def get_config(config=None): if dict_search('member.interface', bond): for interface, interface_config in bond['member']['interface'].items(): # Check if member interface is a new member - if not conf.exists_effective(['member', 'interface', interface]): + if not conf.exists_effective(base + [ifname, 'member', 'interface', interface]): bond['shutdown_required'] = {} # Check if member interface is disabled diff --git a/src/conf_mode/interfaces-ethernet.py b/src/conf_mode/interfaces-ethernet.py index e02841831..b49c945cd 100755 --- a/src/conf_mode/interfaces-ethernet.py +++ b/src/conf_mode/interfaces-ethernet.py @@ -175,7 +175,7 @@ def generate(ethernet): loaded_pki_cert = load_certificate(pki_cert['certificate']) loaded_ca_certs = {load_certificate(c['certificate']) - for c in ethernet['pki']['ca'].values()} + for c in ethernet['pki']['ca'].values()} if 'ca' in ethernet['pki'] else {} cert_full_chain = find_chain(loaded_pki_cert, loaded_ca_certs) diff --git a/src/conf_mode/interfaces-pppoe.py b/src/conf_mode/interfaces-pppoe.py index e2fdc7a42..ee4defa0d 100755 --- a/src/conf_mode/interfaces-pppoe.py +++ b/src/conf_mode/interfaces-pppoe.py @@ -23,7 +23,6 @@ from netifaces import interfaces from vyos.config import Config from vyos.configdict import get_interface_dict from vyos.configdict import is_node_changed -from vyos.configdict import leaf_node_changed from vyos.configdict import get_pppoe_interfaces from vyos.configverify import verify_authentication from vyos.configverify import verify_source_interface diff --git a/src/conf_mode/interfaces-sstpc.py b/src/conf_mode/interfaces-sstpc.py new file mode 100755 index 000000000..6b8094c51 --- /dev/null +++ b/src/conf_mode/interfaces-sstpc.py @@ -0,0 +1,142 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2022 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +import os +from sys import exit + +from vyos.config import Config +from vyos.configdict import get_interface_dict +from vyos.configdict import is_node_changed +from vyos.configverify import verify_authentication +from vyos.configverify import verify_vrf +from vyos.ifconfig import SSTPCIf +from vyos.pki import encode_certificate +from vyos.pki import find_chain +from vyos.pki import load_certificate +from vyos.template import render +from vyos.util import call +from vyos.util import dict_search +from vyos.util import is_systemd_service_running +from vyos.util import write_file +from vyos import ConfigError +from vyos import airbag +airbag.enable() + +def get_config(config=None): + """ + Retrive CLI config as dictionary. Dictionary can never be empty, as at least the + interface name will be added or a deleted flag + """ + if config: + conf = config + else: + conf = Config() + base = ['interfaces', 'sstpc'] + ifname, sstpc = get_interface_dict(conf, base) + + # We should only terminate the SSTP client session if critical parameters + # change. All parameters that can be changed on-the-fly (like interface + # description) should not lead to a reconnect! + for options in ['authentication', 'no_peer_dns', 'no_default_route', + 'server', 'ssl']: + if is_node_changed(conf, base + [ifname, options]): + sstpc.update({'shutdown_required': {}}) + # bail out early - no need to further process other nodes + break + + # Load PKI certificates for later processing + sstpc['pki'] = conf.get_config_dict(['pki'], key_mangling=('-', '_'), + get_first_key=True, + no_tag_node_value_mangle=True) + return sstpc + +def verify(sstpc): + if 'deleted' in sstpc: + return None + + verify_authentication(sstpc) + verify_vrf(sstpc) + + if dict_search('ssl.ca_certificate', sstpc) == None: + raise ConfigError('Missing mandatory CA certificate!') + + return None + +def generate(sstpc): + ifname = sstpc['ifname'] + config_sstpc = f'/etc/ppp/peers/{ifname}' + + sstpc['ca_file_path'] = f'/run/sstpc/{ifname}_ca-cert.pem' + + if 'deleted' in sstpc: + for file in [sstpc['ca_file_path'], config_sstpc]: + if os.path.exists(file): + os.unlink(file) + return None + + ca_name = sstpc['ssl']['ca_certificate'] + pki_ca_cert = sstpc['pki']['ca'][ca_name] + + loaded_ca_cert = load_certificate(pki_ca_cert['certificate']) + loaded_ca_certs = {load_certificate(c['certificate']) + for c in sstpc['pki']['ca'].values()} if 'ca' in sstpc['pki'] else {} + + ca_full_chain = find_chain(loaded_ca_cert, loaded_ca_certs) + + write_file(sstpc['ca_file_path'], '\n'.join(encode_certificate(c) for c in ca_full_chain)) + render(config_sstpc, 'sstp-client/peer.j2', sstpc, permission=0o640) + + return None + +def apply(sstpc): + ifname = sstpc['ifname'] + if 'deleted' in sstpc or 'disable' in sstpc: + if os.path.isdir(f'/sys/class/net/{ifname}'): + p = SSTPCIf(ifname) + p.remove() + call(f'systemctl stop ppp@{ifname}.service') + return None + + # reconnect should only be necessary when specific options change, + # like server, authentication ... (see get_config() for details) + if ((not is_systemd_service_running(f'ppp@{ifname}.service')) or + 'shutdown_required' in sstpc): + + # cleanup system (e.g. FRR routes first) + if os.path.isdir(f'/sys/class/net/{ifname}'): + p = SSTPCIf(ifname) + p.remove() + + call(f'systemctl restart ppp@{ifname}.service') + # When interface comes "live" a hook is called: + # /etc/ppp/ip-up.d/96-vyos-sstpc-callback + # which triggers SSTPCIf.update() + else: + if os.path.isdir(f'/sys/class/net/{ifname}'): + p = SSTPCIf(ifname) + p.update(sstpc) + + 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/interfaces-virtual-ethernet.py b/src/conf_mode/interfaces-virtual-ethernet.py new file mode 100755 index 000000000..8efe89c41 --- /dev/null +++ b/src/conf_mode/interfaces-virtual-ethernet.py @@ -0,0 +1,114 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2022 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +from sys import exit + +from netifaces import interfaces +from vyos import ConfigError +from vyos import airbag +from vyos.config import Config +from vyos.configdict import get_interface_dict +from vyos.configverify import verify_address +from vyos.configverify import verify_bridge_delete +from vyos.configverify import verify_vrf +from vyos.ifconfig import VethIf + +airbag.enable() + +def get_config(config=None): + """ + Retrive CLI config as dictionary. Dictionary can never be empty, as at + least the interface name will be added or a deleted flag + """ + if config: + conf = config + else: + conf = Config() + base = ['interfaces', 'virtual-ethernet'] + ifname, veth = get_interface_dict(conf, base) + + # We need to know all other veth related interfaces as veth requires a 1:1 + # mapping for the peer-names. The Linux kernel automatically creates both + # interfaces, the local one and the peer-name, but VyOS also needs a peer + # interfaces configrued on the CLI so we can assign proper IP addresses etc. + veth['other_interfaces'] = conf.get_config_dict(base, key_mangling=('-', '_'), + get_first_key=True, no_tag_node_value_mangle=True) + + return veth + + +def verify(veth): + if 'deleted' in veth: + verify_bridge_delete(veth) + # Prevent to delete veth interface which used for another "vethX peer-name" + for iface, iface_config in veth['other_interfaces'].items(): + if veth['ifname'] in iface_config['peer_name']: + ifname = veth['ifname'] + raise ConfigError( + f'Cannot delete "{ifname}" used for "interface {iface} peer-name"' + ) + return None + + verify_vrf(veth) + verify_address(veth) + + if 'peer_name' not in veth: + raise ConfigError(f'Remote peer name must be set for "{veth["ifname"]}"!') + + peer_name = veth['peer_name'] + ifname = veth['ifname'] + + if veth['peer_name'] not in veth['other_interfaces']: + raise ConfigError(f'Used peer-name "{peer_name}" on interface "{ifname}" ' \ + 'is not configured!') + + if veth['other_interfaces'][peer_name]['peer_name'] != ifname: + raise ConfigError( + f'Configuration mismatch between "{ifname}" and "{peer_name}"!') + + if peer_name == ifname: + raise ConfigError( + f'Peer-name "{peer_name}" cannot be the same as interface "{ifname}"!') + + return None + + +def generate(peth): + return None + +def apply(veth): + # Check if the Veth interface already exists + if 'rebuild_required' in veth or 'deleted' in veth: + if veth['ifname'] in interfaces(): + p = VethIf(veth['ifname']) + p.remove() + + if 'deleted' not in veth: + p = VethIf(**veth) + p.update(veth) + + 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/pki.py b/src/conf_mode/pki.py index 29ed7b1b7..e8f3cc87a 100755 --- a/src/conf_mode/pki.py +++ b/src/conf_mode/pki.py @@ -16,20 +16,16 @@ from sys import exit -import jmespath - from vyos.config import Config +from vyos.configdep import set_dependents, call_dependents from vyos.configdict import dict_merge from vyos.configdict import node_changed from vyos.pki import is_ca_certificate from vyos.pki import load_certificate -from vyos.pki import load_certificate_request from vyos.pki import load_public_key from vyos.pki import load_private_key from vyos.pki import load_crl from vyos.pki import load_dh_parameters -from vyos.util import ask_input -from vyos.util import call from vyos.util import dict_search_args from vyos.util import dict_search_recursive from vyos.xml import defaults @@ -121,6 +117,39 @@ def get_config(config=None): get_first_key=True, no_tag_node_value_mangle=True) + if 'changed' in pki: + for search in sync_search: + for key in search['keys']: + changed_key = sync_translate[key] + + if changed_key not in pki['changed']: + continue + + for item_name in pki['changed'][changed_key]: + node_present = False + if changed_key == 'openvpn': + node_present = dict_search_args(pki, 'openvpn', 'shared_secret', item_name) + else: + node_present = dict_search_args(pki, changed_key, item_name) + + if node_present: + search_dict = dict_search_args(pki['system'], *search['path']) + + if not search_dict: + continue + + for found_name, found_path in dict_search_recursive(search_dict, key): + if found_name == item_name: + path = search['path'] + path_str = ' '.join(path + found_path) + print(f'pki: Updating config: {path_str} {found_name}') + + if path[0] == 'interfaces': + ifname = found_path[0] + set_dependents(path[1], conf, ifname) + else: + set_dependents(path[1], conf) + return pki def is_valid_certificate(raw_data): @@ -259,37 +288,7 @@ def apply(pki): return None if 'changed' in pki: - for search in sync_search: - for key in search['keys']: - changed_key = sync_translate[key] - - if changed_key not in pki['changed']: - continue - - for item_name in pki['changed'][changed_key]: - node_present = False - if changed_key == 'openvpn': - node_present = dict_search_args(pki, 'openvpn', 'shared_secret', item_name) - else: - node_present = dict_search_args(pki, changed_key, item_name) - - if node_present: - search_dict = dict_search_args(pki['system'], *search['path']) - - if not search_dict: - continue - - for found_name, found_path in dict_search_recursive(search_dict, key): - if found_name == item_name: - path_str = ' '.join(search['path'] + found_path) - print(f'pki: Updating config: {path_str} {found_name}') - - script = search['script'] - if found_path[0] == 'interfaces': - ifname = found_path[2] - call(f'VYOS_TAGNODE_VALUE={ifname} {script}') - else: - call(script) + call_dependents() return None diff --git a/src/conf_mode/policy-route.py b/src/conf_mode/policy-route.py index 1d016695e..40a32efb3 100755 --- a/src/conf_mode/policy-route.py +++ b/src/conf_mode/policy-route.py @@ -36,7 +36,8 @@ valid_groups = [ 'address_group', 'domain_group', 'network_group', - 'port_group' + 'port_group', + 'interface_group' ] def get_config(config=None): diff --git a/src/conf_mode/protocols_failover.py b/src/conf_mode/protocols_failover.py new file mode 100755 index 000000000..048ba7a89 --- /dev/null +++ b/src/conf_mode/protocols_failover.py @@ -0,0 +1,121 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2022 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +import json + +from pathlib import Path + +from vyos.config import Config +from vyos.configdict import dict_merge +from vyos.template import render +from vyos.util import call +from vyos.xml import defaults +from vyos import ConfigError +from vyos import airbag + +airbag.enable() + + +service_name = 'vyos-failover' +service_conf = Path(f'/run/{service_name}.conf') +systemd_service = '/etc/systemd/system/vyos-failover.service' +rt_proto_failover = '/etc/iproute2/rt_protos.d/failover.conf' + + +def get_config(config=None): + if config: + conf = config + else: + conf = Config() + + base = ['protocols', 'failover'] + failover = conf.get_config_dict(base, key_mangling=('-', '_'), get_first_key=True) + + # Set default values only if we set config + if failover.get('route'): + for route, route_config in failover.get('route').items(): + for next_hop, next_hop_config in route_config.get('next_hop').items(): + default_values = defaults(base + ['route']) + failover['route'][route]['next_hop'][next_hop] = dict_merge( + default_values['next_hop'], failover['route'][route]['next_hop'][next_hop]) + + return failover + +def verify(failover): + # bail out early - looks like removal from running config + if not failover: + return None + + if 'route' not in failover: + raise ConfigError(f'Failover "route" is mandatory!') + + for route, route_config in failover['route'].items(): + if not route_config.get('next_hop'): + raise ConfigError(f'Next-hop for "{route}" is mandatory!') + + for next_hop, next_hop_config in route_config.get('next_hop').items(): + if 'interface' not in next_hop_config: + raise ConfigError(f'Interface for route "{route}" next-hop "{next_hop}" is mandatory!') + + if not next_hop_config.get('check'): + raise ConfigError(f'Check target for next-hop "{next_hop}" is mandatory!') + + if 'target' not in next_hop_config['check']: + raise ConfigError(f'Check target for next-hop "{next_hop}" is mandatory!') + + check_type = next_hop_config['check']['type'] + if check_type == 'tcp' and 'port' not in next_hop_config['check']: + raise ConfigError(f'Check port for next-hop "{next_hop}" and type TCP is mandatory!') + + return None + +def generate(failover): + if not failover: + service_conf.unlink(missing_ok=True) + return None + + # Add own rt_proto 'failover' + # Helps to detect all own routes 'proto failover' + with open(rt_proto_failover, 'w') as f: + f.write('111 failover\n') + + # Write configuration file + conf_json = json.dumps(failover, indent=4) + service_conf.write_text(conf_json) + render(systemd_service, 'protocols/systemd_vyos_failover_service.j2', failover) + + return None + +def apply(failover): + if not failover: + call(f'systemctl stop {service_name}.service') + call('ip route flush protocol failover') + else: + call('systemctl daemon-reload') + call(f'systemctl restart {service_name}.service') + call(f'ip route flush protocol failover') + + return None + +if __name__ == '__main__': + try: + c = get_config() + verify(c) + generate(c) + apply(c) + except ConfigError as e: + print(e) + exit(1) diff --git a/src/conf_mode/protocols_mpls.py b/src/conf_mode/protocols_mpls.py index 5da8e7b06..73af6595b 100755 --- a/src/conf_mode/protocols_mpls.py +++ b/src/conf_mode/protocols_mpls.py @@ -24,6 +24,7 @@ from vyos.template import render_to_string from vyos.util import dict_search from vyos.util import read_file from vyos.util import sysctl_write +from vyos.configverify import verify_interface_exists from vyos import ConfigError from vyos import frr from vyos import airbag @@ -46,6 +47,10 @@ def verify(mpls): if not mpls: return None + if 'interface' in mpls: + for interface in mpls['interface']: + verify_interface_exists(interface) + # Checks to see if LDP is properly configured if 'ldp' in mpls: # If router ID not defined diff --git a/src/conf_mode/service_pppoe-server.py b/src/conf_mode/service_pppoe-server.py index ba0249efd..600ba4e92 100755 --- a/src/conf_mode/service_pppoe-server.py +++ b/src/conf_mode/service_pppoe-server.py @@ -20,6 +20,7 @@ from sys import exit from vyos.config import Config from vyos.configdict import get_accel_dict +from vyos.configdict import is_node_changed from vyos.configverify import verify_accel_ppp_base_service from vyos.configverify import verify_interface_exists from vyos.template import render @@ -43,6 +44,13 @@ def get_config(config=None): # retrieve common dictionary keys pppoe = get_accel_dict(conf, base, pppoe_chap_secrets) + + # reload-or-restart does not implemented in accel-ppp + # use this workaround until it will be implemented + # https://phabricator.accel-ppp.org/T3 + if is_node_changed(conf, base + ['client-ip-pool']) or is_node_changed( + conf, base + ['client-ipv6-pool']): + pppoe.update({'restart_required': {}}) return pppoe def verify(pppoe): @@ -95,7 +103,10 @@ def apply(pppoe): os.unlink(file) return None - call(f'systemctl reload-or-restart {systemd_service}') + if 'restart_required' in pppoe: + call(f'systemctl restart {systemd_service}') + else: + call(f'systemctl reload-or-restart {systemd_service}') if __name__ == '__main__': try: diff --git a/src/conf_mode/service_webproxy.py b/src/conf_mode/service_webproxy.py index 32af31bde..41a1deaa3 100755 --- a/src/conf_mode/service_webproxy.py +++ b/src/conf_mode/service_webproxy.py @@ -28,8 +28,10 @@ from vyos.util import dict_search from vyos.util import write_file from vyos.validate import is_addr_assigned from vyos.xml import defaults +from vyos.base import Warning from vyos import ConfigError from vyos import airbag + airbag.enable() squid_config_file = '/etc/squid/squid.conf' @@ -37,24 +39,57 @@ squidguard_config_file = '/etc/squidguard/squidGuard.conf' squidguard_db_dir = '/opt/vyatta/etc/config/url-filtering/squidguard/db' user_group = 'proxy' -def generate_sg_localdb(category, list_type, role, proxy): + +def check_blacklist_categorydb(config_section): + if 'block_category' in config_section: + for category in config_section['block_category']: + check_categorydb(category) + if 'allow_category' in config_section: + for category in config_section['allow_category']: + check_categorydb(category) + + +def check_categorydb(category: str): + """ + Check if category's db exist + :param category: + :type str: + """ + path_to_cat: str = f'{squidguard_db_dir}/{category}' + if not os.path.exists(f'{path_to_cat}/domains.db') \ + and not os.path.exists(f'{path_to_cat}/urls.db') \ + and not os.path.exists(f'{path_to_cat}/expressions.db'): + Warning(f'DB of category {category} does not exist.\n ' + f'Use [update webproxy blacklists] ' + f'or delete undefined category!') + + +def generate_sg_rule_localdb(category, list_type, role, proxy): + if not category or not list_type or not role: + return None + cat_ = category.replace('-', '_') - if isinstance(dict_search(f'url_filtering.squidguard.{cat_}', proxy), - list): + if role == 'default': + path_to_cat = f'{cat_}' + else: + path_to_cat = f'rule.{role}.{cat_}' + if isinstance( + dict_search(f'url_filtering.squidguard.{path_to_cat}', proxy), + list): # local block databases must be generated "on-the-fly" tmp = { - 'squidguard_db_dir' : squidguard_db_dir, - 'category' : f'{category}-default', - 'list_type' : list_type, - 'rule' : role + 'squidguard_db_dir': squidguard_db_dir, + 'category': f'{category}-{role}', + 'list_type': list_type, + 'rule': role } sg_tmp_file = '/tmp/sg.conf' - db_file = f'{category}-default/{list_type}' - domains = '\n'.join(dict_search(f'url_filtering.squidguard.{cat_}', proxy)) - + db_file = f'{category}-{role}/{list_type}' + domains = '\n'.join( + dict_search(f'url_filtering.squidguard.{path_to_cat}', proxy)) # local file - write_file(f'{squidguard_db_dir}/{category}-default/local', '', + write_file(f'{squidguard_db_dir}/{category}-{role}/local', '', user=user_group, group=user_group) # database input file write_file(f'{squidguard_db_dir}/{db_file}', domains, @@ -64,17 +99,18 @@ def generate_sg_localdb(category, list_type, role, proxy): render(sg_tmp_file, 'squid/sg_acl.conf.j2', tmp, user=user_group, group=user_group) - call(f'su - {user_group} -c "squidGuard -d -c {sg_tmp_file} -C {db_file}"') + call( + f'su - {user_group} -c "squidGuard -d -c {sg_tmp_file} -C {db_file}"') if os.path.exists(sg_tmp_file): os.unlink(sg_tmp_file) - else: # if category is not part of our configuration, clean out the # squidguard lists - tmp = f'{squidguard_db_dir}/{category}-default' + tmp = f'{squidguard_db_dir}/{category}-{role}' if os.path.exists(tmp): - rmtree(f'{squidguard_db_dir}/{category}-default') + rmtree(f'{squidguard_db_dir}/{category}-{role}') + def get_config(config=None): if config: @@ -85,7 +121,8 @@ def get_config(config=None): if not conf.exists(base): return None - proxy = conf.get_config_dict(base, key_mangling=('-', '_'), get_first_key=True) + proxy = conf.get_config_dict(base, key_mangling=('-', '_'), + get_first_key=True) # We have gathered the dict representation of the CLI, but there are default # options which we need to update into the dictionary retrived. default_values = defaults(base) @@ -110,10 +147,11 @@ def get_config(config=None): default_values = defaults(base + ['cache-peer']) for peer in proxy['cache_peer']: proxy['cache_peer'][peer] = dict_merge(default_values, - proxy['cache_peer'][peer]) + proxy['cache_peer'][peer]) return proxy + def verify(proxy): if not proxy: return None @@ -170,17 +208,30 @@ def generate(proxy): render(squidguard_config_file, 'squid/squidGuard.conf.j2', proxy) cat_dict = { - 'local-block' : 'domains', - 'local-block-keyword' : 'expressions', - 'local-block-url' : 'urls', - 'local-ok' : 'domains', - 'local-ok-url' : 'urls' + 'local-block': 'domains', + 'local-block-keyword': 'expressions', + 'local-block-url': 'urls', + 'local-ok': 'domains', + 'local-ok-url': 'urls' } - for category, list_type in cat_dict.items(): - generate_sg_localdb(category, list_type, 'default', proxy) + if dict_search(f'url_filtering.squidguard', proxy) is not None: + squidgard_config_section = proxy['url_filtering']['squidguard'] + + for category, list_type in cat_dict.items(): + generate_sg_rule_localdb(category, list_type, 'default', proxy) + check_blacklist_categorydb(squidgard_config_section) + + if 'rule' in squidgard_config_section: + for rule in squidgard_config_section['rule']: + rule_config_section = squidgard_config_section['rule'][ + rule] + for category, list_type in cat_dict.items(): + generate_sg_rule_localdb(category, list_type, rule, proxy) + check_blacklist_categorydb(rule_config_section) return None + def apply(proxy): if not proxy: # proxy is removed in the commit @@ -198,6 +249,7 @@ def apply(proxy): call('systemctl restart squid.service') return None + if __name__ == '__main__': try: c = get_config() diff --git a/src/conf_mode/vpn_l2tp.py b/src/conf_mode/vpn_l2tp.py index fd5a4acd8..27e78db99 100755 --- a/src/conf_mode/vpn_l2tp.py +++ b/src/conf_mode/vpn_l2tp.py @@ -26,7 +26,10 @@ from ipaddress import ip_network from vyos.config import Config from vyos.template import is_ipv4 from vyos.template import render -from vyos.util import call, get_half_cpus +from vyos.util import call +from vyos.util import get_half_cpus +from vyos.util import check_port_availability +from vyos.util import is_listen_port_bind_service from vyos import ConfigError from vyos import airbag @@ -43,6 +46,7 @@ default_config_data = { 'client_ip_pool': None, 'client_ip_subnets': [], 'client_ipv6_pool': [], + 'client_ipv6_pool_configured': False, 'client_ipv6_delegate_prefix': [], 'dnsv4': [], 'dnsv6': [], @@ -64,7 +68,7 @@ default_config_data = { 'radius_source_address': '', 'radius_shaper_attr': '', 'radius_shaper_vendor': '', - 'radius_dynamic_author': '', + 'radius_dynamic_author': {}, 'wins': [], 'ip6_column': [], 'thread_cnt': get_half_cpus() @@ -205,21 +209,21 @@ def get_config(config=None): l2tp['radius_source_address'] = conf.return_value(['source-address']) # Dynamic Authorization Extensions (DOA)/Change Of Authentication (COA) - if conf.exists(['dynamic-author']): + if conf.exists(['dae-server']): dae = { 'port' : '', 'server' : '', 'key' : '' } - if conf.exists(['dynamic-author', 'server']): - dae['server'] = conf.return_value(['dynamic-author', 'server']) + if conf.exists(['dae-server', 'ip-address']): + dae['server'] = conf.return_value(['dae-server', 'ip-address']) - if conf.exists(['dynamic-author', 'port']): - dae['port'] = conf.return_value(['dynamic-author', 'port']) + if conf.exists(['dae-server', 'port']): + dae['port'] = conf.return_value(['dae-server', 'port']) - if conf.exists(['dynamic-author', 'key']): - dae['key'] = conf.return_value(['dynamic-author', 'key']) + if conf.exists(['dae-server', 'secret']): + dae['key'] = conf.return_value(['dae-server', 'secret']) l2tp['radius_dynamic_author'] = dae @@ -244,6 +248,7 @@ def get_config(config=None): l2tp['client_ip_subnets'] = conf.return_values(['client-ip-pool', 'subnet']) if conf.exists(['client-ipv6-pool', 'prefix']): + l2tp['client_ipv6_pool_configured'] = True l2tp['ip6_column'].append('ip6') for prefix in conf.list_nodes(['client-ipv6-pool', 'prefix']): tmp = { @@ -306,6 +311,9 @@ def get_config(config=None): if conf.exists(['ppp-options', 'lcp-echo-interval']): l2tp['ppp_echo_interval'] = conf.return_value(['ppp-options', 'lcp-echo-interval']) + if conf.exists(['ppp-options', 'ipv6']): + l2tp['ppp_ipv6'] = conf.return_value(['ppp-options', 'ipv6']) + return l2tp @@ -329,6 +337,19 @@ def verify(l2tp): if not radius['key']: raise ConfigError(f"Missing RADIUS secret for server { radius['key'] }") + if l2tp['radius_dynamic_author']: + if not l2tp['radius_dynamic_author']['server']: + raise ConfigError("Missing ip-address for dae-server") + if not l2tp['radius_dynamic_author']['key']: + raise ConfigError("Missing secret for dae-server") + address = l2tp['radius_dynamic_author']['server'] + port = l2tp['radius_dynamic_author']['port'] + proto = 'tcp' + # check if dae listen port is not used by another service + if check_port_availability(address, int(port), proto) is not True and \ + not is_listen_port_bind_service(int(port), 'accel-pppd'): + raise ConfigError(f'"{proto}" port "{port}" is used by another service') + # check for the existence of a client ip pool if not (l2tp['client_ip_pool'] or l2tp['client_ip_subnets']): raise ConfigError( diff --git a/src/conf_mode/vpn_openconnect.py b/src/conf_mode/vpn_openconnect.py index c050b796b..af3c51efc 100755 --- a/src/conf_mode/vpn_openconnect.py +++ b/src/conf_mode/vpn_openconnect.py @@ -58,7 +58,7 @@ def get_config(): default_values = defaults(base) ocserv = dict_merge(default_values, ocserv) - if "local" in ocserv["authentication"]["mode"]: + if 'mode' in ocserv["authentication"] and "local" in ocserv["authentication"]["mode"]: # workaround a "know limitation" - https://phabricator.vyos.net/T2665 del ocserv['authentication']['local_users']['username']['otp'] if not ocserv["authentication"]["local_users"]["username"]: @@ -157,7 +157,7 @@ def verify(ocserv): ocserv["network_settings"]["push_route"].remove("0.0.0.0/0") ocserv["network_settings"]["push_route"].append("default") else: - ocserv["network_settings"]["push_route"] = "default" + ocserv["network_settings"]["push_route"] = ["default"] else: raise ConfigError('openconnect network settings required') @@ -247,7 +247,7 @@ def apply(ocserv): if os.path.exists(file): os.unlink(file) else: - call('systemctl restart ocserv.service') + call('systemctl reload-or-restart ocserv.service') counter = 0 while True: # exit early when service runs diff --git a/src/etc/ppp/ip-up.d/96-vyos-sstpc-callback b/src/etc/ppp/ip-up.d/96-vyos-sstpc-callback new file mode 100755 index 000000000..4e8804f29 --- /dev/null +++ b/src/etc/ppp/ip-up.d/96-vyos-sstpc-callback @@ -0,0 +1,49 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2022 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +# This is a Python hook script which is invoked whenever a SSTP client session +# goes "ip-up". It will call into our vyos.ifconfig library and will then +# execute common tasks for the SSTP interface. The reason we have to "hook" this +# is that we can not create a sstpcX interface in advance in linux and then +# connect pppd to this already existing interface. + +from sys import argv +from sys import exit + +from vyos.configquery import ConfigTreeQuery +from vyos.configdict import get_interface_dict +from vyos.ifconfig import SSTPCIf + +# When the ppp link comes up, this script is called with the following +# parameters +# $1 the interface name used by pppd (e.g. ppp3) +# $2 the tty device name +# $3 the tty device speed +# $4 the local IP address for the interface +# $5 the remote IP address +# $6 the parameter specified by the 'ipparam' option to pppd + +if (len(argv) < 7): + exit(1) + +interface = argv[6] + +conf = ConfigTreeQuery() +_, sstpc = get_interface_dict(conf.config, ['interfaces', 'sstpc'], interface) + +# Update the config +p = SSTPCIf(interface) +p.update(sstpc) diff --git a/src/helpers/vyos-failover.py b/src/helpers/vyos-failover.py new file mode 100755 index 000000000..1ac193423 --- /dev/null +++ b/src/helpers/vyos-failover.py @@ -0,0 +1,184 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2022 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +import argparse +import json +import subprocess +import socket +import time + +from vyos.util import rc_cmd +from pathlib import Path +from systemd import journal + + +my_name = Path(__file__).stem + + +def get_best_route_options(route, debug=False): + """ + Return current best route ('gateway, interface, metric) + + % get_best_route_options('203.0.113.1') + ('192.168.0.1', 'eth1', 1) + + % get_best_route_options('203.0.113.254') + (None, None, None) + """ + rc, data = rc_cmd(f'ip --detail --json route show protocol failover {route}') + if rc == 0: + data = json.loads(data) + if len(data) == 0: + print(f'\nRoute {route} for protocol failover was not found') + return None, None, None + # Fake metric 999 by default + # Search route with the lowest metric + best_metric = 999 + for entry in data: + if debug: print('\n', entry) + metric = entry.get('metric') + gateway = entry.get('gateway') + iface = entry.get('dev') + if metric < best_metric: + best_metric = metric + best_gateway = gateway + best_interface = iface + if debug: + print(f'### Best_route exists: {route}, best_gateway: {best_gateway}, ' + f'best_metric: {best_metric}, best_iface: {best_interface}') + return best_gateway, best_interface, best_metric + +def is_port_open(ip, port): + """ + Check connection to remote host and port + Return True if host alive + + % is_port_open('example.com', 8080) + True + """ + s = socket.socket(socket.AF_INET, socket.SOCK_STREAM, socket.IPPROTO_TCP) + s.settimeout(2) + try: + s.connect((ip, int(port))) + s.shutdown(socket.SHUT_RDWR) + return True + except: + return False + finally: + s.close() + +def is_target_alive(target=None, iface='', proto='icmp', port=None, debug=False): + """ + Host availability check by ICMP, ARP, TCP + Return True if target checks is successful + + % is_target_alive('192.0.2.1', 'eth1', proto='arp') + True + """ + if iface != '': + iface = f'-I {iface}' + if proto == 'icmp': + command = f'/usr/bin/ping -q {target} {iface} -n -c 2 -W 1' + rc, response = rc_cmd(command) + if debug: print(f' [ CHECK-TARGET ]: [{command}] -- return-code [RC: {rc}]') + if rc == 0: + return True + elif proto == 'arp': + command = f'/usr/bin/arping -b -c 2 -f -w 1 -i 1 {iface} {target}' + rc, response = rc_cmd(command) + if debug: print(f' [ CHECK-TARGET ]: [{command}] -- return-code [RC: {rc}]') + if rc == 0: + return True + elif proto == 'tcp' and port is not None: + return True if is_port_open(target, port) else False + else: + return False + + +if __name__ == '__main__': + # Parse command arguments and get config + parser = argparse.ArgumentParser() + parser.add_argument('-c', + '--config', + action='store', + help='Path to protocols failover configuration', + required=True, + type=Path) + + args = parser.parse_args() + try: + config_path = Path(args.config) + config = json.loads(config_path.read_text()) + except Exception as err: + print( + f'Configuration file "{config_path}" does not exist or malformed: {err}' + ) + exit(1) + + # Useful debug info to console, use debug = True + # sudo systemctl stop vyos-failover.service + # sudo /usr/libexec/vyos/vyos-failover.py --config /run/vyos-failover.conf + debug = False + + while(True): + + for route, route_config in config.get('route').items(): + + exists_route = exists_gateway, exists_iface, exists_metric = get_best_route_options(route, debug=debug) + + for next_hop, nexthop_config in route_config.get('next_hop').items(): + conf_iface = nexthop_config.get('interface') + conf_metric = int(nexthop_config.get('metric')) + port = nexthop_config.get('check').get('port') + port_opt = f'port {port}' if port else '' + proto = nexthop_config.get('check').get('type') + target = nexthop_config.get('check').get('target') + timeout = nexthop_config.get('check').get('timeout') + + # Best route not fonund in the current routing table + if exists_route == (None, None, None): + if debug: print(f" [NEW_ROUTE_DETECTED] route: [{route}]") + # Add route if check-target alive + if is_target_alive(target, conf_iface, proto, port, debug=debug): + if debug: print(f' [ ADD ] -- ip route add {route} via {next_hop} dev {conf_iface} ' + f'metric {conf_metric} proto failover\n###') + rc, command = rc_cmd(f'ip route add {route} via {next_hop} dev {conf_iface} ' + f'metric {conf_metric} proto failover') + # If something is wrong and gateway not added + # Example: Error: Next-hop has invalid gateway. + if rc !=0: + if debug: print(f'{command} -- return-code [RC: {rc}] {next_hop} dev {conf_iface}') + else: + journal.send(f'ip route add {route} via {next_hop} dev {conf_iface} ' + f'metric {conf_metric} proto failover', SYSLOG_IDENTIFIER=my_name) + else: + if debug: print(f' [ TARGET_FAIL ] target checks fails for [{target}], do nothing') + journal.send(f'Check fail for route {route} target {target} proto {proto} ' + f'{port_opt}', SYSLOG_IDENTIFIER=my_name) + + # Route was added, check if the target is alive + # We should delete route if check fails only if route exists in the routing table + if not is_target_alive(target, conf_iface, proto, port, debug=debug) and \ + exists_route != (None, None, None): + if debug: + print(f'Nexh_hop {next_hop} fail, target not response') + print(f' [ DEL ] -- ip route del {route} via {next_hop} dev {conf_iface} ' + f'metric {conf_metric} proto failover [DELETE]') + rc_cmd(f'ip route del {route} via {next_hop} dev {conf_iface} metric {conf_metric} proto failover') + journal.send(f'ip route del {route} via {next_hop} dev {conf_iface} ' + f'metric {conf_metric} proto failover', SYSLOG_IDENTIFIER=my_name) + + time.sleep(int(timeout)) diff --git a/src/migration-scripts/firewall/8-to-9 b/src/migration-scripts/firewall/8-to-9 new file mode 100755 index 000000000..f7c1bb90d --- /dev/null +++ b/src/migration-scripts/firewall/8-to-9 @@ -0,0 +1,91 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2022 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +# T4780: Add firewall interface group +# cli changes from: +# set firewall [name | ipv6-name] <name> rule <number> [inbound-interface | outbound-interface] <interface_name> +# To +# set firewall [name | ipv6-name] <name> rule <number> [inbound-interface | outbound-interface] [interface-name | interface-group] <interface_name | interface_group> + +import re + +from sys import argv +from sys import exit + +from vyos.configtree import ConfigTree +from vyos.ifconfig import Section + +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() + +base = ['firewall'] +config = ConfigTree(config_file) + +if not config.exists(base): + # Nothing to do + exit(0) + +if config.exists(base + ['name']): + for name in config.list_nodes(base + ['name']): + if not config.exists(base + ['name', name, 'rule']): + continue + + for rule in config.list_nodes(base + ['name', name, 'rule']): + rule_iiface = base + ['name', name, 'rule', rule, 'inbound-interface'] + rule_oiface = base + ['name', name, 'rule', rule, 'outbound-interface'] + + if config.exists(rule_iiface): + tmp = config.return_value(rule_iiface) + config.delete(rule_iiface) + config.set(rule_iiface + ['interface-name'], value=tmp) + + if config.exists(rule_oiface): + tmp = config.return_value(rule_oiface) + config.delete(rule_oiface) + config.set(rule_oiface + ['interface-name'], value=tmp) + + +if config.exists(base + ['ipv6-name']): + for name in config.list_nodes(base + ['ipv6-name']): + if not config.exists(base + ['ipv6-name', name, 'rule']): + continue + + for rule in config.list_nodes(base + ['ipv6-name', name, 'rule']): + rule_iiface = base + ['ipv6-name', name, 'rule', rule, 'inbound-interface'] + rule_oiface = base + ['ipv6-name', name, 'rule', rule, 'outbound-interface'] + + if config.exists(rule_iiface): + tmp = config.return_value(rule_iiface) + config.delete(rule_iiface) + config.set(rule_iiface + ['interface-name'], value=tmp) + + if config.exists(rule_oiface): + tmp = config.return_value(rule_oiface) + config.delete(rule_oiface) + config.set(rule_oiface + ['interface-name'], value=tmp) + +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)
\ No newline at end of file diff --git a/src/op_mode/connect_disconnect.py b/src/op_mode/connect_disconnect.py index 936c20bcb..d39e88bf3 100755 --- a/src/op_mode/connect_disconnect.py +++ b/src/op_mode/connect_disconnect.py @@ -41,7 +41,7 @@ def check_ppp_running(interface): def connect(interface): """ Connect dialer interface """ - if interface.startswith('ppp'): + if interface.startswith('pppoe') or interface.startswith('sstpc'): check_ppp_interface(interface) # Check if interface is already dialed if os.path.isdir(f'/sys/class/net/{interface}'): @@ -62,7 +62,7 @@ def connect(interface): def disconnect(interface): """ Disconnect dialer interface """ - if interface.startswith('ppp'): + if interface.startswith('pppoe') or interface.startswith('sstpc'): check_ppp_interface(interface) # Check if interface is already down diff --git a/src/op_mode/generate_ipsec_debug_archive.py b/src/op_mode/generate_ipsec_debug_archive.py new file mode 100755 index 000000000..1422559a8 --- /dev/null +++ b/src/op_mode/generate_ipsec_debug_archive.py @@ -0,0 +1,89 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2022 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +from datetime import datetime +from pathlib import Path +from shutil import rmtree +from socket import gethostname +from sys import exit +from tarfile import open as tar_open +from vyos.util import rc_cmd + +# define a list of commands that needs to be executed +CMD_LIST: list[str] = [ + 'ipsec status', + 'swanctl -L', + 'swanctl -l', + 'swanctl -P', + 'ip x sa show', + 'ip x policy show', + 'ip tunnel show', + 'ip address', + 'ip rule show', + 'ip route | head -100', + 'ip route show table 220' +] +JOURNALCTL_CMD: str = 'journalctl -b -n 10000 /usr/lib/ipsec/charon' + +# execute a command and save the output to a file +def save_stdout(command: str, file: Path) -> None: + rc, stdout = rc_cmd(command) + body: str = f'''### {command} ### +Command: {command} +Exit code: {rc} +Stdout: +{stdout} + +''' + with file.open(mode='a') as f: + f.write(body) + + +# get local host name +hostname: str = gethostname() +# get current time +time_now: str = datetime.now().isoformat(timespec='seconds') + +# define a temporary directory for logs and collected data +tmp_dir: Path = Path(f'/tmp/ipsec_debug_{time_now}') +# set file paths +ipsec_status_file: Path = Path(f'{tmp_dir}/ipsec_status.txt') +journalctl_charon_file: Path = Path(f'{tmp_dir}/journalctl_charon.txt') +archive_file: str = f'/tmp/ipsec_debug_{time_now}.tar.bz2' + +# create files +tmp_dir.mkdir() +ipsec_status_file.touch() +journalctl_charon_file.touch() + +try: + # execute all commands + for command in CMD_LIST: + save_stdout(command, ipsec_status_file) + save_stdout(JOURNALCTL_CMD, journalctl_charon_file) + + # create an archive + with tar_open(name=archive_file, mode='x:bz2') as tar_file: + tar_file.add(tmp_dir) + + # inform user about success + print(f'Debug file is generated and located in {archive_file}') +except Exception as err: + print(f'Error during generating a debug file: {err}') +finally: + # cleanup + rmtree(tmp_dir) + exit() diff --git a/src/op_mode/generate_ipsec_debug_archive.sh b/src/op_mode/generate_ipsec_debug_archive.sh deleted file mode 100755 index 53d0a6eaa..000000000 --- a/src/op_mode/generate_ipsec_debug_archive.sh +++ /dev/null @@ -1,36 +0,0 @@ -#!/usr/bin/env bash - -# Collecting IPSec Debug Information - -DATE=`date +%d-%m-%Y` - -a_CMD=( - "sudo ipsec status" - "sudo swanctl -L" - "sudo swanctl -l" - "sudo swanctl -P" - "sudo ip x sa show" - "sudo ip x policy show" - "sudo ip tunnel show" - "sudo ip address" - "sudo ip rule show" - "sudo ip route" - "sudo ip route show table 220" - ) - - -echo "DEBUG: ${DATE} on host \"$(hostname)\"" > /tmp/ipsec-status-${DATE}.txt -date >> /tmp/ipsec-status-${DATE}.txt - -# Execute all DEBUG commands and save it to file -for cmd in "${a_CMD[@]}"; do - echo -e "\n### ${cmd} ###" >> /tmp/ipsec-status-${DATE}.txt - ${cmd} >> /tmp/ipsec-status-${DATE}.txt 2>/dev/null -done - -# Collect charon logs, build .tgz archive -sudo journalctl /usr/lib/ipsec/charon > /tmp/journalctl-charon-${DATE}.txt && \ -sudo tar -zcvf /tmp/ipsec-debug-${DATE}.tgz /tmp/journalctl-charon-${DATE}.txt /tmp/ipsec-status-${DATE}.txt >& /dev/null -sudo rm -f /tmp/journalctl-charon-${DATE}.txt /tmp/ipsec-status-${DATE}.txt - -echo "Debug file is generated and located in /tmp/ipsec-debug-${DATE}.tgz" diff --git a/src/op_mode/generate_system_login_user.py b/src/op_mode/generate_system_login_user.py new file mode 100755 index 000000000..8f8827b1b --- /dev/null +++ b/src/op_mode/generate_system_login_user.py @@ -0,0 +1,77 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2022 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +import argparse +import os + +from vyos.util import popen +from secrets import token_hex +from base64 import b32encode + +if os.geteuid() != 0: + exit("You need to have root privileges to run this script.\nPlease try again, this time using 'sudo'. Exiting.") + +if __name__ == '__main__': + parser = argparse.ArgumentParser() + parser.add_argument("-u", "--username", type=str, help='Username used for authentication', required=True) + parser.add_argument("-l", "--rate_limit", type=str, help='Limit number of logins (rate-limit) per rate-time (default: 3)', default="3", required=False) + parser.add_argument("-t", "--rate_time", type=str, help='Limit number of logins (rate-limit) per rate-time (default: 30)', default="30", required=False) + parser.add_argument("-w", "--window_size", type=str, help='Set window of concurrently valid codes (default: 3)', default="3", required=False) + parser.add_argument("-i", "--interval", type=str, help='Duration of single time interval', default="30", required=False) + parser.add_argument("-d", "--digits", type=str, help='The number of digits in the one-time password', default="6", required=False) + args = parser.parse_args() + + hostname = os.uname()[1] + username = args.username + rate_limit = args.rate_limit + rate_time = args.rate_time + window_size = args.window_size + digits = args.digits + period = args.interval + + # check variables: + if int(rate_limit) < 1 or int(rate_limit) > 10: + print("") + quit("Number of logins (rate-limit) must be between '1' and '10'") + + if int(rate_time) < 15 or int(rate_time) > 600: + print("") + quit("The rate-time must be between '15' and '600' seconds") + + if int(window_size) < 1 or int(window_size) > 21: + print("") + quit("Window of concurrently valid codes must be between '1' and '21' seconds") + + # generate OTP key, URL & QR: + key_hex = token_hex(20) + key_base32 = b32encode(bytes.fromhex(key_hex)).decode() + + otp_url=''.join(["otpauth://totp/",username,"@",hostname,"?secret=",key_base32,"&digits=",digits,"&period=",period]) + qrcode,err = popen('qrencode -t ansiutf8', input=otp_url) + + print("# You can share it with the user, he just needs to scan the QR in his OTP app") + print("# username: ", username) + print("# OTP KEY: ", key_base32) + print("# OTP URL: ", otp_url) + print(qrcode) + print('# To add this OTP key to configuration, run the following commands:') + print(f"set system login user {username} authentication otp key '{key_base32}'") + if rate_limit != "3": + print(f"set system login user {username} authentication otp rate-limit '{rate_limit}'") + if rate_time != "30": + print(f"set system login user {username} authentication otp rate-time '{rate_time}'") + if window_size != "3": + print(f"set system login user {username} authentication otp window-size '{window_size}'") diff --git a/src/op_mode/ipsec.py b/src/op_mode/ipsec.py index 83e4241d7..e0d204a0a 100755 --- a/src/op_mode/ipsec.py +++ b/src/op_mode/ipsec.py @@ -17,11 +17,13 @@ import os import re import sys +import typing from collections import OrderedDict from hurry import filesize from re import split as re_split from tabulate import tabulate +from subprocess import TimeoutExpired from vyos.util import call from vyos.util import convert_data @@ -136,23 +138,293 @@ def _get_formatted_output_sas(sas): return output -def get_peer_connections(peer, tunnel, return_all = False): +# Connections block +def _get_vici_connections(): + from vici import Session as vici_session + + try: + session = vici_session() + except Exception: + raise vyos.opmode.UnconfiguredSubsystem("IPsec not initialized") + connections = list(session.list_conns()) + return connections + + +def _get_convert_data_connections(): + get_connections = _get_vici_connections() + connections = convert_data(get_connections) + return connections + + +def _get_parent_sa_proposal(connection_name: str, data: list) -> dict: + """Get parent SA proposals by connection name + if connections not in the 'down' state + + Args: + connection_name (str): Connection name + data (list): List of current SAs from vici + + Returns: + str: Parent SA connection proposal + AES_CBC/256/HMAC_SHA2_256_128/MODP_1024 + """ + if not data: + return {} + for sa in data: + # check if parent SA exist + if connection_name not in sa.keys(): + return {} + if 'encr-alg' in sa[connection_name]: + encr_alg = sa.get(connection_name, '').get('encr-alg') + cipher = encr_alg.split('_')[0] + mode = encr_alg.split('_')[1] + encr_keysize = sa.get(connection_name, '').get('encr-keysize') + integ_alg = sa.get(connection_name, '').get('integ-alg') + # prf_alg = sa.get(connection_name, '').get('prf-alg') + dh_group = sa.get(connection_name, '').get('dh-group') + proposal = { + 'cipher': cipher, + 'mode': mode, + 'key_size': encr_keysize, + 'hash': integ_alg, + 'dh': dh_group + } + return proposal + return {} + + +def _get_parent_sa_state(connection_name: str, data: list) -> str: + """Get parent SA state by connection name + + Args: + connection_name (str): Connection name + data (list): List of current SAs from vici + + Returns: + Parent SA connection state + """ + if not data: + return 'down' + for sa in data: + # check if parent SA exist + if connection_name not in sa.keys(): + return 'down' + if sa[connection_name]['state'].lower() == 'established': + return 'up' + else: + return 'down' + + +def _get_child_sa_state(connection_name: str, tunnel_name: str, + data: list) -> str: + """Get child SA state by connection and tunnel name + + Args: + connection_name (str): Connection name + tunnel_name (str): Tunnel name + data (list): List of current SAs from vici + + Returns: + str: `up` if child SA state is 'installed' otherwise `down` + """ + if not data: + return 'down' + for sa in data: + # check if parent SA exist + if connection_name not in sa.keys(): + return 'down' + child_sas = sa[connection_name]['child-sas'] + # Get all child SA states + # there can be multiple SAs per tunnel + child_sa_states = [ + v['state'] for k, v in child_sas.items() if v['name'] == tunnel_name + ] + return 'up' if 'INSTALLED' in child_sa_states else 'down' + + +def _get_child_sa_info(connection_name: str, tunnel_name: str, + data: list) -> dict: + """Get child SA installed info by connection and tunnel name + + Args: + connection_name (str): Connection name + tunnel_name (str): Tunnel name + data (list): List of current SAs from vici + + Returns: + dict: Info of the child SA in the dictionary format + """ + for sa in data: + # check if parent SA exist + if connection_name not in sa.keys(): + return {} + child_sas = sa[connection_name]['child-sas'] + # Get all child SA data + # Skip temp SA name (first key), get only SA values as dict + # {'OFFICE-B-tunnel-0-46': {'name': 'OFFICE-B-tunnel-0'}...} + # i.e get all data after 'OFFICE-B-tunnel-0-46' + child_sa_info = [ + v for k, v in child_sas.items() if 'name' in v and + v['name'] == tunnel_name and v['state'] == 'INSTALLED' + ] + return child_sa_info[-1] if child_sa_info else {} + + +def _get_child_sa_proposal(child_sa_data: dict) -> dict: + if child_sa_data and 'encr-alg' in child_sa_data: + encr_alg = child_sa_data.get('encr-alg') + cipher = encr_alg.split('_')[0] + mode = encr_alg.split('_')[1] + key_size = child_sa_data.get('encr-keysize') + integ_alg = child_sa_data.get('integ-alg') + dh_group = child_sa_data.get('dh-group') + proposal = { + 'cipher': cipher, + 'mode': mode, + 'key_size': key_size, + 'hash': integ_alg, + 'dh': dh_group + } + return proposal + return {} + + +def _get_raw_data_connections(list_connections: list, list_sas: list) -> list: + """Get configured VPN IKE connections and IPsec states + + Args: + list_connections (list): List of configured connections from vici + list_sas (list): List of current SAs from vici + + Returns: + list: List and status of IKE/IPsec connections/tunnels + """ + base_dict = [] + for connections in list_connections: + base_list = {} + for connection, conn_conf in connections.items(): + base_list['ike_connection_name'] = connection + base_list['ike_connection_state'] = _get_parent_sa_state( + connection, list_sas) + base_list['ike_remote_address'] = conn_conf['remote_addrs'] + base_list['ike_proposal'] = _get_parent_sa_proposal( + connection, list_sas) + base_list['local_id'] = conn_conf.get('local-1', '').get('id') + base_list['remote_id'] = conn_conf.get('remote-1', '').get('id') + base_list['version'] = conn_conf.get('version', 'IKE') + base_list['children'] = [] + children = conn_conf['children'] + for tunnel, tun_options in children.items(): + state = _get_child_sa_state(connection, tunnel, list_sas) + local_ts = tun_options.get('local-ts') + remote_ts = tun_options.get('remote-ts') + dpd_action = tun_options.get('dpd_action') + close_action = tun_options.get('close_action') + sa_info = _get_child_sa_info(connection, tunnel, list_sas) + esp_proposal = _get_child_sa_proposal(sa_info) + base_list['children'].append({ + 'name': tunnel, + 'state': state, + 'local_ts': local_ts, + 'remote_ts': remote_ts, + 'dpd_action': dpd_action, + 'close_action': close_action, + 'sa': sa_info, + 'esp_proposal': esp_proposal + }) + base_dict.append(base_list) + return base_dict + + +def _get_raw_connections_summary(list_conn, list_sas): + import jmespath + data = _get_raw_data_connections(list_conn, list_sas) + match = '[*].children[]' + child = jmespath.search(match, data) + tunnels_down = len([k for k in child if k['state'] == 'down']) + tunnels_up = len([k for k in child if k['state'] == 'up']) + tun_dict = { + 'tunnels': child, + 'total': len(child), + 'down': tunnels_down, + 'up': tunnels_up + } + return tun_dict + + +def _get_formatted_output_conections(data): + from tabulate import tabulate + data_entries = '' + connections = [] + for entry in data: + tunnels = [] + ike_name = entry['ike_connection_name'] + ike_state = entry['ike_connection_state'] + conn_type = entry.get('version', 'IKE') + remote_addrs = ','.join(entry['ike_remote_address']) + local_ts, remote_ts = '-', '-' + local_id = entry['local_id'] + remote_id = entry['remote_id'] + proposal = '-' + if entry.get('ike_proposal'): + proposal = (f'{entry["ike_proposal"]["cipher"]}_' + f'{entry["ike_proposal"]["mode"]}/' + f'{entry["ike_proposal"]["key_size"]}/' + f'{entry["ike_proposal"]["hash"]}/' + f'{entry["ike_proposal"]["dh"]}') + connections.append([ + ike_name, ike_state, conn_type, remote_addrs, local_ts, remote_ts, + local_id, remote_id, proposal + ]) + for tun in entry['children']: + tun_name = tun.get('name') + tun_state = tun.get('state') + conn_type = 'IPsec' + local_ts = '\n'.join(tun.get('local_ts')) + remote_ts = '\n'.join(tun.get('remote_ts')) + proposal = '-' + if tun.get('esp_proposal'): + proposal = (f'{tun["esp_proposal"]["cipher"]}_' + f'{tun["esp_proposal"]["mode"]}/' + f'{tun["esp_proposal"]["key_size"]}/' + f'{tun["esp_proposal"]["hash"]}/' + f'{tun["esp_proposal"]["dh"]}') + connections.append([ + tun_name, tun_state, conn_type, remote_addrs, local_ts, + remote_ts, local_id, remote_id, proposal + ]) + connection_headers = [ + 'Connection', 'State', 'Type', 'Remote address', 'Local TS', + 'Remote TS', 'Local id', 'Remote id', 'Proposal' + ] + output = tabulate(connections, connection_headers, numalign='left') + return output + + +# Connections block end + + +def get_peer_connections(peer, tunnel): search = rf'^[\s]*({peer}-(tunnel-[\d]+|vti)).*' matches = [] if not os.path.exists(SWANCTL_CONF): raise vyos.opmode.UnconfiguredSubsystem("IPsec not initialized") + suffix = None if tunnel is None else (f'tunnel-{tunnel}' if + tunnel.isnumeric() else tunnel) with open(SWANCTL_CONF, 'r') as f: for line in f.readlines(): result = re.match(search, line) if result: - suffix = f'tunnel-{tunnel}' if tunnel.isnumeric() else tunnel - if return_all or (result[2] == suffix): + if tunnel is None: matches.append(result[1]) + else: + if result[2] == suffix: + matches.append(result[1]) return matches -def reset_peer(peer: str, tunnel:str): - conns = get_peer_connections(peer, tunnel, return_all = (not tunnel or tunnel == 'all')) +def reset_peer(peer: str, tunnel:typing.Optional[str]): + conns = get_peer_connections(peer, tunnel) if not conns: raise vyos.opmode.IncorrectValue('Peer or tunnel(s) not found, aborting') @@ -174,6 +446,23 @@ def show_sa(raw: bool): return _get_formatted_output_sas(sa_data) +def show_connections(raw: bool): + list_conns = _get_convert_data_connections() + list_sas = _get_raw_data_sas() + if raw: + return _get_raw_data_connections(list_conns, list_sas) + + connections = _get_raw_data_connections(list_conns, list_sas) + return _get_formatted_output_conections(connections) + + +def show_connections_summary(raw: bool): + list_conns = _get_convert_data_connections() + list_sas = _get_raw_data_sas() + if raw: + return _get_raw_connections_summary(list_conns, list_sas) + + if __name__ == '__main__': try: res = vyos.opmode.run(sys.modules[__name__]) diff --git a/src/op_mode/openconnect.py b/src/op_mode/openconnect.py index 00992c66a..b21890728 100755 --- a/src/op_mode/openconnect.py +++ b/src/op_mode/openconnect.py @@ -31,14 +31,7 @@ occtl_socket = '/run/ocserv/occtl.socket' def _get_raw_data_sessions(): rc, out = rc_cmd(f'sudo {occtl} --json --socket-file {occtl_socket} show users') if rc != 0: - output = {'openconnect': - { - 'configured': False, - 'return_code': rc, - 'reason': out - } - } - return output + raise vyos.opmode.DataUnavailable(out) sessions = json.loads(out) return sessions @@ -61,9 +54,8 @@ def _get_formatted_sessions(data): def show_sessions(raw: bool): config = ConfigTreeQuery() - if not config.exists('vpn openconnect') and not raw: - print('Openconnect is not configured') - exit(0) + if not config.exists('vpn openconnect'): + raise vyos.opmode.UnconfiguredSubsystem('Openconnect is not configured') openconnect_data = _get_raw_data_sessions() if raw: diff --git a/src/op_mode/openvpn.py b/src/op_mode/openvpn.py new file mode 100755 index 000000000..3797a7153 --- /dev/null +++ b/src/op_mode/openvpn.py @@ -0,0 +1,220 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2022 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. +# +# + +import os +import sys +from tabulate import tabulate + +import vyos.opmode +from vyos.util import bytes_to_human +from vyos.util import commit_in_progress +from vyos.util import call +from vyos.config import Config + +def _get_tunnel_address(peer_host, peer_port, status_file): + peer = peer_host + ':' + peer_port + lst = [] + + with open(status_file, 'r') as f: + lines = f.readlines() + for line in lines: + if peer in line: + lst.append(line) + + # filter out subnet entries if iroute: + # in the case that one sets, say: + # [ ..., 'vtun10', 'server', 'client', 'client1', 'subnet','10.10.2.0/25'] + # the status file will have an entry: + # 10.10.2.0/25,client1,... + lst = [l for l in lst[1:] if '/' not in l.split(',')[0]] + + tunnel_ip = lst[0].split(',')[0] + + return tunnel_ip + +def _get_interface_status(mode: str, interface: str) -> dict: + status_file = f'/run/openvpn/{interface}.status' + + data = { + 'mode': mode, + 'intf': interface, + 'local_host': '', + 'local_port': '', + 'date': '', + 'clients': [], + } + + if not os.path.exists(status_file): + raise vyos.opmode.DataUnavailable('No information for interface {interface}') + + with open(status_file, 'r') as f: + lines = f.readlines() + for line_no, line in enumerate(lines): + # remove trailing newline character first + line = line.rstrip('\n') + + # check first line header + if line_no == 0: + if mode == 'server': + if not line == 'OpenVPN CLIENT LIST': + raise vyos.opmode.InternalError('Expected "OpenVPN CLIENT LIST"') + else: + if not line == 'OpenVPN STATISTICS': + raise vyos.opmode.InternalError('Expected "OpenVPN STATISTICS"') + + continue + + # second line informs us when the status file has been last updated + if line_no == 1: + data['date'] = line.lstrip('Updated,').rstrip('\n') + continue + + if mode == 'server': + # for line_no > 1, lines appear as follows: + # + # Common Name,Real Address,Bytes Received,Bytes Sent,Connected Since + # client1,172.18.202.10:55904,2880587,2882653,Fri Aug 23 16:25:48 2019 + # client3,172.18.204.10:41328,2850832,2869729,Fri Aug 23 16:25:43 2019 + # client2,172.18.203.10:48987,2856153,2871022,Fri Aug 23 16:25:45 2019 + # ... + # ROUTING TABLE + # ... + if line_no >= 3: + # indicator that there are no more clients + if line == 'ROUTING TABLE': + break + # otherwise, get client data + remote = (line.split(',')[1]).rsplit(':', maxsplit=1) + + client = { + 'name': line.split(',')[0], + 'remote_host': remote[0], + 'remote_port': remote[1], + 'tunnel': 'N/A', + 'rx_bytes': bytes_to_human(int(line.split(',')[2]), + precision=1), + 'tx_bytes': bytes_to_human(int(line.split(',')[3]), + precision=1), + 'online_since': line.split(',')[4] + } + client['tunnel'] = _get_tunnel_address(client['remote_host'], + client['remote_port'], + status_file) + data['clients'].append(client) + continue + else: # mode == 'client' or mode == 'site-to-site' + if line_no == 2: + client = { + 'name': 'N/A', + 'remote_host': 'N/A', + 'remote_port': 'N/A', + 'tunnel': 'N/A', + 'rx_bytes': bytes_to_human(int(line.split(',')[1]), + precision=1), + 'tx_bytes': '', + 'online_since': 'N/A' + } + continue + + if line_no == 3: + client['tx_bytes'] = bytes_to_human(int(line.split(',')[1]), + precision=1) + data['clients'].append(client) + break + + return data + +def _get_raw_data(mode: str) -> dict: + data = {} + conf = Config() + conf_dict = conf.get_config_dict(['interfaces', 'openvpn'], + get_first_key=True) + if not conf_dict: + return data + + interfaces = [x for x in list(conf_dict) if conf_dict[x]['mode'] == mode] + for intf in interfaces: + data[intf] = _get_interface_status(mode, intf) + d = data[intf] + d['local_host'] = conf_dict[intf].get('local-host', '') + d['local_port'] = conf_dict[intf].get('local-port', '') + if mode in ['client', 'site-to-site']: + for client in d['clients']: + if 'shared-secret-key-file' in list(conf_dict[intf]): + client['name'] = 'None (PSK)' + client['remote_host'] = conf_dict[intf].get('remote-host', [''])[0] + client['remote_port'] = conf_dict[intf].get('remote-port', '1194') + + return data + +def _format_openvpn(data: dict) -> str: + if not data: + out = 'No OpenVPN interfaces configured' + return out + + headers = ['Client CN', 'Remote Host', 'Tunnel IP', 'Local Host', + 'TX bytes', 'RX bytes', 'Connected Since'] + + out = '' + data_out = [] + for intf in list(data): + l_host = data[intf]['local_host'] + l_port = data[intf]['local_port'] + for client in list(data[intf]['clients']): + r_host = client['remote_host'] + r_port = client['remote_port'] + + out += f'\nOpenVPN status on {intf}\n\n' + name = client['name'] + remote = r_host + ':' + r_port if r_host and r_port else 'N/A' + tunnel = client['tunnel'] + local = l_host + ':' + l_port if l_host and l_port else 'N/A' + tx_bytes = client['tx_bytes'] + rx_bytes = client['rx_bytes'] + online_since = client['online_since'] + data_out.append([name, remote, tunnel, local, tx_bytes, + rx_bytes, online_since]) + + out += tabulate(data_out, headers) + + return out + +def show(raw: bool, mode: str) -> str: + openvpn_data = _get_raw_data(mode) + + if raw: + return openvpn_data + + return _format_openvpn(openvpn_data) + +def reset(interface: str): + if os.path.isfile(f'/run/openvpn/{interface}.conf'): + if commit_in_progress(): + raise vyos.opmode.CommitInProgress('Retry OpenVPN reset: commit in progress.') + call(f'systemctl restart openvpn@{interface}.service') + else: + raise vyos.opmode.IncorrectValue(f'OpenVPN interface "{interface}" does not exist!') + +if __name__ == '__main__': + try: + res = vyos.opmode.run(sys.modules[__name__]) + if res: + print(res) + except (ValueError, vyos.opmode.Error) as e: + print(e) + sys.exit(1) diff --git a/src/op_mode/route.py b/src/op_mode/route.py index d11b00ba0..d07a34180 100755 --- a/src/op_mode/route.py +++ b/src/op_mode/route.py @@ -54,6 +54,18 @@ frr_command_template = Template(""" {% endif %} """) +def show_summary(raw: bool): + from vyos.util import cmd + + if raw: + from json import loads + + output = cmd(f"vtysh -c 'show ip route summary json'") + return loads(output) + else: + output = cmd(f"vtysh -c 'show ip route summary'") + return output + def show(raw: bool, family: str, net: typing.Optional[str], diff --git a/src/op_mode/show_acceleration.py b/src/op_mode/show_acceleration.py index 752db3deb..48c31d4d9 100755 --- a/src/op_mode/show_acceleration.py +++ b/src/op_mode/show_acceleration.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2019 VyOS maintainers and contributors +# Copyright (C) 2019-2022 VyOS maintainers and contributors # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 or later as @@ -13,7 +13,6 @@ # # You should have received a copy of the GNU General Public License # along with this program. If not, see <http://www.gnu.org/licenses/>. -# import sys import os @@ -24,12 +23,11 @@ from vyos.config import Config from vyos.util import popen from vyos.util import call - def detect_qat_dev(): - output, err = popen('sudo lspci -nn', decode='utf-8') + output, err = popen('lspci -nn', decode='utf-8') if not err: data = re.findall('(8086:19e2)|(8086:37c8)|(8086:0435)|(8086:6f54)', output) - #If QAT devices found + # QAT devices found if data: return print("\t No QAT device found") @@ -44,11 +42,11 @@ def show_qat_status(): sys.exit(1) # Show QAT service - call('sudo /etc/init.d/qat_service status') + call('/etc/init.d/qat_service status') # Return QAT devices def get_qat_devices(): - data_st, err = popen('sudo /etc/init.d/qat_service status', decode='utf-8') + data_st, err = popen('/etc/init.d/qat_service status', decode='utf-8') if not err: elm_lst = re.findall('qat_dev\d', data_st) print('\n'.join(elm_lst)) @@ -57,7 +55,7 @@ def get_qat_devices(): def get_qat_proc_path(qat_dev): q_type = "" q_bsf = "" - output, err = popen('sudo /etc/init.d/qat_service status', decode='utf-8') + output, err = popen('/etc/init.d/qat_service status', decode='utf-8') if not err: # Parse QAT service output data_st = output.split("\n") @@ -95,20 +93,20 @@ args = parser.parse_args() if args.hw: detect_qat_dev() # Show availible Intel QAT devices - call('sudo lspci -nn | egrep -e \'8086:37c8|8086:19e2|8086:0435|8086:6f54\'') + call('lspci -nn | egrep -e \'8086:37c8|8086:19e2|8086:0435|8086:6f54\'') elif args.flow and args.dev: check_qat_if_conf() - call('sudo cat '+get_qat_proc_path(args.dev)+"fw_counters") + call('cat '+get_qat_proc_path(args.dev)+"fw_counters") elif args.interrupts: check_qat_if_conf() # Delete _dev from args.dev - call('sudo cat /proc/interrupts | grep qat') + call('cat /proc/interrupts | grep qat') elif args.status: check_qat_if_conf() show_qat_status() elif args.conf and args.dev: check_qat_if_conf() - call('sudo cat '+get_qat_proc_path(args.dev)+"dev_cfg") + call('cat '+get_qat_proc_path(args.dev)+"dev_cfg") elif args.dev_list: get_qat_devices() else: diff --git a/src/op_mode/show_openvpn.py b/src/op_mode/show_openvpn.py index 9a5adcffb..e29e594a5 100755 --- a/src/op_mode/show_openvpn.py +++ b/src/op_mode/show_openvpn.py @@ -59,7 +59,11 @@ def get_vpn_tunnel_address(peer, interface): for line in lines: if peer in line: lst.append(line) - tunnel_ip = lst[1].split(',')[0] + + # filter out subnet entries + lst = [l for l in lst[1:] if '/' not in l.split(',')[0]] + + tunnel_ip = lst[0].split(',')[0] return tunnel_ip diff --git a/src/op_mode/show_raid.sh b/src/op_mode/show_raid.sh index ba4174692..ab5d4d50f 100755 --- a/src/op_mode/show_raid.sh +++ b/src/op_mode/show_raid.sh @@ -1,5 +1,13 @@ #!/bin/bash +if [ "$EUID" -ne 0 ]; then + # This should work without sudo because we have read + # access to the dev, but for some reason mdadm must be + # run as root in order to succeed. + echo "Please run as root" + exit 1 +fi + raid_set_name=$1 raid_sets=`cat /proc/partitions | grep md | awk '{ print $4 }'` valid_set=`echo $raid_sets | grep $raid_set_name` @@ -10,7 +18,7 @@ else # This should work without sudo because we have read # access to the dev, but for some reason mdadm must be # run as root in order to succeed. - sudo /sbin/mdadm --detail /dev/${raid_set_name} + mdadm --detail /dev/${raid_set_name} else echo "Must be administrator or root to display RAID status" fi diff --git a/src/op_mode/vpn_ipsec.py b/src/op_mode/vpn_ipsec.py index 68dc5bc45..2392cfe92 100755 --- a/src/op_mode/vpn_ipsec.py +++ b/src/op_mode/vpn_ipsec.py @@ -48,8 +48,8 @@ def reset_peer(peer, tunnel): result = True for conn in conns: try: - call(f'sudo /usr/sbin/ipsec down {conn}{{*}}', timeout = 10) - call(f'sudo /usr/sbin/ipsec up {conn}', timeout = 10) + call(f'/usr/sbin/ipsec down {conn}{{*}}', timeout = 10) + call(f'/usr/sbin/ipsec up {conn}', timeout = 10) except TimeoutExpired as e: print(f'Timed out while resetting {conn}') result = False @@ -81,8 +81,8 @@ def reset_profile(profile, tunnel): print('Profile not found, aborting') return - call(f'sudo /usr/sbin/ipsec down {conn}') - result = call(f'sudo /usr/sbin/ipsec up {conn}') + call(f'/usr/sbin/ipsec down {conn}') + result = call(f'/usr/sbin/ipsec up {conn}') print('Profile reset result: ' + ('success' if result == 0 else 'failed')) @@ -90,17 +90,17 @@ def debug_peer(peer, tunnel): peer = peer.replace(':', '-') if not peer or peer == "all": debug_commands = [ - "sudo ipsec statusall", - "sudo swanctl -L", - "sudo swanctl -l", - "sudo swanctl -P", - "sudo ip x sa show", - "sudo ip x policy show", - "sudo ip tunnel show", - "sudo ip address", - "sudo ip rule show", - "sudo ip route | head -100", - "sudo ip route show table 220" + "ipsec statusall", + "swanctl -L", + "swanctl -l", + "swanctl -P", + "ip x sa show", + "ip x policy show", + "ip tunnel show", + "ip address", + "ip rule show", + "ip route | head -100", + "ip route show table 220" ] for debug_cmd in debug_commands: print(f'\n### {debug_cmd} ###') @@ -117,7 +117,7 @@ def debug_peer(peer, tunnel): return for conn in conns: - call(f'sudo /usr/sbin/ipsec statusall | grep {conn}') + call(f'/usr/sbin/ipsec statusall | grep {conn}') if __name__ == '__main__': parser = argparse.ArgumentParser() diff --git a/src/op_mode/webproxy_update_blacklist.sh b/src/op_mode/webproxy_update_blacklist.sh index 43a4b79fc..4fb9a54c6 100755 --- a/src/op_mode/webproxy_update_blacklist.sh +++ b/src/op_mode/webproxy_update_blacklist.sh @@ -18,6 +18,23 @@ blacklist_url='ftp://ftp.univ-tlse1.fr/pub/reseau/cache/squidguard_contrib/black data_dir="/opt/vyatta/etc/config/url-filtering" archive="${data_dir}/squidguard/archive" db_dir="${data_dir}/squidguard/db" +conf_file="/etc/squidguard/squidGuard.conf" +tmp_conf_file="/tmp/sg_update_db.conf" + +#$1-category +#$2-type +#$3-list +create_sg_db () +{ + FILE=$db_dir/$1/$2 + if test -f "$FILE"; then + rm -f ${tmp_conf_file} + printf "dbhome $db_dir\ndest $1 {\n $3 $1/$2\n}\nacl {\n default {\n pass any\n }\n}" >> ${tmp_conf_file} + /usr/bin/squidGuard -b -c ${tmp_conf_file} -C $FILE + rm -f ${tmp_conf_file} + fi + +} while [ $# -gt 0 ] do @@ -88,7 +105,17 @@ if [[ -n $update ]] && [[ $update -eq "yes" ]]; then # fix permissions chown -R proxy:proxy ${db_dir} - chmod 2770 ${db_dir} + + #create db + category_list=(`find $db_dir -type d -exec basename {} \; `) + for category in ${category_list[@]} + do + create_sg_db $category "domains" "domainlist" + create_sg_db $category "urls" "urllist" + create_sg_db $category "expressions" "expressionlist" + done + chown -R proxy:proxy ${db_dir} + chmod 755 ${db_dir} logger --priority WARNING "webproxy blacklist entries updated (${count_before}/${count_after})" diff --git a/src/services/api/graphql/generate/schema_from_op_mode.py b/src/services/api/graphql/generate/schema_from_op_mode.py index 1fd198a37..fc63b0100 100755 --- a/src/services/api/graphql/generate/schema_from_op_mode.py +++ b/src/services/api/graphql/generate/schema_from_op_mode.py @@ -25,15 +25,16 @@ from inspect import signature, getmembers, isfunction, isclass, getmro from jinja2 import Template from vyos.defaults import directories +from vyos.util import load_as_module if __package__ is None or __package__ == '': sys.path.append("/usr/libexec/vyos/services/api") - from graphql.libs.op_mode import load_as_module, is_op_mode_function_name, is_show_function_name + from graphql.libs.op_mode import is_op_mode_function_name, is_show_function_name from graphql.libs.op_mode import snake_to_pascal_case, map_type_name from vyos.config import Config from vyos.configdict import dict_merge from vyos.xml import defaults else: - from .. libs.op_mode import load_as_module, is_op_mode_function_name, is_show_function_name + from .. libs.op_mode import is_op_mode_function_name, is_show_function_name from .. libs.op_mode import snake_to_pascal_case, map_type_name from .. import state diff --git a/src/services/api/graphql/graphql/mutations.py b/src/services/api/graphql/graphql/mutations.py index 2778feb69..87ea59c43 100644 --- a/src/services/api/graphql/graphql/mutations.py +++ b/src/services/api/graphql/graphql/mutations.py @@ -14,7 +14,7 @@ # along with this library. If not, see <http://www.gnu.org/licenses/>. from importlib import import_module -from typing import Any, Dict +from typing import Any, Dict, Optional from ariadne import ObjectType, convert_kwargs_to_snake_case, convert_camel_case_to_snake from graphql import GraphQLResolveInfo from makefun import with_signature @@ -42,7 +42,7 @@ def make_mutation_resolver(mutation_name, class_name, session_func): func_base_name = convert_camel_case_to_snake(class_name) resolver_name = f'resolve_{func_base_name}' - func_sig = '(obj: Any, info: GraphQLResolveInfo, data: Dict = {})' + func_sig = '(obj: Any, info: GraphQLResolveInfo, data: Optional[Dict]=None)' @mutation.field(mutation_name) @convert_kwargs_to_snake_case @@ -67,20 +67,18 @@ def make_mutation_resolver(mutation_name, class_name, session_func): del data['key'] elif auth_type == 'token': - # there is a subtlety here: with the removal of the key entry, - # some requests will now have empty input, hence no data arg, so - # make it optional in the func_sig. However, it can not be None, - # as the makefun package provides accurate TypeError exceptions; - # hence set it to {}, but now it is a mutable default argument, - # so clear the key 'result', which is added at the end of - # this function. data = kwargs['data'] - if 'result' in data: - del data['result'] - + if data is None: + data = {} info = kwargs['info'] user = info.context.get('user') if user is None: + error = info.context.get('error') + if error is not None: + return { + "success": False, + "errors": [error] + } return { "success": False, "errors": ['not authenticated'] diff --git a/src/services/api/graphql/graphql/queries.py b/src/services/api/graphql/graphql/queries.py index 9c8a4f064..1ad586428 100644 --- a/src/services/api/graphql/graphql/queries.py +++ b/src/services/api/graphql/graphql/queries.py @@ -14,7 +14,7 @@ # along with this library. If not, see <http://www.gnu.org/licenses/>. from importlib import import_module -from typing import Any, Dict +from typing import Any, Dict, Optional from ariadne import ObjectType, convert_kwargs_to_snake_case, convert_camel_case_to_snake from graphql import GraphQLResolveInfo from makefun import with_signature @@ -42,7 +42,7 @@ def make_query_resolver(query_name, class_name, session_func): func_base_name = convert_camel_case_to_snake(class_name) resolver_name = f'resolve_{func_base_name}' - func_sig = '(obj: Any, info: GraphQLResolveInfo, data: Dict = {})' + func_sig = '(obj: Any, info: GraphQLResolveInfo, data: Optional[Dict]=None)' @query.field(query_name) @convert_kwargs_to_snake_case @@ -67,20 +67,18 @@ def make_query_resolver(query_name, class_name, session_func): del data['key'] elif auth_type == 'token': - # there is a subtlety here: with the removal of the key entry, - # some requests will now have empty input, hence no data arg, so - # make it optional in the func_sig. However, it can not be None, - # as the makefun package provides accurate TypeError exceptions; - # hence set it to {}, but now it is a mutable default argument, - # so clear the key 'result', which is added at the end of - # this function. data = kwargs['data'] - if 'result' in data: - del data['result'] - + if data is None: + data = {} info = kwargs['info'] user = info.context.get('user') if user is None: + error = info.context.get('error') + if error is not None: + return { + "success": False, + "errors": [error] + } return { "success": False, "errors": ['not authenticated'] diff --git a/src/services/api/graphql/libs/op_mode.py b/src/services/api/graphql/libs/op_mode.py index 97a26520e..6939ed5d6 100644 --- a/src/services/api/graphql/libs/op_mode.py +++ b/src/services/api/graphql/libs/op_mode.py @@ -21,14 +21,9 @@ from typing import Union from humps import decamelize from vyos.defaults import directories +from vyos.util import load_as_module from vyos.opmode import _normalize_field_names -def load_as_module(name: str, path: str): - spec = importlib.util.spec_from_file_location(name, path) - mod = importlib.util.module_from_spec(spec) - spec.loader.exec_module(mod) - return mod - def load_op_mode_as_module(name: str): path = os.path.join(directories['op_mode'], name) name = os.path.splitext(name)[0].replace('-', '_') diff --git a/src/services/api/graphql/libs/token_auth.py b/src/services/api/graphql/libs/token_auth.py index 3ecd8b855..2100eba7f 100644 --- a/src/services/api/graphql/libs/token_auth.py +++ b/src/services/api/graphql/libs/token_auth.py @@ -54,6 +54,9 @@ def get_user_context(request): user_id: str = payload.get('sub') if user_id is None: return context + except jwt.exceptions.ExpiredSignatureError: + context['error'] = 'expired token' + return context except jwt.PyJWTError: return context try: diff --git a/src/validators/file-exists b/src/validators/file-exists deleted file mode 100755 index 5cef6b199..000000000 --- a/src/validators/file-exists +++ /dev/null @@ -1,61 +0,0 @@ -#!/usr/bin/env python3 -# -# Copyright (C) 2019 VyOS maintainers and contributors -# -# This program is free software; you can redistribute it and/or modify -# it under the terms of the GNU General Public License version 2 or later as -# published by the Free Software Foundation. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see <http://www.gnu.org/licenses/>. -# -# Description: -# Check if a given file exists on the system. Used for files that -# are referenced from the CLI and need to be preserved during an image upgrade. -# Warn the user if these aren't under /config - -import os -import sys -import argparse - - -def exit(strict, message): - if strict: - sys.exit(f'ERROR: {message}') - print(f'WARNING: {message}', file=sys.stderr) - sys.exit() - - -if __name__ == '__main__': - parser = argparse.ArgumentParser() - parser.add_argument("-d", "--directory", type=str, help="File must be present in this directory.") - parser.add_argument("-e", "--error", action="store_true", help="Tread warnings as errors - change exit code to '1'") - parser.add_argument("file", type=str, help="Path of file to validate") - - args = parser.parse_args() - - # - # Always check if the given file exists - # - if not os.path.exists(args.file): - exit(args.error, f"File '{args.file}' not found") - - # - # Optional check if the file is under a certain directory path - # - if args.directory: - # remove directory path from path to verify - rel_filename = args.file.replace(args.directory, '').lstrip('/') - - if not os.path.exists(args.directory + '/' + rel_filename): - exit(args.error, - f"'{args.file}' lies outside of '{args.directory}' directory.\n" - "It will not get preserved during image upgrade!" - ) - - sys.exit() |