diff options
464 files changed, 14843 insertions, 5601 deletions
diff --git a/.github/reviewers.yml b/.github/reviewers.yml index 8463681fc..a1647d20d 100644 --- a/.github/reviewers.yml +++ b/.github/reviewers.yml @@ -1,8 +1,3 @@ --- "**/*": - - dmbaturin - - UnicronNL - - 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 81134206b..1a7f8ef0b 100644 --- a/.github/workflows/auto-author-assign.yml +++ b/.github/workflows/auto-author-assign.yml @@ -12,7 +12,7 @@ jobs: runs-on: ubuntu-latest steps: - name: "Assign Author to PR" - uses: toshimaru/auto-author-assign@v1.3.5 + uses: toshimaru/auto-author-assign@v1.6.2 with: repo-token: ${{ secrets.GITHUB_TOKEN }} @@ -21,7 +21,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Request review based on files changes and/or groups the author belongs to - uses: shufo/auto-assign-reviewer-by-files@v1.1.1 + 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 diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml new file mode 100644 index 000000000..c39800ac8 --- /dev/null +++ b/.github/workflows/codeql.yml @@ -0,0 +1,74 @@ +# For most projects, this workflow file will not need changing; you simply need +# to commit it to your repository. +# +# You may wish to alter this file to override the set of languages analyzed, +# or to provide custom queries or build logic. +# +# ******** NOTE ******** +# We have attempted to detect the languages in your repository. Please check +# the `language` matrix defined below to confirm you have the correct set of +# supported CodeQL languages. +# +name: "CodeQL" + +on: + push: + branches: [ "current", crux, equuleus ] + pull_request: + # The branches below must be a subset of the branches above + branches: [ "current" ] + schedule: + - cron: '22 10 * * 0' + +jobs: + analyze: + name: Analyze + runs-on: ubuntu-latest + permissions: + actions: read + contents: read + security-events: write + + strategy: + fail-fast: false + matrix: + language: [ 'python' ] + # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ] + # Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support + + steps: + - name: Checkout repository + uses: actions/checkout@v3 + + # Initializes the CodeQL tools for scanning. + - name: Initialize CodeQL + uses: github/codeql-action/init@v2 + with: + languages: ${{ matrix.language }} + # If you wish to specify custom queries, you can do so here or in a config file. + # By default, queries listed here will override any specified in a config file. + # Prefix the list here with "+" to use these queries and those in the config file. + + # Details on CodeQL's query packs refer to : https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs + # queries: security-extended,security-and-quality + + + # Autobuild attempts to build any compiled languages (C/C++, C#, Go, or Java). + # If this step fails, then you should remove it and run the build manually (see below) + - name: Autobuild + uses: github/codeql-action/autobuild@v2 + + # ℹ️ Command-line programs to run using the OS shell. + # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun + + # If the Autobuild fails above, remove it and uncomment the following three lines. + # modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance. + + # - run: | + # echo "Run, Build Application using script" + # ./location_of_script_within_repo/buildscript.sh + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v2 + with: + category: "/language:${{matrix.language}}" @@ -33,11 +33,6 @@ interface_definitions: $(config_xml_obj) # IPSec VPN EAP-RADIUS does not support source-address rm -rf $(TMPL_DIR)/vpn/ipsec/remote-access/radius/source-address - # T4284 neq QoS implementation is not yet live - find $(TMPL_DIR)/interfaces -name redirect -type d -exec rm -rf {} \; - rm -rf $(TMPL_DIR)/qos - rm -rf $(TMPL_DIR)/interfaces/input - # T2472 - EIGRP support rm -rf $(TMPL_DIR)/protocols/eigrp # T2773 - EIGRP support for VRF @@ -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 9500d3aa7..abf562984 100644 --- a/data/op-mode-standardized.json +++ b/data/op-mode-standardized.json @@ -1,17 +1,27 @@ [ +"accelppp.py", +"bgp.py", "bridge.py", +"config_mgmt.py", "conntrack.py", "container.py", "cpu.py", +"dhcp.py", +"dns.py", +"interfaces.py", +"lldp.py", +"log.py", "memory.py", "nat.py", "neighbor.py", "openconnect.py", +"openvpn.py", "route.py", "system.py", "ipsec.py", "storage.py", "uptime.py", "version.py", -"vrf.py" +"vrf.py", +"zone.py" ] diff --git a/data/templates/accel-ppp/l2tp.config.j2 b/data/templates/accel-ppp/l2tp.config.j2 index 9eeaf7622..5914fd375 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,10 +121,18 @@ 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 %} - +{% if ppp_ipv6_intf_id is vyos_defined %} +ipv6-intf-id={{ ppp_ipv6_intf_id }} +{% endif %} +{% if ppp_ipv6_peer_intf_id is vyos_defined %} +ipv6-peer-intf-id={{ ppp_ipv6_peer_intf_id }} +{% endif %} +ipv6-accept-peer-intf-id={{ "1" if ppp_ipv6_accept_peer_intf_id else "0" }} {% if client_ipv6_pool %} [ipv6-pool] diff --git a/data/templates/chrony/chrony.conf.j2 b/data/templates/chrony/chrony.conf.j2 new file mode 100644 index 000000000..b3bfc8c0c --- /dev/null +++ b/data/templates/chrony/chrony.conf.j2 @@ -0,0 +1,58 @@ +### Autogenerated by ntp.py ### + +# This would step the system clock if the adjustment is larger than 0.1 seconds, +# but only in the first three clock updates. +makestep 1.0 3 + +# The rtcsync directive enables a mode where the system time is periodically +# copied to the RTC and chronyd does not try to track its drift. This directive +# cannot be used with the rtcfile directive. On Linux, the RTC copy is performed +# by the kernel every 11 minutes. +rtcsync + +# This directive specifies the maximum amount of memory that chronyd is allowed +# to allocate for logging of client accesses and the state that chronyd as an +# NTP server needs to support the interleaved mode for its clients. +clientloglimit 1048576 + +driftfile /run/chrony/drift +dumpdir /run/chrony +pidfile {{ config_file | replace('.conf', '.pid') }} + +# Determine when will the next leap second occur and what is the current offset +leapsectz right/UTC + +user {{ user }} + +# NTP servers to reach out to +{% if server is vyos_defined %} +{% for server, config in server.items() %} +{% set association = 'server' %} +{% if config.pool is vyos_defined %} +{% set association = 'pool' %} +{% endif %} +{{ association }} {{ server | replace('_', '-') }} iburst {{ 'noselect' if config.noselect is vyos_defined }} {{ 'prefer' if config.prefer is vyos_defined }} +{% endfor %} +{% endif %} + +# Allowed clients configuration +{% if allow_client.address is vyos_defined %} +{% for address in allow_client.address %} +allow {{ address }} +{% endfor %} +{% endif %} +deny all + +{% if listen_address is vyos_defined or interface is vyos_defined %} +# NTP should listen on configured addresses only +{% if listen_address is vyos_defined %} +{% for address in listen_address %} +bindaddress {{ address }} +{% endfor %} +{% endif %} +{% if interface is vyos_defined %} +{% for ifname in interface %} +binddevice {{ ifname }} +{% endfor %} +{% endif %} +{% endif %} diff --git a/data/templates/ntp/override.conf.j2 b/data/templates/chrony/override.conf.j2 index 6fed9d7d2..9eaea7608 100644 --- a/data/templates/ntp/override.conf.j2 +++ b/data/templates/chrony/override.conf.j2 @@ -5,10 +5,13 @@ ConditionPathExists={{ config_file }} After=vyos-router.service [Service] +EnvironmentFile= ExecStart= -ExecStart={{ vrf_command }}/usr/sbin/ntpd -g -p {{ config_file | replace('.conf', '.pid') }} -c {{ config_file }} -u ntp:ntp +ExecStart={{ vrf_command }}/usr/sbin/chronyd -F 1 -f {{ config_file }} PIDFile= PIDFile={{ config_file | replace('.conf', '.pid') }} Restart=always RestartSec=10 +# Required for VRF support +ProtectControlGroups=No diff --git a/data/templates/container/storage.conf.j2 b/data/templates/container/storage.conf.j2 index 665f9bf95..39a072c70 100644 --- a/data/templates/container/storage.conf.j2 +++ b/data/templates/container/storage.conf.j2 @@ -1,4 +1,4 @@ ### Autogenerated by container.py ### [storage] - driver = "vfs" + driver = "overlay2" graphroot = "/usr/lib/live/mount/persistence/container/storage" diff --git a/data/templates/container/systemd-unit.j2 b/data/templates/container/systemd-unit.j2 new file mode 100644 index 000000000..fa48384ab --- /dev/null +++ b/data/templates/container/systemd-unit.j2 @@ -0,0 +1,17 @@ +### Autogenerated by container.py ### +[Unit] +Description=VyOS Container {{ name }} + +[Service] +Environment=PODMAN_SYSTEMD_UNIT=%n +Restart=on-failure +ExecStartPre=/bin/rm -f %t/%n.pid %t/%n.cid +ExecStart=/usr/bin/podman run \ + --conmon-pidfile %t/%n.pid --cidfile %t/%n.cid --cgroups=no-conmon \ + {{ run_args }} +ExecStop=/usr/bin/podman stop --ignore --cidfile %t/%n.cid -t 5 +ExecStopPost=/usr/bin/podman rm --ignore -f --cidfile %t/%n.cid +ExecStopPost=/bin/rm -f %t/%n.cid +PIDFile=%t/%n.pid +KillMode=none +Type=forking diff --git a/data/templates/dhcp-server/dhcpd.conf.j2 b/data/templates/dhcp-server/dhcpd.conf.j2 index 4c2da0aa5..639526532 100644 --- a/data/templates/dhcp-server/dhcpd.conf.j2 +++ b/data/templates/dhcp-server/dhcpd.conf.j2 @@ -22,6 +22,7 @@ ddns-update-style {{ 'interim' if dynamic_dns_update is vyos_defined else 'none' option rfc3442-static-route code 121 = array of integer 8; option windows-static-route code 249 = array of integer 8; option wpad-url code 252 = text; +option rfc8925-ipv6-only-preferred code 108 = unsigned integer 32; # Vendor specific options - Ubiquiti Networks option space ubnt; @@ -127,6 +128,9 @@ shared-network {{ network }} { {% if subnet_config.wins_server is vyos_defined %} option netbios-name-servers {{ subnet_config.wins_server | join(', ') }}; {% endif %} +{% if subnet_config.ipv6_only_preferred is vyos_defined %} + option rfc8925-ipv6-only-preferred {{ subnet_config.ipv6_only_preferred }}; +{% endif %} {% if subnet_config.static_route is vyos_defined %} {% set static_default_route = '' %} {% if subnet_config.default_router is vyos_defined %} diff --git a/data/templates/dns-forwarding/recursor.conf.j2 b/data/templates/dns-forwarding/recursor.conf.j2 index ce1b676d1..e02e6c13d 100644 --- a/data/templates/dns-forwarding/recursor.conf.j2 +++ b/data/templates/dns-forwarding/recursor.conf.j2 @@ -29,6 +29,9 @@ export-etc-hosts={{ 'no' if ignore_hosts_file is vyos_defined else 'yes' }} # listen-address local-address={{ listen_address | join(',') }} +# listen-port +local-port={{ port }} + # dnssec dnssec={{ dnssec }} diff --git a/data/templates/firewall/nftables-defines.j2 b/data/templates/firewall/nftables-defines.j2 index 5336f7ee6..0a7e79edd 100644 --- a/data/templates/firewall/nftables-defines.j2 +++ b/data/templates/firewall/nftables-defines.j2 @@ -27,6 +27,14 @@ } {% endfor %} {% endif %} +{% if group.domain_group is vyos_defined %} +{% for name, name_config in group.domain_group.items() %} + set D_{{ name }} { + type {{ ip_type }} + flags interval + } +{% endfor %} +{% endif %} {% if group.mac_group is vyos_defined %} {% for group_name, group_conf in group.mac_group.items() %} {% set includes = group_conf.include if group_conf.include is vyos_defined else [] %} @@ -77,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/firewall/nftables-nat.j2 b/data/templates/firewall/nftables-nat.j2 index 55fe6024b..f0be3cf5d 100644 --- a/data/templates/firewall/nftables-nat.j2 +++ b/data/templates/firewall/nftables-nat.j2 @@ -1,5 +1,7 @@ #!/usr/sbin/nft -f +{% import 'firewall/nftables-defines.j2' as group_tmpl %} + {% if helper_functions is vyos_defined('remove') %} {# NAT if going to be disabled - remove rules and targets from nftables #} {% set base_command = 'delete rule ip raw' %} @@ -24,6 +26,7 @@ add rule ip raw NAT_CONNTRACK counter accept {% if first_install is not vyos_defined %} delete table ip vyos_nat {% endif %} +{% if deleted is not vyos_defined %} table ip vyos_nat { # # Destination NAT rules build up here @@ -31,11 +34,11 @@ table ip vyos_nat { chain PREROUTING { type nat hook prerouting priority -100; policy accept; counter jump VYOS_PRE_DNAT_HOOK -{% if destination.rule is vyos_defined %} -{% for rule, config in destination.rule.items() if config.disable is not vyos_defined %} +{% if destination.rule is vyos_defined %} +{% for rule, config in destination.rule.items() if config.disable is not vyos_defined %} {{ config | nat_rule(rule, 'destination') }} -{% endfor %} -{% endif %} +{% endfor %} +{% endif %} } # @@ -44,11 +47,11 @@ table ip vyos_nat { chain POSTROUTING { type nat hook postrouting priority 100; policy accept; counter jump VYOS_PRE_SNAT_HOOK -{% if source.rule is vyos_defined %} -{% for rule, config in source.rule.items() if config.disable is not vyos_defined %} +{% if source.rule is vyos_defined %} +{% for rule, config in source.rule.items() if config.disable is not vyos_defined %} {{ config | nat_rule(rule, 'source') }} -{% endfor %} -{% endif %} +{% endfor %} +{% endif %} } chain VYOS_PRE_DNAT_HOOK { @@ -58,4 +61,7 @@ table ip vyos_nat { chain VYOS_PRE_SNAT_HOOK { return } + +{{ group_tmpl.groups(firewall_group, False) }} } +{% endif %} diff --git a/data/templates/firewall/nftables-policy.j2 b/data/templates/firewall/nftables-policy.j2 index 40118930b..6cb3b2f95 100644 --- a/data/templates/firewall/nftables-policy.j2 +++ b/data/templates/firewall/nftables-policy.j2 @@ -2,21 +2,24 @@ {% import 'firewall/nftables-defines.j2' as group_tmpl %} -{% if cleanup_commands is vyos_defined %} -{% for command in cleanup_commands %} -{{ command }} -{% endfor %} +{% if first_install is not vyos_defined %} +delete table ip vyos_mangle +delete table ip6 vyos_mangle {% endif %} - -table ip mangle { -{% if first_install is vyos_defined %} +table ip vyos_mangle { chain VYOS_PBR_PREROUTING { type filter hook prerouting priority -150; policy accept; +{% if route is vyos_defined %} +{% for route_text, conf in route.items() if conf.interface is vyos_defined %} + iifname { {{ ",".join(conf.interface) }} } counter jump VYOS_PBR_{{ route_text }} +{% endfor %} +{% endif %} } + chain VYOS_PBR_POSTROUTING { type filter hook postrouting priority -150; policy accept; } -{% endif %} + {% if route is vyos_defined %} {% for route_text, conf in route.items() %} chain VYOS_PBR_{{ route_text }} { @@ -32,15 +35,20 @@ table ip mangle { {{ group_tmpl.groups(firewall_group, False) }} } -table ip6 mangle { -{% if first_install is vyos_defined %} +table ip6 vyos_mangle { chain VYOS_PBR6_PREROUTING { type filter hook prerouting priority -150; policy accept; +{% if route6 is vyos_defined %} +{% for route_text, conf in route6.items() if conf.interface is vyos_defined %} + iifname { {{ ",".join(conf.interface) }} } counter jump VYOS_PBR6_{{ route_text }} +{% endfor %} +{% endif %} } + chain VYOS_PBR6_POSTROUTING { type filter hook postrouting priority -150; policy accept; } -{% endif %} + {% if route6 is vyos_defined %} {% for route_text, conf in route6.items() %} chain VYOS_PBR6_{{ route_text }} { @@ -52,5 +60,6 @@ table ip6 mangle { } {% endfor %} {% endif %} + {{ group_tmpl.groups(firewall_group, True) }} } diff --git a/data/templates/firewall/nftables-static-nat.j2 b/data/templates/firewall/nftables-static-nat.j2 index 790c33ce9..e5e3da867 100644 --- a/data/templates/firewall/nftables-static-nat.j2 +++ b/data/templates/firewall/nftables-static-nat.j2 @@ -3,6 +3,7 @@ {% if first_install is not vyos_defined %} delete table ip vyos_static_nat {% endif %} +{% if deleted is not vyos_defined %} table ip vyos_static_nat { # # Destination NAT rules build up here @@ -10,11 +11,11 @@ table ip vyos_static_nat { chain PREROUTING { type nat hook prerouting priority -100; policy accept; -{% if static.rule is vyos_defined %} -{% for rule, config in static.rule.items() if config.disable is not vyos_defined %} +{% if static.rule is vyos_defined %} +{% for rule, config in static.rule.items() if config.disable is not vyos_defined %} {{ config | nat_static_rule(rule, 'destination') }} -{% endfor %} -{% endif %} +{% endfor %} +{% endif %} } # @@ -22,10 +23,11 @@ table ip vyos_static_nat { # chain POSTROUTING { type nat hook postrouting priority 100; policy accept; -{% if static.rule is vyos_defined %} -{% for rule, config in static.rule.items() if config.disable is not vyos_defined %} +{% if static.rule is vyos_defined %} +{% for rule, config in static.rule.items() if config.disable is not vyos_defined %} {{ config | nat_static_rule(rule, 'source') }} -{% endfor %} -{% endif %} +{% endfor %} +{% endif %} } } +{% endif %} diff --git a/data/templates/firewall/nftables.j2 b/data/templates/firewall/nftables.j2 index a0f0b8c11..2c7115134 100644 --- a/data/templates/firewall/nftables.j2 +++ b/data/templates/firewall/nftables.j2 @@ -67,14 +67,12 @@ table ip vyos_filter { {{ conf | nft_default_rule(name_text) }} } {% endfor %} -{% if group is vyos_defined and group.domain_group is vyos_defined %} -{% for name, name_config in group.domain_group.items() %} - set D_{{ name }} { +{% for set_name in ip_fqdn %} + set FQDN_{{ set_name }} { type ipv4_addr flags interval } -{% endfor %} -{% endif %} +{% endfor %} {% for set_name in ns.sets %} set RECENT_{{ set_name }} { type ipv4_addr @@ -178,6 +176,12 @@ table ip6 vyos_filter { {{ conf | nft_default_rule(name_text, ipv6=True) }} } {% endfor %} +{% for set_name in ip6_fqdn %} + set FQDN_{{ set_name }} { + type ipv6_addr + flags interval + } +{% endfor %} {% for set_name in ns.sets %} set RECENT6_{{ set_name }} { type ipv6_addr diff --git a/data/templates/frr/bgpd.frr.j2 b/data/templates/frr/bgpd.frr.j2 index e8d135c78..b8692f344 100644 --- a/data/templates/frr/bgpd.frr.j2 +++ b/data/templates/frr/bgpd.frr.j2 @@ -9,6 +9,11 @@ {% if config.remote_as is vyos_defined %} neighbor {{ neighbor }} remote-as {{ config.remote_as }} {% endif %} +{% if config.local_role is vyos_defined %} +{% for role, strict in config.local_role.items() %} + neighbor {{ neighbor }} local-role {{ role }} {{ 'strict-mode' if strict }} +{% endfor %} +{% endif %} {% if config.interface.remote_as is vyos_defined %} neighbor {{ neighbor }} interface remote-as {{ config.interface.remote_as }} {% endif %} @@ -414,10 +419,14 @@ router bgp {{ system_as }} {{ 'vrf ' ~ vrf if vrf is vyos_defined }} route-target both {{ vni_config.route_target.both }} {% endif %} {% if vni_config.route_target.export is vyos_defined %} - route-target export {{ vni_config.route_target.export }} +{% for route_target in vni_config.route_target.export %} + route-target export {{ route_target }} +{% endfor %} {% endif %} {% if vni_config.route_target.import is vyos_defined %} - route-target import {{ vni_config.route_target.import }} +{% for route_target in vni_config.route_target.import %} + route-target import {{ route_target }} +{% endfor %} {% endif %} exit-vni {% endfor %} @@ -517,6 +526,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..8c4a81c57 100644 --- a/data/templates/frr/ospfd.frr.j2 +++ b/data/templates/frr/ospfd.frr.j2 @@ -84,11 +84,13 @@ router ospf {{ 'vrf ' ~ vrf if vrf is vyos_defined }} {% endfor %} {% if area_config.range is vyos_defined %} {% for range, range_config in area_config.range.items() %} -{% if range_config.cost is vyos_defined %} - area {{ area_id }} range {{ range }} cost {{ range_config.cost }} -{% endif %} {% if range_config.not_advertise is vyos_defined %} area {{ area_id }} range {{ range }} not-advertise +{% else %} + area {{ area_id }} range {{ range }} +{% endif %} +{% if range_config.cost is vyos_defined %} + area {{ area_id }} range {{ range }} cost {{ range_config.cost }} {% endif %} {% if range_config.substitute is vyos_defined %} area {{ area_id }} range {{ range }} substitute {{ range_config.substitute }} @@ -161,10 +163,16 @@ 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 %} -{% if passive_interface.default is vyos_defined %} +{% if passive_interface is vyos_defined('default') %} passive-interface default {% endif %} {% if redistribute is vyos_defined %} diff --git a/data/templates/frr/policy.frr.j2 b/data/templates/frr/policy.frr.j2 index 5ad4bd28c..9b5e80aed 100644 --- a/data/templates/frr/policy.frr.j2 +++ b/data/templates/frr/policy.frr.j2 @@ -322,6 +322,9 @@ route-map {{ route_map }} {{ rule_config.action }} {{ rule }} {% if rule_config.set.ipv6_next_hop.prefer_global is vyos_defined %} set ipv6 next-hop prefer-global {% endif %} +{% if rule_config.set.l3vpn_nexthop.encapsulation.gre is vyos_defined %} +set l3vpn next-hop encapsulation gre +{% endif %} {% if rule_config.set.large_community.replace is vyos_defined %} set large-community {{ rule_config.set.large_community.replace | join(' ') }} {% endif %} diff --git a/data/templates/high-availability/keepalived.conf.j2 b/data/templates/high-availability/keepalived.conf.j2 index 706e1c5ae..6ea5f91d0 100644 --- a/data/templates/high-availability/keepalived.conf.j2 +++ b/data/templates/high-availability/keepalived.conf.j2 @@ -2,9 +2,30 @@ # Do not edit this file, all your changes will be lost # on next commit or reboot +# Global definitions configuration block global_defs { dynamic_interfaces script_user root +{% if vrrp.global_parameters.startup_delay is vyos_defined %} + vrrp_startup_delay {{ vrrp.global_parameters.startup_delay }} +{% endif %} +{% if vrrp.global_parameters.garp is vyos_defined %} +{% if vrrp.global_parameters.garp.interval is vyos_defined %} + vrrp_garp_interval {{ vrrp.global_parameters.garp.interval }} +{% endif %} +{% if vrrp.global_parameters.garp.master_delay is vyos_defined %} + vrrp_garp_master_delay {{ vrrp.global_parameters.garp.master_delay }} +{% endif %} +{% if vrrp.global_parameters.garp.master_refresh is vyos_defined %} + vrrp_garp_master_refresh {{ vrrp.global_parameters.garp.master_refresh }} +{% endif %} +{% if vrrp.global_parameters.garp.master_refresh_repeat is vyos_defined %} + vrrp_garp_master_refresh_repeat {{ vrrp.global_parameters.garp.master_refresh_repeat }} +{% endif %} +{% if vrrp.global_parameters.garp.master_repeat is vyos_defined %} + vrrp_garp_master_repeat {{ vrrp.global_parameters.garp.master_repeat }} +{% endif %} +{% endif %} notify_fifo /run/keepalived/keepalived_notify_fifo notify_fifo_script /usr/libexec/vyos/system/keepalived-fifo.py } @@ -28,6 +49,23 @@ vrrp_instance {{ name }} { virtual_router_id {{ group_config.vrid }} priority {{ group_config.priority }} advert_int {{ group_config.advertise_interval }} +{% if group_config.garp is vyos_defined %} +{% if group_config.garp.interval is vyos_defined %} + garp_interval {{ group_config.garp.interval }} +{% endif %} +{% if group_config.garp.master_delay is vyos_defined %} + garp_master_delay {{ group_config.garp.master_delay }} +{% endif %} +{% if group_config.garp.master_repeat is vyos_defined %} + garp_master_repeat {{ group_config.garp.master_repeat }} +{% endif %} +{% if group_config.garp.master_refresh is vyos_defined %} + garp_master_refresh {{ group_config.garp.master_refresh }} +{% endif %} +{% if group_config.garp.master_refresh_repeat is vyos_defined %} + garp_master_refresh_repeat {{ group_config.garp.master_refresh_repeat }} +{% endif %} +{% endif %} {% if group_config.track.exclude_vrrp_interface is vyos_defined %} dont_track_primary {% endif %} @@ -126,7 +164,12 @@ vrrp_sync_group {{ name }} { {% if virtual_server is vyos_defined %} # Virtual-server configuration {% for vserver, vserver_config in virtual_server.items() %} +# Vserver {{ vserver }} +{% if vserver_config.port is vyos_defined %} virtual_server {{ vserver }} {{ vserver_config.port }} { +{% else %} +virtual_server fwmark {{ vserver_config.fwmark }} { +{% endif %} delay_loop {{ vserver_config.delay_loop }} {% if vserver_config.algorithm is vyos_defined('round-robin') %} lb_algo rr @@ -156,9 +199,14 @@ virtual_server {{ vserver }} {{ vserver_config.port }} { {% for rserver, rserver_config in vserver_config.real_server.items() %} real_server {{ rserver }} {{ rserver_config.port }} { weight 1 +{% if rserver_config.health_check.script is vyos_defined %} + MISC_CHECK { + misc_path {{ rserver_config.health_check.script }} +{% else %} {{ vserver_config.protocol | upper }}_CHECK { -{% if rserver_config.connection_timeout is vyos_defined %} +{% if rserver_config.connection_timeout is vyos_defined %} connect_timeout {{ rserver_config.connection_timeout }} +{% endif %} {% endif %} } } diff --git a/data/templates/https/nginx.default.j2 b/data/templates/https/nginx.default.j2 index dbb08e187..753c3a5c9 100644 --- a/data/templates/https/nginx.default.j2 +++ b/data/templates/https/nginx.default.j2 @@ -34,7 +34,7 @@ server { ssl_protocols TLSv1.2 TLSv1.3; # proxy settings for HTTP API, if enabled; 503, if not - location ~ /(retrieve|configure|config-file|image|generate|show|reset|docs|openapi.json|redoc|graphql) { + location ~ /(retrieve|configure|config-file|image|container-image|generate|show|reset|docs|openapi.json|redoc|graphql) { {% if server.api %} {% if server.api.socket %} proxy_pass http://unix:/run/api.sock; diff --git a/data/templates/iproute2/static.conf.j2 b/data/templates/iproute2/static.conf.j2 new file mode 100644 index 000000000..10c9bdab7 --- /dev/null +++ b/data/templates/iproute2/static.conf.j2 @@ -0,0 +1,8 @@ +# Generated by VyOS (protocols_static.py), do not edit by hand +{% if table is vyos_defined %} +{% for t, t_options in table.items() %} +{% if t_options.description is vyos_defined %} +{{ "%-6s" | format(t) }} {{ "%-40s" | format(t_options.description) }} +{% endif %} +{% endfor %} +{% endif %} diff --git a/data/templates/vrf/vrf.conf.j2 b/data/templates/iproute2/vrf.conf.j2 index d31d23574..d31d23574 100644 --- a/data/templates/vrf/vrf.conf.j2 +++ b/data/templates/iproute2/vrf.conf.j2 diff --git a/data/templates/ipsec/charon/eap-radius.conf.j2 b/data/templates/ipsec/charon/eap-radius.conf.j2 index 8495011fe..364377473 100644 --- a/data/templates/ipsec/charon/eap-radius.conf.j2 +++ b/data/templates/ipsec/charon/eap-radius.conf.j2 @@ -49,8 +49,10 @@ eap-radius { # Base to use for calculating exponential back off. # retransmit_base = 1.4 +{% if remote_access.radius.timeout is vyos_defined %} # Timeout in seconds before sending first retransmit. - # retransmit_timeout = 2.0 + retransmit_timeout = {{ remote_access.radius.timeout | float }} +{% endif %} # Number of times to retransmit a packet before giving up. # retransmit_tries = 4 diff --git a/data/templates/ipsec/swanctl.conf.j2 b/data/templates/ipsec/swanctl.conf.j2 index 38d7981c6..d44d0f5e4 100644 --- a/data/templates/ipsec/swanctl.conf.j2 +++ b/data/templates/ipsec/swanctl.conf.j2 @@ -58,23 +58,7 @@ secrets { {% if site_to_site.peer is vyos_defined %} {% for peer, peer_conf in site_to_site.peer.items() if peer not in dhcp_no_address and peer_conf.disable is not vyos_defined %} {% set peer_name = peer.replace("@", "") | dot_colon_to_dash %} -{% if peer_conf.authentication.mode is vyos_defined('pre-shared-secret') %} - ike_{{ peer_name }} { -{% if peer_conf.local_address is vyos_defined %} - id-local = {{ peer_conf.local_address }} # dhcp:{{ peer_conf.dhcp_interface if 'dhcp_interface' in peer_conf else 'no' }} -{% endif %} -{% for address in peer_conf.remote_address %} - id-remote_{{ address | dot_colon_to_dash }} = {{ address }} -{% endfor %} -{% if peer_conf.authentication.local_id is vyos_defined %} - id-localid = {{ peer_conf.authentication.local_id }} -{% endif %} -{% if peer_conf.authentication.remote_id is vyos_defined %} - id-remoteid = {{ peer_conf.authentication.remote_id }} -{% endif %} - secret = "{{ peer_conf.authentication.pre_shared_secret }}" - } -{% elif peer_conf.authentication.mode is vyos_defined('x509') %} +{% if peer_conf.authentication.mode is vyos_defined('x509') %} private_{{ peer_name }} { file = {{ peer_conf.authentication.x509.certificate }}.pem {% if peer_conf.authentication.x509.passphrase is vyos_defined %} @@ -91,6 +75,21 @@ secrets { {% endif %} {% endfor %} {% endif %} +{% if authentication.psk is vyos_defined %} +{% for psk, psk_config in authentication.psk.items() %} + ike-{{ psk }} { +{% if psk_config.id is vyos_defined %} + # ID's from auth psk <tag> id xxx +{% for id in psk_config.id %} +{% set gen_uuid = '' | generate_uuid4 %} + id-{{ gen_uuid }} = "{{ id }}" +{% endfor %} +{% endif %} + secret = "{{ psk_config.secret }}" + } +{% endfor %} +{% endif %} + {% if remote_access.connection is vyos_defined %} {% for ra, ra_conf in remote_access.connection.items() if ra_conf.disable is not vyos_defined %} {% if ra_conf.authentication.server_mode is vyos_defined('pre-shared-secret') %} @@ -130,4 +129,3 @@ secrets { {% endif %} {% endif %} } - diff --git a/data/templates/ipsec/swanctl/peer.j2 b/data/templates/ipsec/swanctl/peer.j2 index d097a04fc..9d95271fe 100644 --- a/data/templates/ipsec/swanctl/peer.j2 +++ b/data/templates/ipsec/swanctl/peer.j2 @@ -45,11 +45,7 @@ {% endif %} } remote { -{% if peer_conf.authentication.remote_id is vyos_defined %} id = "{{ peer_conf.authentication.remote_id }}" -{% else %} - id = "{{ peer }}" -{% endif %} auth = {{ 'psk' if peer_conf.authentication.mode == 'pre-shared-secret' else 'pubkey' }} {% if peer_conf.authentication.mode == 'rsa' %} pubkeys = {{ peer_conf.authentication.rsa.remote_key }}.pem @@ -124,7 +120,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/ntp/ntpd.conf.j2 b/data/templates/ntp/ntpd.conf.j2 deleted file mode 100644 index 8921826fa..000000000 --- a/data/templates/ntp/ntpd.conf.j2 +++ /dev/null @@ -1,49 +0,0 @@ -### Autogenerated by ntp.py ### - -# -# Non-configurable defaults -# -driftfile /var/lib/ntp/ntp.drift -# By default, only allow ntpd to query time sources, ignore any incoming requests -restrict default noquery nopeer notrap nomodify -# Allow pool associations -restrict source nomodify notrap noquery -# Local users have unrestricted access, allowing reconfiguration via ntpdc -restrict 127.0.0.1 -restrict -6 ::1 - -# -# Configurable section -# -{% if server is vyos_defined %} -{% for server, config in server.items() %} -{% set association = 'server' %} -{% if config.pool is vyos_defined %} -{% set association = 'pool' %} -{% endif %} -{{ association }} {{ server | replace('_', '-') }} iburst {{ 'noselect' if config.noselect is vyos_defined }} {{ 'preempt' if config.preempt is vyos_defined }} {{ 'prefer' if config.prefer is vyos_defined }} -{% endfor %} -{% endif %} - -{% if allow_clients.address is vyos_defined %} -# Allowed clients configuration -restrict default ignore -{% for address in allow_clients.address %} -restrict {{ address | address_from_cidr }} mask {{ address | netmask_from_cidr }} nomodify notrap nopeer -{% endfor %} -{% endif %} - -{% if listen_address is vyos_defined or interface is vyos_defined %} -# NTP should listen on configured addresses only -interface ignore wildcard -{% if listen_address is vyos_defined %} -{% for address in listen_address %} -interface listen {{ address }} -{% endfor %} -{% endif %} -{% if interface is vyos_defined %} -{% for ifname in interface %} -interface listen {{ ifname }} -{% endfor %} -{% endif %} -{% endif %} diff --git a/data/templates/ocserv/ocserv_config.j2 b/data/templates/ocserv/ocserv_config.j2 index 3194354e6..aa1073bca 100644 --- a/data/templates/ocserv/ocserv_config.j2 +++ b/data/templates/ocserv/ocserv_config.j2 @@ -10,6 +10,10 @@ udp-port = {{ listen_ports.udp }} run-as-user = nobody run-as-group = daemon +{% if accounting.mode.radius is vyos_defined %} +acct = "radius [config=/run/ocserv/radiusclient.conf]" +{% endif %} + {% if "radius" in authentication.mode %} auth = "radius [config=/run/ocserv/radiusclient.conf{{ ',groupconfig=true' if authentication.radius.groupconfig is vyos_defined else '' }}]" {% elif "local" in authentication.mode %} diff --git a/data/templates/ocserv/radius_conf.j2 b/data/templates/ocserv/radius_conf.j2 index b6612fee5..1ab322f69 100644 --- a/data/templates/ocserv/radius_conf.j2 +++ b/data/templates/ocserv/radius_conf.j2 @@ -1,20 +1,34 @@ ### generated by vpn_openconnect.py ### nas-identifier VyOS -{% for srv in server %} -{% if not "disable" in server[srv] %} -{% if "port" in server[srv] %} -authserver {{ srv }}:{{ server[srv]["port"] }} + +#### Accounting +{% if accounting.mode.radius is vyos_defined %} +{% for acctsrv, srv_conf in accounting.radius.server.items() if 'disable' not in srv_conf %} +{% if srv_conf.port is vyos_defined %} +acctserver {{ acctsrv }}:{{ srv_conf.port }} {% else %} -authserver {{ srv }} +acctserver {{ acctsrv }} {% endif %} -{% endif %} -{% endfor %} -radius_timeout {{ timeout }} -{% if source_address %} -bindaddr {{ source_address }} -{% else %} +{% endfor %} +{% endif %} + +#### Authentication +{% if authentication.mode.radius is vyos_defined %} +{% for authsrv, srv_conf in authentication.radius.server.items() if 'disable' not in srv_conf %} +{% if srv_conf.port is vyos_defined %} +authserver {{ authsrv }}:{{ srv_conf.port }} +{% else %} +authserver {{ authsrv }} +{% endif %} +{% endfor %} +radius_timeout {{ authentication['radius']['timeout'] }} +{% if source_address %} +bindaddr {{ authentication['radius']['source_address'] }} +{% else %} bindaddr * +{% endif %} {% endif %} + servers /run/ocserv/radius_servers dictionary /etc/radcli/dictionary default_realm diff --git a/data/templates/pppoe/peer.j2 b/data/templates/pppoe/peer.j2 index 6221abb9b..f433a9b03 100644 --- a/data/templates/pppoe/peer.j2 +++ b/data/templates/pppoe/peer.j2 @@ -36,10 +36,13 @@ maxfail 0 plugin rp-pppoe.so {{ source_interface }} {% if access_concentrator is vyos_defined %} -rp_pppoe_ac '{{ access_concentrator }}' +pppoe-ac "{{ access_concentrator }}" {% endif %} {% if service_name is vyos_defined %} -rp_pppoe_service '{{ service_name }}' +pppoe-service "{{ service_name }}" +{% endif %} +{% if host_uniq is vyos_defined %} +pppoe-host-uniq "{{ host_uniq }}" {% endif %} persist 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..4ef4751dd 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..793facc3f 100644 --- a/data/templates/snmp/etc.snmpd.conf.j2 +++ b/data/templates/snmp/etc.snmpd.conf.j2 @@ -26,6 +26,9 @@ monitor -r 10 -e linkDownTrap "Generate linkDown" ifOperStatus == 2 # interface (with different ifIndex) - this is the case on e.g. ppp interfaces interface_replace_old yes +# T4902: exclude container storage from monitoring +ignoreDisk /usr/lib/live/mount/persistence/container + ######################## # configurable section # ######################## @@ -59,27 +62,47 @@ agentaddress unix:/run/snmpd.socket{{ ',' ~ options | join(',') if options is vy {% if comm_config.client is vyos_defined %} {% for client in comm_config.client %} {% if client | is_ipv4 %} -{{ comm_config.authorization }}community {{ comm }} {{ client }} +{{ comm_config.authorization }}community {{ comm }} {{ client }} -V RESTRICTED {% elif client | is_ipv6 %} -{{ comm_config.authorization }}community6 {{ comm }} {{ client }} +{{ comm_config.authorization }}community6 {{ comm }} {{ client }} -V RESTRICTED {% endif %} {% endfor %} {% endif %} {% if comm_config.network is vyos_defined %} {% for network in comm_config.network %} {% if network | is_ipv4 %} -{{ comm_config.authorization }}community {{ comm }} {{ network }} -{% elif client | is_ipv6 %} -{{ comm_config.authorization }}community6 {{ comm }} {{ network }} +{{ comm_config.authorization }}community {{ comm }} {{ network }} -V RESTRICTED +{% elif network | is_ipv6 %} +{{ comm_config.authorization }}community6 {{ comm }} {{ network }} -V RESTRICTED {% 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 }}community {{ comm }} -V RESTRICTED +{{ comm_config.authorization }}community6 {{ comm }} -V RESTRICTED {% endif %} {% endfor %} {% endif %} +# Default RESTRICTED view +view RESTRICTED included .1 80 +{% if 'ip-route-table' not in oid_enable %} +# ipRouteTable oid: excluded +view RESTRICTED excluded .1.3.6.1.2.1.4.21 +{% endif %} +{% if 'ip-net-to-media-table' not in oid_enable %} +# ipNetToMediaTable oid: excluded +view RESTRICTED excluded .1.3.6.1.2.1.4.22 +{% endif %} +{% if 'ip-net-to-physical-phys-address' not in oid_enable %} +# ipNetToPhysicalPhysAddress oid: excluded +view RESTRICTED excluded .1.3.6.1.2.1.4.35 +{% endif %} +{% if 'ip-forward' not in oid_enable %} +# ipForward oid: excluded +view RESTRICTED excluded .1.3.6.1.2.1.4.24 +{% endif %} + {% if contact is vyos_defined %} # system contact information SysContact {{ contact }} diff --git a/data/templates/snmp/override.conf.j2 b/data/templates/snmp/override.conf.j2 index 5d787de86..443ee64db 100644 --- a/data/templates/snmp/override.conf.j2 +++ b/data/templates/snmp/override.conf.j2 @@ -1,5 +1,4 @@ {% set vrf_command = 'ip vrf exec ' ~ vrf ~ ' ' if vrf is vyos_defined else '' %} -{% set oid_route_table = ' ' if oid_enable is vyos_defined('route-table') else '-I -ipCidrRouteTable,inetCidrRouteTable' %} [Unit] StartLimitIntervalSec=0 After=vyos-router.service @@ -8,7 +7,7 @@ After=vyos-router.service Environment= Environment="MIBDIRS=/usr/share/snmp/mibs:/usr/share/snmp/mibs/iana:/usr/share/snmp/mibs/ietf:/usr/share/vyos/mibs" ExecStart= -ExecStart={{ vrf_command }}/usr/sbin/snmpd -LS0-5d -Lf /dev/null -u Debian-snmp -g Debian-snmp {{ oid_route_table }} -f -p /run/snmpd.pid +ExecStart={{ vrf_command }}/usr/sbin/snmpd -LS0-5d -Lf /dev/null -u Debian-snmp -g Debian-snmp -f -p /run/snmpd.pid Restart=always RestartSec=10 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/ssh/sshd_config.j2 b/data/templates/ssh/sshd_config.j2 index 5bbfdeb88..93735020c 100644 --- a/data/templates/ssh/sshd_config.j2 +++ b/data/templates/ssh/sshd_config.j2 @@ -62,6 +62,11 @@ ListenAddress {{ address }} Ciphers {{ ciphers | join(',') }} {% endif %} +{% if hostkey_algorithm is vyos_defined %} +# Specifies the available Host Key signature algorithms +HostKeyAlgorithms {{ hostkey_algorithm | join(',') }} +{% endif %} + {% if mac is vyos_defined %} # Specifies the available MAC (message authentication code) algorithms MACs {{ mac | join(',') }} diff --git a/data/templates/sstp-client/peer.j2 b/data/templates/sstp-client/peer.j2 new file mode 100644 index 000000000..7a0b0e1f7 --- /dev/null +++ b/data/templates/sstp-client/peer.j2 @@ -0,0 +1,53 @@ +### 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 + +# We don't need the server to auth itself +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 + +# pppd should create a UUCP-style lock file for the serial device to ensure +# exclusive access to the device. By default, pppd will not create a lock file. +lock + +# Disables Deflate compression +nodeflate + +{% 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/data/templates/system/ssh_config.j2 b/data/templates/system/ssh_config.j2 index 1449f95b1..d3ede0971 100644 --- a/data/templates/system/ssh_config.j2 +++ b/data/templates/system/ssh_config.j2 @@ -1,3 +1,6 @@ {% if ssh_client.source_address is vyos_defined %} BindAddress {{ ssh_client.source_address }} {% endif %} +{% if ssh_client.source_interface is vyos_defined %} +BindInterface {{ ssh_client.source_interface }} +{% endif %} diff --git a/data/templates/telegraf/telegraf.j2 b/data/templates/telegraf/telegraf.j2 index 36571ce98..c9f402281 100644 --- a/data/templates/telegraf/telegraf.j2 +++ b/data/templates/telegraf/telegraf.j2 @@ -102,7 +102,7 @@ dirs = ["/proc/sys/net/ipv4/netfilter","/proc/sys/net/netfilter"] [[inputs.ethtool]] interface_include = {{ interfaces_ethernet }} -[[inputs.ntpq]] +[[inputs.chrony]] dns_lookup = true [[inputs.internal]] [[inputs.nstat]] diff --git a/debian/control b/debian/control index 1f2151284..1e593d378 100644 --- a/debian/control +++ b/debian/control @@ -9,6 +9,7 @@ Build-Depends: gcc-multilib [amd64], clang [amd64], llvm [amd64], + libbpf-dev [amd64], libelf-dev (>= 0.2) [amd64], libpcap-dev [amd64], build-essential, @@ -24,6 +25,7 @@ Build-Depends: python3-setuptools, python3-sphinx, python3-xmltodict, + python3-pyhumps, quilt, whois Standards-Version: 3.9.6 @@ -66,7 +68,7 @@ Depends: ipaddrcheck, iperf, iperf3, - iproute2, + iproute2 (>= 6.0.0), iputils-arping, isc-dhcp-client, isc-dhcp-relay, @@ -76,6 +78,7 @@ Depends: lcdproc, lcdproc-extra-drivers, libatomic1, + libbpf0 [amd64], libcharon-extra-plugins (>=5.9), libcharon-extauth-plugins (>=5.9), libndp-tools, @@ -98,8 +101,7 @@ Depends: nfct, nftables (>= 0.9.3), nginx-light, - ntp, - ntpdate, + chrony, nvme-cli, ocserv, opennhrp, @@ -129,6 +131,7 @@ Depends: python3-netifaces, python3-paramiko, python3-psutil, + python3-pyhumps, python3-pystache, python3-pyudev, python3-six, @@ -150,8 +153,10 @@ Depends: squidguard, sshguard, ssl-cert, + sstp-client, strongswan (>= 5.9), strongswan-swanctl (>= 5.9), + stunnel4, sudo, systemd, telegraf (>= 1.20), @@ -191,6 +196,7 @@ Description: VyOS configuration scripts and data for VMware Package: vyos-1x-smoketest Architecture: all Depends: + skopeo, snmp, vyos-1x Description: VyOS build sanity checking toolkit diff --git a/debian/vyos-1x-smoketest.postinst b/debian/vyos-1x-smoketest.postinst new file mode 100755 index 000000000..18612804c --- /dev/null +++ b/debian/vyos-1x-smoketest.postinst @@ -0,0 +1,10 @@ +#!/bin/sh -e + +BUSYBOX_TAG="docker.io/library/busybox:stable" +OUTPUT_PATH="/usr/share/vyos/busybox-stable.tar" + +if [[ -f $OUTPUT_PATH ]]; then + rm -f $OUTPUT_PATH +fi + +skopeo copy --additional-tag "$BUSYBOX_TAG" "docker://$BUSYBOX_TAG" "docker-archive:/$OUTPUT_PATH" diff --git a/debian/vyos-1x.install b/debian/vyos-1x.install index edd090993..6b5c7793d 100644 --- a/debian/vyos-1x.install +++ b/debian/vyos-1x.install @@ -1,8 +1,10 @@ +etc/commit etc/dhcp etc/ipsec.d etc/logrotate.d etc/netplug etc/opennhrp +etc/modprobe.d etc/ppp etc/rsyslog.d etc/securetty @@ -17,6 +19,7 @@ etc/vyos lib/ opt/ usr/sbin +usr/bin/config-mgmt usr/bin/initial-setup usr/bin/vyos-config-file-query usr/bin/vyos-config-to-commands diff --git a/debian/vyos-1x.postinst b/debian/vyos-1x.postinst index 031e91595..d5f5cbbc7 100644 --- a/debian/vyos-1x.postinst +++ b/debian/vyos-1x.postinst @@ -21,13 +21,13 @@ if ! grep -q '^openvpn' /etc/passwd; then adduser --quiet --firstuid 100 --system --group --shell /usr/sbin/nologin openvpn fi -# Add 2FA support for SSH -sudo grep -qF -- "auth required pam_google_authenticator.so nullok" "/etc/pam.d/sshd" || \ -sudo sed -i '/^@include common-auth/a # Check OTP 2FA, if configured for the user\nauth required pam_google_authenticator.so nullok' /etc/pam.d/sshd - -# Add 2FA support for local authentication -sudo grep -qF -- "auth required pam_google_authenticator.so nullok" "/etc/pam.d/login" || \ -sudo sed -i '/^@include common-auth/a # Check OTP 2FA, if configured for the user\nauth required pam_google_authenticator.so nullok' /etc/pam.d/login +# Enable 2FA/MFA support for SSH and local logins +for file in /etc/pam.d/sshd /etc/pam.d/login +do + PAM_CONFIG="auth required pam_google_authenticator.so nullok" + grep -qF -- "${PAM_CONFIG}" $file || \ + sed -i "/^@include common-auth/a # Check 2FA/MFA authentication token if enabled (per user)\n${PAM_CONFIG}" $file +done # Add RADIUS operator user for RADIUS authenticated users to map to if ! grep -q '^radius_user' /etc/passwd; then @@ -103,7 +103,8 @@ DELETE="/etc/logrotate.d/conntrackd.distrib /etc/init.d/conntrackd /etc/default/ /etc/default/pmacctd /etc/pmacct /etc/networks_list /etc/networks_whitelist /etc/fastnetmon.conf /etc/ntp.conf /etc/default/ssh - /etc/powerdns /etc/default/pdns-recursor" + /etc/powerdns /etc/default/pdns-recursor + /etc/ppp/ip-up.d/0000usepeerdns /etc/ppp/ip-down.d/0000usepeerdns" for tmp in $DELETE; do if [ -e ${tmp} ]; then rm -rf ${tmp} diff --git a/interface-definitions/container.xml.in b/interface-definitions/container.xml.in index 51171d881..b61664125 100644 --- a/interface-definitions/container.xml.in +++ b/interface-definitions/container.xml.in @@ -111,7 +111,7 @@ </leafNode> <leafNode name="memory"> <properties> - <help>Constrain the memory available to a container</help> + <help>Memory (RAM) available to this container</help> <valueHelp> <format>u32:0</format> <description>Unlimited</description> @@ -127,6 +127,24 @@ </properties> <defaultValue>512</defaultValue> </leafNode> + <leafNode name="shared-memory"> + <properties> + <help>Shared memory available to this container</help> + <valueHelp> + <format>u32:0</format> + <description>Unlimited</description> + </valueHelp> + <valueHelp> + <format>u32:1-8192</format> + <description>Container memory in megabytes (MB)</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 0-8192"/> + </constraint> + <constraintErrorMessage>Container memory must be in range 0 to 8192 MB</constraintErrorMessage> + </properties> + <defaultValue>64</defaultValue> + </leafNode> <tagNode name="network"> <properties> <help>Attach user defined network to container</help> @@ -189,14 +207,23 @@ </leafNode> <leafNode name="protocol"> <properties> - <help>Protocol tcp/udp</help> + <help>Transport protocol used for port mapping</help> <completionHelp> <list>tcp udp</list> </completionHelp> + <valueHelp> + <format>tcp</format> + <description>Use Transmission Control Protocol for given port</description> + </valueHelp> + <valueHelp> + <format>udp</format> + <description>Use User Datagram Protocol for given port</description> + </valueHelp> <constraint> <regex>(tcp|udp)</regex> </constraint> </properties> + <defaultValue>tcp</defaultValue> </leafNode> </children> </tagNode> @@ -247,6 +274,26 @@ </valueHelp> </properties> </leafNode> + <leafNode name="mode"> + <properties> + <help>Volume access mode ro/rw</help> + <completionHelp> + <list>ro rw</list> + </completionHelp> + <valueHelp> + <format>ro</format> + <description>Volume mounted into the container as read-only</description> + </valueHelp> + <valueHelp> + <format>rw</format> + <description>Volume mounted into the container as read-write</description> + </valueHelp> + <constraint> + <regex>(ro|rw)</regex> + </constraint> + </properties> + <defaultValue>rw</defaultValue> + </leafNode> </children> </tagNode> </children> @@ -254,6 +301,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/dhcp-server.xml.in b/interface-definitions/dhcp-server.xml.in index 6e1592200..1830cc1ad 100644 --- a/interface-definitions/dhcp-server.xml.in +++ b/interface-definitions/dhcp-server.xml.in @@ -373,6 +373,19 @@ </leafNode> </children> </tagNode > + <leafNode name="ipv6-only-preferred"> + <properties> + <help>Disable IPv4 on IPv6 only hosts (RFC 8925)</help> + <valueHelp> + <format>u32</format> + <description>Seconds</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 0-4294967295"/> + </constraint> + <constraintErrorMessage>Seconds must be between 0 and 4294967295 (49 days)</constraintErrorMessage> + </properties> + </leafNode> <leafNode name="subnet-parameters"> <properties> <help>Additional subnet parameters for DHCP server. You must use the syntax of dhcpd.conf in this text-field. Using this without proper knowledge may result in a crashed DHCP server. Check system log to look for errors.</help> 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/dns-forwarding.xml.in b/interface-definitions/dns-forwarding.xml.in index 3de0dc0eb..409028572 100644 --- a/interface-definitions/dns-forwarding.xml.in +++ b/interface-definitions/dns-forwarding.xml.in @@ -605,6 +605,10 @@ </properties> </leafNode> #include <include/listen-address.xml.i> + #include <include/port-number.xml.i> + <leafNode name="port"> + <defaultValue>53</defaultValue> + </leafNode> <leafNode name="negative-ttl"> <properties> <help>Maximum amount of time negative entries are cached</help> diff --git a/interface-definitions/firewall.xml.in b/interface-definitions/firewall.xml.in index 673461036..7d7e0a38f 100644 --- a/interface-definitions/firewall.xml.in +++ b/interface-definitions/firewall.xml.in @@ -126,7 +126,7 @@ <description>Domain address to match</description> </valueHelp> <constraint> - <regex>[a-z0-9]+([\-\.]{1}[a-z0-9]+)*\.[a-z]{2,99}?(\/.*)?</regex> + <validator name="fqdn"/> </constraint> <multi/> </properties> @@ -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> @@ -218,7 +247,7 @@ <properties> <help>Mac-group member</help> <valueHelp> - <format><MAC address></format> + <format>macaddr</format> <description>MAC address to match</description> </valueHelp> <constraint> @@ -408,9 +437,11 @@ </properties> <children> #include <include/firewall/address-ipv6.xml.i> + #include <include/firewall/fqdn.xml.i> #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"> @@ -419,15 +450,18 @@ </properties> <children> #include <include/firewall/address-ipv6.xml.i> + #include <include/firewall/fqdn.xml.i> #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> #include <include/firewall/dscp.xml.i> #include <include/firewall/packet-length.xml.i> #include <include/firewall/hop-limit.xml.i> + #include <include/firewall/connection-mark.xml.i> <node name="icmpv6"> <properties> <help>ICMPv6 type and code information</help> @@ -572,9 +606,11 @@ </properties> <children> #include <include/firewall/address.xml.i> + #include <include/firewall/fqdn.xml.i> #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"> @@ -583,14 +619,17 @@ </properties> <children> #include <include/firewall/address.xml.i> + #include <include/firewall/fqdn.xml.i> #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> #include <include/firewall/dscp.xml.i> #include <include/firewall/packet-length.xml.i> + #include <include/firewall/connection-mark.xml.i> <node name="icmp"> <properties> <help>ICMP type and code information</help> @@ -656,6 +695,25 @@ </properties> <defaultValue>disable</defaultValue> </leafNode> + <leafNode name="resolver-cache"> + <properties> + <help>Retains last successful value if domain resolution fails</help> + <valueless/> + </properties> + </leafNode> + <leafNode name="resolver-interval"> + <properties> + <help>Domain resolver update interval</help> + <valueHelp> + <format>u32:10-3600</format> + <description>Interval (seconds)</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 10-3600"/> + </constraint> + </properties> + <defaultValue>300</defaultValue> + </leafNode> <leafNode name="send-redirects"> <properties> <help>Policy for sending IPv4 ICMP redirect messages</help> diff --git a/interface-definitions/high-availability.xml.in b/interface-definitions/high-availability.xml.in index 0631acdda..6cb40247a 100644 --- a/interface-definitions/high-availability.xml.in +++ b/interface-definitions/high-availability.xml.in @@ -11,12 +11,33 @@ <help>Virtual Router Redundancy Protocol settings</help> </properties> <children> + <node name="global-parameters"> + <properties> + <help>VRRP global parameters</help> + </properties> + <children> + #include <include/vrrp/garp.xml.i> + <leafNode name="startup-delay"> + <properties> + <help>Time VRRP startup process (in seconds)</help> + <valueHelp> + <format>u32:1-600</format> + <description>Interval in seconds</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 1-600"/> + </constraint> + </properties> + </leafNode> + </children> + </node> <tagNode name="group"> <properties> <help>VRRP group</help> </properties> <children> #include <include/generic-interface-broadcast.xml.i> + #include <include/vrrp/garp.xml.i> <leafNode name="advertise-interval"> <properties> <help>Advertise interval</help> @@ -199,7 +220,7 @@ <description>Interface name</description> </valueHelp> <constraint> - <validator name="interface-name"/> + #include <include/constraint/interface-name.xml.in> </constraint> <multi/> </properties> @@ -211,16 +232,15 @@ <properties> <help>Virtual IP address</help> <valueHelp> - <format>ipv4</format> - <description>IPv4 virtual address</description> + <format>ipv4net</format> + <description>IPv4 address and prefix length</description> </valueHelp> <valueHelp> - <format>ipv6</format> - <description>IPv6 virtual address</description> + <format>ipv6net</format> + <description>IPv6 address and prefix length</description> </valueHelp> <constraint> - <validator name="ipv4-host"/> - <validator name="ipv6-host"/> + <validator name="ip-host"/> </constraint> </properties> <children> @@ -365,7 +385,8 @@ </properties> <defaultValue>nat</defaultValue> </leafNode> - #include <include/port-number.xml.i> + #include <include/firewall/fwmark.xml.i> + #include <include/port-number-start-zero.xml.i> <leafNode name="persistence-timeout"> <properties> <help>Timeout for persistent connections</help> @@ -404,7 +425,7 @@ <help>Real server address</help> </properties> <children> - #include <include/port-number.xml.i> + #include <include/port-number-start-zero.xml.i> <leafNode name="connection-timeout"> <properties> <help>Server connection timeout</help> @@ -417,6 +438,21 @@ </constraint> </properties> </leafNode> + <node name="health-check"> + <properties> + <help>Health check script</help> + </properties> + <children> + <leafNode name="script"> + <properties> + <help>Health check script file</help> + <constraint> + <validator name="script"/> + </constraint> + </properties> + </leafNode> + </children> + </node> </children> </tagNode> </children> diff --git a/interface-definitions/https.xml.in b/interface-definitions/https.xml.in index d096c4ff1..6adb07598 100644 --- a/interface-definitions/https.xml.in +++ b/interface-definitions/https.xml.in @@ -107,7 +107,7 @@ <valueless/> </properties> </leafNode> - <node name="gql"> + <node name="graphql"> <properties> <help>GraphQL support</help> </properties> @@ -118,6 +118,59 @@ <valueless/> </properties> </leafNode> + <node name="authentication"> + <properties> + <help>GraphQL authentication</help> + </properties> + <children> + <leafNode name="type"> + <properties> + <help>Authentication type</help> + <completionHelp> + <list>key token</list> + </completionHelp> + <valueHelp> + <format>key</format> + <description>Use API keys</description> + </valueHelp> + <valueHelp> + <format>token</format> + <description>Use JWT token</description> + </valueHelp> + <constraint> + <regex>(key|token)</regex> + </constraint> + </properties> + <defaultValue>key</defaultValue> + </leafNode> + <leafNode name="expiration"> + <properties> + <help>Token time to expire in seconds</help> + <valueHelp> + <format>u32:60-31536000</format> + <description>Token lifetime in seconds</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 60-31536000"/> + </constraint> + </properties> + <defaultValue>3600</defaultValue> + </leafNode> + <leafNode name="secret-length"> + <properties> + <help>Length of shared secret in bytes</help> + <valueHelp> + <format>u32:16-65535</format> + <description>Byte length of generated shared secret</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 16-65535"/> + </constraint> + </properties> + <defaultValue>32</defaultValue> + </leafNode> + </children> + </node> </children> </node> <node name="cors"> diff --git a/interface-definitions/include/accel-ppp/ppp-options-ipv6-interface-id.xml.i b/interface-definitions/include/accel-ppp/ppp-options-ipv6-interface-id.xml.i new file mode 100644 index 000000000..265f7f97c --- /dev/null +++ b/interface-definitions/include/accel-ppp/ppp-options-ipv6-interface-id.xml.i @@ -0,0 +1,54 @@ +<!-- include start from accel-ppp/ppp-options-ipv6-interface-id.xml.i --> +<leafNode name="ipv6-intf-id"> + <properties> + <help>Fixed or random interface identifier for IPv6</help> + <completionHelp> + <list>random</list> + </completionHelp> + <valueHelp> + <format>random</format> + <description>Random interface identifier for IPv6</description> + </valueHelp> + <valueHelp> + <format>x:x:x:x</format> + <description>specify interface identifier for IPv6</description> + </valueHelp> + <constraint> + <regex>(random|((\d+){1,4}:){3}(\d+){1,4})</regex> + </constraint> + </properties> +</leafNode> +<leafNode name="ipv6-peer-intf-id"> + <properties> + <help>Peer interface identifier for IPv6</help> + <completionHelp> + <list>random calling-sid ipv4</list> + </completionHelp> + <valueHelp> + <format>x:x:x:x</format> + <description>Interface identifier for IPv6</description> + </valueHelp> + <valueHelp> + <format>random</format> + <description>Use a random interface identifier for IPv6</description> + </valueHelp> + <valueHelp> + <format>ipv4</format> + <description>Calculate interface identifier from IPv4 address, for example 192:168:0:1</description> + </valueHelp> + <valueHelp> + <format>calling-sid</format> + <description>Calculate interface identifier from calling-station-id</description> + </valueHelp> + <constraint> + <regex>(random|calling-sid|ipv4|((\d+){1,4}:){3}(\d+){1,4})</regex> + </constraint> + </properties> +</leafNode> +<leafNode name="ipv6-accept-peer-intf-id"> + <properties> + <help>Accept peer interface identifier</help> + <valueless/> + </properties> +</leafNode> +<!-- include end --> 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-local-role.xml.i b/interface-definitions/include/bgp/neighbor-local-role.xml.i new file mode 100644 index 000000000..6ddb4908f --- /dev/null +++ b/interface-definitions/include/bgp/neighbor-local-role.xml.i @@ -0,0 +1,42 @@ +<!-- include start from bgp/neigbhor-local-role.xml.i --> +<tagNode name="local-role"> + <properties> + <help>Local role for BGP neighbor (RFC9234)</help> + <completionHelp> + <list>customer peer provider rs-client rs-server</list> + </completionHelp> + <valueHelp> + <format>customer</format> + <description>Using Transit</description> + </valueHelp> + <valueHelp> + <format>peer</format> + <description>Public/Private Peering</description> + </valueHelp> + <valueHelp> + <format>provider</format> + <description>Providing Transit</description> + </valueHelp> + <valueHelp> + <format>rs-client</format> + <description>RS Client</description> + </valueHelp> + <valueHelp> + <format>rs-server</format> + <description>Route Server</description> + </valueHelp> + <constraint> + <regex>(provider|rs-server|rs-client|customer|peer)</regex> + </constraint> + <constraintErrorMessage>BGP local-role must be one of the following: customer, peer, provider, rs-client or rs-server</constraintErrorMessage> + </properties> + <children> + <leafNode name="strict"> + <properties> + <help>Neighbor must send this exact capability, otherwise a role missmatch notification will be sent</help> + <valueless/> + </properties> + </leafNode> + </children> +</tagNode> +<!-- include end --> 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..ec065347c 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> @@ -987,6 +987,7 @@ </children> </node> #include <include/bgp/neighbor-local-as.xml.i> + #include <include/bgp/neighbor-local-role.xml.i> #include <include/bgp/neighbor-override-capability.xml.i> #include <include/bgp/neighbor-passive.xml.i> #include <include/bgp/neighbor-password.xml.i> @@ -1431,6 +1432,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> @@ -1497,6 +1504,7 @@ #include <include/bgp/neighbor-graceful-restart.xml.i> #include <include/bgp/neighbor-graceful-restart.xml.i> #include <include/bgp/neighbor-local-as.xml.i> + #include <include/bgp/neighbor-local-role.xml.i> #include <include/bgp/neighbor-override-capability.xml.i> #include <include/bgp/neighbor-passive.xml.i> #include <include/bgp/neighbor-password.xml.i> 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-multi.xml.i b/interface-definitions/include/dhcp-interface-multi.xml.i new file mode 100644 index 000000000..c74751a19 --- /dev/null +++ b/interface-definitions/include/dhcp-interface-multi.xml.i @@ -0,0 +1,18 @@ +<!-- include start from dhcp-interface-multi.xml.i --> +<leafNode name="dhcp-interface"> + <properties> + <help>DHCP interface supplying next-hop IP address</help> + <completionHelp> + <script>${vyos_completion_dir}/list_interfaces.py</script> + </completionHelp> + <valueHelp> + <format>txt</format> + <description>DHCP interface name</description> + </valueHelp> + <constraint> + #include <include/constraint/interface-name.xml.in> + </constraint> + <multi/> + </properties> +</leafNode> +<!-- include end -->
\ No newline at end of file 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 a4f66f5cb..3fe3ca872 100644 --- a/interface-definitions/include/firewall/common-rule.xml.i +++ b/interface-definitions/include/firewall/common-rule.xml.i @@ -1,6 +1,14 @@ <!-- include start from firewall/common-rule.xml.i --> #include <include/firewall/action.xml.i> #include <include/generic-description.xml.i> +<node name="destination"> + <properties> + <help>Destination parameters</help> + </properties> + <children> + #include <include/firewall/mac-address.xml.i> + </children> +</node> <leafNode name="disable"> <properties> <help>Option to disable firewall rule</help> @@ -26,14 +34,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 +146,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> @@ -219,22 +227,7 @@ <children> #include <include/firewall/address.xml.i> #include <include/firewall/source-destination-group.xml.i> - <leafNode name="mac-address"> - <properties> - <help>Source MAC address</help> - <valueHelp> - <format><MAC address></format> - <description>MAC address to match</description> - </valueHelp> - <valueHelp> - <format>!<MAC address></format> - <description>Match everything except the specified MAC address</description> - </valueHelp> - <constraint> - <validator name="mac-address-firewall"/> - </constraint> - </properties> - </leafNode> + #include <include/firewall/mac-address.xml.i> #include <include/firewall/port.xml.i> </children> </node> diff --git a/interface-definitions/include/firewall/connection-mark.xml.i b/interface-definitions/include/firewall/connection-mark.xml.i new file mode 100644 index 000000000..2cb826635 --- /dev/null +++ b/interface-definitions/include/firewall/connection-mark.xml.i @@ -0,0 +1,15 @@ +<!-- include start from firewall/connection-mark.xml.i --> +<leafNode name="connection-mark"> + <properties> + <help>Connection mark</help> + <valueHelp> + <format>u32:1-2147483647</format> + <description>Connection-mark to match</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 1-2147483647"/> + </constraint> + <multi/> + </properties> +</leafNode> +<!-- include end --> diff --git a/interface-definitions/include/firewall/fqdn.xml.i b/interface-definitions/include/firewall/fqdn.xml.i new file mode 100644 index 000000000..9eb3925b5 --- /dev/null +++ b/interface-definitions/include/firewall/fqdn.xml.i @@ -0,0 +1,14 @@ +<!-- include start from firewall/fqdn.xml.i --> +<leafNode name="fqdn"> + <properties> + <help>Fully qualified domain name</help> + <valueHelp> + <format><fqdn></format> + <description>Fully qualified domain name</description> + </valueHelp> + <constraint> + <validator name="fqdn"/> + </constraint> + </properties> +</leafNode> +<!-- include end --> diff --git a/interface-definitions/include/firewall/fwmark.xml.i b/interface-definitions/include/firewall/fwmark.xml.i new file mode 100644 index 000000000..4607ef58f --- /dev/null +++ b/interface-definitions/include/firewall/fwmark.xml.i @@ -0,0 +1,14 @@ +<!-- include start from firewall/fwmark.xml.i --> +<leafNode name="fwmark"> + <properties> + <help>Match fwmark value</help> + <valueHelp> + <format>u32:1-2147483647</format> + <description>Match firewall mark value</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 1-2147483647"/> + </constraint> + </properties> +</leafNode> +<!-- include end --> 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/mac-address.xml.i b/interface-definitions/include/firewall/mac-address.xml.i new file mode 100644 index 000000000..db3e1e312 --- /dev/null +++ b/interface-definitions/include/firewall/mac-address.xml.i @@ -0,0 +1,19 @@ +<!-- include start from firewall/mac-address.xml.i --> +<leafNode name="mac-address"> + <properties> + <help>MAC address</help> + <valueHelp> + <format>macaddr</format> + <description>MAC address to match</description> + </valueHelp> + <valueHelp> + <format>!macaddr</format> + <description>Match everything except the specified MAC address</description> + </valueHelp> + <constraint> + <validator name="mac-address"/> + <validator name="mac-address-exclude"/> + </constraint> + </properties> +</leafNode> +<!-- include end --> 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/firewall/rule-log-level.xml.i b/interface-definitions/include/firewall/rule-log-level.xml.i index 10c8de5e3..3ac473844 100644 --- a/interface-definitions/include/firewall/rule-log-level.xml.i +++ b/interface-definitions/include/firewall/rule-log-level.xml.i @@ -1,4 +1,4 @@ -<!-- include start from firewall/common-rule.xml.i --> +<!-- include start from firewall/rule-log-level.xml.i --> <leafNode name="log-level"> <properties> <help>Set log-level. Log must be enable.</help> diff --git a/interface-definitions/include/firewall/source-destination-group-ipv6.xml.i b/interface-definitions/include/firewall/source-destination-group-ipv6.xml.i index c2cc7edb3..2a42d236c 100644 --- a/interface-definitions/include/firewall/source-destination-group-ipv6.xml.i +++ b/interface-definitions/include/firewall/source-destination-group-ipv6.xml.i @@ -12,6 +12,14 @@ </completionHelp> </properties> </leafNode> + <leafNode name="domain-group"> + <properties> + <help>Group of domains</help> + <completionHelp> + <path>firewall group domain-group</path> + </completionHelp> + </properties> + </leafNode> #include <include/firewall/mac-group.xml.i> <leafNode name="network-group"> <properties> diff --git a/interface-definitions/include/generic-description.xml.i b/interface-definitions/include/generic-description.xml.i index 03fc564e6..63e5e174e 100644 --- a/interface-definitions/include/generic-description.xml.i +++ b/interface-definitions/include/generic-description.xml.i @@ -6,6 +6,10 @@ <format>txt</format> <description>Description</description> </valueHelp> + <constraint> + <regex>[[:ascii:]]{0,256}</regex> + </constraint> + <constraintErrorMessage>Description too long (limit 256 characters)</constraintErrorMessage> </properties> </leafNode> <!-- include end --> 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/description.xml.i b/interface-definitions/include/interface/description.xml.i deleted file mode 100644 index de01d22ca..000000000 --- a/interface-definitions/include/interface/description.xml.i +++ /dev/null @@ -1,11 +0,0 @@ -<!-- include start from interface/description.xml.i --> -<leafNode name="description"> - <properties> - <help>Interface specific description</help> - <constraint> - <regex>.{1,256}</regex> - </constraint> - <constraintErrorMessage>Description too long (limit 256 characters)</constraintErrorMessage> - </properties> -</leafNode> -<!-- include end --> diff --git a/interface-definitions/include/interface/interface-policy-vif-c.xml.i b/interface-definitions/include/interface/interface-policy-vif-c.xml.i deleted file mode 100644 index 866fcd5c0..000000000 --- a/interface-definitions/include/interface/interface-policy-vif-c.xml.i +++ /dev/null @@ -1,26 +0,0 @@ -<!-- include start from interface/interface-policy-vif-c.xml.i --> -<node name="policy" owner="${vyos_conf_scripts_dir}/policy-route-interface.py $VAR(../../../@).$VAR(../../@).$VAR(../@)"> - <properties> - <priority>620</priority> - <help>Policy route options</help> - </properties> - <children> - <leafNode name="route"> - <properties> - <help>IPv4 policy route ruleset for interface</help> - <completionHelp> - <path>policy route</path> - </completionHelp> - </properties> - </leafNode> - <leafNode name="route6"> - <properties> - <help>IPv6 policy route ruleset for interface</help> - <completionHelp> - <path>policy route6</path> - </completionHelp> - </properties> - </leafNode> - </children> -</node> -<!-- include end --> diff --git a/interface-definitions/include/interface/interface-policy-vif.xml.i b/interface-definitions/include/interface/interface-policy-vif.xml.i deleted file mode 100644 index 83510fe59..000000000 --- a/interface-definitions/include/interface/interface-policy-vif.xml.i +++ /dev/null @@ -1,26 +0,0 @@ -<!-- include start from interface/interface-policy-vif.xml.i --> -<node name="policy" owner="${vyos_conf_scripts_dir}/policy-route-interface.py $VAR(../../@).$VAR(../@)"> - <properties> - <priority>620</priority> - <help>Policy route options</help> - </properties> - <children> - <leafNode name="route"> - <properties> - <help>IPv4 policy route ruleset for interface</help> - <completionHelp> - <path>policy route</path> - </completionHelp> - </properties> - </leafNode> - <leafNode name="route6"> - <properties> - <help>IPv6 policy route ruleset for interface</help> - <completionHelp> - <path>policy route6</path> - </completionHelp> - </properties> - </leafNode> - </children> -</node> -<!-- include end --> diff --git a/interface-definitions/include/interface/interface-policy.xml.i b/interface-definitions/include/interface/interface-policy.xml.i deleted file mode 100644 index 42a8fd009..000000000 --- a/interface-definitions/include/interface/interface-policy.xml.i +++ /dev/null @@ -1,26 +0,0 @@ -<!-- include start from interface/interface-policy.xml.i --> -<node name="policy" owner="${vyos_conf_scripts_dir}/policy-route-interface.py $VAR(../@)"> - <properties> - <priority>620</priority> - <help>Policy route options</help> - </properties> - <children> - <leafNode name="route"> - <properties> - <help>IPv4 policy route ruleset for interface</help> - <completionHelp> - <path>policy route</path> - </completionHelp> - </properties> - </leafNode> - <leafNode name="route6"> - <properties> - <help>IPv6 policy route ruleset for interface</help> - <completionHelp> - <path>policy route6</path> - </completionHelp> - </properties> - </leafNode> - </children> -</node> -<!-- include end --> diff --git a/interface-definitions/include/interface/mirror.xml.i b/interface-definitions/include/interface/mirror.xml.i index 2959551f0..74a172b50 100644 --- a/interface-definitions/include/interface/mirror.xml.i +++ b/interface-definitions/include/interface/mirror.xml.i @@ -1,23 +1,31 @@ <!-- include start from interface/mirror.xml.i --> <node name="mirror"> <properties> - <help>Incoming/outgoing packet mirroring destination</help> + <help>Mirror ingress/egress packets</help> </properties> <children> <leafNode name="ingress"> <properties> - <help>Mirror the ingress traffic of the interface to the destination interface</help> + <help>Mirror ingress traffic to destination interface</help> <completionHelp> - <script>${vyos_completion_dir}/list_interfaces.py</script> + <script>${vyos_completion_dir}/list_interfaces.py</script> </completionHelp> + <valueHelp> + <format>txt</format> + <description>Destination interface name</description> + </valueHelp> </properties> </leafNode> <leafNode name="egress"> <properties> - <help>Mirror the egress traffic of the interface to the destination interface</help> + <help>Mirror egress traffic to destination interface</help> <completionHelp> - <script>${vyos_completion_dir}/list_interfaces.py</script> + <script>${vyos_completion_dir}/list_interfaces.py</script> </completionHelp> + <valueHelp> + <format>txt</format> + <description>Destination interface name</description> + </valueHelp> </properties> </leafNode> </children> 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..b01e486ce 100644 --- a/interface-definitions/include/interface/redirect.xml.i +++ b/interface-definitions/include/interface/redirect.xml.i @@ -1,16 +1,16 @@ <!-- include start from interface/redirect.xml.i --> <leafNode name="redirect"> <properties> - <help>Incoming packet redirection destination</help> + <help>Redirect incoming packet to destination</help> <completionHelp> <script>${vyos_completion_dir}/list_interfaces.py</script> </completionHelp> <valueHelp> <format>txt</format> - <description>Interface name</description> + <description>Destination 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/vif-s.xml.i b/interface-definitions/include/interface/vif-s.xml.i index 916349ade..fdd62b63d 100644 --- a/interface-definitions/include/interface/vif-s.xml.i +++ b/interface-definitions/include/interface/vif-s.xml.i @@ -12,13 +12,12 @@ <constraintErrorMessage>VLAN ID must be between 0 and 4094</constraintErrorMessage> </properties> <children> + #include <include/generic-description.xml.i> #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-link-detect.xml.i> #include <include/interface/disable.xml.i> - #include <include/interface/interface-policy-vif.xml.i> <leafNode name="protocol"> <properties> <help>Protocol used for service VLAN (default: 802.1ad)</help> @@ -54,8 +53,8 @@ <constraintErrorMessage>VLAN ID must be between 0 and 4094</constraintErrorMessage> </properties> <children> + #include <include/generic-description.xml.i> #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-link-detect.xml.i> @@ -67,7 +66,6 @@ #include <include/interface/mtu-68-16000.xml.i> #include <include/interface/redirect.xml.i> #include <include/interface/vrf.xml.i> - #include <include/interface/interface-policy-vif-c.xml.i> </children> </tagNode> #include <include/interface/redirect.xml.i> diff --git a/interface-definitions/include/interface/vif.xml.i b/interface-definitions/include/interface/vif.xml.i index 73a8c98ff..ec3921bf6 100644 --- a/interface-definitions/include/interface/vif.xml.i +++ b/interface-definitions/include/interface/vif.xml.i @@ -12,13 +12,12 @@ <constraintErrorMessage>VLAN ID must be between 0 and 4094</constraintErrorMessage> </properties> <children> + #include <include/generic-description.xml.i> #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-link-detect.xml.i> #include <include/interface/disable.xml.i> - #include <include/interface/interface-policy-vif.xml.i> <leafNode name="egress-qos"> <properties> <help>VLAN egress QoS</help> diff --git a/interface-definitions/include/listen-address-ipv4-single.xml.i b/interface-definitions/include/listen-address-ipv4-single.xml.i new file mode 100644 index 000000000..81e947953 --- /dev/null +++ b/interface-definitions/include/listen-address-ipv4-single.xml.i @@ -0,0 +1,17 @@ +<!-- include start from listen-address-ipv4-single.xml.i --> +<leafNode name="listen-address"> + <properties> + <help>Local IPv4 addresses to listen on</help> + <completionHelp> + <script>${vyos_completion_dir}/list_local_ips.sh --ipv4</script> + </completionHelp> + <valueHelp> + <format>ipv4</format> + <description>IPv4 address to listen for incoming connections</description> + </valueHelp> + <constraint> + <validator name="ipv4-address"/> + </constraint> + </properties> +</leafNode> +<!-- include end --> 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/nat-rule.xml.i b/interface-definitions/include/nat-rule.xml.i index 84941aa6a..8f2029388 100644 --- a/interface-definitions/include/nat-rule.xml.i +++ b/interface-definitions/include/nat-rule.xml.i @@ -20,6 +20,7 @@ <children> #include <include/nat-address.xml.i> #include <include/nat-port.xml.i> + #include <include/firewall/source-destination-group.xml.i> </children> </node> #include <include/generic-disable-node.xml.i> @@ -285,6 +286,7 @@ <children> #include <include/nat-address.xml.i> #include <include/nat-port.xml.i> + #include <include/firewall/source-destination-group.xml.i> </children> </node> </children> 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/policy/route-common-rule-ipv6.xml.i b/interface-definitions/include/policy/route-common-rule-ipv6.xml.i deleted file mode 100644 index 662206336..000000000 --- a/interface-definitions/include/policy/route-common-rule-ipv6.xml.i +++ /dev/null @@ -1,557 +0,0 @@ -<!-- include start from policy/route-common-rule.xml.i --> -#include <include/policy/route-rule-action.xml.i> -#include <include/generic-description.xml.i> -<leafNode name="disable"> - <properties> - <help>Option to disable firewall rule</help> - <valueless/> - </properties> -</leafNode> -<node name="fragment"> - <properties> - <help>IP fragment match</help> - </properties> - <children> - <leafNode name="match-frag"> - <properties> - <help>Second and further fragments of fragmented packets</help> - <valueless/> - </properties> - </leafNode> - <leafNode name="match-non-frag"> - <properties> - <help>Head fragments or unfragmented packets</help> - <valueless/> - </properties> - </leafNode> - </children> -</node> -<node name="ipsec"> - <properties> - <help>Inbound IPsec packets</help> - </properties> - <children> - <leafNode name="match-ipsec"> - <properties> - <help>Inbound IPsec packets</help> - <valueless/> - </properties> - </leafNode> - <leafNode name="match-none"> - <properties> - <help>Inbound non-IPsec packets</help> - <valueless/> - </properties> - </leafNode> - </children> -</node> -<node name="limit"> - <properties> - <help>Rate limit using a token bucket filter</help> - </properties> - <children> - <leafNode name="burst"> - <properties> - <help>Maximum number of packets to allow in excess of rate</help> - <valueHelp> - <format>u32:0-4294967295</format> - <description>Maximum number of packets to allow in excess of rate</description> - </valueHelp> - <constraint> - <validator name="numeric" argument="--range 0-4294967295"/> - </constraint> - </properties> - </leafNode> - <leafNode name="rate"> - <properties> - <help>Maximum average matching rate</help> - <valueHelp> - <format>u32:0-4294967295</format> - <description>Maximum average matching rate</description> - </valueHelp> - <constraint> - <validator name="numeric" argument="--range 0-4294967295"/> - </constraint> - </properties> - </leafNode> - </children> -</node> -<leafNode name="log"> - <properties> - <help>Option to log packets matching rule</help> - <completionHelp> - <list>enable disable</list> - </completionHelp> - <valueHelp> - <format>enable</format> - <description>Enable log</description> - </valueHelp> - <valueHelp> - <format>disable</format> - <description>Disable log</description> - </valueHelp> - <constraint> - <regex>(enable|disable)</regex> - </constraint> - </properties> -</leafNode> -<leafNode name="protocol"> - <properties> - <help>Protocol to match (protocol name, number, or "all")</help> - <completionHelp> - <script>cat /etc/protocols | sed -e '/^#.*/d' | awk '{ print $1 }'</script> - </completionHelp> - <valueHelp> - <format>all</format> - <description>All IP protocols</description> - </valueHelp> - <valueHelp> - <format>tcp_udp</format> - <description>Both TCP and UDP</description> - </valueHelp> - <valueHelp> - <format>0-255</format> - <description>IP protocol number</description> - </valueHelp> - <valueHelp> - <format>!<protocol></format> - <description>IP protocol number</description> - </valueHelp> - <constraint> - <validator name="ip-protocol"/> - </constraint> - </properties> - <defaultValue>all</defaultValue> -</leafNode> -<node name="recent"> - <properties> - <help>Parameters for matching recently seen sources</help> - </properties> - <children> - <leafNode name="count"> - <properties> - <help>Source addresses seen more than N times</help> - <valueHelp> - <format>u32:1-255</format> - <description>Source addresses seen more than N times</description> - </valueHelp> - <constraint> - <validator name="numeric" argument="--range 1-255"/> - </constraint> - </properties> - </leafNode> - <leafNode name="time"> - <properties> - <help>Source addresses seen in the last N seconds</help> - <valueHelp> - <format>u32:0-4294967295</format> - <description>Source addresses seen in the last N seconds</description> - </valueHelp> - <constraint> - <validator name="numeric" argument="--range 0-4294967295"/> - </constraint> - </properties> - </leafNode> - </children> -</node> -<node name="set"> - <properties> - <help>Packet modifications</help> - </properties> - <children> - <leafNode name="dscp"> - <properties> - <help>Packet Differentiated Services Codepoint (DSCP)</help> - <valueHelp> - <format>u32:0-63</format> - <description>DSCP number</description> - </valueHelp> - <constraint> - <validator name="numeric" argument="--range 0-63"/> - </constraint> - </properties> - </leafNode> - <leafNode name="mark"> - <properties> - <help>Packet marking</help> - <valueHelp> - <format>u32:1-2147483647</format> - <description>Packet marking</description> - </valueHelp> - <constraint> - <validator name="numeric" argument="--range 1-2147483647"/> - </constraint> - </properties> - </leafNode> - <leafNode name="table"> - <properties> - <help>Routing table to forward packet with</help> - <valueHelp> - <format>u32:1-200</format> - <description>Table number</description> - </valueHelp> - <valueHelp> - <format>main</format> - <description>Main table</description> - </valueHelp> - <constraint> - <validator name="numeric" argument="--range 1-200"/> - <regex>(main)</regex> - </constraint> - <completionHelp> - <list>main</list> - <path>protocols static table</path> - </completionHelp> - </properties> - </leafNode> - <leafNode name="tcp-mss"> - <properties> - <help>TCP Maximum Segment Size</help> - <valueHelp> - <format>u32:500-1460</format> - <description>Explicitly set TCP MSS value</description> - </valueHelp> - <constraint> - <validator name="numeric" argument="--range 500-1460"/> - </constraint> - </properties> - </leafNode> - </children> -</node> -<node name="source"> - <properties> - <help>Source parameters</help> - </properties> - <children> - #include <include/firewall/address-ipv6.xml.i> - #include <include/firewall/source-destination-group.xml.i> - <leafNode name="mac-address"> - <properties> - <help>Source MAC address</help> - <valueHelp> - <format><MAC address></format> - <description>MAC address to match</description> - </valueHelp> - <valueHelp> - <format>!<MAC address></format> - <description>Match everything except the specified MAC address</description> - </valueHelp> - <constraint> - <validator name="mac-address-firewall"/> - </constraint> - </properties> - </leafNode> - #include <include/firewall/port.xml.i> - </children> -</node> -<node name="state"> - <properties> - <help>Session state</help> - </properties> - <children> - <leafNode name="established"> - <properties> - <help>Established state</help> - <completionHelp> - <list>enable disable</list> - </completionHelp> - <valueHelp> - <format>enable</format> - <description>Enable</description> - </valueHelp> - <valueHelp> - <format>disable</format> - <description>Disable</description> - </valueHelp> - <constraint> - <regex>(enable|disable)</regex> - </constraint> - </properties> - </leafNode> - <leafNode name="invalid"> - <properties> - <help>Invalid state</help> - <completionHelp> - <list>enable disable</list> - </completionHelp> - <valueHelp> - <format>enable</format> - <description>Enable</description> - </valueHelp> - <valueHelp> - <format>disable</format> - <description>Disable</description> - </valueHelp> - <constraint> - <regex>(enable|disable)</regex> - </constraint> - </properties> - </leafNode> - <leafNode name="new"> - <properties> - <help>New state</help> - <completionHelp> - <list>enable disable</list> - </completionHelp> - <valueHelp> - <format>enable</format> - <description>Enable</description> - </valueHelp> - <valueHelp> - <format>disable</format> - <description>Disable</description> - </valueHelp> - <constraint> - <regex>(enable|disable)</regex> - </constraint> - </properties> - </leafNode> - <leafNode name="related"> - <properties> - <help>Related state</help> - <completionHelp> - <list>enable disable</list> - </completionHelp> - <valueHelp> - <format>enable</format> - <description>Enable</description> - </valueHelp> - <valueHelp> - <format>disable</format> - <description>Disable</description> - </valueHelp> - <constraint> - <regex>(enable|disable)</regex> - </constraint> - </properties> - </leafNode> - </children> -</node> -#include <include/firewall/tcp-flags.xml.i> -<node name="time"> - <properties> - <help>Time to match rule</help> - </properties> - <children> - <leafNode name="monthdays"> - <properties> - <help>Monthdays to match rule on</help> - </properties> - </leafNode> - <leafNode name="startdate"> - <properties> - <help>Date to start matching rule</help> - </properties> - </leafNode> - <leafNode name="starttime"> - <properties> - <help>Time of day to start matching rule</help> - </properties> - </leafNode> - <leafNode name="stopdate"> - <properties> - <help>Date to stop matching rule</help> - </properties> - </leafNode> - <leafNode name="stoptime"> - <properties> - <help>Time of day to stop matching rule</help> - </properties> - </leafNode> - <leafNode name="utc"> - <properties> - <help>Interpret times for startdate, stopdate, starttime and stoptime to be UTC</help> - <valueless/> - </properties> - </leafNode> - <leafNode name="weekdays"> - <properties> - <help>Weekdays to match rule on</help> - </properties> - </leafNode> - </children> -</node> -<node name="icmpv6"> - <properties> - <help>ICMPv6 type and code information</help> - </properties> - <children> - <leafNode name="type"> - <properties> - <help>ICMP type-name</help> - <completionHelp> - <list>any echo-reply pong destination-unreachable network-unreachable host-unreachable protocol-unreachable port-unreachable fragmentation-needed source-route-failed network-unknown host-unknown network-prohibited host-prohibited TOS-network-unreachable TOS-host-unreachable communication-prohibited host-precedence-violation precedence-cutoff source-quench redirect network-redirect host-redirect TOS-network-redirect TOS host-redirect echo-request ping router-advertisement router-solicitation time-exceeded ttl-exceeded ttl-zero-during-transit ttl-zero-during-reassembly parameter-problem ip-header-bad required-option-missing timestamp-request timestamp-reply address-mask-request address-mask-reply packet-too-big</list> - </completionHelp> - <valueHelp> - <format>any</format> - <description>Any ICMP type/code</description> - </valueHelp> - <valueHelp> - <format>echo-reply</format> - <description>ICMP type/code name</description> - </valueHelp> - <valueHelp> - <format>pong</format> - <description>ICMP type/code name</description> - </valueHelp> - <valueHelp> - <format>destination-unreachable</format> - <description>ICMP type/code name</description> - </valueHelp> - <valueHelp> - <format>network-unreachable</format> - <description>ICMP type/code name</description> - </valueHelp> - <valueHelp> - <format>host-unreachable</format> - <description>ICMP type/code name</description> - </valueHelp> - <valueHelp> - <format>protocol-unreachable</format> - <description>ICMP type/code name</description> - </valueHelp> - <valueHelp> - <format>port-unreachable</format> - <description>ICMP type/code name</description> - </valueHelp> - <valueHelp> - <format>fragmentation-needed</format> - <description>ICMP type/code name</description> - </valueHelp> - <valueHelp> - <format>source-route-failed</format> - <description>ICMP type/code name</description> - </valueHelp> - <valueHelp> - <format>network-unknown</format> - <description>ICMP type/code name</description> - </valueHelp> - <valueHelp> - <format>host-unknown</format> - <description>ICMP type/code name</description> - </valueHelp> - <valueHelp> - <format>network-prohibited</format> - <description>ICMP type/code name</description> - </valueHelp> - <valueHelp> - <format>host-prohibited</format> - <description>ICMP type/code name</description> - </valueHelp> - <valueHelp> - <format>TOS-network-unreachable</format> - <description>ICMP type/code name</description> - </valueHelp> - <valueHelp> - <format>TOS-host-unreachable</format> - <description>ICMP type/code name</description> - </valueHelp> - <valueHelp> - <format>communication-prohibited</format> - <description>ICMP type/code name</description> - </valueHelp> - <valueHelp> - <format>host-precedence-violation</format> - <description>ICMP type/code name</description> - </valueHelp> - <valueHelp> - <format>precedence-cutoff</format> - <description>ICMP type/code name</description> - </valueHelp> - <valueHelp> - <format>source-quench</format> - <description>ICMP type/code name</description> - </valueHelp> - <valueHelp> - <format>redirect</format> - <description>ICMP type/code name</description> - </valueHelp> - <valueHelp> - <format>network-redirect</format> - <description>ICMP type/code name</description> - </valueHelp> - <valueHelp> - <format>host-redirect</format> - <description>ICMP type/code name</description> - </valueHelp> - <valueHelp> - <format>TOS-network-redirect</format> - <description>ICMP type/code name</description> - </valueHelp> - <valueHelp> - <format>TOS host-redirect</format> - <description>ICMP type/code name</description> - </valueHelp> - <valueHelp> - <format>echo-request</format> - <description>ICMP type/code name</description> - </valueHelp> - <valueHelp> - <format>ping</format> - <description>ICMP type/code name</description> - </valueHelp> - <valueHelp> - <format>router-advertisement</format> - <description>ICMP type/code name</description> - </valueHelp> - <valueHelp> - <format>router-solicitation</format> - <description>ICMP type/code name</description> - </valueHelp> - <valueHelp> - <format>time-exceeded</format> - <description>ICMP type/code name</description> - </valueHelp> - <valueHelp> - <format>ttl-exceeded</format> - <description>ICMP type/code name</description> - </valueHelp> - <valueHelp> - <format>ttl-zero-during-transit</format> - <description>ICMP type/code name</description> - </valueHelp> - <valueHelp> - <format>ttl-zero-during-reassembly</format> - <description>ICMP type/code name</description> - </valueHelp> - <valueHelp> - <format>parameter-problem</format> - <description>ICMP type/code name</description> - </valueHelp> - <valueHelp> - <format>ip-header-bad</format> - <description>ICMP type/code name</description> - </valueHelp> - <valueHelp> - <format>required-option-missing</format> - <description>ICMP type/code name</description> - </valueHelp> - <valueHelp> - <format>timestamp-request</format> - <description>ICMP type/code name</description> - </valueHelp> - <valueHelp> - <format>timestamp-reply</format> - <description>ICMP type/code name</description> - </valueHelp> - <valueHelp> - <format>address-mask-request</format> - <description>ICMP type/code name</description> - </valueHelp> - <valueHelp> - <format>address-mask-reply</format> - <description>ICMP type/code name</description> - </valueHelp> - <valueHelp> - <format>packet-too-big</format> - <description>ICMP type/code name</description> - </valueHelp> - <constraint> - <regex>(any|echo-reply|pong|destination-unreachable|network-unreachable|host-unreachable|protocol-unreachable|port-unreachable|fragmentation-needed|source-route-failed|network-unknown|host-unknown|network-prohibited|host-prohibited|TOS-network-unreachable|TOS-host-unreachable|communication-prohibited|host-precedence-violation|precedence-cutoff|source-quench|redirect|network-redirect|host-redirect|TOS-network-redirect|TOS host-redirect|echo-request|ping|router-advertisement|router-solicitation|time-exceeded|ttl-exceeded|ttl-zero-during-transit|ttl-zero-during-reassembly|parameter-problem|ip-header-bad|required-option-missing|timestamp-request|timestamp-reply|address-mask-request|address-mask-reply|packet-too-big)</regex> - <validator name="numeric" argument="--range 0-255"/> - </constraint> - </properties> - </leafNode> - </children> -</node> -<!-- include end --> diff --git a/interface-definitions/include/policy/route-common-rule.xml.i b/interface-definitions/include/policy/route-common.xml.i index 35fccca50..6973d7a8f 100644 --- a/interface-definitions/include/policy/route-common-rule.xml.i +++ b/interface-definitions/include/policy/route-common.xml.i @@ -1,406 +1,360 @@ -<!-- include start from policy/route-common-rule.xml.i --> -#include <include/policy/route-rule-action.xml.i> -#include <include/generic-description.xml.i> -<leafNode name="disable"> - <properties> - <help>Option to disable firewall rule</help> - <valueless/> - </properties> -</leafNode> -<node name="fragment"> - <properties> - <help>IP fragment match</help> - </properties> - <children> - <leafNode name="match-frag"> - <properties> - <help>Second and further fragments of fragmented packets</help> - <valueless/> - </properties> - </leafNode> - <leafNode name="match-non-frag"> - <properties> - <help>Head fragments or unfragmented packets</help> - <valueless/> - </properties> - </leafNode> - </children> -</node> -<node name="ipsec"> - <properties> - <help>Inbound IPsec packets</help> - </properties> - <children> - <leafNode name="match-ipsec"> - <properties> - <help>Inbound IPsec packets</help> - <valueless/> - </properties> - </leafNode> - <leafNode name="match-none"> - <properties> - <help>Inbound non-IPsec packets</help> - <valueless/> - </properties> - </leafNode> - </children> -</node> -<node name="limit"> - <properties> - <help>Rate limit using a token bucket filter</help> - </properties> - <children> - <leafNode name="burst"> - <properties> - <help>Maximum number of packets to allow in excess of rate</help> - <valueHelp> - <format>u32:0-4294967295</format> - <description>Maximum number of packets to allow in excess of rate</description> - </valueHelp> - <constraint> - <validator name="numeric" argument="--range 0-4294967295"/> - </constraint> - </properties> - </leafNode> - <leafNode name="rate"> - <properties> - <help>Maximum average matching rate</help> - <valueHelp> - <format>u32:0-4294967295</format> - <description>Maximum average matching rate</description> - </valueHelp> - <constraint> - <validator name="numeric" argument="--range 0-4294967295"/> - </constraint> - </properties> - </leafNode> - </children> -</node> -<leafNode name="log"> - <properties> - <help>Option to log packets matching rule</help> - <completionHelp> - <list>enable disable</list> - </completionHelp> - <valueHelp> - <format>enable</format> - <description>Enable log</description> - </valueHelp> - <valueHelp> - <format>disable</format> - <description>Disable log</description> - </valueHelp> - <constraint> - <regex>(enable|disable)</regex> - </constraint> - </properties> -</leafNode> -<leafNode name="protocol"> - <properties> - <help>Protocol to match (protocol name, number, or "all")</help> - <completionHelp> - <script>cat /etc/protocols | sed -e '/^#.*/d' | awk '{ print $1 }'</script> - </completionHelp> - <valueHelp> - <format>all</format> - <description>All IP protocols</description> - </valueHelp> - <valueHelp> - <format>tcp_udp</format> - <description>Both TCP and UDP</description> - </valueHelp> - <valueHelp> - <format>0-255</format> - <description>IP protocol number</description> - </valueHelp> - <valueHelp> - <format>!<protocol></format> - <description>IP protocol number</description> - </valueHelp> - <constraint> - <validator name="ip-protocol"/> - </constraint> - </properties> - <defaultValue>all</defaultValue> -</leafNode> -<node name="recent"> - <properties> - <help>Parameters for matching recently seen sources</help> - </properties> - <children> - <leafNode name="count"> - <properties> - <help>Source addresses seen more than N times</help> - <valueHelp> - <format>u32:1-255</format> - <description>Source addresses seen more than N times</description> - </valueHelp> - <constraint> - <validator name="numeric" argument="--range 1-255"/> - </constraint> - </properties> - </leafNode> - <leafNode name="time"> - <properties> - <help>Source addresses seen in the last N seconds</help> - <valueHelp> - <format>u32:0-4294967295</format> - <description>Source addresses seen in the last N seconds</description> - </valueHelp> - <constraint> - <validator name="numeric" argument="--range 0-4294967295"/> - </constraint> - </properties> - </leafNode> - </children> -</node> -<node name="set"> - <properties> - <help>Packet modifications</help> - </properties> - <children> - <leafNode name="dscp"> - <properties> - <help>Packet Differentiated Services Codepoint (DSCP)</help> - <valueHelp> - <format>u32:0-63</format> - <description>DSCP number</description> - </valueHelp> - <constraint> - <validator name="numeric" argument="--range 0-63"/> - </constraint> - </properties> - </leafNode> - <leafNode name="mark"> - <properties> - <help>Packet marking</help> - <valueHelp> - <format>u32:1-2147483647</format> - <description>Packet marking</description> - </valueHelp> - <constraint> - <validator name="numeric" argument="--range 1-2147483647"/> - </constraint> - </properties> - </leafNode> - <leafNode name="table"> - <properties> - <help>Routing table to forward packet with</help> - <valueHelp> - <format>u32:1-200</format> - <description>Table number</description> - </valueHelp> - <valueHelp> - <format>main</format> - <description>Main table</description> - </valueHelp> - <constraint> - <validator name="numeric" argument="--range 1-200"/> - <regex>(main)</regex> - </constraint> - <completionHelp> - <list>main</list> - <path>protocols static table</path> - </completionHelp> - </properties> - </leafNode> - <leafNode name="tcp-mss"> - <properties> - <help>TCP Maximum Segment Size</help> - <valueHelp> - <format>u32:500-1460</format> - <description>Explicitly set TCP MSS value</description> - </valueHelp> - <constraint> - <validator name="numeric" argument="--range 500-1460"/> - </constraint> - </properties> - </leafNode> - </children> -</node> -<node name="source"> - <properties> - <help>Source parameters</help> - </properties> - <children> - #include <include/firewall/address.xml.i> - #include <include/firewall/source-destination-group.xml.i> - <leafNode name="mac-address"> - <properties> - <help>Source MAC address</help> - <valueHelp> - <format><MAC address></format> - <description>MAC address to match</description> - </valueHelp> - <valueHelp> - <format>!<MAC address></format> - <description>Match everything except the specified MAC address</description> - </valueHelp> - <constraint> - <validator name="mac-address-firewall"/> - </constraint> - </properties> - </leafNode> - #include <include/firewall/port.xml.i> - </children> -</node> -<node name="state"> - <properties> - <help>Session state</help> - </properties> - <children> - <leafNode name="established"> - <properties> - <help>Established state</help> - <completionHelp> - <list>enable disable</list> - </completionHelp> - <valueHelp> - <format>enable</format> - <description>Enable</description> - </valueHelp> - <valueHelp> - <format>disable</format> - <description>Disable</description> - </valueHelp> - <constraint> - <regex>(enable|disable)</regex> - </constraint> - </properties> - </leafNode> - <leafNode name="invalid"> - <properties> - <help>Invalid state</help> - <completionHelp> - <list>enable disable</list> - </completionHelp> - <valueHelp> - <format>enable</format> - <description>Enable</description> - </valueHelp> - <valueHelp> - <format>disable</format> - <description>Disable</description> - </valueHelp> - <constraint> - <regex>(enable|disable)</regex> - </constraint> - </properties> - </leafNode> - <leafNode name="new"> - <properties> - <help>New state</help> - <completionHelp> - <list>enable disable</list> - </completionHelp> - <valueHelp> - <format>enable</format> - <description>Enable</description> - </valueHelp> - <valueHelp> - <format>disable</format> - <description>Disable</description> - </valueHelp> - <constraint> - <regex>(enable|disable)</regex> - </constraint> - </properties> - </leafNode> - <leafNode name="related"> - <properties> - <help>Related state</help> - <completionHelp> - <list>enable disable</list> - </completionHelp> - <valueHelp> - <format>enable</format> - <description>Enable</description> - </valueHelp> - <valueHelp> - <format>disable</format> - <description>Disable</description> - </valueHelp> - <constraint> - <regex>(enable|disable)</regex> - </constraint> - </properties> - </leafNode> - </children> -</node> -#include <include/firewall/tcp-flags.xml.i> -<node name="time"> - <properties> - <help>Time to match rule</help> - </properties> - <children> - <leafNode name="monthdays"> - <properties> - <help>Monthdays to match rule on</help> - </properties> - </leafNode> - <leafNode name="startdate"> - <properties> - <help>Date to start matching rule</help> - </properties> - </leafNode> - <leafNode name="starttime"> - <properties> - <help>Time of day to start matching rule</help> - </properties> - </leafNode> - <leafNode name="stopdate"> - <properties> - <help>Date to stop matching rule</help> - </properties> - </leafNode> - <leafNode name="stoptime"> - <properties> - <help>Time of day to stop matching rule</help> - </properties> - </leafNode> - <leafNode name="utc"> - <properties> - <help>Interpret times for startdate, stopdate, starttime and stoptime to be UTC</help> - <valueless/> - </properties> - </leafNode> - <leafNode name="weekdays"> - <properties> - <help>Weekdays to match rule on</help> - </properties> - </leafNode> - </children> -</node> -<node name="icmp"> - <properties> - <help>ICMP type and code information</help> - </properties> - <children> - <leafNode name="code"> - <properties> - <help>ICMP code (0-255)</help> - <valueHelp> - <format>u32:0-255</format> - <description>ICMP code (0-255)</description> - </valueHelp> - <constraint> - <validator name="numeric" argument="--range 0-255"/> - </constraint> - </properties> - </leafNode> - <leafNode name="type"> - <properties> - <help>ICMP type (0-255)</help> - <valueHelp> - <format>u32:0-255</format> - <description>ICMP type (0-255)</description> - </valueHelp> - <constraint> - <validator name="numeric" argument="--range 0-255"/> - </constraint> - </properties> - </leafNode> - #include <include/firewall/icmp-type-name.xml.i> - </children> -</node> -<!-- include end --> +<!-- include start from policy/route-common.xml.i -->
+#include <include/policy/route-rule-action.xml.i>
+#include <include/generic-description.xml.i>
+<leafNode name="disable">
+ <properties>
+ <help>Option to disable firewall rule</help>
+ <valueless/>
+ </properties>
+</leafNode>
+<node name="fragment">
+ <properties>
+ <help>IP fragment match</help>
+ </properties>
+ <children>
+ <leafNode name="match-frag">
+ <properties>
+ <help>Second and further fragments of fragmented packets</help>
+ <valueless/>
+ </properties>
+ </leafNode>
+ <leafNode name="match-non-frag">
+ <properties>
+ <help>Head fragments or unfragmented packets</help>
+ <valueless/>
+ </properties>
+ </leafNode>
+ </children>
+</node>
+<node name="ipsec">
+ <properties>
+ <help>Inbound IPsec packets</help>
+ </properties>
+ <children>
+ <leafNode name="match-ipsec">
+ <properties>
+ <help>Inbound IPsec packets</help>
+ <valueless/>
+ </properties>
+ </leafNode>
+ <leafNode name="match-none">
+ <properties>
+ <help>Inbound non-IPsec packets</help>
+ <valueless/>
+ </properties>
+ </leafNode>
+ </children>
+</node>
+<node name="limit">
+ <properties>
+ <help>Rate limit using a token bucket filter</help>
+ </properties>
+ <children>
+ <leafNode name="burst">
+ <properties>
+ <help>Maximum number of packets to allow in excess of rate</help>
+ <valueHelp>
+ <format>u32:0-4294967295</format>
+ <description>Maximum number of packets to allow in excess of rate</description>
+ </valueHelp>
+ <constraint>
+ <validator name="numeric" argument="--range 0-4294967295"/>
+ </constraint>
+ </properties>
+ </leafNode>
+ <leafNode name="rate">
+ <properties>
+ <help>Maximum average matching rate</help>
+ <valueHelp>
+ <format>u32:0-4294967295</format>
+ <description>Maximum average matching rate</description>
+ </valueHelp>
+ <constraint>
+ <validator name="numeric" argument="--range 0-4294967295"/>
+ </constraint>
+ </properties>
+ </leafNode>
+ </children>
+</node>
+<leafNode name="log">
+ <properties>
+ <help>Option to log packets matching rule</help>
+ <completionHelp>
+ <list>enable disable</list>
+ </completionHelp>
+ <valueHelp>
+ <format>enable</format>
+ <description>Enable log</description>
+ </valueHelp>
+ <valueHelp>
+ <format>disable</format>
+ <description>Disable log</description>
+ </valueHelp>
+ <constraint>
+ <regex>(enable|disable)</regex>
+ </constraint>
+ </properties>
+</leafNode>
+<leafNode name="protocol">
+ <properties>
+ <help>Protocol to match (protocol name, number, or "all")</help>
+ <completionHelp>
+ <script>cat /etc/protocols | sed -e '/^#.*/d' | awk '{ print $1 }'</script>
+ </completionHelp>
+ <valueHelp>
+ <format>all</format>
+ <description>All IP protocols</description>
+ </valueHelp>
+ <valueHelp>
+ <format>tcp_udp</format>
+ <description>Both TCP and UDP</description>
+ </valueHelp>
+ <valueHelp>
+ <format>0-255</format>
+ <description>IP protocol number</description>
+ </valueHelp>
+ <valueHelp>
+ <format>!<protocol></format>
+ <description>IP protocol number</description>
+ </valueHelp>
+ <constraint>
+ <validator name="ip-protocol"/>
+ </constraint>
+ </properties>
+ <defaultValue>all</defaultValue>
+</leafNode>
+<node name="recent">
+ <properties>
+ <help>Parameters for matching recently seen sources</help>
+ </properties>
+ <children>
+ <leafNode name="count">
+ <properties>
+ <help>Source addresses seen more than N times</help>
+ <valueHelp>
+ <format>u32:1-255</format>
+ <description>Source addresses seen more than N times</description>
+ </valueHelp>
+ <constraint>
+ <validator name="numeric" argument="--range 1-255"/>
+ </constraint>
+ </properties>
+ </leafNode>
+ <leafNode name="time">
+ <properties>
+ <help>Source addresses seen in the last N seconds</help>
+ <valueHelp>
+ <format>u32:0-4294967295</format>
+ <description>Source addresses seen in the last N seconds</description>
+ </valueHelp>
+ <constraint>
+ <validator name="numeric" argument="--range 0-4294967295"/>
+ </constraint>
+ </properties>
+ </leafNode>
+ </children>
+</node>
+<node name="set">
+ <properties>
+ <help>Packet modifications</help>
+ </properties>
+ <children>
+ <leafNode name="connection-mark">
+ <properties>
+ <help>Connection marking</help>
+ <valueHelp>
+ <format>u32:1-2147483647</format>
+ <description>Connection marking</description>
+ </valueHelp>
+ <constraint>
+ <validator name="numeric" argument="--range 1-2147483647"/>
+ </constraint>
+ </properties>
+ </leafNode>
+ <leafNode name="dscp">
+ <properties>
+ <help>Packet Differentiated Services Codepoint (DSCP)</help>
+ <valueHelp>
+ <format>u32:0-63</format>
+ <description>DSCP number</description>
+ </valueHelp>
+ <constraint>
+ <validator name="numeric" argument="--range 0-63"/>
+ </constraint>
+ </properties>
+ </leafNode>
+ <leafNode name="mark">
+ <properties>
+ <help>Packet marking</help>
+ <valueHelp>
+ <format>u32:1-2147483647</format>
+ <description>Packet marking</description>
+ </valueHelp>
+ <constraint>
+ <validator name="numeric" argument="--range 1-2147483647"/>
+ </constraint>
+ </properties>
+ </leafNode>
+ <leafNode name="table">
+ <properties>
+ <help>Routing table to forward packet with</help>
+ <valueHelp>
+ <format>u32:1-200</format>
+ <description>Table number</description>
+ </valueHelp>
+ <valueHelp>
+ <format>main</format>
+ <description>Main table</description>
+ </valueHelp>
+ <constraint>
+ <validator name="numeric" argument="--range 1-200"/>
+ <regex>(main)</regex>
+ </constraint>
+ <completionHelp>
+ <list>main</list>
+ <path>protocols static table</path>
+ </completionHelp>
+ </properties>
+ </leafNode>
+ <leafNode name="tcp-mss">
+ <properties>
+ <help>TCP Maximum Segment Size</help>
+ <valueHelp>
+ <format>u32:500-1460</format>
+ <description>Explicitly set TCP MSS value</description>
+ </valueHelp>
+ <constraint>
+ <validator name="numeric" argument="--range 500-1460"/>
+ </constraint>
+ </properties>
+ </leafNode>
+ </children>
+</node>
+<node name="state">
+ <properties>
+ <help>Session state</help>
+ </properties>
+ <children>
+ <leafNode name="established">
+ <properties>
+ <help>Established state</help>
+ <completionHelp>
+ <list>enable disable</list>
+ </completionHelp>
+ <valueHelp>
+ <format>enable</format>
+ <description>Enable</description>
+ </valueHelp>
+ <valueHelp>
+ <format>disable</format>
+ <description>Disable</description>
+ </valueHelp>
+ <constraint>
+ <regex>(enable|disable)</regex>
+ </constraint>
+ </properties>
+ </leafNode>
+ <leafNode name="invalid">
+ <properties>
+ <help>Invalid state</help>
+ <completionHelp>
+ <list>enable disable</list>
+ </completionHelp>
+ <valueHelp>
+ <format>enable</format>
+ <description>Enable</description>
+ </valueHelp>
+ <valueHelp>
+ <format>disable</format>
+ <description>Disable</description>
+ </valueHelp>
+ <constraint>
+ <regex>(enable|disable)</regex>
+ </constraint>
+ </properties>
+ </leafNode>
+ <leafNode name="new">
+ <properties>
+ <help>New state</help>
+ <completionHelp>
+ <list>enable disable</list>
+ </completionHelp>
+ <valueHelp>
+ <format>enable</format>
+ <description>Enable</description>
+ </valueHelp>
+ <valueHelp>
+ <format>disable</format>
+ <description>Disable</description>
+ </valueHelp>
+ <constraint>
+ <regex>(enable|disable)</regex>
+ </constraint>
+ </properties>
+ </leafNode>
+ <leafNode name="related">
+ <properties>
+ <help>Related state</help>
+ <completionHelp>
+ <list>enable disable</list>
+ </completionHelp>
+ <valueHelp>
+ <format>enable</format>
+ <description>Enable</description>
+ </valueHelp>
+ <valueHelp>
+ <format>disable</format>
+ <description>Disable</description>
+ </valueHelp>
+ <constraint>
+ <regex>(enable|disable)</regex>
+ </constraint>
+ </properties>
+ </leafNode>
+ </children>
+</node>
+#include <include/firewall/tcp-flags.xml.i>
+<node name="time">
+ <properties>
+ <help>Time to match rule</help>
+ </properties>
+ <children>
+ <leafNode name="monthdays">
+ <properties>
+ <help>Monthdays to match rule on</help>
+ </properties>
+ </leafNode>
+ <leafNode name="startdate">
+ <properties>
+ <help>Date to start matching rule</help>
+ </properties>
+ </leafNode>
+ <leafNode name="starttime">
+ <properties>
+ <help>Time of day to start matching rule</help>
+ </properties>
+ </leafNode>
+ <leafNode name="stopdate">
+ <properties>
+ <help>Date to stop matching rule</help>
+ </properties>
+ </leafNode>
+ <leafNode name="stoptime">
+ <properties>
+ <help>Time of day to stop matching rule</help>
+ </properties>
+ </leafNode>
+ <leafNode name="utc">
+ <properties>
+ <help>Interpret times for startdate, stopdate, starttime and stoptime to be UTC</help>
+ <valueless/>
+ </properties>
+ </leafNode>
+ <leafNode name="weekdays">
+ <properties>
+ <help>Weekdays to match rule on</help>
+ </properties>
+ </leafNode>
+ </children>
+</node>
+<!-- include end -->
diff --git a/interface-definitions/include/policy/route-ipv4.xml.i b/interface-definitions/include/policy/route-ipv4.xml.i new file mode 100644 index 000000000..1f717a1a4 --- /dev/null +++ b/interface-definitions/include/policy/route-ipv4.xml.i @@ -0,0 +1,45 @@ +<!-- include start from policy/route-ipv4.xml.i --> +<node name="source"> + <properties> + <help>Source parameters</help> + </properties> + <children> + #include <include/firewall/address.xml.i> + #include <include/firewall/source-destination-group.xml.i> + #include <include/firewall/mac-address.xml.i> + #include <include/firewall/port.xml.i> + </children> +</node> +<node name="icmp"> + <properties> + <help>ICMP type and code information</help> + </properties> + <children> + <leafNode name="code"> + <properties> + <help>ICMP code (0-255)</help> + <valueHelp> + <format>u32:0-255</format> + <description>ICMP code (0-255)</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 0-255"/> + </constraint> + </properties> + </leafNode> + <leafNode name="type"> + <properties> + <help>ICMP type (0-255)</help> + <valueHelp> + <format>u32:0-255</format> + <description>ICMP type (0-255)</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 0-255"/> + </constraint> + </properties> + </leafNode> + #include <include/firewall/icmp-type-name.xml.i> + </children> +</node> +<!-- include end --> diff --git a/interface-definitions/include/policy/route-ipv6.xml.i b/interface-definitions/include/policy/route-ipv6.xml.i new file mode 100644 index 000000000..d636a654b --- /dev/null +++ b/interface-definitions/include/policy/route-ipv6.xml.i @@ -0,0 +1,196 @@ +<!-- include start from policy/route-ipv6.xml.i --> +<node name="source"> + <properties> + <help>Source parameters</help> + </properties> + <children> + #include <include/firewall/address-ipv6.xml.i> + #include <include/firewall/source-destination-group.xml.i> + #include <include/firewall/mac-address.xml.i> + #include <include/firewall/port.xml.i> + </children> +</node> +<node name="icmpv6"> + <properties> + <help>ICMPv6 type and code information</help> + </properties> + <children> + <leafNode name="type"> + <properties> + <help>ICMP type-name</help> + <completionHelp> + <list>any echo-reply pong destination-unreachable network-unreachable host-unreachable protocol-unreachable port-unreachable fragmentation-needed source-route-failed network-unknown host-unknown network-prohibited host-prohibited TOS-network-unreachable TOS-host-unreachable communication-prohibited host-precedence-violation precedence-cutoff source-quench redirect network-redirect host-redirect TOS-network-redirect TOS host-redirect echo-request ping router-advertisement router-solicitation time-exceeded ttl-exceeded ttl-zero-during-transit ttl-zero-during-reassembly parameter-problem ip-header-bad required-option-missing timestamp-request timestamp-reply address-mask-request address-mask-reply packet-too-big</list> + </completionHelp> + <valueHelp> + <format>any</format> + <description>Any ICMP type/code</description> + </valueHelp> + <valueHelp> + <format>echo-reply</format> + <description>ICMP type/code name</description> + </valueHelp> + <valueHelp> + <format>pong</format> + <description>ICMP type/code name</description> + </valueHelp> + <valueHelp> + <format>destination-unreachable</format> + <description>ICMP type/code name</description> + </valueHelp> + <valueHelp> + <format>network-unreachable</format> + <description>ICMP type/code name</description> + </valueHelp> + <valueHelp> + <format>host-unreachable</format> + <description>ICMP type/code name</description> + </valueHelp> + <valueHelp> + <format>protocol-unreachable</format> + <description>ICMP type/code name</description> + </valueHelp> + <valueHelp> + <format>port-unreachable</format> + <description>ICMP type/code name</description> + </valueHelp> + <valueHelp> + <format>fragmentation-needed</format> + <description>ICMP type/code name</description> + </valueHelp> + <valueHelp> + <format>source-route-failed</format> + <description>ICMP type/code name</description> + </valueHelp> + <valueHelp> + <format>network-unknown</format> + <description>ICMP type/code name</description> + </valueHelp> + <valueHelp> + <format>host-unknown</format> + <description>ICMP type/code name</description> + </valueHelp> + <valueHelp> + <format>network-prohibited</format> + <description>ICMP type/code name</description> + </valueHelp> + <valueHelp> + <format>host-prohibited</format> + <description>ICMP type/code name</description> + </valueHelp> + <valueHelp> + <format>TOS-network-unreachable</format> + <description>ICMP type/code name</description> + </valueHelp> + <valueHelp> + <format>TOS-host-unreachable</format> + <description>ICMP type/code name</description> + </valueHelp> + <valueHelp> + <format>communication-prohibited</format> + <description>ICMP type/code name</description> + </valueHelp> + <valueHelp> + <format>host-precedence-violation</format> + <description>ICMP type/code name</description> + </valueHelp> + <valueHelp> + <format>precedence-cutoff</format> + <description>ICMP type/code name</description> + </valueHelp> + <valueHelp> + <format>source-quench</format> + <description>ICMP type/code name</description> + </valueHelp> + <valueHelp> + <format>redirect</format> + <description>ICMP type/code name</description> + </valueHelp> + <valueHelp> + <format>network-redirect</format> + <description>ICMP type/code name</description> + </valueHelp> + <valueHelp> + <format>host-redirect</format> + <description>ICMP type/code name</description> + </valueHelp> + <valueHelp> + <format>TOS-network-redirect</format> + <description>ICMP type/code name</description> + </valueHelp> + <valueHelp> + <format>TOS host-redirect</format> + <description>ICMP type/code name</description> + </valueHelp> + <valueHelp> + <format>echo-request</format> + <description>ICMP type/code name</description> + </valueHelp> + <valueHelp> + <format>ping</format> + <description>ICMP type/code name</description> + </valueHelp> + <valueHelp> + <format>router-advertisement</format> + <description>ICMP type/code name</description> + </valueHelp> + <valueHelp> + <format>router-solicitation</format> + <description>ICMP type/code name</description> + </valueHelp> + <valueHelp> + <format>time-exceeded</format> + <description>ICMP type/code name</description> + </valueHelp> + <valueHelp> + <format>ttl-exceeded</format> + <description>ICMP type/code name</description> + </valueHelp> + <valueHelp> + <format>ttl-zero-during-transit</format> + <description>ICMP type/code name</description> + </valueHelp> + <valueHelp> + <format>ttl-zero-during-reassembly</format> + <description>ICMP type/code name</description> + </valueHelp> + <valueHelp> + <format>parameter-problem</format> + <description>ICMP type/code name</description> + </valueHelp> + <valueHelp> + <format>ip-header-bad</format> + <description>ICMP type/code name</description> + </valueHelp> + <valueHelp> + <format>required-option-missing</format> + <description>ICMP type/code name</description> + </valueHelp> + <valueHelp> + <format>timestamp-request</format> + <description>ICMP type/code name</description> + </valueHelp> + <valueHelp> + <format>timestamp-reply</format> + <description>ICMP type/code name</description> + </valueHelp> + <valueHelp> + <format>address-mask-request</format> + <description>ICMP type/code name</description> + </valueHelp> + <valueHelp> + <format>address-mask-reply</format> + <description>ICMP type/code name</description> + </valueHelp> + <valueHelp> + <format>packet-too-big</format> + <description>ICMP type/code name</description> + </valueHelp> + <constraint> + <regex>(any|echo-reply|pong|destination-unreachable|network-unreachable|host-unreachable|protocol-unreachable|port-unreachable|fragmentation-needed|source-route-failed|network-unknown|host-unknown|network-prohibited|host-prohibited|TOS-network-unreachable|TOS-host-unreachable|communication-prohibited|host-precedence-violation|precedence-cutoff|source-quench|redirect|network-redirect|host-redirect|TOS-network-redirect|TOS host-redirect|echo-request|ping|router-advertisement|router-solicitation|time-exceeded|ttl-exceeded|ttl-zero-during-transit|ttl-zero-during-reassembly|parameter-problem|ip-header-bad|required-option-missing|timestamp-request|timestamp-reply|address-mask-request|address-mask-reply|packet-too-big)</regex> + <validator name="numeric" argument="--range 0-255"/> + </constraint> + </properties> + </leafNode> + </children> +</node> +<!-- include end --> diff --git a/interface-definitions/include/port-number-start-zero.xml.i b/interface-definitions/include/port-number-start-zero.xml.i new file mode 100644 index 000000000..04a144216 --- /dev/null +++ b/interface-definitions/include/port-number-start-zero.xml.i @@ -0,0 +1,15 @@ +<!-- include start from port-number-start-zero.xml.i --> +<leafNode name="port"> + <properties> + <help>Port number used by connection</help> + <valueHelp> + <format>u32:0-65535</format> + <description>Numeric IP port</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 0-65535"/> + </constraint> + <constraintErrorMessage>Port number must be in range 0 to 65535</constraintErrorMessage> + </properties> +</leafNode> +<!-- include end --> diff --git a/interface-definitions/include/qos/bandwidth-auto.xml.i b/interface-definitions/include/qos/bandwidth-auto.xml.i new file mode 100644 index 000000000..fa16a6cb0 --- /dev/null +++ b/interface-definitions/include/qos/bandwidth-auto.xml.i @@ -0,0 +1,47 @@ +<!-- include start from qos/bandwidth-auto.xml.i --> +<leafNode name="bandwidth"> + <properties> + <help>Available bandwidth for this policy</help> + <completionHelp> + <list>auto</list> + </completionHelp> + <valueHelp> + <format>auto</format> + <description>Bandwidth matches interface speed</description> + </valueHelp> + <valueHelp> + <format><number></format> + <description>Bits per second</description> + </valueHelp> + <valueHelp> + <format><number>bit</format> + <description>Bits per second</description> + </valueHelp> + <valueHelp> + <format><number>kbit</format> + <description>Kilobits per second</description> + </valueHelp> + <valueHelp> + <format><number>mbit</format> + <description>Megabits per second</description> + </valueHelp> + <valueHelp> + <format><number>gbit</format> + <description>Gigabits per second</description> + </valueHelp> + <valueHelp> + <format><number>tbit</format> + <description>Terabits per second</description> + </valueHelp> + <valueHelp> + <format><number>%%</format> + <description>Percentage of interface link speed</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--positive"/> + <regex>(auto|\d+(bit|kbit|mbit|gbit|tbit)?|(100|\d(\d)?)%)</regex> + </constraint> + </properties> + <defaultValue>auto</defaultValue> +</leafNode> +<!-- include end --> diff --git a/interface-definitions/include/qos/bandwidth.xml.i b/interface-definitions/include/qos/bandwidth.xml.i index 82af22f42..cc923f642 100644 --- a/interface-definitions/include/qos/bandwidth.xml.i +++ b/interface-definitions/include/qos/bandwidth.xml.i @@ -1,15 +1,39 @@ <!-- include start from qos/bandwidth.xml.i --> <leafNode name="bandwidth"> <properties> - <help>Traffic-limit used for this class</help> + <help>Available bandwidth for this policy</help> <valueHelp> <format><number></format> - <description>Rate in kbit (kilobit per second)</description> + <description>Bits per second</description> </valueHelp> <valueHelp> - <format><number><suffix></format> - <description>Rate with scaling suffix (mbit, mbps, ...)</description> + <format><number>bit</format> + <description>Bits per second</description> </valueHelp> + <valueHelp> + <format><number>kbit</format> + <description>Kilobits per second</description> + </valueHelp> + <valueHelp> + <format><number>mbit</format> + <description>Megabits per second</description> + </valueHelp> + <valueHelp> + <format><number>gbit</format> + <description>Gigabits per second</description> + </valueHelp> + <valueHelp> + <format><number>tbit</format> + <description>Terabits per second</description> + </valueHelp> + <valueHelp> + <format><number>%</format> + <description>Percentage of interface link speed</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--positive"/> + <regex>(\d+(bit|kbit|mbit|gbit|tbit)?|(100|\d(\d)?)%)</regex> + </constraint> </properties> </leafNode> <!-- include end --> diff --git a/interface-definitions/include/qos/class-match-ipv4-address.xml.i b/interface-definitions/include/qos/class-match-ipv4-address.xml.i new file mode 100644 index 000000000..8e84c988a --- /dev/null +++ b/interface-definitions/include/qos/class-match-ipv4-address.xml.i @@ -0,0 +1,19 @@ +<!-- include start from qos/class-match-ipv4-address.xml.i --> +<leafNode name="address"> + <properties> + <help>IPv4 destination address for this match</help> + <valueHelp> + <format>ipv4</format> + <description>IPv4 address</description> + </valueHelp> + <valueHelp> + <format>ipv4net</format> + <description>IPv4 prefix</description> + </valueHelp> + <constraint> + <validator name="ipv4-address"/> + <validator name="ipv4-prefix"/> + </constraint> + </properties> +</leafNode> +<!-- include end --> diff --git a/interface-definitions/include/qos/class-match-ipv6-address.xml.i b/interface-definitions/include/qos/class-match-ipv6-address.xml.i new file mode 100644 index 000000000..fd7388127 --- /dev/null +++ b/interface-definitions/include/qos/class-match-ipv6-address.xml.i @@ -0,0 +1,14 @@ +<!-- include start from qos/class-match-ipv6-address.xml.i --> +<leafNode name="address"> + <properties> + <help>IPv6 destination address for this match</help> + <valueHelp> + <format>ipv6net</format> + <description>IPv6 address and prefix length</description> + </valueHelp> + <constraint> + <validator name="ipv6"/> + </constraint> + </properties> +</leafNode> +<!-- include end --> diff --git a/interface-definitions/include/qos/match.xml.i b/interface-definitions/include/qos/class-match.xml.i index 7d89e4460..4ba12f8f7 100644 --- a/interface-definitions/include/qos/match.xml.i +++ b/interface-definitions/include/qos/class-match.xml.i @@ -1,4 +1,4 @@ -<!-- include start from qos/match.xml.i --> +<!-- include start from qos/class-match.xml.i --> <tagNode name="match"> <properties> <help>Class matching rule name</help> @@ -99,22 +99,11 @@ <help>Match on destination port or address</help> </properties> <children> - <leafNode name="address"> - <properties> - <help>IPv4 destination address for this match</help> - <valueHelp> - <format>ipv4net</format> - <description>IPv4 address and prefix length</description> - </valueHelp> - <constraint> - <validator name="ipv4"/> - </constraint> - </properties> - </leafNode> + #include <include/qos/class-match-ipv4-address.xml.i> #include <include/port-number.xml.i> </children> </node> - #include <include/qos/dscp.xml.i> + #include <include/qos/match-dscp.xml.i> #include <include/qos/max-length.xml.i> #include <include/ip-protocol.xml.i> <node name="source"> @@ -122,18 +111,7 @@ <help>Match on source port or address</help> </properties> <children> - <leafNode name="address"> - <properties> - <help>IPv4 source address for this match</help> - <valueHelp> - <format>ipv4net</format> - <description>IPv4 address and prefix length</description> - </valueHelp> - <constraint> - <validator name="ipv4"/> - </constraint> - </properties> - </leafNode> + #include <include/qos/class-match-ipv4-address.xml.i> #include <include/port-number.xml.i> </children> </node> @@ -150,22 +128,11 @@ <help>Match on destination port or address</help> </properties> <children> - <leafNode name="address"> - <properties> - <help>IPv6 destination address for this match</help> - <valueHelp> - <format>ipv6net</format> - <description>IPv6 address and prefix length</description> - </valueHelp> - <constraint> - <validator name="ipv6"/> - </constraint> - </properties> - </leafNode> + #include <include/qos/class-match-ipv6-address.xml.i> #include <include/port-number.xml.i> </children> </node> - #include <include/qos/dscp.xml.i> + #include <include/qos/match-dscp.xml.i> #include <include/qos/max-length.xml.i> #include <include/ip-protocol.xml.i> <node name="source"> @@ -173,18 +140,7 @@ <help>Match on source port or address</help> </properties> <children> - <leafNode name="address"> - <properties> - <help>IPv6 source address for this match</help> - <valueHelp> - <format>ipv6net</format> - <description>IPv6 address and prefix length</description> - </valueHelp> - <constraint> - <validator name="ipv6"/> - </constraint> - </properties> - </leafNode> + #include <include/qos/class-match-ipv6-address.xml.i> #include <include/port-number.xml.i> </children> </node> @@ -195,11 +151,11 @@ <properties> <help>Match on mark applied by firewall</help> <valueHelp> - <format>txt</format> + <format>u32</format> <description>FW mark to match</description> </valueHelp> <constraint> - <validator name="numeric" argument="--range 0x0-0xffff"/> + <validator name="numeric" argument="--range 0-4294967295"/> </constraint> </properties> </leafNode> diff --git a/interface-definitions/include/qos/limiter-actions.xml.i b/interface-definitions/include/qos/class-police-exceed.xml.i index a993423aa..ee2ce16a8 100644 --- a/interface-definitions/include/qos/limiter-actions.xml.i +++ b/interface-definitions/include/qos/class-police-exceed.xml.i @@ -1,13 +1,13 @@ -<!-- include start from qos/limiter-actions.xml.i --> -<leafNode name="exceed-action"> +<!-- include start from qos/police.xml.i --> +<leafNode name="exceed"> <properties> - <help>Default action for packets exceeding the limiter (default: drop)</help> + <help>Default action for packets exceeding the limiter</help> <completionHelp> <list>continue drop ok reclassify pipe</list> </completionHelp> <valueHelp> <format>continue</format> - <description>Don't do anything, just continue with the next action in line</description> + <description>Do not do anything, just continue with the next action in line</description> </valueHelp> <valueHelp> <format>drop</format> @@ -31,15 +31,15 @@ </properties> <defaultValue>drop</defaultValue> </leafNode> -<leafNode name="notexceed-action"> +<leafNode name="not-exceed"> <properties> - <help>Default action for packets not exceeding the limiter (default: ok)</help> + <help>Default action for packets not exceeding the limiter</help> <completionHelp> <list>continue drop ok reclassify pipe</list> </completionHelp> <valueHelp> <format>continue</format> - <description>Don't do anything, just continue with the next action in line</description> + <description>Do not do anything, just continue with the next action in line</description> </valueHelp> <valueHelp> <format>drop</format> diff --git a/interface-definitions/include/qos/class-priority.xml.i b/interface-definitions/include/qos/class-priority.xml.i new file mode 100644 index 000000000..3fd848c93 --- /dev/null +++ b/interface-definitions/include/qos/class-priority.xml.i @@ -0,0 +1,15 @@ +<!-- include start from qos/class-priority.xml.i --> +<leafNode name="priority"> + <properties> + <help>Priority for rule evaluation</help> + <valueHelp> + <format>u32:0-20</format> + <description>Priority for match rule evaluation</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 0-20"/> + </constraint> + <constraintErrorMessage>Priority must be between 0 and 20</constraintErrorMessage> + </properties> +</leafNode> +<!-- include end --> diff --git a/interface-definitions/include/qos/dscp.xml.i b/interface-definitions/include/qos/match-dscp.xml.i index bb90850ac..2d2fd0a57 100644 --- a/interface-definitions/include/qos/dscp.xml.i +++ b/interface-definitions/include/qos/match-dscp.xml.i @@ -1,4 +1,4 @@ -<!-- include start from qos/dscp.xml.i --> +<!-- include start from qos/match-dscp.xml.i --> <leafNode name="dscp"> <properties> <help>Match on Differentiated Services Codepoint (DSCP)</help> @@ -137,7 +137,6 @@ <validator name="numeric" argument="--range 0-63"/> <regex>(default|reliability|throughput|lowdelay|priority|immediate|flash|flash-override|critical|internet|network|AF11|AF12|AF13|AF21|AF22|AF23|AF31|AF32|AF33|AF41|AF42|AF43|CS1|CS2|CS3|CS4|CS5|CS6|CS7|EF)</regex> </constraint> - <constraintErrorMessage>Priority must be between 0 and 63</constraintErrorMessage> </properties> </leafNode> <!-- include end --> diff --git a/interface-definitions/include/qos/max-length.xml.i b/interface-definitions/include/qos/max-length.xml.i index 4cc20f8c4..64cdd02ec 100644 --- a/interface-definitions/include/qos/max-length.xml.i +++ b/interface-definitions/include/qos/max-length.xml.i @@ -1,15 +1,15 @@ <!-- include start from qos/max-length.xml.i --> <leafNode name="max-length"> <properties> - <help>Maximum packet length (ipv4)</help> + <help>Maximum packet length</help> <valueHelp> - <format>u32:0-65535</format> + <format>u32:1-65535</format> <description>Maximum packet/payload length</description> </valueHelp> <constraint> - <validator name="numeric" argument="--range 0-65535"/> + <validator name="numeric" argument="--range 1-65535"/> </constraint> - <constraintErrorMessage>Maximum IPv4 total packet length is 65535</constraintErrorMessage> + <constraintErrorMessage>Maximum packet length is 65535</constraintErrorMessage> </properties> </leafNode> <!-- include end --> diff --git a/interface-definitions/include/qos/queue-type.xml.i b/interface-definitions/include/qos/queue-type.xml.i index 634f61024..c7d4cde82 100644 --- a/interface-definitions/include/qos/queue-type.xml.i +++ b/interface-definitions/include/qos/queue-type.xml.i @@ -3,28 +3,31 @@ <properties> <help>Queue type for default traffic</help> <completionHelp> - <list>fq-codel fair-queue drop-tail random-detect</list> + <list>drop-tail fair-queue fq-codel priority random-detect</list> </completionHelp> <valueHelp> - <format>fq-codel</format> - <description>Fair Queue Codel</description> + <format>drop-tail</format> + <description>First-In-First-Out (FIFO)</description> </valueHelp> <valueHelp> <format>fair-queue</format> <description>Stochastic Fair Queue (SFQ)</description> </valueHelp> <valueHelp> - <format>drop-tail</format> - <description>First-In-First-Out (FIFO)</description> + <format>fq-codel</format> + <description>Fair Queue Codel</description> + </valueHelp> + <valueHelp> + <format>priority</format> + <description>Priority queuing</description> </valueHelp> <valueHelp> <format>random-detect</format> <description>Random Early Detection (RED)</description> </valueHelp> <constraint> - <regex>(fq-codel|fair-queue|drop-tail|random-detect)</regex> + <regex>(drop-tail|fair-queue|fq-codel|priority|random-detect)</regex> </constraint> </properties> - <defaultValue>drop-tail</defaultValue> </leafNode> <!-- include end --> diff --git a/interface-definitions/include/qos/set-dscp.xml.i b/interface-definitions/include/qos/set-dscp.xml.i index 55c0ea44d..07f33783f 100644 --- a/interface-definitions/include/qos/set-dscp.xml.i +++ b/interface-definitions/include/qos/set-dscp.xml.i @@ -3,7 +3,7 @@ <properties> <help>Change the Differentiated Services (DiffServ) field in the IP header</help> <completionHelp> - <list>default reliability throughput lowdelay priority immediate flash flash-override critical internet network</list> + <list>default reliability throughput lowdelay priority immediate flash flash-override critical internet network AF11 AF12 AF13 AF21 AF22 AF23 AF31 AF32 AF33 AF41 AF42 AF43 CS1 CS2 CS3 CS4 CS5 CS6 CS7 EF</list> </completionHelp> <valueHelp> <format>u32:0-63</format> @@ -53,9 +53,89 @@ <format>network</format> <description>match DSCP (111000)</description> </valueHelp> + <valueHelp> + <format>AF11</format> + <description>High-throughput data</description> + </valueHelp> + <valueHelp> + <format>AF12</format> + <description>High-throughput data</description> + </valueHelp> + <valueHelp> + <format>AF13</format> + <description>High-throughput data</description> + </valueHelp> + <valueHelp> + <format>AF21</format> + <description>Low-latency data</description> + </valueHelp> + <valueHelp> + <format>AF22</format> + <description>Low-latency data</description> + </valueHelp> + <valueHelp> + <format>AF23</format> + <description>Low-latency data</description> + </valueHelp> + <valueHelp> + <format>AF31</format> + <description>Multimedia streaming</description> + </valueHelp> + <valueHelp> + <format>AF32</format> + <description>Multimedia streaming</description> + </valueHelp> + <valueHelp> + <format>AF33</format> + <description>Multimedia streaming</description> + </valueHelp> + <valueHelp> + <format>AF41</format> + <description>Multimedia conferencing</description> + </valueHelp> + <valueHelp> + <format>AF42</format> + <description>Multimedia conferencing</description> + </valueHelp> + <valueHelp> + <format>AF43</format> + <description>Multimedia conferencing</description> + </valueHelp> + <valueHelp> + <format>CS1</format> + <description>Low-priority data</description> + </valueHelp> + <valueHelp> + <format>CS2</format> + <description>OAM</description> + </valueHelp> + <valueHelp> + <format>CS3</format> + <description>Broadcast video</description> + </valueHelp> + <valueHelp> + <format>CS4</format> + <description>Real-time interactive</description> + </valueHelp> + <valueHelp> + <format>CS5</format> + <description>Signaling</description> + </valueHelp> + <valueHelp> + <format>CS6</format> + <description>Network control</description> + </valueHelp> + <valueHelp> + <format>CS7</format> + <description></description> + </valueHelp> + <valueHelp> + <format>EF</format> + <description>Expedited Forwarding</description> + </valueHelp> <constraint> <validator name="numeric" argument="--range 0-63"/> - <regex>(default|reliability|throughput|lowdelay|priority|immediate|flash|flash-override|critical|internet|network)</regex> + <regex>(default|reliability|throughput|lowdelay|priority|immediate|flash|flash-override|critical|internet|network|AF11|AF12|AF13|AF21|AF22|AF23|AF31|AF32|AF33|AF41|AF42|AF43|CS1|CS2|CS3|CS4|CS5|CS6|CS7|EF)</regex> </constraint> <constraintErrorMessage>Priority must be between 0 and 63</constraintErrorMessage> </properties> diff --git a/interface-definitions/include/radius-acct-server-ipv4.xml.i b/interface-definitions/include/radius-acct-server-ipv4.xml.i new file mode 100644 index 000000000..9365aa8e9 --- /dev/null +++ b/interface-definitions/include/radius-acct-server-ipv4.xml.i @@ -0,0 +1,26 @@ +<!-- include start from radius-acct-server-ipv4.xml.i --> +<node name="radius"> + <properties> + <help>RADIUS accounting for users OpenConnect VPN sessions OpenConnect authentication mode radius</help> + </properties> + <children> + <tagNode name="server"> + <properties> + <help>RADIUS server configuration</help> + <valueHelp> + <format>ipv4</format> + <description>RADIUS server IPv4 address</description> + </valueHelp> + <constraint> + <validator name="ipv4-address"/> + </constraint> + </properties> + <children> + #include <include/generic-disable-node.xml.i> + #include <include/radius-server-key.xml.i> + #include <include/radius-server-acct-port.xml.i> + </children> + </tagNode> + </children> +</node> +<!-- include end --> diff --git a/interface-definitions/include/radius-server-ipv4.xml.i b/interface-definitions/include/radius-auth-server-ipv4.xml.i index ab4c8e10e..dc6f4d878 100644 --- a/interface-definitions/include/radius-server-ipv4.xml.i +++ b/interface-definitions/include/radius-auth-server-ipv4.xml.i @@ -1,4 +1,4 @@ -<!-- include start from radius-server-ipv4.xml.i --> +<!-- include start from radius-auth-server-ipv4.xml.i --> <node name="radius"> <properties> <help>RADIUS based user authentication</help> @@ -19,7 +19,7 @@ <children> #include <include/generic-disable-node.xml.i> #include <include/radius-server-key.xml.i> - #include <include/radius-server-port.xml.i> + #include <include/radius-server-auth-port.xml.i> </children> </tagNode> </children> diff --git a/interface-definitions/include/radius-server-acct-port.xml.i b/interface-definitions/include/radius-server-acct-port.xml.i new file mode 100644 index 000000000..0b356fa18 --- /dev/null +++ b/interface-definitions/include/radius-server-acct-port.xml.i @@ -0,0 +1,15 @@ +<!-- include start from radius-server-acct-port.xml.i --> +<leafNode name="port"> + <properties> + <help>Accounting port</help> + <valueHelp> + <format>u32:1-65535</format> + <description>Numeric IP port</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 1-65535"/> + </constraint> + </properties> + <defaultValue>1813</defaultValue> +</leafNode> +<!-- include end --> diff --git a/interface-definitions/include/radius-server-port.xml.i b/interface-definitions/include/radius-server-auth-port.xml.i index c6b691a0f..660fa540f 100644 --- a/interface-definitions/include/radius-server-port.xml.i +++ b/interface-definitions/include/radius-server-auth-port.xml.i @@ -1,4 +1,4 @@ -<!-- include start from radius-server-port.xml.i --> +<!-- include start from radius-server-auth-port.xml.i --> <leafNode name="port"> <properties> <help>Authentication port</help> diff --git a/interface-definitions/include/radius-server-ipv4-ipv6.xml.i b/interface-definitions/include/radius-server-ipv4-ipv6.xml.i index 5b12bec62..c593512b4 100644 --- a/interface-definitions/include/radius-server-ipv4-ipv6.xml.i +++ b/interface-definitions/include/radius-server-ipv4-ipv6.xml.i @@ -23,7 +23,7 @@ <children> #include <include/generic-disable-node.xml.i> #include <include/radius-server-key.xml.i> - #include <include/radius-server-port.xml.i> + #include <include/radius-server-auth-port.xml.i> </children> </tagNode> <leafNode name="source-address"> diff --git a/interface-definitions/include/radius-timeout.xml.i b/interface-definitions/include/radius-timeout.xml.i new file mode 100644 index 000000000..22bb6d312 --- /dev/null +++ b/interface-definitions/include/radius-timeout.xml.i @@ -0,0 +1,16 @@ +<!-- include start from radius-timeout.xml.i --> +<leafNode name="timeout"> + <properties> + <help>Session timeout</help> + <valueHelp> + <format>u32:1-240</format> + <description>Session timeout in seconds (default: 2)</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 1-240"/> + </constraint> + <constraintErrorMessage>Timeout must be between 1 and 240 seconds</constraintErrorMessage> + </properties> + <defaultValue>2</defaultValue> +</leafNode> +<!-- include end --> 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 2de5dc58f..aeb2044c9 100644 --- a/interface-definitions/include/static/static-route.xml.i +++ b/interface-definitions/include/static/static-route.xml.i @@ -14,6 +14,7 @@ #include <include/static/static-route-blackhole.xml.i> #include <include/static/static-route-reject.xml.i> #include <include/dhcp-interface.xml.i> + #include <include/generic-description.xml.i> <tagNode name="interface"> <properties> <help>Next-hop IPv4 router interface</help> @@ -25,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 35feef41c..d5e7a25bc 100644 --- a/interface-definitions/include/static/static-route6.xml.i +++ b/interface-definitions/include/static/static-route6.xml.i @@ -13,6 +13,7 @@ <children> #include <include/static/static-route-blackhole.xml.i> #include <include/static/static-route-reject.xml.i> + #include <include/generic-description.xml.i> <tagNode name="interface"> <properties> <help>IPv6 gateway interface name</help> @@ -24,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/container-version.xml.i b/interface-definitions/include/version/container-version.xml.i new file mode 100644 index 000000000..129469cec --- /dev/null +++ b/interface-definitions/include/version/container-version.xml.i @@ -0,0 +1,3 @@ +<!-- include start from include/version/container-version.xml.i --> +<syntaxVersion component='container' version='1'></syntaxVersion> +<!-- include end --> 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/include/version/https-version.xml.i b/interface-definitions/include/version/https-version.xml.i index 586083649..111076974 100644 --- a/interface-definitions/include/version/https-version.xml.i +++ b/interface-definitions/include/version/https-version.xml.i @@ -1,3 +1,3 @@ <!-- include start from include/version/https-version.xml.i --> -<syntaxVersion component='https' version='3'></syntaxVersion> +<syntaxVersion component='https' version='4'></syntaxVersion> <!-- include end --> diff --git a/interface-definitions/include/version/ipsec-version.xml.i b/interface-definitions/include/version/ipsec-version.xml.i index 1c978e8e6..8d019b466 100644 --- a/interface-definitions/include/version/ipsec-version.xml.i +++ b/interface-definitions/include/version/ipsec-version.xml.i @@ -1,3 +1,3 @@ <!-- include start from include/version/ipsec-version.xml.i --> -<syntaxVersion component='ipsec' version='10'></syntaxVersion> +<syntaxVersion component='ipsec' version='11'></syntaxVersion> <!-- include end --> diff --git a/interface-definitions/include/version/ntp-version.xml.i b/interface-definitions/include/version/ntp-version.xml.i index cc4ff9a1c..9eafbf7f0 100644 --- a/interface-definitions/include/version/ntp-version.xml.i +++ b/interface-definitions/include/version/ntp-version.xml.i @@ -1,3 +1,3 @@ <!-- include start from include/version/ntp-version.xml.i --> -<syntaxVersion component='ntp' version='1'></syntaxVersion> +<syntaxVersion component='ntp' version='2'></syntaxVersion> <!-- include end --> diff --git a/interface-definitions/include/version/policy-version.xml.i b/interface-definitions/include/version/policy-version.xml.i index 89bde20c7..f1494eaa3 100644 --- a/interface-definitions/include/version/policy-version.xml.i +++ b/interface-definitions/include/version/policy-version.xml.i @@ -1,3 +1,3 @@ <!-- include start from include/version/policy-version.xml.i --> -<syntaxVersion component='policy' version='4'></syntaxVersion> +<syntaxVersion component='policy' version='5'></syntaxVersion> <!-- include end --> diff --git a/interface-definitions/include/version/qos-version.xml.i b/interface-definitions/include/version/qos-version.xml.i index e4d139349..c67e61e91 100644 --- a/interface-definitions/include/version/qos-version.xml.i +++ b/interface-definitions/include/version/qos-version.xml.i @@ -1,3 +1,3 @@ <!-- include start from include/version/qos-version.xml.i --> -<syntaxVersion component='qos' version='1'></syntaxVersion> +<syntaxVersion component='qos' version='2'></syntaxVersion> <!-- include end --> diff --git a/interface-definitions/include/version/snmp-version.xml.i b/interface-definitions/include/version/snmp-version.xml.i index 0416288f0..fa58672a5 100644 --- a/interface-definitions/include/version/snmp-version.xml.i +++ b/interface-definitions/include/version/snmp-version.xml.i @@ -1,3 +1,3 @@ <!-- include start from include/version/snmp-version.xml.i --> -<syntaxVersion component='snmp' version='2'></syntaxVersion> +<syntaxVersion component='snmp' version='3'></syntaxVersion> <!-- include end --> diff --git a/interface-definitions/include/vrrp/garp.xml.i b/interface-definitions/include/vrrp/garp.xml.i new file mode 100644 index 000000000..b56b490df --- /dev/null +++ b/interface-definitions/include/vrrp/garp.xml.i @@ -0,0 +1,78 @@ +<!-- include start from vrrp/garp.xml.i --> +<node name="garp"> + <properties> + <help>Gratuitous ARP parameters</help> + </properties> + <children> + <leafNode name="interval"> + <properties> + <help>Interval between Gratuitous ARP</help> + <valueHelp> + <format><0.000-1000></format> + <description>Interval in seconds, resolution microseconds</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 0.000-1000 --float"/> + </constraint> + </properties> + <defaultValue>0</defaultValue> + </leafNode> + <leafNode name="master-delay"> + <properties> + <help>Delay for second set of gratuitous ARPs after transition to master</help> + <valueHelp> + <format>u32:1-1000</format> + <description>Delay in seconds</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 1-1000"/> + </constraint> + </properties> + <defaultValue>5</defaultValue> + </leafNode> + <leafNode name="master-refresh"> + <properties> + <help>Minimum time interval for refreshing gratuitous ARPs while beeing master</help> + <valueHelp> + <format>u32:0</format> + <description>No refresh</description> + </valueHelp> + <valueHelp> + <format>u32:1-255</format> + <description>Interval in seconds</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 1-255"/> + </constraint> + </properties> + <defaultValue>5</defaultValue> + </leafNode> + <leafNode name="master-refresh-repeat"> + <properties> + <help>Number of gratuitous ARP messages to send at a time while beeing master</help> + <valueHelp> + <format>u32:1-255</format> + <description>Number of gratuitous ARP messages</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 1-255"/> + </constraint> + </properties> + <defaultValue>1</defaultValue> + </leafNode> + <leafNode name="master-repeat"> + <properties> + <help>Number of gratuitous ARP messages to send at a time after transition to master</help> + <valueHelp> + <format>u32:1-255</format> + <description>Number of gratuitous ARP messages</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 1-255"/> + </constraint> + </properties> + <defaultValue>5</defaultValue> + </leafNode> + </children> +</node> +<!-- include end --> diff --git a/interface-definitions/interfaces-bonding.xml.in b/interface-definitions/interfaces-bonding.xml.in index 41e4a68a8..6e8c5283a 100644 --- a/interface-definitions/interfaces-bonding.xml.in +++ b/interface-definitions/interfaces-bonding.xml.in @@ -49,14 +49,13 @@ </leafNode> </children> </node> - #include <include/interface/description.xml.i> + #include <include/generic-description.xml.i> #include <include/interface/dhcp-options.xml.i> #include <include/interface/dhcpv6-options.xml.i> #include <include/interface/disable-link-detect.xml.i> #include <include/interface/disable.xml.i> #include <include/interface/vrf.xml.i> #include <include/interface/mirror.xml.i> - #include <include/interface/interface-policy.xml.i> <leafNode name="hash-policy"> <properties> <help>Bonding transmit hash policy</help> @@ -200,7 +199,7 @@ <description>Interface name</description> </valueHelp> <constraint> - <validator name="interface-name"/> + #include <include/constraint/interface-name.xml.in> </constraint> <multi/> </properties> @@ -219,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-bridge.xml.in b/interface-definitions/interfaces-bridge.xml.in index 1e11cd4c6..1636411ec 100644 --- a/interface-definitions/interfaces-bridge.xml.in +++ b/interface-definitions/interfaces-bridge.xml.in @@ -34,14 +34,13 @@ </properties> <defaultValue>300</defaultValue> </leafNode> - #include <include/interface/description.xml.i> + #include <include/generic-description.xml.i> #include <include/interface/dhcp-options.xml.i> #include <include/interface/dhcpv6-options.xml.i> #include <include/interface/disable-link-detect.xml.i> #include <include/interface/disable.xml.i> #include <include/interface/vrf.xml.i> #include <include/interface/mtu-68-16000.xml.i> - #include <include/interface/interface-policy.xml.i> <leafNode name="forwarding-delay"> <properties> <help>Forwarding delay</help> @@ -151,7 +150,7 @@ <description>VLAN id range allowed on this interface (use '-' as delimiter)</description> </valueHelp> <constraint> - <validator name="allowed-vlan"/> + <validator name="numeric" argument="--allow-range --range 1-4094"/> </constraint> <constraintErrorMessage>not a valid VLAN ID value or range</constraintErrorMessage> <multi/> diff --git a/interface-definitions/interfaces-dummy.xml.in b/interface-definitions/interfaces-dummy.xml.in index fb36741f7..00784fcdf 100644 --- a/interface-definitions/interfaces-dummy.xml.in +++ b/interface-definitions/interfaces-dummy.xml.in @@ -17,17 +17,35 @@ </properties> <children> #include <include/interface/address-ipv4-ipv6.xml.i> - #include <include/interface/description.xml.i> + #include <include/generic-description.xml.i> #include <include/interface/disable.xml.i> - #include <include/interface/interface-policy.xml.i> <node name="ip"> <properties> <help>IPv4 routing parameters</help> </properties> <children> #include <include/interface/source-validation.xml.i> + #include <include/interface/disable-forwarding.xml.i> </children> </node> + <node name="ipv6"> + <properties> + <help>IPv6 routing parameters</help> + </properties> + <children> + #include <include/interface/disable-forwarding.xml.i> + <node name="address"> + <properties> + <help>IPv6 address configuration modes</help> + </properties> + <children> + #include <include/interface/ipv6-address-eui64.xml.i> + #include <include/interface/ipv6-address-no-default-link-local.xml.i> + </children> + </node> + </children> + </node> + #include <include/interface/mtu-68-16000.xml.i> #include <include/interface/mirror.xml.i> #include <include/interface/netns.xml.i> #include <include/interface/redirect.xml.i> diff --git a/interface-definitions/interfaces-ethernet.xml.in b/interface-definitions/interfaces-ethernet.xml.in index 77f130e1c..e7c196c5c 100644 --- a/interface-definitions/interfaces-ethernet.xml.in +++ b/interface-definitions/interfaces-ethernet.xml.in @@ -20,7 +20,7 @@ </properties> <children> #include <include/interface/address-ipv4-ipv6-dhcp.xml.i> - #include <include/interface/description.xml.i> + #include <include/generic-description.xml.i> #include <include/interface/dhcp-options.xml.i> #include <include/interface/dhcpv6-options.xml.i> <leafNode name="disable-flow-control"> @@ -31,7 +31,6 @@ </leafNode> #include <include/interface/disable-link-detect.xml.i> #include <include/interface/disable.xml.i> - #include <include/interface/interface-policy.xml.i> <leafNode name="duplex"> <properties> <help>Duplex mode</help> diff --git a/interface-definitions/interfaces-geneve.xml.in b/interface-definitions/interfaces-geneve.xml.in index b959c787d..ac9794870 100644 --- a/interface-definitions/interfaces-geneve.xml.in +++ b/interface-definitions/interfaces-geneve.xml.in @@ -17,13 +17,12 @@ </properties> <children> #include <include/interface/address-ipv4-ipv6.xml.i> - #include <include/interface/description.xml.i> + #include <include/generic-description.xml.i> #include <include/interface/disable.xml.i> #include <include/interface/ipv4-options.xml.i> #include <include/interface/ipv6-options.xml.i> #include <include/interface/mac.xml.i> #include <include/interface/mtu-1450-16000.xml.i> - #include <include/interface/interface-policy.xml.i> <node name="parameters"> <properties> <help>GENEVE tunnel parameters</help> diff --git a/interface-definitions/interfaces-input.xml.in b/interface-definitions/interfaces-input.xml.in index d01c760f8..d90cf936f 100644 --- a/interface-definitions/interfaces-input.xml.in +++ b/interface-definitions/interfaces-input.xml.in @@ -17,9 +17,8 @@ </valueHelp> </properties> <children> - #include <include/interface/description.xml.i> + #include <include/generic-description.xml.i> #include <include/interface/disable.xml.i> - #include <include/interface/interface-policy.xml.i> #include <include/interface/redirect.xml.i> </children> </tagNode> diff --git a/interface-definitions/interfaces-l2tpv3.xml.in b/interface-definitions/interfaces-l2tpv3.xml.in index bde68dd5a..1f0dd3d19 100644 --- a/interface-definitions/interfaces-l2tpv3.xml.in +++ b/interface-definitions/interfaces-l2tpv3.xml.in @@ -17,7 +17,7 @@ </properties> <children> #include <include/interface/address-ipv4-ipv6.xml.i> - #include <include/interface/description.xml.i> + #include <include/generic-description.xml.i> <leafNode name="destination-port"> <properties> <help>UDP destination port for L2TPv3 tunnel</help> @@ -32,7 +32,6 @@ <defaultValue>5000</defaultValue> </leafNode> #include <include/interface/disable.xml.i> - #include <include/interface/interface-policy.xml.i> <leafNode name="encapsulation"> <properties> <help>Encapsulation type</help> diff --git a/interface-definitions/interfaces-loopback.xml.in b/interface-definitions/interfaces-loopback.xml.in index 7f59db543..fe0944467 100644 --- a/interface-definitions/interfaces-loopback.xml.in +++ b/interface-definitions/interfaces-loopback.xml.in @@ -17,7 +17,7 @@ </properties> <children> #include <include/interface/address-ipv4-ipv6.xml.i> - #include <include/interface/description.xml.i> + #include <include/generic-description.xml.i> <node name="ip"> <properties> <help>IPv4 routing parameters</help> diff --git a/interface-definitions/interfaces-macsec.xml.in b/interface-definitions/interfaces-macsec.xml.in index 5c9f4cd76..4b4f9149d 100644 --- a/interface-definitions/interfaces-macsec.xml.in +++ b/interface-definitions/interfaces-macsec.xml.in @@ -21,7 +21,6 @@ #include <include/interface/dhcpv6-options.xml.i> #include <include/interface/ipv4-options.xml.i> #include <include/interface/ipv6-options.xml.i> - #include <include/interface/interface-policy.xml.i> #include <include/interface/mirror.xml.i> <node name="security"> <properties> @@ -116,7 +115,7 @@ </leafNode> </children> </node> - #include <include/interface/description.xml.i> + #include <include/generic-description.xml.i> #include <include/interface/disable.xml.i> #include <include/interface/mtu-68-16000.xml.i> <leafNode name="mtu"> diff --git a/interface-definitions/interfaces-openvpn.xml.in b/interface-definitions/interfaces-openvpn.xml.in index 3876e31da..63272a25f 100644 --- a/interface-definitions/interfaces-openvpn.xml.in +++ b/interface-definitions/interfaces-openvpn.xml.in @@ -33,8 +33,7 @@ </leafNode> </children> </node> - #include <include/interface/description.xml.i> - #include <include/interface/interface-policy.xml.i> + #include <include/generic-description.xml.i> <leafNode name="device-type"> <properties> <help>OpenVPN interface device-type</help> diff --git a/interface-definitions/interfaces-pppoe.xml.in b/interface-definitions/interfaces-pppoe.xml.in index 84f76a7ee..c6fd7096b 100644 --- a/interface-definitions/interfaces-pppoe.xml.in +++ b/interface-definitions/interfaces-pppoe.xml.in @@ -19,11 +19,10 @@ #include <include/pppoe-access-concentrator.xml.i> #include <include/interface/authentication.xml.i> #include <include/interface/dial-on-demand.xml.i> - #include <include/interface/interface-policy.xml.i> #include <include/interface/no-default-route.xml.i> #include <include/interface/default-route-distance.xml.i> #include <include/interface/dhcpv6-options.xml.i> - #include <include/interface/description.xml.i> + #include <include/generic-description.xml.i> #include <include/interface/disable.xml.i> <leafNode name="idle-timeout"> <properties> @@ -38,6 +37,19 @@ <constraintErrorMessage>Timeout must be in range 0 to 86400</constraintErrorMessage> </properties> </leafNode> + <leafNode name="host-uniq"> + <properties> + <help>PPPoE RFC2516 host-uniq tag</help> + <valueHelp> + <format>txt</format> + <description>Host-uniq tag as byte string in HEX</description> + </valueHelp> + <constraint> + <regex>([a-fA-F0-9][a-fA-F0-9]){1,18}</regex> + </constraint> + <constraintErrorMessage>Host-uniq must be specified as hex-adecimal byte-string (even number of HEX characters)</constraintErrorMessage> + </properties> + </leafNode> <node name="ip"> <properties> <help>IPv4 routing parameters</help> @@ -83,12 +95,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-pseudo-ethernet.xml.in b/interface-definitions/interfaces-pseudo-ethernet.xml.in index 4eb9bf111..5c73825c3 100644 --- a/interface-definitions/interfaces-pseudo-ethernet.xml.in +++ b/interface-definitions/interfaces-pseudo-ethernet.xml.in @@ -17,7 +17,7 @@ </properties> <children> #include <include/interface/address-ipv4-ipv6-dhcp.xml.i> - #include <include/interface/description.xml.i> + #include <include/generic-description.xml.i> #include <include/interface/dhcp-options.xml.i> #include <include/interface/dhcpv6-options.xml.i> #include <include/interface/disable-link-detect.xml.i> @@ -28,7 +28,6 @@ #include <include/source-interface-ethernet.xml.i> #include <include/interface/mac.xml.i> #include <include/interface/mirror.xml.i> - #include <include/interface/interface-policy.xml.i> <leafNode name="mode"> <properties> <help>Receive mode (default: private)</help> diff --git a/interface-definitions/interfaces-sstpc.xml.in b/interface-definitions/interfaces-sstpc.xml.in new file mode 100644 index 000000000..b569e9bde --- /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/generic-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-tunnel.xml.in b/interface-definitions/interfaces-tunnel.xml.in index fe49d337a..17fe1e285 100644 --- a/interface-definitions/interfaces-tunnel.xml.in +++ b/interface-definitions/interfaces-tunnel.xml.in @@ -16,7 +16,7 @@ </valueHelp> </properties> <children> - #include <include/interface/description.xml.i> + #include <include/generic-description.xml.i> #include <include/interface/address-ipv4-ipv6.xml.i> #include <include/interface/disable.xml.i> #include <include/interface/disable-link-detect.xml.i> @@ -29,7 +29,6 @@ #include <include/source-address-ipv4-ipv6.xml.i> #include <include/interface/tunnel-remote.xml.i> #include <include/source-interface.xml.i> - #include <include/interface/interface-policy.xml.i> <leafNode name="6rd-prefix"> <properties> <help>6rd network prefix</help> diff --git a/interface-definitions/interfaces-virtual-ethernet.xml.in b/interface-definitions/interfaces-virtual-ethernet.xml.in new file mode 100644 index 000000000..864f658da --- /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/generic-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/interfaces-vti.xml.in b/interface-definitions/interfaces-vti.xml.in index eeaea0dc3..b116f7386 100644 --- a/interface-definitions/interfaces-vti.xml.in +++ b/interface-definitions/interfaces-vti.xml.in @@ -17,7 +17,7 @@ </properties> <children> #include <include/interface/address-ipv4-ipv6.xml.i> - #include <include/interface/description.xml.i> + #include <include/generic-description.xml.i> #include <include/interface/disable.xml.i> #include <include/interface/ipv4-options.xml.i> #include <include/interface/ipv6-options.xml.i> @@ -25,7 +25,6 @@ #include <include/interface/mirror.xml.i> #include <include/interface/redirect.xml.i> #include <include/interface/vrf.xml.i> - #include <include/interface/interface-policy.xml.i> </children> </tagNode> </children> diff --git a/interface-definitions/interfaces-vxlan.xml.in b/interface-definitions/interfaces-vxlan.xml.in index 4902ff36d..fb60c93d0 100644 --- a/interface-definitions/interfaces-vxlan.xml.in +++ b/interface-definitions/interfaces-vxlan.xml.in @@ -17,7 +17,7 @@ </properties> <children> #include <include/interface/address-ipv4-ipv6.xml.i> - #include <include/interface/description.xml.i> + #include <include/generic-description.xml.i> #include <include/interface/disable.xml.i> <leafNode name="external"> <properties> @@ -54,7 +54,6 @@ #include <include/interface/mac.xml.i> #include <include/interface/mtu-1200-16000.xml.i> #include <include/interface/mirror.xml.i> - #include <include/interface/interface-policy.xml.i> <leafNode name="mtu"> <defaultValue>1450</defaultValue> </leafNode> diff --git a/interface-definitions/interfaces-wireguard.xml.in b/interface-definitions/interfaces-wireguard.xml.in index 23f50d146..6342b21cf 100644 --- a/interface-definitions/interfaces-wireguard.xml.in +++ b/interface-definitions/interfaces-wireguard.xml.in @@ -17,11 +17,10 @@ </properties> <children> #include <include/interface/address-ipv4-ipv6.xml.i> - #include <include/interface/description.xml.i> + #include <include/generic-description.xml.i> #include <include/interface/disable.xml.i> #include <include/port-number.xml.i> #include <include/interface/mtu-68-16000.xml.i> - #include <include/interface/interface-policy.xml.i> #include <include/interface/mirror.xml.i> <leafNode name="mtu"> <defaultValue>1420</defaultValue> diff --git a/interface-definitions/interfaces-wireless.xml.in b/interface-definitions/interfaces-wireless.xml.in index 9e7fc29bc..a9538d577 100644 --- a/interface-definitions/interfaces-wireless.xml.in +++ b/interface-definitions/interfaces-wireless.xml.in @@ -20,7 +20,6 @@ </properties> <children> #include <include/interface/address-ipv4-ipv6-dhcp.xml.i> - #include <include/interface/interface-policy.xml.i> <node name="capabilities"> <properties> <help>HT and VHT capabilities for your card</help> @@ -468,7 +467,7 @@ <constraintErrorMessage>Invalid ISO/IEC 3166-1 Country Code</constraintErrorMessage> </properties> </leafNode> - #include <include/interface/description.xml.i> + #include <include/generic-description.xml.i> #include <include/interface/dhcp-options.xml.i> #include <include/interface/dhcpv6-options.xml.i> <leafNode name="disable-broadcast-ssid"> @@ -726,7 +725,7 @@ <constraintErrorMessage>Invalid WPA pass phrase, must be 8 to 63 printable characters!</constraintErrorMessage> </properties> </leafNode> - #include <include/radius-server-ipv4.xml.i> + #include <include/radius-auth-server-ipv4.xml.i> <node name="radius"> <children> <tagNode name="server"> diff --git a/interface-definitions/interfaces-wwan.xml.in b/interface-definitions/interfaces-wwan.xml.in index b0b8367dc..5fa3be8db 100644 --- a/interface-definitions/interfaces-wwan.xml.in +++ b/interface-definitions/interfaces-wwan.xml.in @@ -28,7 +28,7 @@ #include <include/interface/dhcp-options.xml.i> #include <include/interface/dhcpv6-options.xml.i> #include <include/interface/authentication.xml.i> - #include <include/interface/description.xml.i> + #include <include/generic-description.xml.i> #include <include/interface/disable.xml.i> #include <include/interface/disable-link-detect.xml.i> #include <include/interface/mirror.xml.i> @@ -39,7 +39,6 @@ #include <include/interface/ipv4-options.xml.i> #include <include/interface/ipv6-options.xml.i> #include <include/interface/dial-on-demand.xml.i> - #include <include/interface/interface-policy.xml.i> #include <include/interface/redirect.xml.i> #include <include/interface/vrf.xml.i> </children> diff --git a/interface-definitions/netns.xml.in b/interface-definitions/netns.xml.in index 088985cb6..87880e96a 100644 --- a/interface-definitions/netns.xml.in +++ b/interface-definitions/netns.xml.in @@ -15,7 +15,7 @@ <constraintErrorMessage>Netns name must be alphanumeric and can contain hyphens and underscores.</constraintErrorMessage> </properties> <children> - #include <include/interface/description.xml.i> + #include <include/generic-description.xml.i> </children> </tagNode> </children> diff --git a/interface-definitions/ntp.xml.in b/interface-definitions/ntp.xml.in index 85636a50f..65e40ee32 100644 --- a/interface-definitions/ntp.xml.in +++ b/interface-definitions/ntp.xml.in @@ -1,7 +1,7 @@ <?xml version="1.0"?> <!-- NTP configuration --> <interfaceDefinition> - <node name="system"> + <node name="service"> <children> <node name="ntp" owner="${vyos_conf_scripts_dir}/ntp.py"> <properties> @@ -43,12 +43,6 @@ <valueless/> </properties> </leafNode> - <leafNode name="preempt"> - <properties> - <help>Specifies the association as preemptable rather than the default persistent</help> - <valueless/> - </properties> - </leafNode> <leafNode name="prefer"> <properties> <help>Marks the server as preferred</help> @@ -57,24 +51,33 @@ </leafNode> </children> </tagNode> - <node name="allow-clients"> + <node name="allow-client"> <properties> - <help>Network Time Protocol (NTP) server options</help> + <help>Specify NTP clients allowed to access the server</help> </properties> <children> <leafNode name="address"> <properties> <help>IP address</help> <valueHelp> + <format>ipv4</format> + <description>Allowed IPv4 address</description> + </valueHelp> + <valueHelp> <format>ipv4net</format> - <description>IP address and prefix length</description> + <description>Allowed IPv4 prefix</description> + </valueHelp> + <valueHelp> + <format>ipv6</format> + <description>Allowed IPv6 address</description> </valueHelp> <valueHelp> <format>ipv6net</format> - <description>IPv6 address and prefix length</description> + <description>Allowed IPv6 prefix</description> </valueHelp> <multi/> <constraint> + <validator name="ip-address"/> <validator name="ip-prefix"/> </constraint> </properties> diff --git a/interface-definitions/policy-route.xml.in b/interface-definitions/policy-route.xml.in index f480f3bd5..d7b159839 100644 --- a/interface-definitions/policy-route.xml.in +++ b/interface-definitions/policy-route.xml.in @@ -12,6 +12,7 @@ </properties> <children> #include <include/generic-description.xml.i> + #include <include/generic-interface-multi.xml.i> #include <include/firewall/enable-default-log.xml.i> <tagNode name="rule"> <properties> @@ -46,10 +47,12 @@ #include <include/firewall/port.xml.i> </children> </node> - #include <include/policy/route-common-rule-ipv6.xml.i> + #include <include/policy/route-common.xml.i> + #include <include/policy/route-ipv6.xml.i> #include <include/firewall/dscp.xml.i> #include <include/firewall/packet-length.xml.i> #include <include/firewall/hop-limit.xml.i> + #include <include/firewall/connection-mark.xml.i> </children> </tagNode> </children> @@ -64,6 +67,7 @@ </properties> <children> #include <include/generic-description.xml.i> + #include <include/generic-interface-multi.xml.i> #include <include/firewall/enable-default-log.xml.i> <tagNode name="rule"> <properties> @@ -98,10 +102,12 @@ #include <include/firewall/port.xml.i> </children> </node> - #include <include/policy/route-common-rule.xml.i> + #include <include/policy/route-common.xml.i> + #include <include/policy/route-ipv4.xml.i> #include <include/firewall/dscp.xml.i> #include <include/firewall/packet-length.xml.i> #include <include/firewall/ttl.xml.i> + #include <include/firewall/connection-mark.xml.i> </children> </tagNode> </children> diff --git a/interface-definitions/policy.xml.in b/interface-definitions/policy.xml.in index 6c60276d5..b3745fda0 100644 --- a/interface-definitions/policy.xml.in +++ b/interface-definitions/policy.xml.in @@ -1356,6 +1356,26 @@ </leafNode> </children> </node> + <node name="l3vpn-nexthop"> + <properties> + <help>Next hop Information</help> + </properties> + <children> + <node name="encapsulation"> + <properties> + <help>Encapsulation options (for BGP only)</help> + </properties> + <children> + <leafNode name="gre"> + <properties> + <help>Accept L3VPN traffic over GRE encapsulation</help> + <valueless/> + </properties> + </leafNode> + </children> + </node> + </children> + </node> <leafNode name="local-preference"> <properties> <help>BGP local preference attribute</help> 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/protocols-static.xml.in b/interface-definitions/protocols-static.xml.in index e89433022..ca4ca2d74 100644 --- a/interface-definitions/protocols-static.xml.in +++ b/interface-definitions/protocols-static.xml.in @@ -26,6 +26,13 @@ </constraint> </properties> <children> + <!-- + iproute2 only considers the first "word" until whitespace in the name field + but does not complain about special characters. + We put an artificial limit here to make table descriptions potentially valid node names + to avoid quoting and simplify future syntax changes if we decide to make any. + --> + #include <include/generic-description.xml.i> #include <include/static/static-route.xml.i> #include <include/static/static-route6.xml.i> </children> diff --git a/interface-definitions/qos.xml.in b/interface-definitions/qos.xml.in index e2dbcbeef..757c1f856 100644 --- a/interface-definitions/qos.xml.in +++ b/interface-definitions/qos.xml.in @@ -3,6 +3,7 @@ <node name="qos" owner="${vyos_conf_scripts_dir}/qos.py"> <properties> <help>Quality of Service (QoS)</help> + <priority>900</priority> </properties> <children> <tagNode name="interface"> @@ -16,7 +17,7 @@ <description>Interface name</description> </valueHelp> <constraint> - <validator name="interface-name"/> + #include <include/constraint/interface-name.xml.in> </constraint> </properties> <children> @@ -24,54 +25,137 @@ <properties> <help>Interface ingress traffic policy</help> <completionHelp> - <path>traffic-policy drop-tail</path> - <path>traffic-policy fair-queue</path> - <path>traffic-policy fq-codel</path> - <path>traffic-policy limiter</path> - <path>traffic-policy network-emulator</path> - <path>traffic-policy priority-queue</path> - <path>traffic-policy random-detect</path> - <path>traffic-policy rate-control</path> - <path>traffic-policy round-robin</path> - <path>traffic-policy shaper</path> - <path>traffic-policy shaper-hfsc</path> + <path>qos policy limiter</path> </completionHelp> <valueHelp> <format>txt</format> - <description>QoS Policy name</description> + <description>QoS policy to use</description> </valueHelp> + <constraint> + <regex>[[:alnum:]][-_[:alnum:]]*</regex> + </constraint> + <constraintErrorMessage>Only alpha-numeric policy name allowed</constraintErrorMessage> </properties> </leafNode> <leafNode name="egress"> <properties> <help>Interface egress traffic policy</help> <completionHelp> - <path>traffic-policy drop-tail</path> - <path>traffic-policy fair-queue</path> - <path>traffic-policy fq-codel</path> - <path>traffic-policy limiter</path> - <path>traffic-policy network-emulator</path> - <path>traffic-policy priority-queue</path> - <path>traffic-policy random-detect</path> - <path>traffic-policy rate-control</path> - <path>traffic-policy round-robin</path> - <path>traffic-policy shaper</path> - <path>traffic-policy shaper-hfsc</path> + <path>qos policy cake</path> + <path>qos policy drop-tail</path> + <path>qos policy fair-queue</path> + <path>qos policy fq-codel</path> + <path>qos policy network-emulator</path> + <path>qos policy priority-queue</path> + <path>qos policy random-detect</path> + <path>qos policy rate-control</path> + <path>qos policy round-robin</path> + <path>qos policy shaper</path> + <path>qos policy shaper-hfsc</path> </completionHelp> <valueHelp> <format>txt</format> - <description>QoS Policy name</description> + <description>QoS policy to use</description> </valueHelp> + <constraint> + <regex>[[:alnum:]][-_[:alnum:]]*</regex> + </constraint> + <constraintErrorMessage>Only alpha-numeric policy name allowed</constraintErrorMessage> </properties> </leafNode> </children> </tagNode> - <node name="policy" owner="${vyos_conf_scripts_dir}/qos.py"> + <node name="policy"> <properties> <help>Service Policy definitions</help> - <priority>900</priority> </properties> <children> + <tagNode name="cake"> + <properties> + <help>Common Applications Kept Enhanced (CAKE)</help> + <valueHelp> + <format>txt</format> + <description>Policy name</description> + </valueHelp> + <constraint> + <regex>[[:alnum:]][-_[:alnum:]]*</regex> + </constraint> + <constraintErrorMessage>Only alpha-numeric policy name allowed</constraintErrorMessage> + </properties> + <children> + #include <include/generic-description.xml.i> + #include <include/qos/bandwidth.xml.i> + <node name="flow-isolation"> + <properties> + <help>Flow isolation settings</help> + </properties> + <children> + <leafNode name="blind"> + <properties> + <help>Disables flow isolation, all traffic passes through a single queue</help> + <valueless/> + </properties> + </leafNode> + <leafNode name="src-host"> + <properties> + <help>Flows are defined only by source address</help> + <valueless/> + </properties> + </leafNode> + <leafNode name="dst-host"> + <properties> + <help>Flows are defined only by destination address</help> + <valueless/> + </properties> + </leafNode> + <leafNode name="host"> + <properties> + <help>Flows are defined by source-destination host pairs</help> + <valueless/> + </properties> + </leafNode> + <leafNode name="flow"> + <properties> + <help>Flows are defined by the entire 5-tuple</help> + <valueless/> + </properties> + </leafNode> + <leafNode name="dual-src-host"> + <properties> + <help>Flows are defined by the 5-tuple, and fairness is applied first over source addresses, then over individual flows</help> + <valueless/> + </properties> + </leafNode> + <leafNode name="dual-dst-host"> + <properties> + <help>Flows are defined by the 5-tuple, and fairness is applied first over destination addresses, then over individual flows</help> + <valueless/> + </properties> + </leafNode> + <leafNode name="nat"> + <properties> + <help>Perform NAT lookup before applying flow-isolation rules</help> + <valueless/> + </properties> + </leafNode> + </children> + </node> + <leafNode name="rtt"> + <properties> + <help>Round-Trip-Time for Active Queue Management (AQM)</help> + <valueHelp> + <format>u32:1-3600000</format> + <description>RTT in ms</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 1-3600000"/> + </constraint> + <constraintErrorMessage>RTT must be in range 1 to 3600000 milli-seconds</constraintErrorMessage> + </properties> + <defaultValue>100</defaultValue> + </leafNode> + </children> + </tagNode> <tagNode name="drop-tail"> <properties> <help>Packet limited First In, First Out queue</help> @@ -125,13 +209,13 @@ <properties> <help>Upper limit of the SFQ</help> <valueHelp> - <format>u32:2-127</format> + <format>u32:1-127</format> <description>Queue size in packets</description> </valueHelp> <constraint> - <validator name="numeric" argument="--range 2-127"/> + <validator name="numeric" argument="--range 1-127"/> </constraint> - <constraintErrorMessage>Queue limit must greater than 1 and less than 128</constraintErrorMessage> + <constraintErrorMessage>Queue limit must be in range 1 to 127</constraintErrorMessage> </properties> <defaultValue>127</defaultValue> </leafNode> @@ -139,7 +223,7 @@ </tagNode> <tagNode name="fq-codel"> <properties> - <help>Fair Queuing Controlled Delay</help> + <help>Fair Queuing (FQ) with Controlled Delay (CoDel)</help> <valueHelp> <format>txt</format> <description>Policy name</description> @@ -171,6 +255,7 @@ <constraintErrorMessage>Only alpha-numeric policy name allowed</constraintErrorMessage> </properties> <children> + #include <include/generic-description.xml.i> <tagNode name="class"> <properties> <help>Class ID</help> @@ -184,23 +269,13 @@ <constraintErrorMessage>Class identifier must be between 1 and 4090</constraintErrorMessage> </properties> <children> + #include <include/generic-description.xml.i> #include <include/qos/bandwidth.xml.i> #include <include/qos/burst.xml.i> - #include <include/generic-description.xml.i> - #include <include/qos/match.xml.i> - #include <include/qos/limiter-actions.xml.i> + #include <include/qos/class-police-exceed.xml.i> + #include <include/qos/class-match.xml.i> + #include <include/qos/class-priority.xml.i> <leafNode name="priority"> - <properties> - <help>Priority for rule evaluation</help> - <valueHelp> - <format>u32:0-20</format> - <description>Priority for match rule evaluation</description> - </valueHelp> - <constraint> - <validator name="numeric" argument="--range 0-20"/> - </constraint> - <constraintErrorMessage>Priority must be between 0 and 20</constraintErrorMessage> - </properties> <defaultValue>20</defaultValue> </leafNode> </children> @@ -212,10 +287,9 @@ <children> #include <include/qos/bandwidth.xml.i> #include <include/qos/burst.xml.i> - #include <include/qos/limiter-actions.xml.i> + #include <include/qos/class-police-exceed.xml.i> </children> </node> - #include <include/generic-description.xml.i> </children> </tagNode> <tagNode name="network-emulator"> @@ -231,10 +305,9 @@ <constraintErrorMessage>Only alpha-numeric policy name allowed</constraintErrorMessage> </properties> <children> - #include <include/qos/bandwidth.xml.i> - #include <include/qos/burst.xml.i> #include <include/generic-description.xml.i> - <leafNode name="network-delay"> + #include <include/qos/bandwidth.xml.i> + <leafNode name="delay"> <properties> <help>Adds delay to packets outgoing to chosen network interface</help> <valueHelp> @@ -247,7 +320,7 @@ <constraintErrorMessage>Priority must be between 0 and 65535</constraintErrorMessage> </properties> </leafNode> - <leafNode name="packet-corruption"> + <leafNode name="corruption"> <properties> <help>Introducing error in a random position for chosen percent of packets</help> <valueHelp> @@ -260,9 +333,9 @@ <constraintErrorMessage>Priority must be between 0 and 100</constraintErrorMessage> </properties> </leafNode> - <leafNode name="packet-loss"> + <leafNode name="duplicate"> <properties> - <help>Add independent loss probability to the packets outgoing to chosen network interface</help> + <help>Cosen percent of packets is duplicated before queuing them</help> <valueHelp> <format><number></format> <description>Percentage of packets affected</description> @@ -270,10 +343,10 @@ <constraint> <validator name="numeric" argument="--range 0-100"/> </constraint> - <constraintErrorMessage>Must be between 0 and 100</constraintErrorMessage> + <constraintErrorMessage>Priority must be between 0 and 100</constraintErrorMessage> </properties> </leafNode> - <leafNode name="packet-loss"> + <leafNode name="loss"> <properties> <help>Add independent loss probability to the packets outgoing to chosen network interface</help> <valueHelp> @@ -286,9 +359,9 @@ <constraintErrorMessage>Must be between 0 and 100</constraintErrorMessage> </properties> </leafNode> - <leafNode name="packet-loss"> + <leafNode name="reordering"> <properties> - <help>Packet reordering percentage</help> + <help>Emulated packet reordering percentage</help> <valueHelp> <format><number></format> <description>Percentage of packets affected</description> @@ -315,6 +388,7 @@ <constraintErrorMessage>Only alpha-numeric policy name allowed</constraintErrorMessage> </properties> <children> + #include <include/generic-description.xml.i> <tagNode name="class"> <properties> <help>Class Handle</help> @@ -332,10 +406,13 @@ #include <include/qos/codel-quantum.xml.i> #include <include/qos/flows.xml.i> #include <include/qos/interval.xml.i> - #include <include/qos/match.xml.i> - #include <include/qos/queue-limit-2-10999.xml.i> - #include <include/qos/target.xml.i> + #include <include/qos/class-match.xml.i> + #include <include/qos/queue-limit-1-4294967295.xml.i> #include <include/qos/queue-type.xml.i> + <leafNode name="queue-type"> + <defaultValue>drop-tail</defaultValue> + </leafNode> + #include <include/qos/target.xml.i> </children> </tagNode> <node name="default"> @@ -343,21 +420,22 @@ <help>Default policy</help> </properties> <children> - #include <include/generic-description.xml.i> #include <include/qos/codel-quantum.xml.i> #include <include/qos/flows.xml.i> #include <include/qos/interval.xml.i> - #include <include/qos/queue-limit-2-10999.xml.i> - #include <include/qos/target.xml.i> + #include <include/qos/queue-limit-1-4294967295.xml.i> #include <include/qos/queue-type.xml.i> + <leafNode name="queue-type"> + <defaultValue>drop-tail</defaultValue> + </leafNode> + #include <include/qos/target.xml.i> </children> </node> - #include <include/generic-description.xml.i> </children> </tagNode> <tagNode name="random-detect"> <properties> - <help>Priority queuing based policy</help> + <help>Weighted Random Early Detect policy</help> <valueHelp> <format>txt</format> <description>Policy name</description> @@ -368,11 +446,8 @@ <constraintErrorMessage>Only alpha-numeric policy name allowed</constraintErrorMessage> </properties> <children> - #include <include/qos/bandwidth.xml.i> - <leafNode name="bandwidth"> - <defaultValue>auto</defaultValue> - </leafNode> #include <include/generic-description.xml.i> + #include <include/qos/bandwidth-auto.xml.i> <tagNode name="precedence"> <properties> <help>IP precedence</help> @@ -413,6 +488,7 @@ </constraint> <constraintErrorMessage>Mark probability must be greater than 0</constraintErrorMessage> </properties> + <defaultValue>10</defaultValue> </leafNode> <leafNode name="maximum-threshold"> <properties> @@ -426,6 +502,7 @@ </constraint> <constraintErrorMessage>Threshold must be between 0 and 4096</constraintErrorMessage> </properties> + <defaultValue>18</defaultValue> </leafNode> <leafNode name="minimum-threshold"> <properties> @@ -457,8 +534,8 @@ <constraintErrorMessage>Only alpha-numeric policy name allowed</constraintErrorMessage> </properties> <children> - #include <include/qos/bandwidth.xml.i> #include <include/generic-description.xml.i> + #include <include/qos/bandwidth.xml.i> #include <include/qos/burst.xml.i> <leafNode name="latency"> <properties> @@ -478,7 +555,7 @@ </tagNode> <tagNode name="round-robin"> <properties> - <help>Round-Robin based policy</help> + <help>Deficit Round Robin Scheduler</help> <valueHelp> <format>txt</format> <description>Policy name</description> @@ -503,11 +580,11 @@ <constraintErrorMessage>Class identifier must be between 1 and 4095</constraintErrorMessage> </properties> <children> - #include <include/qos/codel-quantum.xml.i> #include <include/generic-description.xml.i> + #include <include/qos/codel-quantum.xml.i> #include <include/qos/flows.xml.i> #include <include/qos/interval.xml.i> - #include <include/qos/match.xml.i> + #include <include/qos/class-match.xml.i> <leafNode name="quantum"> <properties> <help>Packet scheduling quantum</help> @@ -523,111 +600,26 @@ </leafNode> #include <include/qos/queue-limit-1-4294967295.xml.i> #include <include/qos/queue-type.xml.i> + <leafNode name="queue-type"> + <defaultValue>drop-tail</defaultValue> + </leafNode> #include <include/qos/target.xml.i> </children> </tagNode> - </children> - </tagNode> - <tagNode name="shaper-hfsc"> - <properties> - <help>Hierarchical Fair Service Curve's policy</help> - <valueHelp> - <format>txt</format> - <description>Policy name</description> - </valueHelp> - <constraint> - <regex>[[:alnum:]][-_[:alnum:]]*</regex> - </constraint> - <constraintErrorMessage>Only alpha-numeric policy name allowed</constraintErrorMessage> - </properties> - <children> - #include <include/qos/bandwidth.xml.i> - <leafNode name="bandwidth"> - <defaultValue>auto</defaultValue> - </leafNode> - #include <include/generic-description.xml.i> - <tagNode name="class"> - <properties> - <help>Class ID</help> - <valueHelp> - <format>u32:1-4095</format> - <description>Class Identifier</description> - </valueHelp> - <constraint> - <validator name="numeric" argument="--range 1-4095"/> - </constraint> - <constraintErrorMessage>Class identifier must be between 1 and 4095</constraintErrorMessage> - </properties> - <children> - #include <include/generic-description.xml.i> - <node name="linkshare"> - <properties> - <help>Linkshare class settings</help> - </properties> - <children> - #include <include/qos/hfsc-d.xml.i> - #include <include/qos/hfsc-m1.xml.i> - #include <include/qos/hfsc-m2.xml.i> - </children> - </node> - #include <include/qos/match.xml.i> - <node name="realtime"> - <properties> - <help>Realtime class settings</help> - </properties> - <children> - #include <include/qos/hfsc-d.xml.i> - #include <include/qos/hfsc-m1.xml.i> - #include <include/qos/hfsc-m2.xml.i> - </children> - </node> - <node name="upperlimit"> - <properties> - <help>Upperlimit class settings</help> - </properties> - <children> - #include <include/qos/hfsc-d.xml.i> - #include <include/qos/hfsc-m1.xml.i> - #include <include/qos/hfsc-m2.xml.i> - </children> - </node> - </children> - </tagNode> <node name="default"> <properties> <help>Default policy</help> </properties> <children> - <node name="linkshare"> - <properties> - <help>Linkshare class settings</help> - </properties> - <children> - #include <include/qos/hfsc-d.xml.i> - #include <include/qos/hfsc-m1.xml.i> - #include <include/qos/hfsc-m2.xml.i> - </children> - </node> - <node name="realtime"> - <properties> - <help>Realtime class settings</help> - </properties> - <children> - #include <include/qos/hfsc-d.xml.i> - #include <include/qos/hfsc-m1.xml.i> - #include <include/qos/hfsc-m2.xml.i> - </children> - </node> - <node name="upperlimit"> - <properties> - <help>Upperlimit class settings</help> - </properties> - <children> - #include <include/qos/hfsc-d.xml.i> - #include <include/qos/hfsc-m1.xml.i> - #include <include/qos/hfsc-m2.xml.i> - </children> - </node> + #include <include/qos/codel-quantum.xml.i> + #include <include/qos/flows.xml.i> + #include <include/qos/interval.xml.i> + #include <include/qos/queue-limit-1-4294967295.xml.i> + #include <include/qos/queue-type.xml.i> + <leafNode name="queue-type"> + <defaultValue>fair-queue</defaultValue> + </leafNode> + #include <include/qos/target.xml.i> </children> </node> </children> @@ -645,10 +637,8 @@ <constraintErrorMessage>Only alpha-numeric policy name allowed</constraintErrorMessage> </properties> <children> - #include <include/qos/bandwidth.xml.i> - <leafNode name="bandwidth"> - <defaultValue>auto</defaultValue> - </leafNode> + #include <include/generic-description.xml.i> + #include <include/qos/bandwidth-auto.xml.i> <tagNode name="class"> <properties> <help>Class ID</help> @@ -662,10 +652,8 @@ <constraintErrorMessage>Class identifier must be between 2 and 4095</constraintErrorMessage> </properties> <children> - #include <include/qos/bandwidth.xml.i> - <leafNode name="bandwidth"> - <defaultValue>100%</defaultValue> - </leafNode> + #include <include/generic-description.xml.i> + #include <include/qos/bandwidth-auto.xml.i> #include <include/qos/burst.xml.i> <leafNode name="ceiling"> <properties> @@ -697,31 +685,19 @@ </properties> </leafNode> #include <include/qos/codel-quantum.xml.i> - #include <include/generic-description.xml.i> #include <include/qos/flows.xml.i> #include <include/qos/interval.xml.i> - #include <include/qos/match.xml.i> - <leafNode name="priority"> - <properties> - <help>Priority for usage of excess bandwidth</help> - <valueHelp> - <format>u32:0-7</format> - <description>Priority order for bandwidth pool</description> - </valueHelp> - <constraint> - <validator name="numeric" argument="--range 0-7"/> - </constraint> - <constraintErrorMessage>Priority must be between 0 and 7</constraintErrorMessage> - </properties> - <defaultValue>20</defaultValue> - </leafNode> + #include <include/qos/class-match.xml.i> + #include <include/qos/class-priority.xml.i> #include <include/qos/queue-limit-1-4294967295.xml.i> #include <include/qos/queue-type.xml.i> + <leafNode name="queue-type"> + <defaultValue>fq-codel</defaultValue> + </leafNode> #include <include/qos/set-dscp.xml.i> #include <include/qos/target.xml.i> </children> </tagNode> - #include <include/generic-description.xml.i> <node name="default"> <properties> <help>Default policy</help> @@ -759,7 +735,6 @@ </properties> </leafNode> #include <include/qos/codel-quantum.xml.i> - #include <include/generic-description.xml.i> #include <include/qos/flows.xml.i> #include <include/qos/interval.xml.i> <leafNode name="priority"> @@ -778,12 +753,116 @@ </leafNode> #include <include/qos/queue-limit-1-4294967295.xml.i> #include <include/qos/queue-type.xml.i> + <leafNode name="queue-type"> + <defaultValue>fq-codel</defaultValue> + </leafNode> #include <include/qos/set-dscp.xml.i> #include <include/qos/target.xml.i> </children> </node> </children> </tagNode> + <tagNode name="shaper-hfsc"> + <properties> + <help>Hierarchical Fair Service Curve's policy</help> + <valueHelp> + <format>txt</format> + <description>Policy name</description> + </valueHelp> + <constraint> + <regex>[[:alnum:]][-_[:alnum:]]*</regex> + </constraint> + <constraintErrorMessage>Only alpha-numeric policy name allowed</constraintErrorMessage> + </properties> + <children> + #include <include/generic-description.xml.i> + #include <include/qos/bandwidth-auto.xml.i> + <tagNode name="class"> + <properties> + <help>Class ID</help> + <valueHelp> + <format>u32:1-4095</format> + <description>Class Identifier</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 1-4095"/> + </constraint> + <constraintErrorMessage>Class identifier must be between 1 and 4095</constraintErrorMessage> + </properties> + <children> + #include <include/generic-description.xml.i> + <node name="linkshare"> + <properties> + <help>Linkshare class settings</help> + </properties> + <children> + #include <include/qos/hfsc-d.xml.i> + #include <include/qos/hfsc-m1.xml.i> + #include <include/qos/hfsc-m2.xml.i> + </children> + </node> + #include <include/qos/class-match.xml.i> + <node name="realtime"> + <properties> + <help>Realtime class settings</help> + </properties> + <children> + #include <include/qos/hfsc-d.xml.i> + #include <include/qos/hfsc-m1.xml.i> + #include <include/qos/hfsc-m2.xml.i> + </children> + </node> + <node name="upperlimit"> + <properties> + <help>Upperlimit class settings</help> + </properties> + <children> + #include <include/qos/hfsc-d.xml.i> + #include <include/qos/hfsc-m1.xml.i> + #include <include/qos/hfsc-m2.xml.i> + </children> + </node> + </children> + </tagNode> + <node name="default"> + <properties> + <help>Default policy</help> + </properties> + <children> + <node name="linkshare"> + <properties> + <help>Linkshare class settings</help> + </properties> + <children> + #include <include/qos/hfsc-d.xml.i> + #include <include/qos/hfsc-m1.xml.i> + #include <include/qos/hfsc-m2.xml.i> + </children> + </node> + <node name="realtime"> + <properties> + <help>Realtime class settings</help> + </properties> + <children> + #include <include/qos/hfsc-d.xml.i> + #include <include/qos/hfsc-m1.xml.i> + #include <include/qos/hfsc-m2.xml.i> + </children> + </node> + <node name="upperlimit"> + <properties> + <help>Upperlimit class settings</help> + </properties> + <children> + #include <include/qos/hfsc-d.xml.i> + #include <include/qos/hfsc-m1.xml.i> + #include <include/qos/hfsc-m2.xml.i> + </children> + </node> + </children> + </node> + </children> + </tagNode> </children> </node> </children> diff --git a/interface-definitions/service-console-server.xml.in b/interface-definitions/service-console-server.xml.in index fb71538dd..fc6dbe954 100644 --- a/interface-definitions/service-console-server.xml.in +++ b/interface-definitions/service-console-server.xml.in @@ -27,7 +27,7 @@ </constraint> </properties> <children> - #include <include/interface/description.xml.i> + #include <include/generic-description.xml.i> <leafNode name="alias"> <properties> <help>Human-readable name for this console</help> diff --git a/interface-definitions/service-ipoe-server.xml.in b/interface-definitions/service-ipoe-server.xml.in index ef8569437..d778f9de0 100644 --- a/interface-definitions/service-ipoe-server.xml.in +++ b/interface-definitions/service-ipoe-server.xml.in @@ -220,7 +220,7 @@ #include <include/accel-ppp/radius-additions-rate-limit.xml.i> </children> </node> - #include <include/radius-server-ipv4.xml.i> + #include <include/radius-auth-server-ipv4.xml.i> #include <include/accel-ppp/radius-additions.xml.i> </children> </node> diff --git a/interface-definitions/service-pppoe-server.xml.in b/interface-definitions/service-pppoe-server.xml.in index b31109296..68592b96b 100644 --- a/interface-definitions/service-pppoe-server.xml.in +++ b/interface-definitions/service-pppoe-server.xml.in @@ -20,7 +20,7 @@ #include <include/accel-ppp/auth-local-users.xml.i> #include <include/accel-ppp/auth-mode.xml.i> #include <include/accel-ppp/auth-protocols.xml.i> - #include <include/radius-server-ipv4.xml.i> + #include <include/radius-auth-server-ipv4.xml.i> #include <include/accel-ppp/radius-additions.xml.i> <node name="radius"> <children> @@ -170,52 +170,7 @@ </properties> </leafNode> #include <include/accel-ppp/ppp-options-ipv6.xml.i> - <leafNode name="ipv6-intf-id"> - <properties> - <help>Fixed or random interface identifier for IPv6</help> - <completionHelp> - <list>random</list> - </completionHelp> - <valueHelp> - <format>random</format> - <description>Random interface identifier for IPv6</description> - </valueHelp> - <valueHelp> - <format>x:x:x:x</format> - <description>specify interface identifier for IPv6</description> - </valueHelp> - </properties> - </leafNode> - <leafNode name="ipv6-peer-intf-id"> - <properties> - <help>Peer interface identifier for IPv6</help> - <completionHelp> - <list>random calling-sid ipv4</list> - </completionHelp> - <valueHelp> - <format>x:x:x:x</format> - <description>Interface identifier for IPv6</description> - </valueHelp> - <valueHelp> - <format>random</format> - <description>Use a random interface identifier for IPv6</description> - </valueHelp> - <valueHelp> - <format>ipv4</format> - <description>Calculate interface identifier from IPv4 address, for example 192:168:0:1</description> - </valueHelp> - <valueHelp> - <format>calling-sid</format> - <description>Calculate interface identifier from calling-station-id</description> - </valueHelp> - </properties> - </leafNode> - <leafNode name="ipv6-accept-peer-intf-id"> - <properties> - <help>Accept peer interface identifier</help> - <valueless /> - </properties> - </leafNode> + #include <include/accel-ppp/ppp-options-ipv6-interface-id.xml.i> </children> </node> <tagNode name="pado-delay"> 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/snmp.xml.in b/interface-definitions/snmp.xml.in index b4f72589e..10dd828a5 100644 --- a/interface-definitions/snmp.xml.in +++ b/interface-definitions/snmp.xml.in @@ -13,9 +13,9 @@ <properties> <help>Community name</help> <constraint> - <regex>[a-zA-Z0-9\-_]{1,100}</regex> + <regex>[a-zA-Z0-9\-_!@*#]{1,100}</regex> </constraint> - <constraintErrorMessage>Community string is limited to alphanumerical characters only with a total lenght of 100</constraintErrorMessage> + <constraintErrorMessage>Community string is limited to alphanumerical characters, !, @, * and # with a total lenght of 100</constraintErrorMessage> </properties> <children> <leafNode name="authorization"> @@ -123,18 +123,31 @@ </leafNode> <leafNode name="oid-enable"> <properties> - <help>Enable specific OIDs</help> + <help>Enable specific OIDs that by default are disable</help> <completionHelp> - <list>route-table</list> + <list>ip-forward ip-route-table ip-net-to-media-table ip-net-to-physical-phys-address</list> </completionHelp> <valueHelp> - <format>route-table</format> - <description>Enable routing table OIDs (ipCidrRouteTable inetCidrRouteTable)</description> + <format>ip-forward</format> + <description>Enable ipForward: .1.3.6.1.2.1.4.24</description> + </valueHelp> + <valueHelp> + <format>ip-route-table</format> + <description>Enable ipRouteTable: .1.3.6.1.2.1.4.21</description> + </valueHelp> + <valueHelp> + <format>ip-net-to-media-table</format> + <description>Enable ipNetToMediaTable: .1.3.6.1.2.1.4.22</description> + </valueHelp> + <valueHelp> + <format>ip-net-to-physical-phys-address</format> + <description>Enable ipNetToPhysicalPhysAddress: .1.3.6.1.2.1.4.35</description> </valueHelp> <constraint> - <regex>(route-table)</regex> + <regex>(ip-forward|ip-route-table|ip-net-to-media-table|ip-net-to-physical-phys-address)</regex> </constraint> - <constraintErrorMessage>OID must be 'route-table'</constraintErrorMessage> + <constraintErrorMessage>OID must be one of the liste options</constraintErrorMessage> + <multi/> </properties> </leafNode> #include <include/snmp/protocol.xml.i> diff --git a/interface-definitions/ssh.xml.in b/interface-definitions/ssh.xml.in index f3c731fe5..2bcce2cf0 100644 --- a/interface-definitions/ssh.xml.in +++ b/interface-definitions/ssh.xml.in @@ -133,6 +133,19 @@ </leafNode> </children> </node> + <leafNode name="hostkey-algorithm"> + <properties> + <help>Allowed host key signature algorithms</help> + <completionHelp> + <!-- generated by ssh -Q HostKeyAlgorithms | tr '\n' ' ' as this will not change dynamically --> + <list>ssh-ed25519 ssh-ed25519-cert-v01@openssh.com sk-ssh-ed25519@openssh.com sk-ssh-ed25519-cert-v01@openssh.com ssh-rsa rsa-sha2-256 rsa-sha2-512 ssh-dss ecdsa-sha2-nistp256 ecdsa-sha2-nistp384 ecdsa-sha2-nistp521 sk-ecdsa-sha2-nistp256@openssh.com webauthn-sk-ecdsa-sha2-nistp256@openssh.com ssh-rsa-cert-v01@openssh.com rsa-sha2-256-cert-v01@openssh.com rsa-sha2-512-cert-v01@openssh.com ssh-dss-cert-v01@openssh.com ecdsa-sha2-nistp256-cert-v01@openssh.com ecdsa-sha2-nistp384-cert-v01@openssh.com ecdsa-sha2-nistp521-cert-v01@openssh.com sk-ecdsa-sha2-nistp256-cert-v01@openssh.com</list> + </completionHelp> + <multi/> + <constraint> + <regex>(ssh-ed25519|ssh-ed25519-cert-v01@openssh.com|sk-ssh-ed25519@openssh.com|sk-ssh-ed25519-cert-v01@openssh.com|ssh-rsa|rsa-sha2-256|rsa-sha2-512|ssh-dss|ecdsa-sha2-nistp256|ecdsa-sha2-nistp384|ecdsa-sha2-nistp521|sk-ecdsa-sha2-nistp256@openssh.com|webauthn-sk-ecdsa-sha2-nistp256@openssh.com|ssh-rsa-cert-v01@openssh.com|rsa-sha2-256-cert-v01@openssh.com|rsa-sha2-512-cert-v01@openssh.com|ssh-dss-cert-v01@openssh.com|ecdsa-sha2-nistp256-cert-v01@openssh.com|ecdsa-sha2-nistp384-cert-v01@openssh.com|ecdsa-sha2-nistp521-cert-v01@openssh.com|sk-ecdsa-sha2-nistp256-cert-v01@openssh.com)</regex> + </constraint> + </properties> + </leafNode> <leafNode name="key-exchange"> <properties> <help>Allowed key exchange (KEX) algorithms</help> diff --git a/interface-definitions/system-config-mgmt.xml.in b/interface-definitions/system-config-mgmt.xml.in new file mode 100644 index 000000000..1f852d284 --- /dev/null +++ b/interface-definitions/system-config-mgmt.xml.in @@ -0,0 +1,58 @@ +<?xml version="1.0"?> +<interfaceDefinition> + <node name="system"> + <children> + <node name="config-management" owner="${vyos_conf_scripts_dir}/config_mgmt.py"> + <properties> + <help>Configuration management settings</help> + <priority>400</priority> + </properties> + <children> + <node name="commit-archive"> + <properties> + <help>Commit archive settings</help> + </properties> + <children> + <leafNode name="location"> + <properties> + <help>Commit archive location</help> + <valueHelp> + <format>uri</format> + <description>Uniform Resource Identifier</description> + </valueHelp> + <constraint> + <validator name="url --file-transport"/> + </constraint> + <multi/> + </properties> + </leafNode> + <leafNode name="source-address"> + <properties> + <help>Source address or interface for archive server connections</help> + <constraint> + <validator name="ipv4-address"/> + <validator name="ipv6-address"/> + #include <include/constraint/interface-name.xml.in> + </constraint> + </properties> + </leafNode> + </children> + </node> + <leafNode name="commit-revisions"> + <properties> + <help>Commit revisions</help> + <valueHelp> + <format>u32:1-65535</format> + <description>Number of config backups to keep</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 1-65535"/> + </constraint> + <constraintErrorMessage>Number of revisions must be between 0 and 65535</constraintErrorMessage> + </properties> + </leafNode> + </children> + </node> + </children> + </node> +</interfaceDefinition> diff --git a/interface-definitions/system-login.xml.in b/interface-definitions/system-login.xml.in index def42544a..e71a647ef 100644 --- a/interface-definitions/system-login.xml.in +++ b/interface-definitions/system-login.xml.in @@ -127,32 +127,44 @@ </leafNode> <leafNode name="type"> <properties> - <help>Public key type</help> + <help>SSH public key type</help> <completionHelp> - <list>ssh-dss ssh-rsa ecdsa-sha2-nistp256 ecdsa-sha2-nistp384 ecdsa-sha2-nistp521 ssh-ed25519</list> + <list>ssh-dss ssh-rsa ecdsa-sha2-nistp256 ecdsa-sha2-nistp384 ecdsa-sha2-nistp521 ssh-ed25519 sk-ecdsa-sha2-nistp256@openssh.com sk-ssh-ed25519@openssh.com</list> </completionHelp> <valueHelp> <format>ssh-dss</format> - <description/> + <description>Digital Signature Algorithm (DSA) key support</description> </valueHelp> <valueHelp> <format>ssh-rsa</format> - <description/> + <description>Key pair based on RSA algorithm</description> </valueHelp> <valueHelp> <format>ecdsa-sha2-nistp256</format> - <description/> + <description>Elliptic Curve DSA with NIST P-256 curve</description> </valueHelp> <valueHelp> <format>ecdsa-sha2-nistp384</format> - <description/> + <description>Elliptic Curve DSA with NIST P-384 curve</description> + </valueHelp> + <valueHelp> + <format>ecdsa-sha2-nistp521</format> + <description>Elliptic Curve DSA with NIST P-521 curve</description> </valueHelp> <valueHelp> <format>ssh-ed25519</format> - <description/> + <description>Edwards-curve DSA with elliptic curve 25519</description> + </valueHelp> + <valueHelp> + <format>sk-ecdsa-sha2-nistp256@openssh.com</format> + <description>Elliptic Curve DSA security key</description> + </valueHelp> + <valueHelp> + <format>sk-ssh-ed25519@openssh.com</format> + <description>Elliptic curve 25519 security key</description> </valueHelp> <constraint> - <regex>(ssh-dss|ssh-rsa|ecdsa-sha2-nistp256|ecdsa-sha2-nistp384|ecdsa-sha2-nistp521|ssh-ed25519)</regex> + <regex>(ssh-dss|ssh-rsa|ecdsa-sha2-nistp256|ecdsa-sha2-nistp384|ecdsa-sha2-nistp521|ssh-ed25519|sk-ecdsa-sha2-nistp256@openssh.com|sk-ssh-ed25519@openssh.com)</regex> </constraint> </properties> </leafNode> diff --git a/interface-definitions/system-option.xml.in b/interface-definitions/system-option.xml.in index a9fed81fe..bb15e467e 100644 --- a/interface-definitions/system-option.xml.in +++ b/interface-definitions/system-option.xml.in @@ -121,6 +121,7 @@ </properties> <children> #include <include/source-address-ipv4-ipv6.xml.i> + #include <include/source-interface.xml.i> </children> </node> <leafNode name="startup-beep"> diff --git a/interface-definitions/vpn-ipsec.xml.in b/interface-definitions/vpn-ipsec.xml.in index 4776c53dc..9d20926ec 100644 --- a/interface-definitions/vpn-ipsec.xml.in +++ b/interface-definitions/vpn-ipsec.xml.in @@ -11,6 +11,40 @@ <priority>901</priority> </properties> <children> + <node name="authentication"> + <properties> + <help>Authentication</help> + </properties> + <children> + <tagNode name="psk"> + <properties> + <help>Pre-shared key name</help> + </properties> + <children> + #include <include/dhcp-interface-multi.xml.i> + <leafNode name="id"> + <properties> + <help>ID for authentication</help> + <valueHelp> + <format>txt</format> + <description>ID used for authentication</description> + </valueHelp> + <multi/> + </properties> + </leafNode> + <leafNode name="secret"> + <properties> + <help>IKE pre-shared secret key</help> + <valueHelp> + <format>txt</format> + <description>IKE pre-shared secret key</description> + </valueHelp> + </properties> + </leafNode> + </children> + </tagNode> + </children> + </node> <leafNode name="disable-uniqreqids"> <properties> <help>Disable requirement for unique IDs in the Security Database</help> @@ -465,6 +499,45 @@ </properties> <defaultValue>2</defaultValue> </leafNode> + <leafNode name="prf"> + <properties> + <help>Pseudo-Random Functions</help> + <completionHelp> + <list>prfmd5 prfsha1 prfaesxcbc prfaescmac prfsha256 prfsha384 prfsha512</list> + </completionHelp> + <valueHelp> + <format>prfmd5</format> + <description>MD5 PRF</description> + </valueHelp> + <valueHelp> + <format>prfsha1</format> + <description>SHA1 PRF</description> + </valueHelp> + <valueHelp> + <format>prfaesxcbc</format> + <description>AES XCBC PRF</description> + </valueHelp> + <valueHelp> + <format>prfaescmac</format> + <description>AES CMAC PRF</description> + </valueHelp> + <valueHelp> + <format>prfsha256</format> + <description>SHA2_256 PRF</description> + </valueHelp> + <valueHelp> + <format>prfsha384</format> + <description>SHA2_384 PRF</description> + </valueHelp> + <valueHelp> + <format>prfsha512</format> + <description>SHA2_512 PRF</description> + </valueHelp> + <constraint> + <regex>(prfmd5|prfsha1|prfaesxcbc|prfaescmac|prfsha256|prfsha384|prfsha512)</regex> + </constraint> + </properties> + </leafNode> #include <include/vpn-ipsec-encryption.xml.i> #include <include/vpn-ipsec-hash.xml.i> </children> @@ -884,10 +957,11 @@ #include <include/name-server-ipv4-ipv6.xml.i> </children> </tagNode> - #include <include/radius-server-ipv4.xml.i> + #include <include/radius-auth-server-ipv4.xml.i> <node name="radius"> <children> #include <include/radius-nas-identifier.xml.i> + #include <include/radius-timeout.xml.i> <tagNode name="server"> <children> #include <include/accel-ppp/radius-additions-disable-accounting.xml.i> @@ -947,7 +1021,6 @@ </constraint> </properties> </leafNode> - #include <include/ipsec/authentication-pre-shared-secret.xml.i> <leafNode name="remote-id"> <properties> <help>ID for remote authentication</help> @@ -956,6 +1029,7 @@ <description>ID used for peer authentication</description> </valueHelp> </properties> + <defaultValue>%any</defaultValue> </leafNode> <leafNode name="use-x509-id"> <properties> diff --git a/interface-definitions/vpn-l2tp.xml.in b/interface-definitions/vpn-l2tp.xml.in index cb5900e0d..0a92017bd 100644 --- a/interface-definitions/vpn-l2tp.xml.in +++ b/interface-definitions/vpn-l2tp.xml.in @@ -178,7 +178,7 @@ #include <include/accel-ppp/ppp-mppe.xml.i> #include <include/accel-ppp/auth-mode.xml.i> #include <include/accel-ppp/auth-local-users.xml.i> - #include <include/radius-server-ipv4.xml.i> + #include <include/radius-auth-server-ipv4.xml.i> <node name="radius"> <children> <tagNode name="server"> @@ -230,6 +230,7 @@ <properties> <help>Port for Dynamic Authorization Extension server (DM/CoA)</help> </properties> + <defaultValue>1700</defaultValue> </leafNode> <leafNode name="secret"> <properties> @@ -250,6 +251,7 @@ <children> #include <include/accel-ppp/lcp-echo-interval-failure.xml.i> #include <include/accel-ppp/ppp-options-ipv6.xml.i> + #include <include/accel-ppp/ppp-options-ipv6-interface-id.xml.i> </children> </node> </children> diff --git a/interface-definitions/vpn-openconnect.xml.in b/interface-definitions/vpn-openconnect.xml.in index 3b3a83bd4..a426f604d 100644 --- a/interface-definitions/vpn-openconnect.xml.in +++ b/interface-definitions/vpn-openconnect.xml.in @@ -8,6 +8,27 @@ <priority>901</priority> </properties> <children> + <node name="accounting"> + <properties> + <help>Accounting for users OpenConnect VPN Sessions</help> + </properties> + <children> + <node name="mode"> + <properties> + <help>Accounting mode used by this server</help> + </properties> + <children> + <leafNode name="radius"> + <properties> + <help>Use RADIUS server for accounting</help> + <valueless/> + </properties> + </leafNode> + </children> + </node> + #include <include/radius-acct-server-ipv4.xml.i> + </children> + </node> <node name="authentication"> <properties> <help>Authentication for remote access SSL VPN Server</help> @@ -137,23 +158,10 @@ </tagNode> </children> </node> - #include <include/radius-server-ipv4.xml.i> + #include <include/radius-auth-server-ipv4.xml.i> <node name="radius"> <children> - <leafNode name="timeout"> - <properties> - <help>Session timeout</help> - <valueHelp> - <format>u32:1-240</format> - <description>Session timeout in seconds (default: 2)</description> - </valueHelp> - <constraint> - <validator name="numeric" argument="--range 1-240"/> - </constraint> - <constraintErrorMessage>Timeout must be between 1 and 240 seconds</constraintErrorMessage> - </properties> - <defaultValue>2</defaultValue> - </leafNode> + #include <include/radius-timeout.xml.i> <leafNode name="groupconfig"> <properties> <help>If the groupconfig option is set, then config-per-user will be overriden, and all configuration will be read from RADIUS.</help> @@ -163,7 +171,7 @@ </node> </children> </node> - #include <include/listen-address-ipv4.xml.i> + #include <include/listen-address-ipv4-single.xml.i> <leafNode name="listen-address"> <defaultValue>0.0.0.0</defaultValue> </leafNode> diff --git a/interface-definitions/vpn-pptp.xml.in b/interface-definitions/vpn-pptp.xml.in index 5e52965fd..00ffd26f9 100644 --- a/interface-definitions/vpn-pptp.xml.in +++ b/interface-definitions/vpn-pptp.xml.in @@ -108,7 +108,7 @@ </tagNode> </children> </node> - #include <include/radius-server-ipv4.xml.i> + #include <include/radius-auth-server-ipv4.xml.i> #include <include/accel-ppp/radius-additions.xml.i> #include <include/accel-ppp/radius-additions-rate-limit.xml.i> </children> diff --git a/interface-definitions/vpn-sstp.xml.in b/interface-definitions/vpn-sstp.xml.in index 195d581df..9e912063f 100644 --- a/interface-definitions/vpn-sstp.xml.in +++ b/interface-definitions/vpn-sstp.xml.in @@ -16,7 +16,7 @@ #include <include/accel-ppp/auth-local-users.xml.i> #include <include/accel-ppp/auth-mode.xml.i> #include <include/accel-ppp/auth-protocols.xml.i> - #include <include/radius-server-ipv4.xml.i> + #include <include/radius-auth-server-ipv4.xml.i> #include <include/accel-ppp/radius-additions.xml.i> <node name="radius"> <children> diff --git a/interface-definitions/vrf.xml.in b/interface-definitions/vrf.xml.in index 3604b41c8..96c6d8be2 100644 --- a/interface-definitions/vrf.xml.in +++ b/interface-definitions/vrf.xml.in @@ -26,7 +26,7 @@ </valueHelp> </properties> <children> - #include <include/interface/description.xml.i> + #include <include/generic-description.xml.i> #include <include/interface/disable.xml.i> <node name="ip"> <properties> diff --git a/interface-definitions/xml-component-version.xml.in b/interface-definitions/xml-component-version.xml.in index 914e3bc69..2e6506efc 100644 --- a/interface-definitions/xml-component-version.xml.in +++ b/interface-definitions/xml-component-version.xml.in @@ -6,6 +6,7 @@ #include <include/version/config-management-version.xml.i> #include <include/version/conntrack-sync-version.xml.i> #include <include/version/conntrack-version.xml.i> + #include <include/version/container-version.xml.i> #include <include/version/dhcp-relay-version.xml.i> #include <include/version/dhcp-server-version.xml.i> #include <include/version/dhcpv6-server-version.xml.i> diff --git a/mibs/IANA-ADDRESS-FAMILY-NUMBERS-MIB.txt b/mibs/IANA-ADDRESS-FAMILY-NUMBERS-MIB.txt new file mode 100644 index 000000000..7995fc4ad --- /dev/null +++ b/mibs/IANA-ADDRESS-FAMILY-NUMBERS-MIB.txt @@ -0,0 +1,166 @@ + IANA-ADDRESS-FAMILY-NUMBERS-MIB DEFINITIONS ::= BEGIN + + IMPORTS + MODULE-IDENTITY, + mib-2 FROM SNMPv2-SMI + TEXTUAL-CONVENTION FROM SNMPv2-TC; + + ianaAddressFamilyNumbers MODULE-IDENTITY + LAST-UPDATED "201309250000Z" -- September 25, 2013 + ORGANIZATION "IANA" + CONTACT-INFO + "Postal: Internet Assigned Numbers Authority + Internet Corporation for Assigned Names + and Numbers + 12025 Waterfront Drive, Suite 300 + Los Angeles, CA 90094-2536 + USA + + Tel: +1 310-301-5800 + E-Mail: iana&iana.org" + DESCRIPTION + "The MIB module defines the AddressFamilyNumbers + textual convention." + + -- revision history + + REVISION "201309250000Z" -- September 25, 2013 + DESCRIPTION "Fixed labels for 16389-16390." + + REVISION "201307160000Z" -- July 16, 2013 + DESCRIPTION "Fixed labels for 16389-16390." + + REVISION "201306260000Z" -- June 26, 2013 + DESCRIPTION "Added assignments 26-28." + + REVISION "201306180000Z" -- June 18, 2013 + DESCRIPTION "Added assignments 16384-16390. Assignment + 25 added in 2007 revision." + + REVISION "200203140000Z" -- March 14, 2002 + DESCRIPTION "AddressFamilyNumbers assignment 22 to + fibreChannelWWPN. AddressFamilyNumbers + assignment 23 to fibreChannelWWNN. + AddressFamilyNumers assignment 24 to gwid." + + REVISION "200009080000Z" -- September 8, 2000 + DESCRIPTION "AddressFamilyNumbers assignment 19 to xtpOverIpv4. + AddressFamilyNumbers assignment 20 to xtpOverIpv6. + AddressFamilyNumbers assignment 21 to xtpNativeModeXTP." + + REVISION "200003010000Z" -- March 1, 2000 + DESCRIPTION "AddressFamilyNumbers assignment 17 to distinguishedName. + AddressFamilyNumbers assignment 18 to asNumber." + + REVISION "200002040000Z" -- February 4, 2000 + DESCRIPTION "AddressFamilyNumbers assignment 16 to dns." + + REVISION "9908260000Z" -- August 26, 1999 + DESCRIPTION "Initial version, published as RFC 2677." + ::= { mib-2 72 } + + AddressFamilyNumbers ::= TEXTUAL-CONVENTION + STATUS current + DESCRIPTION + "The definition of this textual convention with the + addition of newly assigned values is published + periodically by the IANA, in either the Assigned + Numbers RFC, or some derivative of it specific to + Internet Network Management number assignments. + (The latest arrangements can be obtained by + contacting the IANA.) + + The enumerations are described as: + + other(0), -- none of the following + ipV4(1), -- IP Version 4 + ipV6(2), -- IP Version 6 + nsap(3), -- NSAP + hdlc(4), -- (8-bit multidrop) + bbn1822(5), + all802(6), -- (includes all 802 media + -- plus Ethernet 'canonical format') + e163(7), + e164(8), -- (SMDS, Frame Relay, ATM) + f69(9), -- (Telex) + x121(10), -- (X.25, Frame Relay) + ipx(11), -- IPX (Internet Protocol Exchange) + appleTalk(12), -- Apple Talk + decnetIV(13), -- DEC Net Phase IV + banyanVines(14), -- Banyan Vines + e164withNsap(15), + -- (E.164 with NSAP format subaddress) + dns(16), -- (Domain Name System) + distinguishedName(17), -- (Distinguished Name, per X.500) + asNumber(18), -- (16-bit quantity, per the AS number space) + xtpOverIpv4(19), -- XTP over IP version 4 + xtpOverIpv6(20), -- XTP over IP version 6 + xtpNativeModeXTP(21), -- XTP native mode XTP + fibreChannelWWPN(22), -- Fibre Channel World-Wide Port Name + fibreChannelWWNN(23), -- Fibre Channel World-Wide Node Name + gwid(24), -- Gateway Identifier + afi(25), -- AFI for L2VPN information + mplsTpSectionEndpointIdentifier(26), -- MPLS-TP Section Endpoint Identifier + mplsTpLspEndpointIdentifier(27), -- MPLS-TP LSP Endpoint Identifier + mplsTpPseudowireEndpointIdentifier(28), -- MPLS-TP Pseudowire Endpoint Identifier + eigrpCommonServiceFamily(16384), -- EIGRP Common Service Family + eigrpIpv4ServiceFamily(16385), -- EIGRP IPv4 Service Family + eigrpIpv6ServiceFamily(16386), -- EIGRP IPv6 Service Family + lispCanonicalAddressFormat(16387), -- LISP Canonical Address Format (LCAF) + bgpLs(16388), -- BGP-LS + fortyeightBitMacBitMac(16389), -- 48-bit MAC + sixtyfourBitMac(16390), -- 64-bit MAC + oui(16391), -- OUI + mac24(16392), -- MAC/24 + mac40(16393), -- MAC/40 + ipv664(16394), -- IPv6/64 + rBridgePortID(16395), -- RBridge Port ID + reserved(65535) + + Requests for new values should be made to IANA via + email (iana&iana.org)." + SYNTAX INTEGER { + other(0), + ipV4(1), + ipV6(2), + nsap(3), + hdlc(4), + bbn1822(5), + all802(6), + e163(7), + e164(8), + f69(9), + x121(10), + ipx(11), + appleTalk(12), + decnetIV(13), + banyanVines(14), + e164withNsap(15), + dns(16), + distinguishedName(17), -- (Distinguished Name, per X.500) + asNumber(18), -- (16-bit quantity, per the AS number space) + xtpOverIpv4(19), + xtpOverIpv6(20), + xtpNativeModeXTP(21), + fibreChannelWWPN(22), + fibreChannelWWNN(23), + gwid(24), + afi(25), + mplsTpSectionEndpointIdentifier(26), + mplsTpLspEndpointIdentifier(27), + mplsTpPseudowireEndpointIdentifier(28), + eigrpCommonServiceFamily(16384), + eigrpIpv4ServiceFamily(16385), + eigrpIpv6ServiceFamily(16386), + lispCanonicalAddressFormat(16387), + bgpLs(16388), + fortyeightBitMac(16389), + sixtyfourBitMac(16390), + oui(16391), + mac24(16392), + mac40(16393), + ipv664(16394), + rBridgePortID(16395), + reserved(65535) + } + END diff --git a/mibs/IANA-LANGUAGE-MIB.txt b/mibs/IANA-LANGUAGE-MIB.txt new file mode 100644 index 000000000..4b97bdd39 --- /dev/null +++ b/mibs/IANA-LANGUAGE-MIB.txt @@ -0,0 +1,126 @@ +IANA-LANGUAGE-MIB DEFINITIONS ::= BEGIN + +IMPORTS + MODULE-IDENTITY, OBJECT-IDENTITY, mib-2 + FROM SNMPv2-SMI; + +ianaLanguages MODULE-IDENTITY + LAST-UPDATED "201405220000Z" -- May 22, 2014 + ORGANIZATION "IANA" + CONTACT-INFO + "Internet Assigned Numbers Authority (IANA) + + Postal: ICANN + 12025 Waterfront Drive, Suite 300 + Los Angeles, CA 90094-2536 + + Tel: +1 310-301-5800 + E-Mail: iana&iana.org" + DESCRIPTION + "The MIB module registers object identifier values for + well-known programming and scripting languages. Every + language registration MUST describe the format used + when transferring scripts written in this language. + + Any additions or changes to the contents of this MIB + module require Designated Expert Review as defined in + the Guidelines for Writing IANA Considerations Section + document. The Designated Expert will be selected by + the IESG Area Director of the OPS Area. + + Note, this module does not have to register all possible + languages since languages are identified by object + identifier values. It is therefore possible to registered + languages in private OID trees. The references given below are not + normative with regard to the language version. Other + references might be better suited to describe some newer + versions of this language. The references are only + provided as `a pointer into the right direction'." + + -- Revision log, in reverse chronological order + + REVISION "201405220000Z" -- May 22, 2014 + DESCRIPTION "Updated contact info." + + REVISION "200005100000Z" -- May 10, 2000 + DESCRIPTION "Import mib-2 instead of experimental, so that + this module compiles" + + REVISION "199909090900Z" -- September 9, 1999 + DESCRIPTION "Initial version as published at time of + publication of RFC 2591." + ::= { mib-2 73 } + +ianaLangJavaByteCode OBJECT-IDENTITY + STATUS current + DESCRIPTION + "Java byte code to be processed by a Java virtual machine. + A script written in Java byte code is transferred by using + the Java archive file format (JAR)." + REFERENCE + "The Java Virtual Machine Specification. + ISBN 0-201-63452-X" + ::= { ianaLanguages 1 } + +ianaLangTcl OBJECT-IDENTITY + STATUS current + DESCRIPTION + "The Tool Command Language (Tcl). A script written in the + Tcl language is transferred in Tcl source code format." + REFERENCE + "Tcl and the Tk Toolkit. + ISBN 0-201-63337-X" + ::= { ianaLanguages 2 } + +ianaLangPerl OBJECT-IDENTITY + STATUS current + DESCRIPTION + "The Perl language. A script written in the Perl language + is transferred in Perl source code format." + REFERENCE + "Programming Perl. + ISBN 1-56592-149-6" + ::= { ianaLanguages 3 } + +ianaLangScheme OBJECT-IDENTITY + STATUS current + DESCRIPTION + "The Scheme language. A script written in the Scheme + language is transferred in Scheme source code format." + REFERENCE + "The Revised^4 Report on the Algorithmic Language Scheme. + MIT Press" + ::= { ianaLanguages 4 } + +ianaLangSRSL OBJECT-IDENTITY + STATUS current + DESCRIPTION + "The SNMP Script Language defined by SNMP Research. A + script written in the SNMP Script Language is transferred + in the SNMP Script Language source code format." + ::= { ianaLanguages 5 } + +ianaLangPSL OBJECT-IDENTITY + STATUS current + DESCRIPTION + "The Patrol Script Language defined by BMC Software. A script + written in the Patrol Script Language is transferred in the + Patrol Script Language source code format." + REFERENCE + "PATROL Script Language Reference Manual, Version 3.0, + November 30, 1995. BMC Software, Inc. 2101 City West Blvd., + Houston, Texas 77042." + ::= { ianaLanguages 6 } + +ianaLangSMSL OBJECT-IDENTITY + STATUS current + DESCRIPTION + "The Systems Management Scripting Language. A script written + in the SMSL language is transferred in the SMSL source code + format." + REFERENCE + "ISO/ITU Command Sequencer. + ISO 10164-21 or ITU X.753" + ::= { ianaLanguages 7 } + +END diff --git a/mibs/IANA-RTPROTO-MIB.txt b/mibs/IANA-RTPROTO-MIB.txt new file mode 100644 index 000000000..f7bc1ebc7 --- /dev/null +++ b/mibs/IANA-RTPROTO-MIB.txt @@ -0,0 +1,95 @@ +IANA-RTPROTO-MIB DEFINITIONS ::= BEGIN + +IMPORTS + MODULE-IDENTITY, mib-2 FROM SNMPv2-SMI + TEXTUAL-CONVENTION FROM SNMPv2-TC; + +ianaRtProtoMIB MODULE-IDENTITY + LAST-UPDATED "201208300000Z" -- August 30, 2012 + ORGANIZATION "IANA" + CONTACT-INFO + " Internet Assigned Numbers Authority + Internet Corporation for Assigned Names and Numbers + 12025 Waterfront Drive, Suite 300 + Los Angeles, CA 90094-2536 + + Phone: +1 310 301 5800 + EMail: iana&iana.org" + DESCRIPTION + "This MIB module defines the IANAipRouteProtocol and + IANAipMRouteProtocol textual conventions for use in MIBs + which need to identify unicast or multicast routing + mechanisms. + + Any additions or changes to the contents of this MIB module + require either publication of an RFC, or Designated Expert + Review as defined in RFC 2434, Guidelines for Writing an + IANA Considerations Section in RFCs. The Designated Expert + will be selected by the IESG Area Director(s) of the Routing + Area." + + REVISION "201208300000Z" -- August 30, 2012 + DESCRIPTION "Added dhcp(19)." + + REVISION "201107220000Z" -- July 22, 2011 + DESCRIPTION "Added rpl(18) ." + + REVISION "200009260000Z" -- September 26, 2000 + DESCRIPTION "Original version, published in coordination + with RFC 2932." + ::= { mib-2 84 } + +IANAipRouteProtocol ::= TEXTUAL-CONVENTION + STATUS current + DESCRIPTION + "A mechanism for learning routes. Inclusion of values for + routing protocols is not intended to imply that those + protocols need be supported." + SYNTAX INTEGER { + other (1), -- not specified + local (2), -- local interface + netmgmt (3), -- static route + icmp (4), -- result of ICMP Redirect + + -- the following are all dynamic + -- routing protocols + + egp (5), -- Exterior Gateway Protocol + ggp (6), -- Gateway-Gateway Protocol + hello (7), -- FuzzBall HelloSpeak + rip (8), -- Berkeley RIP or RIP-II + isIs (9), -- Dual IS-IS + esIs (10), -- ISO 9542 + ciscoIgrp (11), -- Cisco IGRP + bbnSpfIgp (12), -- BBN SPF IGP + ospf (13), -- Open Shortest Path First + bgp (14), -- Border Gateway Protocol + idpr (15), -- InterDomain Policy Routing + ciscoEigrp (16), -- Cisco EIGRP + dvmrp (17), -- DVMRP + rpl (18), -- RPL [RFC-ietf-roll-rpl-19] + dhcp (19) -- DHCP [RFC2132] + } + +IANAipMRouteProtocol ::= TEXTUAL-CONVENTION + STATUS current + DESCRIPTION + "The multicast routing protocol. Inclusion of values for + multicast routing protocols is not intended to imply that + those protocols need be supported." + SYNTAX INTEGER { + other(1), -- none of the following + local(2), -- e.g., manually configured + netmgmt(3), -- set via net.mgmt protocol + dvmrp(4), + mospf(5), + pimSparseDense(6), -- PIMv1, both DM and SM + cbt(7), + pimSparseMode(8), -- PIM-SM + pimDenseMode(9), -- PIM-DM + igmpOnly(10), + bgmp(11), + msdp(12) + } + +END diff --git a/mibs/IANAifType-MIB.txt b/mibs/IANAifType-MIB.txt new file mode 100644 index 000000000..027a1532f --- /dev/null +++ b/mibs/IANAifType-MIB.txt @@ -0,0 +1,646 @@ + IANAifType-MIB DEFINITIONS ::= BEGIN + + IMPORTS + MODULE-IDENTITY, mib-2 FROM SNMPv2-SMI + TEXTUAL-CONVENTION FROM SNMPv2-TC; + + ianaifType MODULE-IDENTITY + LAST-UPDATED "201407030000Z" -- July 3, 2014 + ORGANIZATION "IANA" + CONTACT-INFO " Internet Assigned Numbers Authority + + Postal: ICANN + 12025 Waterfront Drive, Suite 300 + Los Angeles, CA 90094-2536 + + Tel: +1 310-301-5800 + E-Mail: iana&iana.org" + DESCRIPTION "This MIB module defines the IANAifType Textual + Convention, and thus the enumerated values of + the ifType object defined in MIB-II's ifTable." + + REVISION "201407030000Z" -- July 3, 2014 + DESCRIPTION "Registration of new IANAifTypes 277-278." + + REVISION "201405220000Z" -- May 22, 2014 + DESCRIPTION "Updated contact info." + + REVISION "201205170000Z" -- May 17, 2012 + DESCRIPTION "Registration of new IANAifType 272." + + REVISION "201201110000Z" -- January 11, 2012 + DESCRIPTION "Registration of new IANAifTypes 266-271." + + REVISION "201112180000Z" -- December 18, 2011 + DESCRIPTION "Registration of new IANAifTypes 263-265." + + REVISION "201110260000Z" -- October 26, 2011 + DESCRIPTION "Registration of new IANAifType 262." + + REVISION "201109070000Z" -- September 7, 2011 + DESCRIPTION "Registration of new IANAifTypes 260 and 261." + + REVISION "201107220000Z" -- July 22, 2011 + DESCRIPTION "Registration of new IANAifType 259." + + REVISION "201106030000Z" -- June 03, 2011 + DESCRIPTION "Registration of new IANAifType 258." + + REVISION "201009210000Z" -- September 21, 2010 + DESCRIPTION "Registration of new IANAifTypes 256 and 257." + + REVISION "201007210000Z" -- July 21, 2010 + DESCRIPTION "Registration of new IANAifType 255." + + REVISION "201002110000Z" -- February 11, 2010 + DESCRIPTION "Registration of new IANAifType 254." + + REVISION "201002080000Z" -- February 08, 2010 + DESCRIPTION "Registration of new IANAifTypes 252 and 253." + + REVISION "200905060000Z" -- May 06, 2009 + DESCRIPTION "Registration of new IANAifType 251." + + REVISION "200902060000Z" -- February 06, 2009 + DESCRIPTION "Registration of new IANAtunnelType 15." + + REVISION "200810090000Z" -- October 09, 2008 + DESCRIPTION "Registration of new IANAifType 250." + + REVISION "200808120000Z" -- August 12, 2008 + DESCRIPTION "Registration of new IANAifType 249." + + REVISION "200807220000Z" -- July 22, 2008 + DESCRIPTION "Registration of new IANAifTypes 247 and 248." + + REVISION "200806240000Z" -- June 24, 2008 + DESCRIPTION "Registration of new IANAifType 246." + + REVISION "200805290000Z" -- May 29, 2008 + DESCRIPTION "Registration of new IANAifType 245." + + REVISION "200709130000Z" -- September 13, 2007 + DESCRIPTION "Registration of new IANAifTypes 243 and 244." + + REVISION "200705290000Z" -- May 29, 2007 + DESCRIPTION "Changed the description for IANAifType 228." + + REVISION "200703080000Z" -- March 08, 2007 + DESCRIPTION "Registration of new IANAifType 242." + + REVISION "200701230000Z" -- January 23, 2007 + DESCRIPTION "Registration of new IANAifTypes 239, 240, and 241." + + REVISION "200610170000Z" -- October 17, 2006 + DESCRIPTION "Deprecated/Obsoleted IANAifType 230. Registration of + IANAifType 238." + + REVISION "200609250000Z" -- September 25, 2006 + DESCRIPTION "Changed the description for IANA ifType + 184 and added new IANA ifType 237." + + REVISION "200608170000Z" -- August 17, 2006 + DESCRIPTION "Changed the descriptions for IANAifTypes + 20 and 21." + + REVISION "200608110000Z" -- August 11, 2006 + DESCRIPTION "Changed the descriptions for IANAifTypes + 7, 11, 62, 69, and 117." + + REVISION "200607250000Z" -- July 25, 2006 + DESCRIPTION "Registration of new IANA ifType 236." + + REVISION "200606140000Z" -- June 14, 2006 + DESCRIPTION "Registration of new IANA ifType 235." + + REVISION "200603310000Z" -- March 31, 2006 + DESCRIPTION "Registration of new IANA ifType 234." + + REVISION "200603300000Z" -- March 30, 2006 + DESCRIPTION "Registration of new IANA ifType 233." + + REVISION "200512220000Z" -- December 22, 2005 + DESCRIPTION "Registration of new IANA ifTypes 231 and 232." + + REVISION "200510100000Z" -- October 10, 2005 + DESCRIPTION "Registration of new IANA ifType 230." + + REVISION "200509090000Z" -- September 09, 2005 + DESCRIPTION "Registration of new IANA ifType 229." + + REVISION "200505270000Z" -- May 27, 2005 + DESCRIPTION "Registration of new IANA ifType 228." + + REVISION "200503030000Z" -- March 3, 2005 + DESCRIPTION "Added the IANAtunnelType TC and deprecated + IANAifType sixToFour (215) per RFC4087." + + REVISION "200411220000Z" -- November 22, 2004 + DESCRIPTION "Registration of new IANA ifType 227 per RFC4631." + + REVISION "200406170000Z" -- June 17, 2004 + DESCRIPTION "Registration of new IANA ifType 226." + + REVISION "200405120000Z" -- May 12, 2004 + DESCRIPTION "Added description for IANAifType 6, and + changed the descriptions for IANAifTypes + 180, 181, and 182." + + REVISION "200405070000Z" -- May 7, 2004 + DESCRIPTION "Registration of new IANAifType 225." + + REVISION "200308250000Z" -- Aug 25, 2003 + DESCRIPTION "Deprecated IANAifTypes 7 and 11. Obsoleted + IANAifTypes 62, 69, and 117. ethernetCsmacd (6) + should be used instead of these values" + + REVISION "200308180000Z" -- Aug 18, 2003 + DESCRIPTION "Registration of new IANAifType + 224." + + REVISION "200308070000Z" -- Aug 7, 2003 + DESCRIPTION "Registration of new IANAifTypes + 222 and 223." + + REVISION "200303180000Z" -- Mar 18, 2003 + DESCRIPTION "Registration of new IANAifType + 221." + + REVISION "200301130000Z" -- Jan 13, 2003 + DESCRIPTION "Registration of new IANAifType + 220." + + REVISION "200210170000Z" -- Oct 17, 2002 + DESCRIPTION "Registration of new IANAifType + 219." + + REVISION "200207160000Z" -- Jul 16, 2002 + DESCRIPTION "Registration of new IANAifTypes + 217 and 218." + + REVISION "200207100000Z" -- Jul 10, 2002 + DESCRIPTION "Registration of new IANAifTypes + 215 and 216." + + REVISION "200206190000Z" -- Jun 19, 2002 + DESCRIPTION "Registration of new IANAifType + 214." + + REVISION "200201040000Z" -- Jan 4, 2002 + DESCRIPTION "Registration of new IANAifTypes + 211, 212 and 213." + + REVISION "200112200000Z" -- Dec 20, 2001 + DESCRIPTION "Registration of new IANAifTypes + 209 and 210." + + REVISION "200111150000Z" -- Nov 15, 2001 + DESCRIPTION "Registration of new IANAifTypes + 207 and 208." + + REVISION "200111060000Z" -- Nov 6, 2001 + DESCRIPTION "Registration of new IANAifType + 206." + + REVISION "200111020000Z" -- Nov 2, 2001 + DESCRIPTION "Registration of new IANAifType + 205." + + REVISION "200110160000Z" -- Oct 16, 2001 + DESCRIPTION "Registration of new IANAifTypes + 199, 200, 201, 202, 203, and 204." + + REVISION "200109190000Z" -- Sept 19, 2001 + DESCRIPTION "Registration of new IANAifType + 198." + + REVISION "200105110000Z" -- May 11, 2001 + DESCRIPTION "Registration of new IANAifType + 197." + + REVISION "200101120000Z" -- Jan 12, 2001 + DESCRIPTION "Registration of new IANAifTypes + 195 and 196." + + REVISION "200012190000Z" -- Dec 19, 2000 + DESCRIPTION "Registration of new IANAifTypes + 193 and 194." + + REVISION "200012070000Z" -- Dec 07, 2000 + DESCRIPTION "Registration of new IANAifTypes + 191 and 192." + + REVISION "200012040000Z" -- Dec 04, 2000 + DESCRIPTION "Registration of new IANAifType + 190." + + REVISION "200010170000Z" -- Oct 17, 2000 + DESCRIPTION "Registration of new IANAifTypes + 188 and 189." + + REVISION "200010020000Z" -- Oct 02, 2000 + DESCRIPTION "Registration of new IANAifType 187." + + REVISION "200009010000Z" -- Sept 01, 2000 + DESCRIPTION "Registration of new IANAifTypes + 184, 185, and 186." + + REVISION "200008240000Z" -- Aug 24, 2000 + DESCRIPTION "Registration of new IANAifType 183." + + REVISION "200008230000Z" -- Aug 23, 2000 + DESCRIPTION "Registration of new IANAifTypes + 174-182." + + REVISION "200008220000Z" -- Aug 22, 2000 + DESCRIPTION "Registration of new IANAifTypes 170, + 171, 172 and 173." + + REVISION "200004250000Z" -- Apr 25, 2000 + DESCRIPTION "Registration of new IANAifTypes 168 and 169." + + REVISION "200003060000Z" -- Mar 6, 2000 + DESCRIPTION "Fixed a missing semi-colon in the IMPORT. + Also cleaned up the REVISION log a bit. + It is not complete, but from now on it will + be maintained and kept up to date with each + change to this MIB module." + + REVISION "199910081430Z" -- Oct 08, 1999 + DESCRIPTION "Include new name assignments up to cnr(85). + This is the first version available via the WWW + at: ftp://ftp.isi.edu/mib/ianaiftype.mib" + + REVISION "199401310000Z" -- Jan 31, 1994 + DESCRIPTION "Initial version of this MIB as published in + RFC 1573." + ::= { mib-2 30 } + + IANAifType ::= TEXTUAL-CONVENTION + STATUS current + DESCRIPTION + "This data type is used as the syntax of the ifType + object in the (updated) definition of MIB-II's + ifTable. + + The definition of this textual convention with the + addition of newly assigned values is published + periodically by the IANA, in either the Assigned + Numbers RFC, or some derivative of it specific to + Internet Network Management number assignments. (The + latest arrangements can be obtained by contacting the + IANA.) + + Requests for new values should be made to IANA via + email (iana&iana.org). + + The relationship between the assignment of ifType + values and of OIDs to particular media-specific MIBs + is solely the purview of IANA and is subject to change + without notice. Quite often, a media-specific MIB's + OID-subtree assignment within MIB-II's 'transmission' + subtree will be the same as its ifType value. + However, in some circumstances this will not be the + case, and implementors must not pre-assume any + specific relationship between ifType values and + transmission subtree OIDs." + SYNTAX INTEGER { + other(1), -- none of the following + regular1822(2), + hdh1822(3), + ddnX25(4), + rfc877x25(5), + ethernetCsmacd(6), -- for all ethernet-like interfaces, + -- regardless of speed, as per RFC3635 + iso88023Csmacd(7), -- Deprecated via RFC3635 + -- ethernetCsmacd (6) should be used instead + iso88024TokenBus(8), + iso88025TokenRing(9), + iso88026Man(10), + starLan(11), -- Deprecated via RFC3635 + -- ethernetCsmacd (6) should be used instead + proteon10Mbit(12), + proteon80Mbit(13), + hyperchannel(14), + fddi(15), + lapb(16), + sdlc(17), + ds1(18), -- DS1-MIB + e1(19), -- Obsolete see DS1-MIB + basicISDN(20), -- no longer used + -- see also RFC2127 + primaryISDN(21), -- no longer used + -- see also RFC2127 + propPointToPointSerial(22), -- proprietary serial + ppp(23), + softwareLoopback(24), + eon(25), -- CLNP over IP + ethernet3Mbit(26), + nsip(27), -- XNS over IP + slip(28), -- generic SLIP + ultra(29), -- ULTRA technologies + ds3(30), -- DS3-MIB + sip(31), -- SMDS, coffee + frameRelay(32), -- DTE only. + rs232(33), + para(34), -- parallel-port + arcnet(35), -- arcnet + arcnetPlus(36), -- arcnet plus + atm(37), -- ATM cells + miox25(38), + sonet(39), -- SONET or SDH + x25ple(40), + iso88022llc(41), + localTalk(42), + smdsDxi(43), + frameRelayService(44), -- FRNETSERV-MIB + v35(45), + hssi(46), + hippi(47), + modem(48), -- Generic modem + aal5(49), -- AAL5 over ATM + sonetPath(50), + sonetVT(51), + smdsIcip(52), -- SMDS InterCarrier Interface + propVirtual(53), -- proprietary virtual/internal + propMultiplexor(54),-- proprietary multiplexing + ieee80212(55), -- 100BaseVG + fibreChannel(56), -- Fibre Channel + hippiInterface(57), -- HIPPI interfaces + frameRelayInterconnect(58), -- Obsolete, use either + -- frameRelay(32) or + -- frameRelayService(44). + aflane8023(59), -- ATM Emulated LAN for 802.3 + aflane8025(60), -- ATM Emulated LAN for 802.5 + cctEmul(61), -- ATM Emulated circuit + fastEther(62), -- Obsoleted via RFC3635 + -- ethernetCsmacd (6) should be used instead + isdn(63), -- ISDN and X.25 + v11(64), -- CCITT V.11/X.21 + v36(65), -- CCITT V.36 + g703at64k(66), -- CCITT G703 at 64Kbps + g703at2mb(67), -- Obsolete see DS1-MIB + qllc(68), -- SNA QLLC + fastEtherFX(69), -- Obsoleted via RFC3635 + -- ethernetCsmacd (6) should be used instead + channel(70), -- channel + ieee80211(71), -- radio spread spectrum + ibm370parChan(72), -- IBM System 360/370 OEMI Channel + escon(73), -- IBM Enterprise Systems Connection + dlsw(74), -- Data Link Switching + isdns(75), -- ISDN S/T interface + isdnu(76), -- ISDN U interface + lapd(77), -- Link Access Protocol D + ipSwitch(78), -- IP Switching Objects + rsrb(79), -- Remote Source Route Bridging + atmLogical(80), -- ATM Logical Port + ds0(81), -- Digital Signal Level 0 + ds0Bundle(82), -- group of ds0s on the same ds1 + bsc(83), -- Bisynchronous Protocol + async(84), -- Asynchronous Protocol + cnr(85), -- Combat Net Radio + iso88025Dtr(86), -- ISO 802.5r DTR + eplrs(87), -- Ext Pos Loc Report Sys + arap(88), -- Appletalk Remote Access Protocol + propCnls(89), -- Proprietary Connectionless Protocol + hostPad(90), -- CCITT-ITU X.29 PAD Protocol + termPad(91), -- CCITT-ITU X.3 PAD Facility + frameRelayMPI(92), -- Multiproto Interconnect over FR + x213(93), -- CCITT-ITU X213 + adsl(94), -- Asymmetric Digital Subscriber Loop + radsl(95), -- Rate-Adapt. Digital Subscriber Loop + sdsl(96), -- Symmetric Digital Subscriber Loop + vdsl(97), -- Very H-Speed Digital Subscrib. Loop + iso88025CRFPInt(98), -- ISO 802.5 CRFP + myrinet(99), -- Myricom Myrinet + voiceEM(100), -- voice recEive and transMit + voiceFXO(101), -- voice Foreign Exchange Office + voiceFXS(102), -- voice Foreign Exchange Station + voiceEncap(103), -- voice encapsulation + voiceOverIp(104), -- voice over IP encapsulation + atmDxi(105), -- ATM DXI + atmFuni(106), -- ATM FUNI + atmIma (107), -- ATM IMA + pppMultilinkBundle(108), -- PPP Multilink Bundle + ipOverCdlc (109), -- IBM ipOverCdlc + ipOverClaw (110), -- IBM Common Link Access to Workstn + stackToStack (111), -- IBM stackToStack + virtualIpAddress (112), -- IBM VIPA + mpc (113), -- IBM multi-protocol channel support + ipOverAtm (114), -- IBM ipOverAtm + iso88025Fiber (115), -- ISO 802.5j Fiber Token Ring + tdlc (116), -- IBM twinaxial data link control + gigabitEthernet (117), -- Obsoleted via RFC3635 + -- ethernetCsmacd (6) should be used instead + hdlc (118), -- HDLC + lapf (119), -- LAP F + v37 (120), -- V.37 + x25mlp (121), -- Multi-Link Protocol + x25huntGroup (122), -- X25 Hunt Group + transpHdlc (123), -- Transp HDLC + interleave (124), -- Interleave channel + fast (125), -- Fast channel + ip (126), -- IP (for APPN HPR in IP networks) + docsCableMaclayer (127), -- CATV Mac Layer + docsCableDownstream (128), -- CATV Downstream interface + docsCableUpstream (129), -- CATV Upstream interface + a12MppSwitch (130), -- Avalon Parallel Processor + tunnel (131), -- Encapsulation interface + coffee (132), -- coffee pot + ces (133), -- Circuit Emulation Service + atmSubInterface (134), -- ATM Sub Interface + l2vlan (135), -- Layer 2 Virtual LAN using 802.1Q + l3ipvlan (136), -- Layer 3 Virtual LAN using IP + l3ipxvlan (137), -- Layer 3 Virtual LAN using IPX + digitalPowerline (138), -- IP over Power Lines + mediaMailOverIp (139), -- Multimedia Mail over IP + dtm (140), -- Dynamic syncronous Transfer Mode + dcn (141), -- Data Communications Network + ipForward (142), -- IP Forwarding Interface + msdsl (143), -- Multi-rate Symmetric DSL + ieee1394 (144), -- IEEE1394 High Performance Serial Bus + if-gsn (145), -- HIPPI-6400 + dvbRccMacLayer (146), -- DVB-RCC MAC Layer + dvbRccDownstream (147), -- DVB-RCC Downstream Channel + dvbRccUpstream (148), -- DVB-RCC Upstream Channel + atmVirtual (149), -- ATM Virtual Interface + mplsTunnel (150), -- MPLS Tunnel Virtual Interface + srp (151), -- Spatial Reuse Protocol + voiceOverAtm (152), -- Voice Over ATM + voiceOverFrameRelay (153), -- Voice Over Frame Relay + idsl (154), -- Digital Subscriber Loop over ISDN + compositeLink (155), -- Avici Composite Link Interface + ss7SigLink (156), -- SS7 Signaling Link + propWirelessP2P (157), -- Prop. P2P wireless interface + frForward (158), -- Frame Forward Interface + rfc1483 (159), -- Multiprotocol over ATM AAL5 + usb (160), -- USB Interface + ieee8023adLag (161), -- IEEE 802.3ad Link Aggregate + bgppolicyaccounting (162), -- BGP Policy Accounting + frf16MfrBundle (163), -- FRF .16 Multilink Frame Relay + h323Gatekeeper (164), -- H323 Gatekeeper + h323Proxy (165), -- H323 Voice and Video Proxy + mpls (166), -- MPLS + mfSigLink (167), -- Multi-frequency signaling link + hdsl2 (168), -- High Bit-Rate DSL - 2nd generation + shdsl (169), -- Multirate HDSL2 + ds1FDL (170), -- Facility Data Link 4Kbps on a DS1 + pos (171), -- Packet over SONET/SDH Interface + dvbAsiIn (172), -- DVB-ASI Input + dvbAsiOut (173), -- DVB-ASI Output + plc (174), -- Power Line Communtications + nfas (175), -- Non Facility Associated Signaling + tr008 (176), -- TR008 + gr303RDT (177), -- Remote Digital Terminal + gr303IDT (178), -- Integrated Digital Terminal + isup (179), -- ISUP + propDocsWirelessMaclayer (180), -- Cisco proprietary Maclayer + propDocsWirelessDownstream (181), -- Cisco proprietary Downstream + propDocsWirelessUpstream (182), -- Cisco proprietary Upstream + hiperlan2 (183), -- HIPERLAN Type 2 Radio Interface + propBWAp2Mp (184), -- PropBroadbandWirelessAccesspt2multipt + -- use of this iftype for IEEE 802.16 WMAN + -- interfaces as per IEEE Std 802.16f is + -- deprecated and ifType 237 should be used instead. + sonetOverheadChannel (185), -- SONET Overhead Channel + digitalWrapperOverheadChannel (186), -- Digital Wrapper + aal2 (187), -- ATM adaptation layer 2 + radioMAC (188), -- MAC layer over radio links + atmRadio (189), -- ATM over radio links + imt (190), -- Inter Machine Trunks + mvl (191), -- Multiple Virtual Lines DSL + reachDSL (192), -- Long Reach DSL + frDlciEndPt (193), -- Frame Relay DLCI End Point + atmVciEndPt (194), -- ATM VCI End Point + opticalChannel (195), -- Optical Channel + opticalTransport (196), -- Optical Transport + propAtm (197), -- Proprietary ATM + voiceOverCable (198), -- Voice Over Cable Interface + infiniband (199), -- Infiniband + teLink (200), -- TE Link + q2931 (201), -- Q.2931 + virtualTg (202), -- Virtual Trunk Group + sipTg (203), -- SIP Trunk Group + sipSig (204), -- SIP Signaling + docsCableUpstreamChannel (205), -- CATV Upstream Channel + econet (206), -- Acorn Econet + pon155 (207), -- FSAN 155Mb Symetrical PON interface + pon622 (208), -- FSAN622Mb Symetrical PON interface + bridge (209), -- Transparent bridge interface + linegroup (210), -- Interface common to multiple lines + voiceEMFGD (211), -- voice E&M Feature Group D + voiceFGDEANA (212), -- voice FGD Exchange Access North American + voiceDID (213), -- voice Direct Inward Dialing + mpegTransport (214), -- MPEG transport interface + sixToFour (215), -- 6to4 interface (DEPRECATED) + gtp (216), -- GTP (GPRS Tunneling Protocol) + pdnEtherLoop1 (217), -- Paradyne EtherLoop 1 + pdnEtherLoop2 (218), -- Paradyne EtherLoop 2 + opticalChannelGroup (219), -- Optical Channel Group + homepna (220), -- HomePNA ITU-T G.989 + gfp (221), -- Generic Framing Procedure (GFP) + ciscoISLvlan (222), -- Layer 2 Virtual LAN using Cisco ISL + actelisMetaLOOP (223), -- Acteleis proprietary MetaLOOP High Speed Link + fcipLink (224), -- FCIP Link + rpr (225), -- Resilient Packet Ring Interface Type + qam (226), -- RF Qam Interface + lmp (227), -- Link Management Protocol + cblVectaStar (228), -- Cambridge Broadband Networks Limited VectaStar + docsCableMCmtsDownstream (229), -- CATV Modular CMTS Downstream Interface + adsl2 (230), -- Asymmetric Digital Subscriber Loop Version 2 + -- (DEPRECATED/OBSOLETED - please use adsl2plus 238 instead) + macSecControlledIF (231), -- MACSecControlled + macSecUncontrolledIF (232), -- MACSecUncontrolled + aviciOpticalEther (233), -- Avici Optical Ethernet Aggregate + atmbond (234), -- atmbond + voiceFGDOS (235), -- voice FGD Operator Services + mocaVersion1 (236), -- MultiMedia over Coax Alliance (MoCA) Interface + -- as documented in information provided privately to IANA + ieee80216WMAN (237), -- IEEE 802.16 WMAN interface + adsl2plus (238), -- Asymmetric Digital Subscriber Loop Version 2, + -- Version 2 Plus and all variants + dvbRcsMacLayer (239), -- DVB-RCS MAC Layer + dvbTdm (240), -- DVB Satellite TDM + dvbRcsTdma (241), -- DVB-RCS TDMA + x86Laps (242), -- LAPS based on ITU-T X.86/Y.1323 + wwanPP (243), -- 3GPP WWAN + wwanPP2 (244), -- 3GPP2 WWAN + voiceEBS (245), -- voice P-phone EBS physical interface + ifPwType (246), -- Pseudowire interface type + ilan (247), -- Internal LAN on a bridge per IEEE 802.1ap + pip (248), -- Provider Instance Port on a bridge per IEEE 802.1ah PBB + aluELP (249), -- Alcatel-Lucent Ethernet Link Protection + gpon (250), -- Gigabit-capable passive optical networks (G-PON) as per ITU-T G.948 + vdsl2 (251), -- Very high speed digital subscriber line Version 2 (as per ITU-T Recommendation G.993.2) + capwapDot11Profile (252), -- WLAN Profile Interface + capwapDot11Bss (253), -- WLAN BSS Interface + capwapWtpVirtualRadio (254), -- WTP Virtual Radio Interface + bits (255), -- bitsport + docsCableUpstreamRfPort (256), -- DOCSIS CATV Upstream RF Port + cableDownstreamRfPort (257), -- CATV downstream RF port + vmwareVirtualNic (258), -- VMware Virtual Network Interface + ieee802154 (259), -- IEEE 802.15.4 WPAN interface + otnOdu (260), -- OTN Optical Data Unit + otnOtu (261), -- OTN Optical channel Transport Unit + ifVfiType (262), -- VPLS Forwarding Instance Interface Type + g9981 (263), -- G.998.1 bonded interface + g9982 (264), -- G.998.2 bonded interface + g9983 (265), -- G.998.3 bonded interface + aluEpon (266), -- Ethernet Passive Optical Networks (E-PON) + aluEponOnu (267), -- EPON Optical Network Unit + aluEponPhysicalUni (268), -- EPON physical User to Network interface + aluEponLogicalLink (269), -- The emulation of a point-to-point link over the EPON layer + aluGponOnu (270), -- GPON Optical Network Unit + aluGponPhysicalUni (271), -- GPON physical User to Network interface + vmwareNicTeam (272), -- VMware NIC Team + docsOfdmDownstream (277), -- CATV Downstream OFDM interface + docsOfdmaUpstream (278) -- CATV Upstream OFDMA interface + } + +IANAtunnelType ::= TEXTUAL-CONVENTION + STATUS current + DESCRIPTION + "The encapsulation method used by a tunnel. The value + direct indicates that a packet is encapsulated + directly within a normal IP header, with no + intermediate header, and unicast to the remote tunnel + endpoint (e.g., an RFC 2003 IP-in-IP tunnel, or an RFC + 1933 IPv6-in-IPv4 tunnel). The value minimal indicates + that a Minimal Forwarding Header (RFC 2004) is + inserted between the outer header and the payload + packet. The value UDP indicates that the payload + packet is encapsulated within a normal UDP packet + (e.g., RFC 1234). + + The values sixToFour, sixOverFour, and isatap + indicates that an IPv6 packet is encapsulated directly + within an IPv4 header, with no intermediate header, + and unicast to the destination determined by the 6to4, + 6over4, or ISATAP protocol. + + The remaining protocol-specific values indicate that a + header of the protocol of that name is inserted + between the outer header and the payload header. + + The assignment policy for IANAtunnelType values is + identical to the policy for assigning IANAifType + values." + SYNTAX INTEGER { + other(1), -- none of the following + direct(2), -- no intermediate header + gre(3), -- GRE encapsulation + minimal(4), -- Minimal encapsulation + l2tp(5), -- L2TP encapsulation + pptp(6), -- PPTP encapsulation + l2f(7), -- L2F encapsulation + udp(8), -- UDP encapsulation + atmp(9), -- ATMP encapsulation + msdp(10), -- MSDP encapsulation + sixToFour(11), -- 6to4 encapsulation + sixOverFour(12), -- 6over4 encapsulation + isatap(13), -- ISATAP encapsulation + teredo(14), -- Teredo encapsulation + ipHttps(15) -- IPHTTPS + } + + END 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..ada9a4d59 100644 --- a/op-mode-definitions/container.xml.in +++ b/op-mode-definitions/container.xml.in @@ -11,7 +11,7 @@ <properties> <help>Pull a new image for container</help> </properties> - <command>sudo podman image pull "${4}"</command> + <command>sudo ${vyos_op_scripts_dir}/container.py add_image --name "${4}"</command> </tagNode> </children> </node> @@ -44,7 +44,7 @@ <script>sudo podman image ls -q</script> </completionHelp> </properties> - <command>sudo podman image rm --force "${4}"</command> + <command>sudo ${vyos_op_scripts_dir}/container.py delete_image --name "${4}"</command> </tagNode> </children> </node> @@ -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/date.xml.in b/op-mode-definitions/date.xml.in index 15a69dbd9..6d8586025 100644 --- a/op-mode-definitions/date.xml.in +++ b/op-mode-definitions/date.xml.in @@ -37,28 +37,6 @@ </properties> <command>/bin/date "$3"</command> </tagNode> - <node name="date"> - <properties> - <help>Set system date and time</help> - </properties> - <children> - <node name="ntp"> - <properties> - <help>Set system date and time from NTP server (default: 0.pool.ntp.org)</help> - </properties> - <command>/usr/sbin/ntpdate -u 0.pool.ntp.org</command> - </node> - <tagNode name="ntp"> - <properties> - <help>Set system date and time from NTP server</help> - <completionHelp> - <script>${vyos_completion_dir}/list_ntp_servers.sh</script> - </completionHelp> - </properties> - <command>/usr/sbin/ntpdate -u "$4"</command> - </tagNode> - </children> - </node> </children> </node> </interfaceDefinition> diff --git a/op-mode-definitions/dhcp.xml.in b/op-mode-definitions/dhcp.xml.in index 241cca0ce..419abe7ad 100644 --- a/op-mode-definitions/dhcp.xml.in +++ b/op-mode-definitions/dhcp.xml.in @@ -16,7 +16,7 @@ <properties> <help>Show DHCP server leases</help> </properties> - <command>sudo ${vyos_op_scripts_dir}/show_dhcp.py --leases</command> + <command>${vyos_op_scripts_dir}/dhcp.py show_server_leases --family inet</command> <children> <tagNode name="pool"> <properties> @@ -25,25 +25,25 @@ <path>service dhcp-server shared-network-name</path> </completionHelp> </properties> - <command>sudo ${vyos_op_scripts_dir}/show_dhcp.py --leases --pool $6</command> + <command>${vyos_op_scripts_dir}/dhcp.py show_server_leases --family inet --pool $6</command> </tagNode> <tagNode name="sort"> <properties> <help>Show DHCP server leases sorted by the specified key</help> <completionHelp> - <script>sudo ${vyos_op_scripts_dir}/show_dhcp.py --allowed sort</script> + <list>end hostname ip mac pool remaining start state</list> </completionHelp> </properties> - <command>sudo ${vyos_op_scripts_dir}/show_dhcp.py --leases --sort $6</command> + <command>${vyos_op_scripts_dir}/dhcp.py show_server_leases --family inet --sort $6</command> </tagNode> <tagNode name="state"> <properties> <help>Show DHCP server leases with a specific state (can be multiple, comma-separated)</help> <completionHelp> - <script>sudo ${vyos_op_scripts_dir}/show_dhcp.py --allowed state</script> + <list>abandoned active all backup expired free released reset</list> </completionHelp> </properties> - <command>sudo ${vyos_op_scripts_dir}/show_dhcp.py --leases --state $(echo $6 | tr , " ")</command> + <command>${vyos_op_scripts_dir}/dhcp.py show_server_leases --family inet --state $6</command> </tagNode> </children> </node> @@ -51,7 +51,7 @@ <properties> <help>Show DHCP server statistics</help> </properties> - <command>sudo ${vyos_op_scripts_dir}/show_dhcp.py --statistics</command> + <command>${vyos_op_scripts_dir}/dhcp.py show_pool_statistics --family inet</command> <children> <tagNode name="pool"> <properties> @@ -60,7 +60,7 @@ <path>service dhcp-server shared-network-name</path> </completionHelp> </properties> - <command>sudo ${vyos_op_scripts_dir}/show_dhcp.py --statistics --pool $6</command> + <command>${vyos_op_scripts_dir}/dhcp.py show_pool_statistics --family inet --pool $6</command> </tagNode> </children> </node> @@ -82,34 +82,34 @@ <properties> <help>Show DHCPv6 server leases</help> </properties> - <command>sudo ${vyos_op_scripts_dir}/show_dhcpv6.py --leases</command> + <command>sudo ${vyos_op_scripts_dir}/dhcp.py show_server_leases --family inet6</command> <children> <tagNode name="pool"> <properties> <help>Show DHCPv6 server leases for a specific pool</help> <completionHelp> - <script>sudo ${vyos_op_scripts_dir}/show_dhcpv6.py --allowed pool</script> + <path>service dhcpv6-server shared-network-name</path> </completionHelp> </properties> - <command>sudo ${vyos_op_scripts_dir}/show_dhcpv6.py --leases --pool $6</command> + <command>${vyos_op_scripts_dir}/dhcp.py show_server_leases --family inet6 --pool $6</command> </tagNode> <tagNode name="sort"> <properties> <help>Show DHCPv6 server leases sorted by the specified key</help> <completionHelp> - <script>sudo ${vyos_op_scripts_dir}/show_dhcpv6.py --allowed sort</script> + <list>end iaid_duid ip last_communication pool remaining state type</list> </completionHelp> </properties> - <command>sudo ${vyos_op_scripts_dir}/show_dhcpv6.py --leases --sort $6</command> + <command>${vyos_op_scripts_dir}/dhcp.py show_server_leases --family inet6 --sort $6</command> </tagNode> <tagNode name="state"> <properties> <help>Show DHCPv6 server leases with a specific state (can be multiple, comma-separated)</help> <completionHelp> - <script>sudo ${vyos_op_scripts_dir}/show_dhcpv6.py --allowed state</script> + <list>abandoned active all backup expired free released reset</list> </completionHelp> </properties> - <command>sudo ${vyos_op_scripts_dir}/show_dhcpv6.py --leases --state $(echo $6 | tr , " ")</command> + <command>${vyos_op_scripts_dir}/dhcp.py show_server_leases --family inet6 --state $6</command> </tagNode> </children> </node> 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-interfaces-debug-archive.xml.in b/op-mode-definitions/generate-interfaces-debug-archive.xml.in new file mode 100644 index 000000000..5e4f4daad --- /dev/null +++ b/op-mode-definitions/generate-interfaces-debug-archive.xml.in @@ -0,0 +1,20 @@ +<?xml version="1.0"?> +<interfaceDefinition> + <node name="generate"> + <children> + <node name="interfaces"> + <properties> + <help>Interface specific commands</help> + </properties> + <children> + <node name="debug-archive"> + <properties> + <help>Generate interfaces debug-archive</help> + </properties> + <command>${vyos_op_scripts_dir}/generate_interfaces_debug_archive.py</command> + </node> + </children> + </node> + </children> + </node> +</interfaceDefinition> 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-openvpn-config-client.xml.in b/op-mode-definitions/generate-openvpn-config-client.xml.in index 4f9f31bfe..baec0842b 100644 --- a/op-mode-definitions/generate-openvpn-config-client.xml.in +++ b/op-mode-definitions/generate-openvpn-config-client.xml.in @@ -16,7 +16,7 @@ <properties> <help>Local interface used for connection</help> <completionHelp> - <script>${vyos_completion_dir}/list_interfaces.py --type openvpn</script> + <path>interfaces openvpn</path> </completionHelp> </properties> <children> 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/generate-wireguard.xml.in b/op-mode-definitions/generate-wireguard.xml.in index 259c9a898..6c01619be 100644 --- a/op-mode-definitions/generate-wireguard.xml.in +++ b/op-mode-definitions/generate-wireguard.xml.in @@ -7,24 +7,6 @@ <help>Generate WireGuard keys</help> </properties> <children> - <leafNode name="default-keypair"> - <properties> - <help>generates the wireguard default-keypair</help> - </properties> - <command>echo "This command is deprecated. Please use: \"generate pki wireguard key-pair\""</command> - </leafNode> - <leafNode name="preshared-key"> - <properties> - <help>generate a wireguard preshared key</help> - </properties> - <command>echo "This command is deprecated. Please use: \"generate pki wireguard pre-shared-key\""</command> - </leafNode> - <tagNode name="named-keypairs"> - <properties> - <help>Generates named wireguard keypairs</help> - </properties> - <command>echo "This command is deprecated. Please use: \"generate pki wireguard key-pair install wgN\""</command> - </tagNode> <tagNode name="client-config"> <properties> <help>Generate Client config QR code</help> @@ -37,7 +19,7 @@ <properties> <help>Local interface used for connection</help> <completionHelp> - <script>${vyos_completion_dir}/list_interfaces.py --type wireguard</script> + <path>interfaces wireguard</path> </completionHelp> </properties> <children> diff --git a/op-mode-definitions/include/show-route-summary.xml.i b/op-mode-definitions/include/show-route-summary.xml.i deleted file mode 100644 index 471124562..000000000 --- a/op-mode-definitions/include/show-route-summary.xml.i +++ /dev/null @@ -1,8 +0,0 @@ -<!-- included start from show-route-summary.xml.i --> -<leafNode name="summary"> - <properties> - <help>Summary of all routes</help> - </properties> - <command>${vyos_op_scripts_dir}/vtysh_wrapper.sh $@</command> -</leafNode> -<!-- included end --> diff --git a/op-mode-definitions/ipv6-route.xml.in b/op-mode-definitions/ipv6-route.xml.in index d75caf308..46e416a8a 100644 --- a/op-mode-definitions/ipv6-route.xml.in +++ b/op-mode-definitions/ipv6-route.xml.in @@ -26,7 +26,7 @@ <properties> <help>Show IPv6 neighbor table for specified interface</help> <completionHelp> - <script>${vyos_completion_dir}/list_interfaces.py -b</script> + <script>${vyos_completion_dir}/list_interfaces.py --broadcast</script> </completionHelp> </properties> <command>${vyos_op_scripts_dir}/neighbor.py show --family inet6 --interface "$5"</command> diff --git a/op-mode-definitions/lldp.xml.in b/op-mode-definitions/lldp.xml.in index 297ccf1f4..07cafa77f 100644 --- a/op-mode-definitions/lldp.xml.in +++ b/op-mode-definitions/lldp.xml.in @@ -11,14 +11,8 @@ <properties> <help>Show LLDP neighbors</help> </properties> - <command>${vyos_op_scripts_dir}/lldp_op.py --all</command> + <command>${vyos_op_scripts_dir}/lldp.py show_neighbors</command> <children> - <node name="detail"> - <properties> - <help>Show LLDP neighbor details</help> - </properties> - <command>${vyos_op_scripts_dir}/lldp_op.py --detail</command> - </node> <tagNode name="interface"> <properties> <help>Show LLDP for specified interface</help> @@ -26,7 +20,7 @@ <script>${vyos_completion_dir}/list_interfaces.py</script> </completionHelp> </properties> - <command>${vyos_op_scripts_dir}/lldp_op.py --interface $5</command> + <command>${vyos_op_scripts_dir}/lldp.py show_neighbors --interface $5</command> </tagNode> </children> </node> diff --git a/op-mode-definitions/monitor-log.xml.in b/op-mode-definitions/monitor-log.xml.in index 01462ad8f..ec428a676 100644 --- a/op-mode-definitions/monitor-log.xml.in +++ b/op-mode-definitions/monitor-log.xml.in @@ -93,6 +93,12 @@ </properties> <command>journalctl --no-hostname --boot --follow --unit uacctd.service</command> </leafNode> + <leafNode name="ipoe-server"> + <properties> + <help>Monitor last lines of IPoE server log</help> + </properties> + <command>journalctl --no-hostname --boot --follow --unit accel-ppp@ipoe.service</command> + </leafNode> <leafNode name="kernel"> <properties> <help>Monitor last lines of Linux Kernel log</help> @@ -101,13 +107,19 @@ </leafNode> <leafNode name="nhrp"> <properties> - <help>Monitor last lines of NHRP log</help> + <help>Monitor last lines of Next Hop Resolution Protocol (NHRP) log</help> </properties> - <command>journalctl --no-hostname --boot --unit opennhrp.service</command> + <command>journalctl --no-hostname --boot --follow --unit opennhrp.service</command> + </leafNode> + <leafNode name="ntp"> + <properties> + <help>Monitor last lines of Network Time Protocol (NTP) log</help> + </properties> + <command>journalctl --no-hostname --boot --follow --unit chrony.service</command> </leafNode> <node name="pppoe"> <properties> - <help>Monitor last lines of PPPoE log</help> + <help>Monitor last lines of PPPoE interface log</help> </properties> <command>journalctl --no-hostname --boot --follow --unit "ppp@pppoe*.service"</command> <children> @@ -115,13 +127,19 @@ <properties> <help>Monitor last lines of PPPoE log for specific interface</help> <completionHelp> - <script>${vyos_completion_dir}/list_interfaces.py -t pppoe</script> + <path>interfaces pppoe</path> </completionHelp> </properties> - <command>journalctl --no-hostname --boot --follow --unit "ppp@$6.service"</command> + <command>journalctl --no-hostname --boot --follow --unit "ppp@$5.service"</command> </tagNode> </children> </node> + <leafNode name="pppoe-server"> + <properties> + <help>Monitor last lines of PPPoE server log</help> + </properties> + <command>journalctl --no-hostname --boot --follow --unit accel-ppp@pppoe.service</command> + </leafNode> <node name="protocol"> <properties> <help>Monitor log for Routing Protocol</help> @@ -205,13 +223,19 @@ <properties> <help>Monitor last lines of specific MACsec interface</help> <completionHelp> - <script>${vyos_completion_dir}/list_interfaces.py -t macsec</script> + <path>interfaces macsec</path> </completionHelp> </properties> <command>SRC=$(cli-shell-api returnValue interfaces macsec "$5" source-interface); journalctl --no-hostname --boot --follow --unit "wpa_supplicant-macsec@$SRC.service"</command> </tagNode> </children> </node> + <leafNode name="router-advert"> + <properties> + <help>Monitor last lines of Router Advertisement Daemon (radvd)</help> + </properties> + <command>journalctl --no-hostname --boot --follow --unit radvd.service</command> + </leafNode> <leafNode name="snmp"> <properties> <help>Monitor last lines of Simple Network Monitoring Protocol (SNMP)</help> @@ -224,16 +248,33 @@ </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> + <path>interfaces sstpc</path> + </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> + <help>Monitor Virtual Private Network (VPN) services</help> </properties> <children> <leafNode name="all"> <properties> <help>Monitor last lines of ALL VPNs</help> </properties> - <command>journalctl --no-hostname --boot --follow --unit strongswan-starter.service --unit accel-ppp@*.service</command> + <command>journalctl --no-hostname --boot --follow --unit strongswan-starter.service --unit accel-ppp@*.service --unit ocserv.service</command> </leafNode> <leafNode name="ipsec"> <properties> @@ -247,6 +288,12 @@ </properties> <command>journalctl --no-hostname --boot --follow --unit accel-ppp@l2tp.service</command> </leafNode> + <leafNode name="openconnect"> + <properties> + <help>Monitor last lines of OpenConnect</help> + </properties> + <command>journalctl --no-hostname --boot --follow --unit ocserv.service</command> + </leafNode> <leafNode name="pptp"> <properties> <help>Monitor last lines of PPTP</help> diff --git a/op-mode-definitions/nat.xml.in b/op-mode-definitions/nat.xml.in index ce0544390..307a91337 100644 --- a/op-mode-definitions/nat.xml.in +++ b/op-mode-definitions/nat.xml.in @@ -4,7 +4,7 @@ <children> <node name="nat"> <properties> - <help>Show IPv4 to IPv4 Network Address Translation (NAT) information</help> + <help>Show IPv4 Network Address Translation (NAT) information</help> </properties> <children> <node name="source"> @@ -16,13 +16,13 @@ <properties> <help>Show configured source NAT rules</help> </properties> - <command>${vyos_op_scripts_dir}/nat.py show_rules --direction source --family inet</command> + <command>sudo ${vyos_op_scripts_dir}/nat.py show_rules --direction source --family inet</command> </node> <node name="statistics"> <properties> <help>Show statistics for configured source NAT rules</help> </properties> - <command>${vyos_op_scripts_dir}/nat.py show_statistics --direction source --family inet</command> + <command>sudo ${vyos_op_scripts_dir}/nat.py show_statistics --direction source --family inet</command> </node> <node name="translations"> <properties> @@ -36,16 +36,10 @@ <list><x.x.x.x></list> </completionHelp> </properties> - <command>${vyos_op_scripts_dir}/show_nat_translations.py --type=source --verbose --ipaddr="$6"</command> + <command>sudo ${vyos_op_scripts_dir}/nat.py show_translations --direction source --family inet --address "$6"</command> </tagNode> - <node name="detail"> - <properties> - <help>Show active source NAT translations detail</help> - </properties> - <command>${vyos_op_scripts_dir}/show_nat_translations.py --type=source --verbose</command> - </node> </children> - <command>${vyos_op_scripts_dir}/nat.py show_translations --direction source --family inet</command> + <command>sudo ${vyos_op_scripts_dir}/nat.py show_translations --direction source --family inet</command> </node> </children> </node> @@ -58,13 +52,13 @@ <properties> <help>Show configured destination NAT rules</help> </properties> - <command>${vyos_op_scripts_dir}/nat.py show_rules --direction destination --family inet</command> + <command>sudo ${vyos_op_scripts_dir}/nat.py show_rules --direction destination --family inet</command> </node> <node name="statistics"> <properties> <help>Show statistics for configured destination NAT rules</help> </properties> - <command>${vyos_op_scripts_dir}/show_nat_statistics.py --destination</command> + <command>sudo ${vyos_op_scripts_dir}/nat.py show_statistics --direction destination --family inet</command> </node> <node name="translations"> <properties> @@ -78,16 +72,10 @@ <list><x.x.x.x></list> </completionHelp> </properties> - <command>${vyos_op_scripts_dir}/show_nat_translations.py --type=destination --verbose --ipaddr="$6"</command> + <command>sudo ${vyos_op_scripts_dir}/nat.py show_translations --direction destination --family inet --address "$6"</command> </tagNode> - <node name="detail"> - <properties> - <help>Show active destination NAT translations detail</help> - </properties> - <command>${vyos_op_scripts_dir}/show_nat_translations.py --type=destination --verbose</command> - </node> </children> - <command>${vyos_op_scripts_dir}/nat.py show_translations --direction destination --family inet</command> + <command>sudo ${vyos_op_scripts_dir}/nat.py show_translations --direction destination --family inet</command> </node> </children> </node> diff --git a/op-mode-definitions/nat66.xml.in b/op-mode-definitions/nat66.xml.in index 25aa04d59..6a8a39000 100644 --- a/op-mode-definitions/nat66.xml.in +++ b/op-mode-definitions/nat66.xml.in @@ -4,7 +4,7 @@ <children> <node name="nat66"> <properties> - <help>Show IPv6 to IPv6 Network Address Translation (NAT66) information</help> + <help>Show IPv6 Network Address Translation (NAT66) information</help> </properties> <children> <node name="source"> @@ -22,7 +22,7 @@ <properties> <help>Show statistics for configured source NAT66 rules</help> </properties> - <command>${vyos_op_scripts_dir}/show_nat66_statistics.py --source</command> + <command>sudo ${vyos_op_scripts_dir}/nat.py show_statistics --direction source --family inet6</command> </node> <node name="translations"> <properties> @@ -36,14 +36,8 @@ <list><h:h:h:h:h:h:h:h></list> </completionHelp> </properties> - <command>${vyos_op_scripts_dir}/show_nat66_translations.py --type=source --verbose --ipaddr="$6"</command> + <command>sudo ${vyos_op_scripts_dir}/nat.py show_translations --direction source --family inet6 --address "$6"</command> </tagNode> - <node name="detail"> - <properties> - <help>Show active source NAT66 translations detail</help> - </properties> - <command>${vyos_op_scripts_dir}/show_nat66_translations.py --type=source --verbose</command> - </node> </children> <command>${vyos_op_scripts_dir}/nat.py show_translations --direction source --family inet6</command> </node> @@ -64,7 +58,7 @@ <properties> <help>Show statistics for configured destination NAT66 rules</help> </properties> - <command>${vyos_op_scripts_dir}/show_nat66_statistics.py --destination</command> + <command>sudo ${vyos_op_scripts_dir}/nat.py show_statistics --direction destination --family inet6</command> </node> <node name="translations"> <properties> @@ -78,14 +72,8 @@ <list><h:h:h:h:h:h:h:h></list> </completionHelp> </properties> - <command>${vyos_op_scripts_dir}/show_nat66_translations.py --type=destination --verbose --ipaddr="$6"</command> + <command>sudo ${vyos_op_scripts_dir}/nat.py show_translations --direction destination --family inet6 --address "$6"</command> </tagNode> - <node name="detail"> - <properties> - <help>Show active destination NAT66 translations detail</help> - </properties> - <command>${vyos_op_scripts_dir}/show_nat66_translations.py --type=destination --verbose</command> - </node> </children> <command>${vyos_op_scripts_dir}/nat.py show_translations --direction destination --family inet6</command> </node> diff --git a/op-mode-definitions/openvpn.xml.in b/op-mode-definitions/openvpn.xml.in index 301688271..0a2657398 100644 --- a/op-mode-definitions/openvpn.xml.in +++ b/op-mode-definitions/openvpn.xml.in @@ -20,10 +20,10 @@ <properties> <help>Reset OpenVPN process on interface</help> <completionHelp> - <script>sudo ${vyos_completion_dir}/list_interfaces.py --type openvpn</script> + <path>interfaces openvpn</path> </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> @@ -37,12 +37,13 @@ <properties> <help>Show OpenVPN interface information</help> </properties> + <command>${vyos_op_scripts_dir}/interfaces.py show_summary --intf_type=openvpn</command> <children> <leafNode name="detail"> <properties> <help>Show detailed OpenVPN interface information</help> </properties> - <command>${vyos_op_scripts_dir}/show_interfaces.py --intf-type=openvpn --action=show</command> + <command>${vyos_op_scripts_dir}/interfaces.py show --intf_type=openvpn</command> </leafNode> </children> </node> @@ -50,10 +51,10 @@ <properties> <help>Show OpenVPN interface information</help> <completionHelp> - <script>sudo ${vyos_completion_dir}/list_interfaces.py --type openvpn</script> + <path>interfaces openvpn</path> </completionHelp> </properties> - <command>${vyos_op_scripts_dir}/show_interfaces.py --intf=$4</command> + <command>${vyos_op_scripts_dir}/interfaces.py show --intf_name=$4</command> <children> <tagNode name="user"> <properties> @@ -94,7 +95,7 @@ <properties> <help>Show summary of specified OpenVPN interface information</help> </properties> - <command>${vyos_op_scripts_dir}/show_interfaces.py --intf="$4" --action=show-brief</command> + <command>${vyos_op_scripts_dir}/interfaces.py show_summary --intf_name="$4"</command> </leafNode> </children> </tagNode> @@ -109,19 +110,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-arp.xml.in b/op-mode-definitions/show-arp.xml.in index 8662549fc..3680c20c6 100644 --- a/op-mode-definitions/show-arp.xml.in +++ b/op-mode-definitions/show-arp.xml.in @@ -12,7 +12,7 @@ <properties> <help>Show Address Resolution Protocol (ARP) cache for specified interface</help> <completionHelp> - <script>${vyos_completion_dir}/list_interfaces.py -b</script> + <script>${vyos_completion_dir}/list_interfaces.py --broadcast</script> </completionHelp> </properties> <command>${vyos_op_scripts_dir}/neighbor.py show --family inet --interface "$4"</command> diff --git a/op-mode-definitions/show-bridge.xml.in b/op-mode-definitions/show-bridge.xml.in index dd2a28931..e7a646fdc 100644 --- a/op-mode-definitions/show-bridge.xml.in +++ b/op-mode-definitions/show-bridge.xml.in @@ -25,7 +25,7 @@ <properties> <help>Show bridge information for a given bridge interface</help> <completionHelp> - <script>${vyos_completion_dir}/list_interfaces.py --type bridge</script> + <path>interfaces bridge</path> </completionHelp> </properties> <command>bridge -c link show | grep "master $3"</command> diff --git a/op-mode-definitions/show-interfaces-bonding.xml.in b/op-mode-definitions/show-interfaces-bonding.xml.in index c5f82b70e..c41e7bd5f 100644 --- a/op-mode-definitions/show-interfaces-bonding.xml.in +++ b/op-mode-definitions/show-interfaces-bonding.xml.in @@ -11,13 +11,13 @@ <path>interfaces bonding</path> </completionHelp> </properties> - <command>${vyos_op_scripts_dir}/show_interfaces.py --intf="$4"</command> + <command>${vyos_op_scripts_dir}/interfaces.py show --intf_name="$4" --intf_type=bonding</command> <children> <leafNode name="brief"> <properties> <help>Show summary of the specified bonding interface information</help> </properties> - <command>${vyos_op_scripts_dir}/show_interfaces.py --intf="$4" --action=show-brief</command> + <command>${vyos_op_scripts_dir}/interfaces.py show_summary --intf_name="$4" --intf_type=bonding</command> </leafNode> <leafNode name="detail"> <properties> @@ -38,13 +38,13 @@ <path>interfaces bonding ${COMP_WORDS[3]} vif</path> </completionHelp> </properties> - <command>${vyos_op_scripts_dir}/show_interfaces.py --intf="$4.$6"</command> + <command>${vyos_op_scripts_dir}/interfaces.py show --intf_name="$4.$6" --intf_type=bonding</command> <children> <leafNode name="brief"> <properties> <help>Show summary of specified virtual network interface (vif) information</help> </properties> - <command>${vyos_op_scripts_dir}/show_interfaces.py --intf="$4.$6" --action=show-brief</command> + <command>${vyos_op_scripts_dir}/interfaces.py show_summary --intf_name="$4.$6" --intf_type=bonding</command> </leafNode> </children> </tagNode> @@ -60,13 +60,13 @@ <properties> <help>Show Bonding interface information</help> </properties> - <command>${vyos_op_scripts_dir}/show_interfaces.py --intf-type=bonding --action=show-brief</command> + <command>${vyos_op_scripts_dir}/interfaces.py show_summary --intf_type=bonding</command> <children> <leafNode name="detail"> <properties> <help>Show detailed bonding interface information</help> </properties> - <command>${vyos_op_scripts_dir}/show_interfaces.py --intf-type=bonding --action=show</command> + <command>${vyos_op_scripts_dir}/interfaces.py show --intf_type=bonding</command> </leafNode> <leafNode name="slaves"> <properties> diff --git a/op-mode-definitions/show-interfaces-bridge.xml.in b/op-mode-definitions/show-interfaces-bridge.xml.in index e1444bd84..22cd3ee67 100644 --- a/op-mode-definitions/show-interfaces-bridge.xml.in +++ b/op-mode-definitions/show-interfaces-bridge.xml.in @@ -11,13 +11,13 @@ <path>interfaces bridge</path> </completionHelp> </properties> - <command>${vyos_op_scripts_dir}/show_interfaces.py --intf="$4"</command> + <command>${vyos_op_scripts_dir}/interfaces.py show --intf_name="$4" --intf_type=bridge</command> <children> <leafNode name="brief"> <properties> <help>Show summary of the specified bridge interface information</help> </properties> - <command>${vyos_op_scripts_dir}/show_interfaces.py --intf="$4" --action=show-brief</command> + <command>${vyos_op_scripts_dir}/interfaces.py show_summary --intf_name="$4" --intf_type=bridge</command> </leafNode> </children> </tagNode> @@ -25,13 +25,13 @@ <properties> <help>Show Bridge interface information</help> </properties> - <command>${vyos_op_scripts_dir}/show_interfaces.py --intf-type=bridge --action=show-brief</command> + <command>${vyos_op_scripts_dir}/interfaces.py show_summary --intf_type=bridge</command> <children> <leafNode name="detail"> <properties> <help>Show detailed bridge interface information</help> </properties> - <command>${vyos_op_scripts_dir}/show_interfaces.py --intf-type=bridge --action=show</command> + <command>${vyos_op_scripts_dir}/interfaces.py show --intf_type=bridge</command> </leafNode> </children> </node> diff --git a/op-mode-definitions/show-interfaces-dummy.xml.in b/op-mode-definitions/show-interfaces-dummy.xml.in index 52d2cc7ee..958d3483d 100644 --- a/op-mode-definitions/show-interfaces-dummy.xml.in +++ b/op-mode-definitions/show-interfaces-dummy.xml.in @@ -11,13 +11,13 @@ <path>interfaces dummy</path> </completionHelp> </properties> - <command>${vyos_op_scripts_dir}/show_interfaces.py --intf="$4"</command> + <command>${vyos_op_scripts_dir}/interfaces.py show --intf_name="$4" --intf_type=dummy</command> <children> <leafNode name="brief"> <properties> <help>Show summary of the specified dummy interface information</help> </properties> - <command>${vyos_op_scripts_dir}/show_interfaces.py --intf="$4" --action=show-brief</command> + <command>${vyos_op_scripts_dir}/interfaces.py show_summary --intf_name="$4" --intf_type=dummy</command> </leafNode> </children> </tagNode> @@ -25,13 +25,13 @@ <properties> <help>Show Dummy interface information</help> </properties> - <command>${vyos_op_scripts_dir}/show_interfaces.py --intf-type=dummy --action=show-brief</command> + <command>${vyos_op_scripts_dir}/interfaces.py show_summary --intf_type=dummy</command> <children> <leafNode name="detail"> <properties> <help>Show detailed dummy interface information</help> </properties> - <command>${vyos_op_scripts_dir}/show_interfaces.py --intf-type=dummy --action=show</command> + <command>${vyos_op_scripts_dir}/interfaces.py show --intf_type=dummy</command> </leafNode> </children> </node> diff --git a/op-mode-definitions/show-interfaces-ethernet.xml.in b/op-mode-definitions/show-interfaces-ethernet.xml.in index f8d1c9395..81759c2b6 100644 --- a/op-mode-definitions/show-interfaces-ethernet.xml.in +++ b/op-mode-definitions/show-interfaces-ethernet.xml.in @@ -11,13 +11,13 @@ <path>interfaces ethernet</path> </completionHelp> </properties> - <command>${vyos_op_scripts_dir}/show_interfaces.py --intf="$4"</command> + <command>${vyos_op_scripts_dir}/interfaces.py show --intf_name="$4" --intf_type=ethernet</command> <children> <leafNode name="brief"> <properties> <help>Show summary of the specified ethernet interface information</help> </properties> - <command>${vyos_op_scripts_dir}/show_interfaces.py --intf="$4" --action=show-brief</command> + <command>${vyos_op_scripts_dir}/interfaces.py show_summary --intf_name="$4" --intf_type=ethernet</command> </leafNode> <leafNode name="identify"> <properties> @@ -58,13 +58,13 @@ <path>interfaces ethernet ${COMP_WORDS[3]} vif</path> </completionHelp> </properties> - <command>${vyos_op_scripts_dir}/show_interfaces.py --intf="$4.$6"</command> + <command>${vyos_op_scripts_dir}/interfaces.py show --intf_name="$4.$6" --intf_type=ethernet</command> <children> <leafNode name="brief"> <properties> <help>Show summary of specified virtual network interface (vif) information</help> </properties> - <command>${vyos_op_scripts_dir}/show_interfaces.py --intf="$4.$6" --action=show-brief</command> + <command>${vyos_op_scripts_dir}/interfaces.py show_summary --intf_name="$4.$6" --intf_type=ethernet</command> </leafNode> </children> </tagNode> @@ -80,13 +80,13 @@ <properties> <help>Show Ethernet interface information</help> </properties> - <command>${vyos_op_scripts_dir}/show_interfaces.py --intf-type=ethernet --action=show-brief</command> + <command>${vyos_op_scripts_dir}/interfaces.py show_summary --intf_type=ethernet</command> <children> <leafNode name="detail"> <properties> <help>Show detailed ethernet interface information</help> </properties> - <command>${vyos_op_scripts_dir}/show_interfaces.py --intf-type=ethernet --action=show</command> + <command>${vyos_op_scripts_dir}/interfaces.py show --intf_type=ethernet</command> </leafNode> </children> </node> diff --git a/op-mode-definitions/show-interfaces-geneve.xml.in b/op-mode-definitions/show-interfaces-geneve.xml.in index a47933315..3cf45878d 100644 --- a/op-mode-definitions/show-interfaces-geneve.xml.in +++ b/op-mode-definitions/show-interfaces-geneve.xml.in @@ -11,13 +11,13 @@ <path>interfaces geneve</path> </completionHelp> </properties> - <command>${vyos_op_scripts_dir}/show_interfaces.py --intf="$4"</command> + <command>${vyos_op_scripts_dir}/interfaces.py show --intf_name="$4" --intf_type=geneve</command> <children> <leafNode name="brief"> <properties> <help>Show summary of the specified GENEVE interface information</help> </properties> - <command>${vyos_op_scripts_dir}/show_interfaces.py --intf="$4" --action=show-brief</command> + <command>${vyos_op_scripts_dir}/interfaces.py show_summary --intf_name="$4" --intf_type=geneve</command> </leafNode> </children> </tagNode> @@ -25,13 +25,13 @@ <properties> <help>Show GENEVE interface information</help> </properties> - <command>${vyos_op_scripts_dir}/show_interfaces.py --intf-type=geneve --action=show-brief</command> + <command>${vyos_op_scripts_dir}/interfaces.py show_summary --intf_type=geneve</command> <children> <leafNode name="detail"> <properties> <help>Show detailed GENEVE interface information</help> </properties> - <command>${vyos_op_scripts_dir}/show_interfaces.py --intf-type=geneve --action=show</command> + <command>${vyos_op_scripts_dir}/interfaces.py show --intf_type=geneve</command> </leafNode> </children> </node> diff --git a/op-mode-definitions/show-interfaces-input.xml.in b/op-mode-definitions/show-interfaces-input.xml.in index 9ae3828c8..5d93dcee6 100644 --- a/op-mode-definitions/show-interfaces-input.xml.in +++ b/op-mode-definitions/show-interfaces-input.xml.in @@ -11,13 +11,13 @@ <path>interfaces input</path> </completionHelp> </properties> - <command>${vyos_op_scripts_dir}/show_interfaces.py --intf="$4"</command> + <command>${vyos_op_scripts_dir}/interfaces.py show --intf_name="$4" --intf_type=input</command> <children> <leafNode name="brief"> <properties> <help>Show summary of the specified input interface information</help> </properties> - <command>${vyos_op_scripts_dir}/show_interfaces.py --intf="$4" --action=show-brief</command> + <command>${vyos_op_scripts_dir}/interfaces.py show_summary --intf_name="$4" --intf_type=input</command> </leafNode> </children> </tagNode> @@ -25,13 +25,13 @@ <properties> <help>Show Input (ifb) interface information</help> </properties> - <command>${vyos_op_scripts_dir}/show_interfaces.py --intf-type=input --action=show-brief</command> + <command>${vyos_op_scripts_dir}/interfaces.py show_summary --intf_type=input</command> <children> <leafNode name="detail"> <properties> <help>Show detailed input interface information</help> </properties> - <command>${vyos_op_scripts_dir}/show_interfaces.py --intf-type=input --action=show</command> + <command>${vyos_op_scripts_dir}/interfaces.py show --intf_type=input</command> </leafNode> </children> </node> diff --git a/op-mode-definitions/show-interfaces-l2tpv3.xml.in b/op-mode-definitions/show-interfaces-l2tpv3.xml.in index 2a1d6a1c6..713e36dac 100644 --- a/op-mode-definitions/show-interfaces-l2tpv3.xml.in +++ b/op-mode-definitions/show-interfaces-l2tpv3.xml.in @@ -11,13 +11,13 @@ <path>interfaces l2tpv3</path> </completionHelp> </properties> - <command>${vyos_op_scripts_dir}/show_interfaces.py --intf="$4"</command> + <command>${vyos_op_scripts_dir}/interfaces.py show --intf_name="$4" --intf_type=l2tpv3</command> <children> <leafNode name="brief"> <properties> <help>Show summary of the specified L2TPv3 interface information</help> </properties> - <command>${vyos_op_scripts_dir}/show_interfaces.py --intf="$4" --action=show-brief</command> + <command>${vyos_op_scripts_dir}/interfaces.py show_summary --intf_name="$4" --intf_type=l2tpv3</command> </leafNode> </children> </tagNode> @@ -25,13 +25,13 @@ <properties> <help>Show L2TPv3 interface information</help> </properties> - <command>${vyos_op_scripts_dir}/show_interfaces.py --intf-type=l2tpv3 --action=show-brief</command> + <command>${vyos_op_scripts_dir}/interfaces.py show_summary --intf_type=l2tpv3</command> <children> <leafNode name="detail"> <properties> <help>Show detailed L2TPv3 interface information</help> </properties> - <command>${vyos_op_scripts_dir}/show_interfaces.py --intf-type=l2tpv3 --action=show</command> + <command>${vyos_op_scripts_dir}/interfaces.py show --intf_type=l2tpv3</command> </leafNode> </children> </node> diff --git a/op-mode-definitions/show-interfaces-loopback.xml.in b/op-mode-definitions/show-interfaces-loopback.xml.in index 25a75ffff..a24151cc3 100644 --- a/op-mode-definitions/show-interfaces-loopback.xml.in +++ b/op-mode-definitions/show-interfaces-loopback.xml.in @@ -11,13 +11,13 @@ <path>interfaces loopback</path> </completionHelp> </properties> - <command>${vyos_op_scripts_dir}/show_interfaces.py --intf="$4"</command> + <command>${vyos_op_scripts_dir}/interfaces.py show --intf_name="$4" --intf_type=loopback</command> <children> <leafNode name="brief"> <properties> - <help>Show summary of the specified dummy interface information</help> + <help>Show summary of the specified Loopback interface information</help> </properties> - <command>${vyos_op_scripts_dir}/show_interfaces.py --intf="$4" --action=show-brief</command> + <command>${vyos_op_scripts_dir}/interfaces.py show_summary --intf_name="$4" --intf_type=loopback</command> </leafNode> </children> </tagNode> @@ -25,13 +25,13 @@ <properties> <help>Show Loopback interface information</help> </properties> - <command>${vyos_op_scripts_dir}/show_interfaces.py --intf-type=loopback --action=show-brief</command> + <command>${vyos_op_scripts_dir}/interfaces.py show_summary --intf_type=loopback</command> <children> <leafNode name="detail"> <properties> - <help>Show detailed dummy interface information</help> + <help>Show detailed Loopback interface information</help> </properties> - <command>${vyos_op_scripts_dir}/show_interfaces.py --intf-type=dummy --action=show</command> + <command>${vyos_op_scripts_dir}/interfaces.py show --intf_type=loopback</command> </leafNode> </children> </node> diff --git a/op-mode-definitions/show-interfaces-pppoe.xml.in b/op-mode-definitions/show-interfaces-pppoe.xml.in index 09608bc04..a34473148 100644 --- a/op-mode-definitions/show-interfaces-pppoe.xml.in +++ b/op-mode-definitions/show-interfaces-pppoe.xml.in @@ -11,7 +11,7 @@ <path>interfaces pppoe</path> </completionHelp> </properties> - <command>${vyos_op_scripts_dir}/show_interfaces.py --intf="$4"</command> + <command>${vyos_op_scripts_dir}/interfaces.py show --intf_name="$4" --intf_type=pppoe</command> <children> <leafNode name="log"> <properties> @@ -34,13 +34,13 @@ <properties> <help>Show PPPoE interface information</help> </properties> - <command>${vyos_op_scripts_dir}/show_interfaces.py --intf-type=pppoe --action=show-brief</command> + <command>${vyos_op_scripts_dir}/interfaces.py show_summary --intf_type=pppoe</command> <children> <leafNode name="detail"> <properties> <help>Show detailed PPPoE interface information</help> </properties> - <command>${vyos_op_scripts_dir}/show_interfaces.py --intf-type=pppoe --action=show</command> + <command>${vyos_op_scripts_dir}/interfaces.py show --intf_type=pppoe</command> </leafNode> </children> </node> diff --git a/op-mode-definitions/show-interfaces-pseudo-ethernet.xml.in b/op-mode-definitions/show-interfaces-pseudo-ethernet.xml.in index 2ae4b5a9e..cb62639ee 100644 --- a/op-mode-definitions/show-interfaces-pseudo-ethernet.xml.in +++ b/op-mode-definitions/show-interfaces-pseudo-ethernet.xml.in @@ -11,13 +11,13 @@ <path>interfaces pseudo-ethernet</path> </completionHelp> </properties> - <command>${vyos_op_scripts_dir}/show_interfaces.py --intf="$4"</command> + <command>${vyos_op_scripts_dir}/interfaces.py show --intf_name="$4" --intf_type=pseudo-ethernet</command> <children> <leafNode name="brief"> <properties> <help>Show summary of the specified pseudo-ethernet/MACvlan interface information</help> </properties> - <command>${vyos_op_scripts_dir}/show_interfaces.py --intf="$4" --action=show-brief</command> + <command>${vyos_op_scripts_dir}/interfaces.py show_summary --intf_name="$4" --intf_type=pseudo-ethernet</command> </leafNode> </children> </tagNode> @@ -25,13 +25,13 @@ <properties> <help>Show Pseudo-Ethernet/MACvlan interface information</help> </properties> - <command>${vyos_op_scripts_dir}/show_interfaces.py --intf-type=pseudo-ethernet --action=show-brief</command> + <command>${vyos_op_scripts_dir}/interfaces.py show_summary --intf_type=pseudo-ethernet</command> <children> <leafNode name="detail"> <properties> <help>Show detailed pseudo-ethernet/MACvlan interface information</help> </properties> - <command>${vyos_op_scripts_dir}/show_interfaces.py --intf-type=pseudo-ethernet --action=show</command> + <command>${vyos_op_scripts_dir}/interfaces.py show --intf_type=pseudo-ethernet</command> </leafNode> </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..a619a9fd2 --- /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}/interfaces.py show --intf_name="$4" --intf_type=sstpc</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}/interfaces.py show_summary --intf_type=sstpc</command> + <children> + <leafNode name="detail"> + <properties> + <help>Show detailed SSTP client interface information</help> + </properties> + <command>${vyos_op_scripts_dir}/interfaces.py show --intf_type=sstpc</command> + </leafNode> + </children> + </node> + </children> + </node> + </children> + </node> +</interfaceDefinition> diff --git a/op-mode-definitions/show-interfaces-tunnel.xml.in b/op-mode-definitions/show-interfaces-tunnel.xml.in index 51b25efd9..10e10e655 100644 --- a/op-mode-definitions/show-interfaces-tunnel.xml.in +++ b/op-mode-definitions/show-interfaces-tunnel.xml.in @@ -11,13 +11,13 @@ <path>interfaces tunnel</path> </completionHelp> </properties> - <command>${vyos_op_scripts_dir}/show_interfaces.py --intf="$4"</command> + <command>${vyos_op_scripts_dir}/interfaces.py show --intf_name="$4" --intf_type=tunnel</command> <children> <leafNode name="brief"> <properties> <help>Show summary of the specified tunnel interface information</help> </properties> - <command>${vyos_op_scripts_dir}/show_interfaces.py --intf="$4" --action=show-brief</command> + <command>${vyos_op_scripts_dir}/interfaces.py show_summary --intf_name="$4" --intf_type=tunnel</command> </leafNode> </children> </tagNode> @@ -25,13 +25,13 @@ <properties> <help>Show Tunnel interface information</help> </properties> - <command>${vyos_op_scripts_dir}/show_interfaces.py --intf-type=tunnel --action=show-brief</command> + <command>${vyos_op_scripts_dir}/interfaces.py show_summary --intf_type=tunnel</command> <children> <leafNode name="detail"> <properties> <help>Show detailed tunnel interface information</help> </properties> - <command>${vyos_op_scripts_dir}/show_interfaces.py --intf-type=tunnel --action=show</command> + <command>${vyos_op_scripts_dir}/interfaces.py show --intf_type=tunnel</command> </leafNode> </children> </node> 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..c743492fb --- /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}/interfaces.py show --intf_name="$4" --intf_type=virtual-ethernet</command> + <children> + <leafNode name="brief"> + <properties> + <help>Show summary of the specified virtual-ethernet interface information</help> + </properties> + <command>${vyos_op_scripts_dir}/interfaces.py show_summary --intf_name="$4" --intf_type=virtual-ethernet</command> + </leafNode> + </children> + </tagNode> + <node name="virtual-ethernet"> + <properties> + <help>Show virtual-ethernet interface information</help> + </properties> + <command>${vyos_op_scripts_dir}/interfaces.py show_summary --intf_type=virtual-ethernet</command> + <children> + <leafNode name="detail"> + <properties> + <help>Show detailed virtual-ethernet interface information</help> + </properties> + <command>${vyos_op_scripts_dir}/interfaces.py show --intf_type=virtual-ethernet</command> + </leafNode> + </children> + </node> + </children> + </node> + </children> + </node> +</interfaceDefinition> diff --git a/op-mode-definitions/show-interfaces-vti.xml.in b/op-mode-definitions/show-interfaces-vti.xml.in index b436b8414..d532894b7 100644 --- a/op-mode-definitions/show-interfaces-vti.xml.in +++ b/op-mode-definitions/show-interfaces-vti.xml.in @@ -11,13 +11,13 @@ <path>interfaces vti</path> </completionHelp> </properties> - <command>${vyos_op_scripts_dir}/show_interfaces.py --intf="$4"</command> + <command>${vyos_op_scripts_dir}/interfaces.py show --intf_name="$4" --intf_type=vti</command> <children> <leafNode name="brief"> <properties> <help>Show summary of the specified vti interface information</help> </properties> - <command>${vyos_op_scripts_dir}/show_interfaces.py --intf="$4" --action=show-brief</command> + <command>${vyos_op_scripts_dir}/interfaces.py show_summary --intf_name="$4" --intf_type=vti</command> </leafNode> </children> </tagNode> @@ -25,13 +25,13 @@ <properties> <help>Show VTI interface information</help> </properties> - <command>${vyos_op_scripts_dir}/show_interfaces.py --intf-type=vti --action=show-brief</command> + <command>${vyos_op_scripts_dir}/interfaces.py show_summary --intf_type=vti</command> <children> <leafNode name="detail"> <properties> <help>Show detailed vti interface information</help> </properties> - <command>${vyos_op_scripts_dir}/show_interfaces.py --intf-type=vti --action=show</command> + <command>${vyos_op_scripts_dir}/interfaces.py show --intf_type=vti</command> </leafNode> </children> </node> diff --git a/op-mode-definitions/show-interfaces-vxlan.xml.in b/op-mode-definitions/show-interfaces-vxlan.xml.in index 1befd428c..fde832551 100644 --- a/op-mode-definitions/show-interfaces-vxlan.xml.in +++ b/op-mode-definitions/show-interfaces-vxlan.xml.in @@ -11,13 +11,13 @@ <path>interfaces vxlan</path> </completionHelp> </properties> - <command>${vyos_op_scripts_dir}/show_interfaces.py --intf="$4"</command> + <command>${vyos_op_scripts_dir}/interfaces.py show --intf_name="$4" --intf_type=vxlan</command> <children> <leafNode name="brief"> <properties> <help>Show summary of the specified VXLAN interface information</help> </properties> - <command>${vyos_op_scripts_dir}/show_interfaces.py --intf="$4" --action=show-brief</command> + <command>${vyos_op_scripts_dir}/interfaces.py show_summary --intf_name="$4" --intf_type=vxlan</command> </leafNode> </children> </tagNode> @@ -25,13 +25,13 @@ <properties> <help>Show VXLAN interface information</help> </properties> - <command>${vyos_op_scripts_dir}/show_interfaces.py --intf-type=vxlan --action=show-brief</command> + <command>${vyos_op_scripts_dir}/interfaces.py show_summary --intf_type=vxlan</command> <children> <leafNode name="detail"> <properties> <help>Show detailed VXLAN interface information</help> </properties> - <command>${vyos_op_scripts_dir}/show_interfaces.py --intf-type=vxlan --action=show</command> + <command>${vyos_op_scripts_dir}/interfaces.py show --intf_type=vxlan</command> </leafNode> </children> </node> diff --git a/op-mode-definitions/show-interfaces-wireguard.xml.in b/op-mode-definitions/show-interfaces-wireguard.xml.in index c9b754dcd..75b0cc88e 100644 --- a/op-mode-definitions/show-interfaces-wireguard.xml.in +++ b/op-mode-definitions/show-interfaces-wireguard.xml.in @@ -8,10 +8,10 @@ <properties> <help>Show specified WireGuard interface information</help> <completionHelp> - <script>${vyos_completion_dir}/list_interfaces.py --type wireguard</script> + <path>interfaces wireguard</path> </completionHelp> </properties> - <command>${vyos_op_scripts_dir}/show_interfaces.py --intf="$4"</command> + <command>${vyos_op_scripts_dir}/interfaces.py show --intf_name="$4" --intf_type=wireguard</command> <children> <leafNode name="allowed-ips"> <properties> @@ -49,13 +49,13 @@ <properties> <help>Show WireGuard interface information</help> </properties> - <command>${vyos_op_scripts_dir}/show_interfaces.py --intf-type=wireguard --action=show-brief</command> + <command>${vyos_op_scripts_dir}/interfaces.py show_summary --intf_type=wireguard</command> <children> <leafNode name="detail"> <properties> <help>Show detailed Wireguard interface information</help> </properties> - <command>${vyos_op_scripts_dir}/show_interfaces.py --intf-type=wireguard --action=show</command> + <command>${vyos_op_scripts_dir}/interfaces.py show --intf_type=wireguard</command> </leafNode> </children> </node> diff --git a/op-mode-definitions/show-interfaces-wireless.xml.in b/op-mode-definitions/show-interfaces-wireless.xml.in index 4a37417aa..cdd591f82 100644 --- a/op-mode-definitions/show-interfaces-wireless.xml.in +++ b/op-mode-definitions/show-interfaces-wireless.xml.in @@ -8,13 +8,13 @@ <properties> <help>Show Wireless (WLAN) interface information</help> </properties> - <command>${vyos_op_scripts_dir}/show_interfaces.py --intf-type=wireless --action=show-brief</command> + <command>${vyos_op_scripts_dir}/interfaces.py show_summary --intf_type=wireless</command> <children> <leafNode name="detail"> <properties> <help>Show detailed wireless interface information</help> </properties> - <command>${vyos_op_scripts_dir}/show_interfaces.py --intf-type=wireless --action=show</command> + <command>${vyos_op_scripts_dir}/interfaces.py show --intf_type=wireless</command> </leafNode> <leafNode name="info"> <properties> @@ -28,16 +28,16 @@ <properties> <help>Show specified wireless interface information</help> <completionHelp> - <script>${vyos_completion_dir}/list_interfaces.py --type wireless</script> + <path>interfaces wireless</path> </completionHelp> </properties> - <command>${vyos_op_scripts_dir}/show_interfaces.py --intf="$4"</command> + <command>${vyos_op_scripts_dir}/interfaces.py show --intf_name="$4" --intf_type=wireless</command> <children> <leafNode name="brief"> <properties> <help>Show summary of the specified wireless interface information</help> </properties> - <command>${vyos_op_scripts_dir}/show_interfaces.py --intf="$4" --action=show-brief</command> + <command>${vyos_op_scripts_dir}/interfaces.py show_summary --intf_name="$4" --intf_type=wireless</command> </leafNode> <node name="scan"> <properties> @@ -63,13 +63,13 @@ <properties> <help>Show specified virtual network interface (vif) information</help> </properties> - <command>${vyos_op_scripts_dir}/show_interfaces.py --intf="$4.$6"</command> + <command>${vyos_op_scripts_dir}/interfaces.py show --intf_name="$4.$6" --intf_type=wireless</command> <children> <leafNode name="brief"> <properties> <help>Show summary of specified virtual network interface (vif) information</help> </properties> - <command>${vyos_op_scripts_dir}/show_interfaces.py --intf="$4.$6" --action=show-brief</command> + <command>${vyos_op_scripts_dir}/interfaces.py show_summary --intf_name="$4.$6" --intf_type=wireless</command> </leafNode> </children> </tagNode> diff --git a/op-mode-definitions/show-interfaces-wwan.xml.in b/op-mode-definitions/show-interfaces-wwan.xml.in index 3cd29b38a..17d4111a9 100644 --- a/op-mode-definitions/show-interfaces-wwan.xml.in +++ b/op-mode-definitions/show-interfaces-wwan.xml.in @@ -12,7 +12,7 @@ <script>cd /sys/class/net; ls -d wwan*</script> </completionHelp> </properties> - <command>${vyos_op_scripts_dir}/show_interfaces.py --intf="$4"</command> + <command>${vyos_op_scripts_dir}/interfaces.py show --intf_name="$4" --intf_type=wirelessmodem</command> <children> <leafNode name="capabilities"> <properties> @@ -86,13 +86,13 @@ <properties> <help>Show Wireless Modem (WWAN) interface information</help> </properties> - <command>${vyos_op_scripts_dir}/show_interfaces.py --intf-type=wirelessmodem --action=show-brief</command> + <command>${vyos_op_scripts_dir}/interfaces.py show_summary --intf_type=wirelessmodem</command> <children> <leafNode name="detail"> <properties> <help>Show detailed Wireless Modem (WWAN( interface information</help> </properties> - <command>${vyos_op_scripts_dir}/show_interfaces.py --intf-type=wirelessmodem --action=show</command> + <command>${vyos_op_scripts_dir}/interfaces.py show --intf_type=wirelessmodem</command> </leafNode> </children> </node> diff --git a/op-mode-definitions/show-interfaces.xml.in b/op-mode-definitions/show-interfaces.xml.in index 39b0f0a2c..dc61a6f5c 100644 --- a/op-mode-definitions/show-interfaces.xml.in +++ b/op-mode-definitions/show-interfaces.xml.in @@ -6,19 +6,19 @@ <properties> <help>Show network interface information</help> </properties> - <command>${vyos_op_scripts_dir}/show_interfaces.py --action=show-brief</command> + <command>${vyos_op_scripts_dir}/interfaces.py show_summary</command> <children> <leafNode name="counters"> <properties> <help>Show network interface counters</help> </properties> - <command>${vyos_op_scripts_dir}/show_interfaces.py --action=show-count</command> + <command>${vyos_op_scripts_dir}/interfaces.py show_counters</command> </leafNode> <leafNode name="detail"> <properties> <help>Show detailed information of all interfaces</help> </properties> - <command>${vyos_op_scripts_dir}/show_interfaces.py --action=show</command> + <command>${vyos_op_scripts_dir}/interfaces.py show</command> </leafNode> </children> </node> diff --git a/op-mode-definitions/show-ip-multicast.xml.in b/op-mode-definitions/show-ip-multicast.xml.in index 80d83b424..605d61e8d 100644 --- a/op-mode-definitions/show-ip-multicast.xml.in +++ b/op-mode-definitions/show-ip-multicast.xml.in @@ -13,13 +13,7 @@ <properties> <help>Show multicast interfaces</help> </properties> - <command>if ps -C igmpproxy &>/dev/null; then ${vyos_op_scripts_dir}/show_igmpproxy.py --interface; else echo IGMP proxy not configured; fi</command> - </leafNode> - <leafNode name="mfc"> - <properties> - <help>Show multicast fowarding cache</help> - </properties> - <command>if ps -C igmpproxy &>/dev/null; then ${vyos_op_scripts_dir}/show_igmpproxy.py --mfc; else echo IGMP proxy not configured; fi</command> + <command>${vyos_op_scripts_dir}/igmp-proxy.py show_interface</command> </leafNode> <leafNode name="summary"> <properties> diff --git a/op-mode-definitions/show-ip-route.xml.in b/op-mode-definitions/show-ip-route.xml.in index 1e906672d..681aa089f 100644 --- a/op-mode-definitions/show-ip-route.xml.in +++ b/op-mode-definitions/show-ip-route.xml.in @@ -50,10 +50,23 @@ #include <include/show-route-ospf.xml.i> #include <include/show-route-rip.xml.i> #include <include/show-route-static.xml.i> - #include <include/show-route-summary.xml.i> #include <include/show-route-supernets-only.xml.i> #include <include/show-route-table.xml.i> #include <include/show-route-tag.xml.i> + <node name="summary"> + <properties> + <help>Summary of all routes</help> + </properties> + <command>${vyos_op_scripts_dir}/route.py show_summary --family inet</command> + <children> + <tagNode name="table"> + <properties> + <help>Summary of routes in a particular table</help> + </properties> + <command>${vyos_op_scripts_dir}/route.py show_summary --family inet --table $6</command> + </tagNode> + </children> + </node> <tagNode name="vrf"> <properties> <help>Show IP routes in VRF</help> @@ -64,6 +77,12 @@ </properties> <command>${vyos_op_scripts_dir}/vtysh_wrapper.sh $@</command> <children> + <node name="summary"> + <properties> + <help>Summary of all routes in the VRF</help> + </properties> + <command>${vyos_op_scripts_dir}/route.py show_summary --family inet --vrf $5</command> + </node> #include <include/show-route-bgp.xml.i> #include <include/show-route-connected.xml.i> #include <include/show-route-isis.xml.i> @@ -71,7 +90,6 @@ #include <include/show-route-ospf.xml.i> #include <include/show-route-rip.xml.i> #include <include/show-route-static.xml.i> - #include <include/show-route-summary.xml.i> #include <include/show-route-supernets-only.xml.i> #include <include/show-route-tag.xml.i> </children> diff --git a/op-mode-definitions/show-ip.xml.in b/op-mode-definitions/show-ip.xml.in index 0751c50cb..a710e33d2 100644 --- a/op-mode-definitions/show-ip.xml.in +++ b/op-mode-definitions/show-ip.xml.in @@ -17,7 +17,7 @@ <properties> <help>Show IPv4 neighbor table for specified interface</help> <completionHelp> - <script>${vyos_completion_dir}/list_interfaces.py -b</script> + <script>${vyos_completion_dir}/list_interfaces.py --broadcast</script> </completionHelp> </properties> <command>${vyos_op_scripts_dir}/neighbor.py show --family inet --interface "$5"</command> diff --git a/op-mode-definitions/show-ipv6-route.xml.in b/op-mode-definitions/show-ipv6-route.xml.in index 2c5024991..7df1a873a 100644 --- a/op-mode-definitions/show-ipv6-route.xml.in +++ b/op-mode-definitions/show-ipv6-route.xml.in @@ -50,9 +50,22 @@ #include <include/show-route-ospfv3.xml.i> #include <include/show-route-ripng.xml.i> #include <include/show-route-static.xml.i> - #include <include/show-route-summary.xml.i> #include <include/show-route-table.xml.i> #include <include/show-route-tag.xml.i> + <node name="summary"> + <properties> + <help>Summary of all routes</help> + </properties> + <command>${vyos_op_scripts_dir}/route.py show_summary --family inet6</command> + <children> + <tagNode name="table"> + <properties> + <help>Summary of routes in a particular table</help> + </properties> + <command>${vyos_op_scripts_dir}/route.py show_summary --family inet6 --table $6</command> + </tagNode> + </children> + </node> <tagNode name="vrf"> <properties> <help>Show IPv6 routes in VRF</help> @@ -63,6 +76,12 @@ </properties> <command>${vyos_op_scripts_dir}/vtysh_wrapper.sh $@</command> <children> + <node name="summary"> + <properties> + <help>Summary of all routes in the VRF</help> + </properties> + <command>${vyos_op_scripts_dir}/route.py show_summary --family inet6 --vrf $5</command> + </node> #include <include/show-route-bgp.xml.i> #include <include/show-route-connected.xml.i> #include <include/show-route-isis.xml.i> @@ -70,7 +89,6 @@ #include <include/show-route-ospfv3.xml.i> #include <include/show-route-ripng.xml.i> #include <include/show-route-static.xml.i> - #include <include/show-route-summary.xml.i> #include <include/show-route-supernets-only.xml.i> #include <include/show-route-table.xml.i> #include <include/show-route-tag.xml.i> diff --git a/op-mode-definitions/show-log.xml.in b/op-mode-definitions/show-log.xml.in index 8906d9ef3..f5e5b1493 100644 --- a/op-mode-definitions/show-log.xml.in +++ b/op-mode-definitions/show-log.xml.in @@ -196,6 +196,12 @@ </tagNode> </children> </tagNode> + <leafNode name="ipoe-server"> + <properties> + <help>Show log for IPoE server</help> + </properties> + <command>journalctl --no-hostname --boot --unit accel-ppp@ipoe.service</command> + </leafNode> <leafNode name="kernel"> <properties> <help>Show log for Linux Kernel</help> @@ -204,7 +210,7 @@ </leafNode> <leafNode name="lldp"> <properties> - <help>Show log for LLDP</help> + <help>Show log for Link Layer Discovery Protocol (LLDP)</help> </properties> <command>journalctl --no-hostname --boot --unit lldpd.service</command> </leafNode> @@ -216,10 +222,16 @@ </leafNode> <leafNode name="nhrp"> <properties> - <help>Show log for NHRP</help> + <help>Show log for Next Hop Resolution Protocol (NHRP)</help> </properties> <command>journalctl --no-hostname --boot --unit opennhrp.service</command> </leafNode> + <leafNode name="ntp"> + <properties> + <help>Show log for Network Time Protocol (NTP)</help> + </properties> + <command>journalctl --no-hostname --boot --unit chrony.service</command> + </leafNode> <node name="macsec"> <properties> <help>Show log for MACsec</help> @@ -230,7 +242,7 @@ <properties> <help>Show MACsec log on specific interface</help> <completionHelp> - <script>${vyos_completion_dir}/list_interfaces.py -t macsec</script> + <path>interfaces macsec</path> </completionHelp> </properties> <command>SRC=$(cli-shell-api returnValue interfaces macsec "$5" source-interface); journalctl --no-hostname --boot --unit "wpa_supplicant-macsec@$SRC.service"</command> @@ -256,7 +268,7 @@ </node> <node name="pppoe"> <properties> - <help>Show log for PPPoE</help> + <help>Show log for PPPoE interface</help> </properties> <command>journalctl --no-hostname --boot --unit "ppp@pppoe*.service"</command> <children> @@ -264,13 +276,19 @@ <properties> <help>Show PPPoE log on specific interface</help> <completionHelp> - <script>${vyos_completion_dir}/list_interfaces.py -t pppoe</script> + <path>interfaces pppoe</path> </completionHelp> </properties> - <command>journalctl --no-hostname --boot --unit "ppp@$6.service"</command> + <command>journalctl --no-hostname --boot --unit "ppp@$5.service"</command> </tagNode> </children> </node> + <leafNode name="pppoe-server"> + <properties> + <help>Show log for PPPoE server</help> + </properties> + <command>journalctl --no-hostname --boot --unit accel-ppp@pppoe.service</command> + </leafNode> <node name="protocol"> <properties> <help>Show log for Routing Protocol</help> @@ -344,6 +362,12 @@ </leafNode> </children> </node> + <leafNode name="router-advert"> + <properties> + <help>Show log for Router Advertisement Daemon (radvd)</help> + </properties> + <command>journalctl --no-hostname --boot --unit radvd.service</command> + </leafNode> <leafNode name="snmp"> <properties> <help>Show log for Simple Network Monitoring Protocol (SNMP)</help> @@ -356,6 +380,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> + <path>interfaces sstpc</path> + </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> @@ -380,7 +421,7 @@ <properties> <help>Show log for ALL</help> </properties> - <command>journalctl --no-hostname --boot --unit strongswan-starter.service --unit accel-ppp@*.service</command> + <command>journalctl --no-hostname --boot --unit strongswan-starter.service --unit accel-ppp@*.service --unit ocserv.service</command> </leafNode> <leafNode name="ipsec"> <properties> @@ -394,6 +435,12 @@ </properties> <command>journalctl --no-hostname --boot --unit accel-ppp@l2tp.service</command> </leafNode> + <leafNode name="openconnect"> + <properties> + <help>Show log for OpenConnect</help> + </properties> + <command>journalctl --no-hostname --boot --unit ocserv.service</command> + </leafNode> <leafNode name="pptp"> <properties> <help>Show log for PPTP</help> diff --git a/op-mode-definitions/show-ntp.xml.in b/op-mode-definitions/show-ntp.xml.in index 01f4477d8..0907722af 100644 --- a/op-mode-definitions/show-ntp.xml.in +++ b/op-mode-definitions/show-ntp.xml.in @@ -6,22 +6,13 @@ <properties> <help>Show peer status of NTP daemon</help> </properties> - <command>${vyos_op_scripts_dir}/show_ntp.sh --basic</command> + <command>${vyos_op_scripts_dir}/show_ntp.sh --sourcestats</command> <children> - <tagNode name="server"> + <node name="system"> <properties> - <help>Show date and time of specified NTP server</help> - <completionHelp> - <script>${vyos_completion_dir}/list_ntp_servers.sh</script> - </completionHelp> + <help>Show parameters about the system clock performance</help> </properties> - <command>${vyos_op_scripts_dir}/show_ntp.sh --server "$4"</command> - </tagNode> - <node name="info"> - <properties> - <help>Show NTP operational summary</help> - </properties> - <command>${vyos_op_scripts_dir}/show_ntp.sh --info</command> + <command>${vyos_op_scripts_dir}/show_ntp.sh --tracking</command> </node> </children> </node> diff --git a/op-mode-definitions/show-protocols.xml.in b/op-mode-definitions/show-protocols.xml.in index 698001b76..27146f90d 100644 --- a/op-mode-definitions/show-protocols.xml.in +++ b/op-mode-definitions/show-protocols.xml.in @@ -22,7 +22,7 @@ <properties> <help>Show Address Resolution Protocol (ARP) cache for specified interface</help> <completionHelp> - <script>${vyos_completion_dir}/list_interfaces.py -b</script> + <script>${vyos_completion_dir}/list_interfaces.py --broadcast</script> </completionHelp> </properties> <command>/usr/sbin/arp -e -n -i "$6"</command> 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/show-system.xml.in b/op-mode-definitions/show-system.xml.in index 4a0e6c3b2..85bfdcdba 100644 --- a/op-mode-definitions/show-system.xml.in +++ b/op-mode-definitions/show-system.xml.in @@ -7,6 +7,42 @@ <help>Show system information</help> </properties> <children> + <node name="commit"> + <properties> + <help>Show commit revision log</help> + </properties> + <command>${vyos_op_scripts_dir}/config_mgmt.py show_commit_log</command> + <children> + <tagNode name="diff"> + <properties> + <help>Show commit revision diff</help> + </properties> + <command>${vyos_op_scripts_dir}/config_mgmt.py show_commit_diff --rev "$5"</command> + </tagNode> + <tagNode name="file"> + <properties> + <help>Show commit revision file</help> + </properties> + <command>${vyos_op_scripts_dir}/config_mgmt.py show_commit_file --rev "$5"</command> + <children> + <tagNode name="compare"> + <properties> + <help>Compare config file revisions</help> + </properties> + <command>${vyos_op_scripts_dir}/config_mgmt.py show_commit_diff --rev "$5" --rev2 "$7"</command> + <children> + <leafNode name="commands"> + <properties> + <help>Compare config file revision commands</help> + </properties> + <command>${vyos_op_scripts_dir}/config_mgmt.py show_commit_diff --rev "$5" --rev2 "$7" --commands</command> + </leafNode> + </children> + </tagNode> + </children> + </tagNode> + </children> + </node> <node name="connections"> <properties> <help>Show active network connections on the system</help> 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/op-mode-definitions/wireless.xml.in b/op-mode-definitions/wireless.xml.in index 5d9db1544..f8e53ad21 100644 --- a/op-mode-definitions/wireless.xml.in +++ b/op-mode-definitions/wireless.xml.in @@ -21,7 +21,7 @@ <properties> <help>Clear interface information for a given wireless interface</help> <completionHelp> - <script>${vyos_completion_dir}/list_interfaces.py --type wireless</script> + <path>interfaces wireless</path> </completionHelp> </properties> <children> diff --git a/op-mode-definitions/zone-policy.xml.in b/op-mode-definitions/zone-policy.xml.in index c4b02bcee..9d65ddd3d 100644 --- a/op-mode-definitions/zone-policy.xml.in +++ b/op-mode-definitions/zone-policy.xml.in @@ -11,13 +11,13 @@ <properties> <help>Show summary of zone policy for a specific zone</help> <completionHelp> - <path>zone-policy zone</path> + <path>firewall zone</path> </completionHelp> </properties> - <command>sudo ${vyos_op_scripts_dir}/zone_policy.py --action show --name $4</command> + <command>sudo ${vyos_op_scripts_dir}/zone.py show --zone $4</command> </tagNode> </children> - <command>sudo ${vyos_op_scripts_dir}/zone_policy.py --action show</command> + <command>sudo ${vyos_op_scripts_dir}/zone.py show</command> </node> </children> </node> diff --git a/python/setup.py b/python/setup.py index e2d28bd6b..2d614e724 100644 --- a/python/setup.py +++ b/python/setup.py @@ -24,4 +24,9 @@ setup( "Topic :: Utilities", "License :: OSI Approved :: GNU Lesser General Public License v2 or later (LGPLv2+)", ], + entry_points={ + "console_scripts": [ + "config-mgmt = vyos.config_mgmt:run", + ], + }, ) diff --git a/python/vyos/accel_ppp.py b/python/vyos/accel_ppp.py new file mode 100644 index 000000000..bfc8ee5a9 --- /dev/null +++ b/python/vyos/accel_ppp.py @@ -0,0 +1,74 @@ +#!/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 sys + +import vyos.opmode +from vyos.util import rc_cmd + + +def get_server_statistics(accel_statistics, pattern, sep=':') -> dict: + import re + + stat_dict = {'sessions': {}} + + cpu = re.search(r'cpu(.*)', accel_statistics).group(0) + # Find all lines with pattern, for example 'sstp:' + data = re.search(rf'{pattern}(.*)', accel_statistics, re.DOTALL).group(0) + session_starting = re.search(r'starting(.*)', data).group(0) + session_active = re.search(r'active(.*)', data).group(0) + + for entry in {cpu, session_starting, session_active}: + if sep in entry: + key, value = entry.split(sep) + if key in ['starting', 'active', 'finishing']: + stat_dict['sessions'][key] = value.strip() + continue + stat_dict[key] = value.strip() + return stat_dict + + +def accel_cmd(port: int, command: str) -> str: + _, output = rc_cmd(f'/usr/bin/accel-cmd -p{port} {command}') + return output + + +def accel_out_parse(accel_output: list[str]) -> list[dict[str, str]]: + """ Parse accel-cmd show sessions output """ + data_list: list[dict[str, str]] = list() + field_names: list[str] = list() + + field_names_unstripped: list[str] = accel_output.pop(0).split('|') + for field_name in field_names_unstripped: + field_names.append(field_name.strip()) + + while accel_output: + if '|' not in accel_output[0]: + accel_output.pop(0) + continue + + current_item: list[str] = accel_output.pop(0).split('|') + item_dict: dict[str, str] = {} + + for field_index in range(len(current_item)): + field_name: str = field_names[field_index] + field_value: str = current_item[field_index].strip() + item_dict[field_name] = field_value + + data_list.append(item_dict) + + return data_list diff --git a/python/vyos/base.py b/python/vyos/base.py index 78067d5b2..9b93cb2f2 100644 --- a/python/vyos/base.py +++ b/python/vyos/base.py @@ -15,17 +15,47 @@ from textwrap import fill + +class BaseWarning: + def __init__(self, header, message, **kwargs): + self.message = message + self.kwargs = kwargs + if 'width' not in kwargs: + self.width = 72 + if 'initial_indent' in kwargs: + del self.kwargs['initial_indent'] + if 'subsequent_indent' in kwargs: + del self.kwargs['subsequent_indent'] + self.textinitindent = header + self.standardindent = '' + + def print(self): + messages = self.message.split('\n') + isfirstmessage = True + initial_indent = self.textinitindent + print('') + for mes in messages: + mes = fill(mes, initial_indent=initial_indent, + subsequent_indent=self.standardindent, **self.kwargs) + if isfirstmessage: + isfirstmessage = False + initial_indent = self.standardindent + print(f'{mes}') + print('') + + class Warning(): - def __init__(self, message): - # Reformat the message and trim it to 72 characters in length - message = fill(message, width=72) - print(f'\nWARNING: {message}') + def __init__(self, message, **kwargs): + self.BaseWarn = BaseWarning('WARNING: ', message, **kwargs) + self.BaseWarn.print() + class DeprecationWarning(): - def __init__(self, message): + def __init__(self, message, **kwargs): # Reformat the message and trim it to 72 characters in length - message = fill(message, width=72) - print(f'\nDEPRECATION WARNING: {message}\n') + self.BaseWarn = BaseWarning('DEPRECATION WARNING: ', message, **kwargs) + self.BaseWarn.print() + class ConfigError(Exception): def __init__(self, message): diff --git a/python/vyos/component_version.py b/python/vyos/component_version.py new file mode 100644 index 000000000..a4e318d08 --- /dev/null +++ b/python/vyos/component_version.py @@ -0,0 +1,192 @@ +# 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/>. + +""" +Functions for reading/writing component versions. + +The config file version string has the following form: + +VyOS 1.3/1.4: + +// Warning: Do not remove the following line. +// vyos-config-version: "broadcast-relay@1:cluster@1:config-management@1:conntrack@3:conntrack-sync@2:dhcp-relay@2:dhcp-server@6:dhcpv6-server@1:dns-forwarding@3:firewall@5:https@2:interfaces@22:ipoe-server@1:ipsec@5:isis@1:l2tp@3:lldp@1:mdns@1:nat@5:ntp@1:pppoe-server@5:pptp@2:qos@1:quagga@8:rpki@1:salt@1:snmp@2:ssh@2:sstp@3:system@21:vrrp@2:vyos-accel-ppp@2:wanloadbalance@3:webproxy@2:zone-policy@1" +// Release version: 1.3.0 + +VyOS 1.2: + +/* Warning: Do not remove the following line. */ +/* === vyatta-config-version: "broadcast-relay@1:cluster@1:config-management@1:conntrack-sync@1:conntrack@1:dhcp-relay@2:dhcp-server@5:dns-forwarding@1:firewall@5:ipsec@5:l2tp@1:mdns@1:nat@4:ntp@1:pppoe-server@2:pptp@1:qos@1:quagga@7:snmp@1:ssh@1:system@10:vrrp@2:wanloadbalance@3:webgui@1:webproxy@2:zone-policy@1" === */ +/* Release version: 1.2.8 */ + +""" + +import os +import re +import sys +import fileinput + +from vyos.xml import component_version +from vyos.version import get_version +from vyos.defaults import directories + +DEFAULT_CONFIG_PATH = os.path.join(directories['config'], 'config.boot') + +def from_string(string_line, vintage='vyos'): + """ + Get component version dictionary from string. + Return empty dictionary if string contains no config information + or raise error if component version string malformed. + """ + version_dict = {} + + if vintage == 'vyos': + if re.match(r'// vyos-config-version:.+', string_line): + if not re.match(r'// vyos-config-version:\s+"([\w,-]+@\d+:)+([\w,-]+@\d+)"\s*', string_line): + raise ValueError(f"malformed configuration string: {string_line}") + + for pair in re.findall(r'([\w,-]+)@(\d+)', string_line): + version_dict[pair[0]] = int(pair[1]) + + elif vintage == 'vyatta': + if re.match(r'/\* === vyatta-config-version:.+=== \*/$', string_line): + if not re.match(r'/\* === vyatta-config-version:\s+"([\w,-]+@\d+:)+([\w,-]+@\d+)"\s+=== \*/$', string_line): + raise ValueError(f"malformed configuration string: {string_line}") + + for pair in re.findall(r'([\w,-]+)@(\d+)', string_line): + version_dict[pair[0]] = int(pair[1]) + else: + raise ValueError("Unknown config string vintage") + + return version_dict + +def from_file(config_file_name=DEFAULT_CONFIG_PATH, vintage='vyos'): + """ + Get component version dictionary parsing config file line by line + """ + with open(config_file_name, 'r') as f: + for line_in_config in f: + version_dict = from_string(line_in_config, vintage=vintage) + if version_dict: + return version_dict + + # no version information + return {} + +def from_system(): + """ + Get system component version dict. + """ + return component_version() + +def legacy_from_system(): + """ + Get system component version dict from legacy location. + This is for a transitional sanity check; the directory will eventually + be removed. + """ + system_versions = {} + legacy_dir = directories['current'] + + # To be removed: + if not os.path.isdir(legacy_dir): + return system_versions + + try: + version_info = os.listdir(legacy_dir) + except OSError as err: + sys.exit(repr(err)) + + for info in version_info: + if re.match(r'[\w,-]+@\d+', info): + pair = info.split('@') + system_versions[pair[0]] = int(pair[1]) + + return system_versions + +def format_string(ver: dict) -> str: + """ + Version dict to string. + """ + keys = list(ver) + keys.sort() + l = [] + for k in keys: + v = ver[k] + l.append(f'{k}@{v}') + sep = ':' + return sep.join(l) + +def version_footer(ver: dict, vintage='vyos') -> str: + """ + Version footer as string. + """ + ver_str = format_string(ver) + release = get_version() + if vintage == 'vyos': + ret_str = (f'// Warning: Do not remove the following line.\n' + + f'// vyos-config-version: "{ver_str}"\n' + + f'// Release version: {release}\n') + elif vintage == 'vyatta': + ret_str = (f'/* Warning: Do not remove the following line. */\n' + + f'/* === vyatta-config-version: "{ver_str}" === */\n' + + f'/* Release version: {release} */\n') + else: + raise ValueError("Unknown config string vintage") + + return ret_str + +def system_footer(vintage='vyos') -> str: + """ + System version footer as string. + """ + ver_d = from_system() + return version_footer(ver_d, vintage=vintage) + +def write_version_footer(ver: dict, file_name, vintage='vyos'): + """ + Write version footer to file. + """ + footer = version_footer(ver=ver, vintage=vintage) + if file_name: + with open(file_name, 'a') as f: + f.write(footer) + else: + sys.stdout.write(footer) + +def write_system_footer(file_name, vintage='vyos'): + """ + Write system version footer to file. + """ + ver_d = from_system() + return write_version_footer(ver_d, file_name=file_name, vintage=vintage) + +def remove_footer(file_name): + """ + Remove old version footer. + """ + for line in fileinput.input(file_name, inplace=True): + if re.match(r'/\* Warning:.+ \*/$', line): + continue + if re.match(r'/\* === vyatta-config-version:.+=== \*/$', line): + continue + if re.match(r'/\* Release version:.+ \*/$', line): + continue + if re.match('// vyos-config-version:.+', line): + continue + if re.match('// Warning:.+', line): + continue + if re.match('// Release version:.+', line): + continue + sys.stdout.write(line) diff --git a/python/vyos/component_versions.py b/python/vyos/component_versions.py deleted file mode 100644 index 90b458aae..000000000 --- a/python/vyos/component_versions.py +++ /dev/null @@ -1,57 +0,0 @@ -# Copyright 2017 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/>. - -""" -The version data looks like: - -/* Warning: Do not remove the following line. */ -/* === vyatta-config-version: -"cluster@1:config-management@1:conntrack-sync@1:conntrack@1:dhcp-relay@1:dhcp-server@4:firewall@5:ipsec@4:nat@4:qos@1:quagga@2:system@8:vrrp@1:wanloadbalance@3:webgui@1:webproxy@1:zone-policy@1" -=== */ -/* Release version: 1.2.0-rolling+201806131737 */ -""" - -import re - -def get_component_version(string_line): - """ - Get component version dictionary from string - return empty dictionary if string contains no config information - or raise error if component version string malformed - """ - return_value = {} - if re.match(r'/\* === vyatta-config-version:.+=== \*/$', string_line): - - if not re.match(r'/\* === vyatta-config-version:\s+"([\w,-]+@\d+:)+([\w,-]+@\d+)"\s+=== \*/$', string_line): - raise ValueError("malformed configuration string: " + str(string_line)) - - for pair in re.findall(r'([\w,-]+)@(\d+)', string_line): - if pair[0] in return_value.keys(): - raise ValueError("duplicate unit name: \"" + str(pair[0]) + "\" in string: \"" + string_line + "\"") - return_value[pair[0]] = int(pair[1]) - - return return_value - - -def get_component_versions_from_file(config_file_name='/opt/vyatta/etc/config/config.boot'): - """ - Get component version dictionary parsing config file line by line - """ - f = open(config_file_name, 'r') - for line_in_config in f: - component_version = get_component_version(line_in_config) - if component_version: - return component_version - raise ValueError("no config string in file:", config_file_name) diff --git a/python/vyos/config_mgmt.py b/python/vyos/config_mgmt.py new file mode 100644 index 000000000..22a49ff50 --- /dev/null +++ b/python/vyos/config_mgmt.py @@ -0,0 +1,686 @@ +# Copyright 2023 VyOS maintainers and contributors <maintainers@vyos.io> +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with this library. If not, see <http://www.gnu.org/licenses/>. + +import os +import re +import sys +import gzip +import logging +from typing import Optional, Tuple, Union +from filecmp import cmp +from datetime import datetime +from tabulate import tabulate + +from vyos.config import Config +from vyos.configtree import ConfigTree +from vyos.defaults import directories +from vyos.util import is_systemd_service_active, ask_yes_no, rc_cmd + +SAVE_CONFIG = '/opt/vyatta/sbin/vyatta-save-config.pl' + +# created by vyatta-cfg-postinst +commit_post_hook_dir = '/etc/commit/post-hooks.d' + +commit_hooks = {'commit_revision': '01vyos-commit-revision', + 'commit_archive': '02vyos-commit-archive'} + +DEFAULT_TIME_MINUTES = 10 +timer_name = 'commit-confirm' + +config_file = os.path.join(directories['config'], 'config.boot') +archive_dir = os.path.join(directories['config'], 'archive') +archive_config_file = os.path.join(archive_dir, 'config.boot') +commit_log_file = os.path.join(archive_dir, 'commits') +logrotate_conf = os.path.join(archive_dir, 'lr.conf') +logrotate_state = os.path.join(archive_dir, 'lr.state') +rollback_config = os.path.join(archive_dir, 'config.boot-rollback') +prerollback_config = os.path.join(archive_dir, 'config.boot-prerollback') +tmp_log_entry = '/tmp/commit-rev-entry' + +logger = logging.getLogger('config_mgmt') +logger.setLevel(logging.INFO) +ch = logging.StreamHandler() +formatter = logging.Formatter('%(funcName)s: %(levelname)s:%(message)s') +ch.setFormatter(formatter) +logger.addHandler(ch) + +class ConfigMgmtError(Exception): + pass + +class ConfigMgmt: + def __init__(self, session_env=None, config=None): + if session_env: + self._session_env = session_env + else: + self._session_env = None + + if config is None: + config = Config() + + d = config.get_config_dict(['system', 'config-management'], + key_mangling=('-', '_'), + get_first_key=True) + + self.max_revisions = int(d.get('commit_revisions', 0)) + self.locations = d.get('commit_archive', {}).get('location', []) + self.source_address = d.get('commit_archive', + {}).get('source_address', '') + if config.exists(['system', 'host-name']): + self.hostname = config.return_value(['system', 'host-name']) + else: + self.hostname = 'vyos' + + # upload only on existence of effective values, notably, on boot. + # one still needs session self.locations (above) for setting + # post-commit hook in conf_mode script + path = ['system', 'config-management', 'commit-archive', 'location'] + if config.exists_effective(path): + self.effective_locations = config.return_effective_values(path) + else: + self.effective_locations = [] + + # a call to compare without args is edit_level aware + edit_level = os.getenv('VYATTA_EDIT_LEVEL', '') + edit_path = [l for l in edit_level.split('/') if l] + if edit_path: + eff_conf = config.show_config(edit_path, effective=True) + self.edit_level_active_config = ConfigTree(eff_conf) + conf = config.show_config(edit_path) + self.edit_level_working_config = ConfigTree(conf) + else: + self.edit_level_active_config = None + self.edit_level_working_config = None + + self.active_config = config._running_config + self.working_config = config._session_config + + @staticmethod + def save_config(target): + cmd = f'{SAVE_CONFIG} {target}' + rc, out = rc_cmd(cmd) + if rc != 0: + logger.critical(f'save config failed: {out}') + + def _unsaved_commits(self) -> bool: + tmp_save = '/tmp/config.boot.check-save' + self.save_config(tmp_save) + ret = not cmp(tmp_save, config_file, shallow=False) + os.unlink(tmp_save) + return ret + + # Console script functions + # + def commit_confirm(self, minutes: int=DEFAULT_TIME_MINUTES, + no_prompt: bool=False) -> Tuple[str,int]: + """Commit with reboot to saved config in 'minutes' minutes if + 'confirm' call is not issued. + """ + if is_systemd_service_active(f'{timer_name}.timer'): + msg = 'Another confirm is pending' + return msg, 1 + + if self._unsaved_commits(): + W = '\nYou should save previous commits before commit-confirm !\n' + else: + W = '' + + prompt_str = f''' +commit-confirm will automatically reboot in {minutes} minutes unless changes +are confirmed.\n +Proceed ?''' + prompt_str = W + prompt_str + if not no_prompt and not ask_yes_no(prompt_str, default=True): + msg = 'commit-confirm canceled' + return msg, 1 + + action = 'sg vyattacfg "/usr/bin/config-mgmt revert"' + cmd = f'sudo systemd-run --quiet --on-active={minutes}m --unit={timer_name} {action}' + rc, out = rc_cmd(cmd) + if rc != 0: + raise ConfigMgmtError(out) + + # start notify + cmd = f'sudo -b /usr/libexec/vyos/commit-confirm-notify.py {minutes}' + os.system(cmd) + + msg = f'Initialized commit-confirm; {minutes} minutes to confirm before reboot' + return msg, 0 + + def confirm(self) -> Tuple[str,int]: + """Do not reboot to saved config following 'commit-confirm'. + Update commit log and archive. + """ + if not is_systemd_service_active(f'{timer_name}.timer'): + msg = 'No confirm pending' + return msg, 0 + + cmd = f'sudo systemctl stop --quiet {timer_name}.timer' + rc, out = rc_cmd(cmd) + if rc != 0: + raise ConfigMgmtError(out) + + # kill notify + cmd = 'sudo pkill -f commit-confirm-notify.py' + rc, out = rc_cmd(cmd) + if rc != 0: + raise ConfigMgmtError(out) + + entry = self._read_tmp_log_entry() + self._add_log_entry(**entry) + + if self._archive_active_config(): + self._update_archive() + + msg = 'Reboot timer stopped' + return msg, 0 + + def revert(self) -> Tuple[str,int]: + """Reboot to saved config, dropping commits from 'commit-confirm'. + """ + _ = self._read_tmp_log_entry() + + # archived config will be reverted on boot + rc, out = rc_cmd('sudo systemctl reboot') + if rc != 0: + raise ConfigMgmtError(out) + + return '', 0 + + def rollback(self, rev: int, no_prompt: bool=False) -> Tuple[str,int]: + """Reboot to config revision 'rev'. + """ + from shutil import copy + + msg = '' + + if not self._check_revision_number(rev): + msg = f'Invalid revision number {rev}: must be 0 < rev < {maxrev}' + return msg, 1 + + prompt_str = 'Proceed with reboot ?' + if not no_prompt and not ask_yes_no(prompt_str, default=True): + msg = 'Canceling rollback' + return msg, 0 + + rc, out = rc_cmd(f'sudo cp {archive_config_file} {prerollback_config}') + if rc != 0: + raise ConfigMgmtError(out) + + path = os.path.join(archive_dir, f'config.boot.{rev}.gz') + with gzip.open(path) as f: + config = f.read() + try: + with open(rollback_config, 'wb') as f: + f.write(config) + copy(rollback_config, config_file) + except OSError as e: + raise ConfigMgmtError from e + + rc, out = rc_cmd('sudo systemctl reboot') + if rc != 0: + raise ConfigMgmtError(out) + + return msg, 0 + + def compare(self, saved: bool=False, commands: bool=False, + rev1: Optional[int]=None, + rev2: Optional[int]=None) -> Tuple[str,int]: + """General compare function for config file revisions: + revision n vs. revision m; working version vs. active version; + or working version vs. saved version. + """ + from difflib import unified_diff + + ct1 = self.edit_level_active_config + if ct1 is None: + ct1 = self.active_config + ct2 = self.edit_level_working_config + if ct2 is None: + ct2 = self.working_config + msg = 'No changes between working and active configurations.\n' + if saved: + ct1 = self._get_saved_config_tree() + ct2 = self.working_config + msg = 'No changes between working and saved configurations.\n' + if rev1 is not None: + if not self._check_revision_number(rev1): + return f'Invalid revision number {rev1}', 1 + ct1 = self._get_config_tree_revision(rev1) + ct2 = self.working_config + msg = f'No changes between working and revision {rev1} configurations.\n' + if rev2 is not None: + if not self._check_revision_number(rev2): + return f'Invalid revision number {rev2}', 1 + # compare older to newer + ct2 = ct1 + ct1 = self._get_config_tree_revision(rev2) + msg = f'No changes between revisions {rev2} and {rev1} configurations.\n' + + if commands: + lines1 = ct1.to_commands().splitlines(keepends=True) + lines2 = ct2.to_commands().splitlines(keepends=True) + else: + lines1 = ct1.to_string().splitlines(keepends=True) + lines2 = ct2.to_string().splitlines(keepends=True) + + out = '' + comp = unified_diff(lines1, lines2) + for line in comp: + if re.match(r'(\-\-)|(\+\+)|(@@)', line): + continue + out += line + if out: + msg = out + + return msg, 0 + + def wrap_compare(self, options) -> Tuple[str,int]: + """Interface to vyatta-cfg-run: args collected as 'options' to parse + for compare. + """ + cmnds = False + r1 = None + r2 = None + if 'commands' in options: + cmnds=True + options.remove('commands') + for i in options: + if not i.isnumeric(): + options.remove(i) + if len(options) > 0: + r1 = int(options[0]) + if len(options) > 1: + r2 = int(options[1]) + + return self.compare(commands=cmnds, rev1=r1, rev2=r2) + + # Initialization and post-commit hooks for conf-mode + # + def initialize_revision(self): + """Initialize config archive, logrotate conf, and commit log. + """ + mask = os.umask(0o002) + os.makedirs(archive_dir, exist_ok=True) + + self._add_logrotate_conf() + + if (not os.path.exists(commit_log_file) or + self._get_number_of_revisions() == 0): + user = self._get_user() + via = 'init' + comment = '' + self._add_log_entry(user, via, comment) + # add empty init config before boot-config load for revision + # and diff consistency + if self._archive_active_config(): + self._update_archive() + + os.umask(mask) + + def commit_revision(self): + """Update commit log and rotate archived config.boot. + + commit_revision is called in post-commit-hooks, if + ['commit-archive', 'commit-revisions'] is configured. + """ + if os.getenv('IN_COMMIT_CONFIRM', ''): + self._new_log_entry(tmp_file=tmp_log_entry) + return + + self._add_log_entry() + + if self._archive_active_config(): + self._update_archive() + + def commit_archive(self): + """Upload config to remote archive. + """ + from vyos.remote import upload + + hostname = self.hostname + t = datetime.now() + timestamp = t.strftime('%Y%m%d_%H%M%S') + remote_file = f'config.boot-{hostname}.{timestamp}' + source_address = self.source_address + + for location in self.effective_locations: + upload(archive_config_file, f'{location}/{remote_file}', + source_host=source_address) + + # op-mode functions + # + def get_raw_log_data(self) -> list: + """Return list of dicts of log data: + keys: [timestamp, user, commit_via, commit_comment] + """ + log = self._get_log_entries() + res_l = [] + for line in log: + d = self._get_log_entry(line) + res_l.append(d) + + return res_l + + @staticmethod + def format_log_data(data: list) -> str: + """Return formatted log data as str. + """ + res_l = [] + for l_no, l in enumerate(data): + time_d = datetime.fromtimestamp(int(l['timestamp'])) + time_str = time_d.strftime("%Y-%m-%d %H:%M:%S") + + res_l.append([l_no, time_str, + f"by {l['user']}", f"via {l['commit_via']}"]) + + if l['commit_comment'] != 'commit': # default comment + res_l.append([None, l['commit_comment']]) + + ret = tabulate(res_l, tablefmt="plain") + return ret + + @staticmethod + def format_log_data_brief(data: list) -> str: + """Return 'brief' form of log data as str. + + Slightly compacted format used in completion help for + 'rollback'. + """ + res_l = [] + for l_no, l in enumerate(data): + time_d = datetime.fromtimestamp(int(l['timestamp'])) + time_str = time_d.strftime("%Y-%m-%d %H:%M:%S") + + res_l.append(['\t', l_no, time_str, + f"{l['user']}", f"by {l['commit_via']}"]) + + ret = tabulate(res_l, tablefmt="plain") + return ret + + def show_commit_diff(self, rev: int, rev2: Optional[int]=None, + commands: bool=False) -> str: + """Show commit diff at revision number, compared to previous + revision, or to another revision. + """ + if rev2 is None: + out, _ = self.compare(commands=commands, rev1=rev, rev2=(rev+1)) + return out + + out, _ = self.compare(commands=commands, rev1=rev, rev2=rev2) + return out + + def show_commit_file(self, rev: int) -> str: + return self._get_file_revision(rev) + + # utility functions + # + @staticmethod + def _strip_version(s): + return re.split(r'(^//)', s, maxsplit=1, flags=re.MULTILINE)[0] + + def _get_saved_config_tree(self): + with open(config_file) as f: + c = self._strip_version(f.read()) + return ConfigTree(c) + + def _get_file_revision(self, rev: int): + if rev not in range(0, self._get_number_of_revisions()): + raise ConfigMgmtError('revision not available') + revision = os.path.join(archive_dir, f'config.boot.{rev}.gz') + with gzip.open(revision) as f: + r = f.read().decode() + return r + + def _get_config_tree_revision(self, rev: int): + c = self._strip_version(self._get_file_revision(rev)) + return ConfigTree(c) + + def _add_logrotate_conf(self): + conf = f"""{archive_config_file} {{ + su root vyattacfg + rotate {self.max_revisions} + start 0 + compress + copy +}}""" + mask = os.umask(0o133) + + with open(logrotate_conf, 'w') as f: + f.write(conf) + + os.umask(mask) + + def _archive_active_config(self) -> bool: + mask = os.umask(0o113) + + ext = os.getpid() + tmp_save = f'/tmp/config.boot.{ext}' + self.save_config(tmp_save) + + try: + if cmp(tmp_save, archive_config_file, shallow=False): + # this will be the case on boot, as well as certain + # re-initialiation instances after delete/set + os.unlink(tmp_save) + return False + except FileNotFoundError: + pass + + rc, out = rc_cmd(f'sudo mv {tmp_save} {archive_config_file}') + os.umask(mask) + + if rc != 0: + logger.critical(f'mv file to archive failed: {out}') + return False + + return True + + @staticmethod + def _update_archive(): + cmd = f"sudo logrotate -f -s {logrotate_state} {logrotate_conf}" + rc, out = rc_cmd(cmd) + if rc != 0: + logger.critical(f'logrotate failure: {out}') + + @staticmethod + def _get_log_entries() -> list: + """Return lines of commit log as list of strings + """ + entries = [] + if os.path.exists(commit_log_file): + with open(commit_log_file) as f: + entries = f.readlines() + + return entries + + def _get_number_of_revisions(self) -> int: + l = self._get_log_entries() + return len(l) + + def _check_revision_number(self, rev: int) -> bool: + # exclude init revision: + maxrev = self._get_number_of_revisions() + if not 0 <= rev < maxrev - 1: + return False + return True + + @staticmethod + def _get_user() -> str: + import pwd + + try: + user = os.getlogin() + except OSError: + try: + user = pwd.getpwuid(os.geteuid())[0] + except KeyError: + user = 'unknown' + return user + + def _new_log_entry(self, user: str='', commit_via: str='', + commit_comment: str='', timestamp: Optional[int]=None, + tmp_file: str=None) -> Optional[str]: + # Format log entry and return str or write to file. + # + # Usage is within a post-commit hook, using env values. In case of + # commit-confirm, it can be written to a temporary file for + # inclusion on 'confirm'. + from time import time + + if timestamp is None: + timestamp = int(time()) + + if not user: + user = self._get_user() + if not commit_via: + commit_via = os.getenv('COMMIT_VIA', 'other') + if not commit_comment: + commit_comment = os.getenv('COMMIT_COMMENT', 'commit') + + # the commit log reserves '|' as field demarcation, so replace in + # comment if present; undo this in _get_log_entry, below + if re.search(r'\|', commit_comment): + commit_comment = commit_comment.replace('|', '%%') + + entry = f'|{timestamp}|{user}|{commit_via}|{commit_comment}|\n' + + mask = os.umask(0o113) + if tmp_file is not None: + try: + with open(tmp_file, 'w') as f: + f.write(entry) + except OSError as e: + logger.critical(f'write to {tmp_file} failed: {e}') + os.umask(mask) + return None + + os.umask(mask) + return entry + + @staticmethod + def _get_log_entry(line: str) -> dict: + log_fmt = re.compile(r'\|.*\|\n?$') + keys = ['user', 'commit_via', 'commit_comment', 'timestamp'] + if not log_fmt.match(line): + logger.critical(f'Invalid log format {line}') + return {} + + timestamp, user, commit_via, commit_comment = ( + tuple(line.strip().strip('|').split('|'))) + + commit_comment = commit_comment.replace('%%', '|') + d = dict(zip(keys, [user, commit_via, + commit_comment, timestamp])) + + return d + + def _read_tmp_log_entry(self) -> dict: + try: + with open(tmp_log_entry) as f: + entry = f.read() + os.unlink(tmp_log_entry) + except OSError as e: + logger.critical(f'error on file {tmp_log_entry}: {e}') + + return self._get_log_entry(entry) + + def _add_log_entry(self, user: str='', commit_via: str='', + commit_comment: str='', timestamp: Optional[int]=None): + mask = os.umask(0o113) + + entry = self._new_log_entry(user=user, commit_via=commit_via, + commit_comment=commit_comment, + timestamp=timestamp) + + log_entries = self._get_log_entries() + log_entries.insert(0, entry) + if len(log_entries) > self.max_revisions: + log_entries = log_entries[:-1] + + try: + with open(commit_log_file, 'w') as f: + f.writelines(log_entries) + except OSError as e: + logger.critical(e) + + os.umask(mask) + +# entry_point for console script +# +def run(): + from argparse import ArgumentParser, REMAINDER + + config_mgmt = ConfigMgmt() + + for s in list(commit_hooks): + if sys.argv[0].replace('-', '_').endswith(s): + func = getattr(config_mgmt, s) + try: + func() + except Exception as e: + print(f'{s}: {e}') + sys.exit(0) + + parser = ArgumentParser() + subparsers = parser.add_subparsers(dest='subcommand') + + commit_confirm = subparsers.add_parser('commit_confirm', + help="Commit with opt-out reboot to saved config") + commit_confirm.add_argument('-t', dest='minutes', type=int, + default=DEFAULT_TIME_MINUTES, + help="Minutes until reboot, unless 'confirm'") + commit_confirm.add_argument('-y', dest='no_prompt', action='store_true', + help="Execute without prompt") + + subparsers.add_parser('confirm', help="Confirm commit") + subparsers.add_parser('revert', help="Revert commit-confirm") + + rollback = subparsers.add_parser('rollback', + help="Rollback to earlier config") + rollback.add_argument('--rev', type=int, + help="Revision number for rollback") + rollback.add_argument('-y', dest='no_prompt', action='store_true', + help="Excute without prompt") + + compare = subparsers.add_parser('compare', + help="Compare config files") + + compare.add_argument('--saved', action='store_true', + help="Compare session config with saved config") + compare.add_argument('--commands', action='store_true', + help="Show difference between commands") + compare.add_argument('--rev1', type=int, default=None, + help="Compare revision with session config or other revision") + compare.add_argument('--rev2', type=int, default=None, + help="Compare revisions") + + wrap_compare = subparsers.add_parser('wrap_compare', + help="Wrapper interface for vyatta-cfg-run") + wrap_compare.add_argument('--options', nargs=REMAINDER) + + args = vars(parser.parse_args()) + + func = getattr(config_mgmt, args['subcommand']) + del args['subcommand'] + + res = '' + try: + res, rc = func(**args) + except ConfigMgmtError as e: + print(e) + sys.exit(1) + if res: + print(res) + sys.exit(rc) diff --git a/python/vyos/configdep.py b/python/vyos/configdep.py new file mode 100644 index 000000000..d4b2cc78f --- /dev/null +++ b/python/vyos/configdep.py @@ -0,0 +1,95 @@ +# 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/>. + +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 + +# 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('-', '_') + +def canon_name_of_path(path: str) -> str: + script = os.path.basename(path) + return canon_name(script) + +def caller_name() -> str: + return stack()[-1].filename + +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) + + config.set_level([]) + try: + c = mod.get_config(config) + mod.verify(c) + mod.generate(c) + mod.apply(c) + except (VyOSError, ConfigError) as e: + raise ConfigError(repr(e)) + +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_dependents(case: str, config: 'Config', + tagnode: typing.Optional[str] = None): + d = get_dependency_dict(config) + k = canon_name_of_path(caller_name()) + 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 = dependent_func.get(k, []) + while l: + f = l.pop(0) + f() diff --git a/python/vyos/configdiff.py b/python/vyos/configdiff.py index 9185575df..ac86af09c 100644 --- a/python/vyos/configdiff.py +++ b/python/vyos/configdiff.py @@ -78,23 +78,34 @@ def get_config_diff(config, key_mangling=None): isinstance(key_mangling[1], str)): raise ValueError("key_mangling must be a tuple of two strings") - diff_t = DiffTree(config._running_config, config._session_config) + if hasattr(config, 'cached_diff_tree'): + diff_t = getattr(config, 'cached_diff_tree') + else: + diff_t = DiffTree(config._running_config, config._session_config) + setattr(config, 'cached_diff_tree', diff_t) - return ConfigDiff(config, key_mangling, diff_tree=diff_t) + if hasattr(config, 'cached_diff_dict'): + diff_d = getattr(config, 'cached_diff_dict') + else: + diff_d = diff_t.dict + setattr(config, 'cached_diff_dict', diff_d) + + return ConfigDiff(config, key_mangling, diff_tree=diff_t, + diff_dict=diff_d) class ConfigDiff(object): """ The class of config changes as represented by comparison between the session config dict and the effective config dict. """ - def __init__(self, config, key_mangling=None, diff_tree=None): + def __init__(self, config, key_mangling=None, diff_tree=None, diff_dict=None): self._level = config.get_level() self._session_config_dict = config.get_cached_root_dict(effective=False) self._effective_config_dict = config.get_cached_root_dict(effective=True) self._key_mangling = key_mangling self._diff_tree = diff_tree - self._diff_dict = diff_tree.dict if diff_tree else {} + self._diff_dict = diff_dict # mirrored from Config; allow path arguments relative to level def _make_path(self, path): @@ -209,9 +220,9 @@ class ConfigDiff(object): if self._diff_tree is None: raise NotImplementedError("diff_tree class not available") else: - add = get_sub_dict(self._diff_tree.dict, ['add'], get_first_key=True) - sub = get_sub_dict(self._diff_tree.dict, ['sub'], get_first_key=True) - inter = get_sub_dict(self._diff_tree.dict, ['inter'], get_first_key=True) + add = get_sub_dict(self._diff_dict, ['add'], get_first_key=True) + sub = get_sub_dict(self._diff_dict, ['sub'], get_first_key=True) + inter = get_sub_dict(self._diff_dict, ['inter'], get_first_key=True) ret = {} ret[enum_to_key(Diff.MERGE)] = session_dict ret[enum_to_key(Diff.DELETE)] = get_sub_dict(sub, self._make_path(path), @@ -284,9 +295,9 @@ class ConfigDiff(object): if self._diff_tree is None: raise NotImplementedError("diff_tree class not available") else: - add = get_sub_dict(self._diff_tree.dict, ['add'], get_first_key=True) - sub = get_sub_dict(self._diff_tree.dict, ['sub'], get_first_key=True) - inter = get_sub_dict(self._diff_tree.dict, ['inter'], get_first_key=True) + add = get_sub_dict(self._diff_dict, ['add'], get_first_key=True) + sub = get_sub_dict(self._diff_dict, ['sub'], get_first_key=True) + inter = get_sub_dict(self._diff_dict, ['inter'], get_first_key=True) ret = {} ret[enum_to_key(Diff.MERGE)] = session_dict ret[enum_to_key(Diff.DELETE)] = get_sub_dict(sub, self._make_path(path)) diff --git a/python/vyos/configsession.py b/python/vyos/configsession.py index 3a60f6d92..df44fd8d6 100644 --- a/python/vyos/configsession.py +++ b/python/vyos/configsession.py @@ -34,6 +34,8 @@ REMOVE_IMAGE = ['/opt/vyatta/bin/vyatta-boot-image.pl', '--del'] GENERATE = ['/opt/vyatta/bin/vyatta-op-cmd-wrapper', 'generate'] SHOW = ['/opt/vyatta/bin/vyatta-op-cmd-wrapper', 'show'] RESET = ['/opt/vyatta/bin/vyatta-op-cmd-wrapper', 'reset'] +OP_CMD_ADD = ['/opt/vyatta/bin/vyatta-op-cmd-wrapper', 'add'] +OP_CMD_DELETE = ['/opt/vyatta/bin/vyatta-op-cmd-wrapper', 'delete'] # Default "commit via" string APP = "vyos-http-api" @@ -204,3 +206,15 @@ class ConfigSession(object): def reset(self, path): out = self.__run_command(RESET + path) return out + + def add_container_image(self, name): + out = self.__run_command(OP_CMD_ADD + ['container', 'image'] + [name]) + return out + + def delete_container_image(self, name): + out = self.__run_command(OP_CMD_DELETE + ['container', 'image'] + [name]) + return out + + def show_container_image(self): + out = self.__run_command(SHOW + ['container', 'image']) + return out diff --git a/python/vyos/configtree.py b/python/vyos/configtree.py index e9cdb69e4..f2358ee4f 100644 --- a/python/vyos/configtree.py +++ b/python/vyos/configtree.py @@ -1,5 +1,5 @@ # configtree -- a standalone VyOS config file manipulation library (Python bindings) -# Copyright (C) 2018 VyOS maintainers and contributors +# Copyright (C) 2018-2022 VyOS maintainers and contributors # # 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; @@ -12,6 +12,7 @@ # You should have received a copy of the GNU Lesser General Public License along with this library; # if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +import os import re import json @@ -147,6 +148,8 @@ class ConfigTree(object): self.__config = address self.__version = '' + self.__migration = os.environ.get('VYOS_MIGRATION') + def __del__(self): if self.__config is not None: self.__destroy(self.__config) @@ -191,18 +194,27 @@ class ConfigTree(object): else: self.__set_add_value(self.__config, path_str, str(value).encode()) + if self.__migration: + print(f"- op: set path: {path} value: {value} replace: {replace}") + def delete(self, path): check_path(path) path_str = " ".join(map(str, path)).encode() self.__delete(self.__config, path_str) + if self.__migration: + print(f"- op: delete path: {path}") + def delete_value(self, path, value): check_path(path) path_str = " ".join(map(str, path)).encode() self.__delete_value(self.__config, path_str, value.encode()) + if self.__migration: + print(f"- op: delete_value path: {path} value: {value}") + def rename(self, path, new_name): check_path(path) path_str = " ".join(map(str, path)).encode() @@ -216,6 +228,9 @@ class ConfigTree(object): if (res != 0): raise ConfigTreeError("Path [{}] doesn't exist".format(path)) + if self.__migration: + print(f"- op: rename old_path: {path} new_path: {new_path}") + def copy(self, old_path, new_path): check_path(old_path) check_path(new_path) @@ -227,7 +242,11 @@ class ConfigTree(object): raise ConfigTreeError() res = self.__copy(self.__config, oldpath_str, newpath_str) if (res != 0): - raise ConfigTreeError("Path [{}] doesn't exist".format(old_path)) + msg = self.__get_error().decode() + raise ConfigTreeError(msg) + + if self.__migration: + print(f"- op: copy old_path: {old_path} new_path: {new_path}") def exists(self, path): check_path(path) 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/cpu.py b/python/vyos/cpu.py index 488ae79fb..d2e5f6504 100644 --- a/python/vyos/cpu.py +++ b/python/vyos/cpu.py @@ -73,7 +73,7 @@ def _find_physical_cpus(): # On other architectures, e.g. on ARM, there's no such field. # We just assume they are different CPUs, # whether single core ones or cores of physical CPUs. - phys_cpus[num] = cpu[num] + phys_cpus[num] = cpus[num] return phys_cpus diff --git a/python/vyos/ethtool.py b/python/vyos/ethtool.py index 2b6012a73..abf8de688 100644 --- a/python/vyos/ethtool.py +++ b/python/vyos/ethtool.py @@ -56,10 +56,10 @@ class Ethtool: def __init__(self, ifname): # Get driver used for interface - sysfs_file = f'/sys/class/net/{ifname}/device/driver/module' - if os.path.exists(sysfs_file): - link = os.readlink(sysfs_file) - self._driver_name = os.path.basename(link) + out, err = popen(f'ethtool --driver {ifname}') + driver = re.search(r'driver:\s(\w+)', out) + if driver: + self._driver_name = driver.group(1) # Build a dictinary of supported link-speed and dupley settings. out, err = popen(f'ethtool {ifname}') diff --git a/python/vyos/firewall.py b/python/vyos/firewall.py index 4075e55b0..b4b9e67bb 100644 --- a/python/vyos/firewall.py +++ b/python/vyos/firewall.py @@ -20,6 +20,9 @@ import os import re from pathlib import Path +from socket import AF_INET +from socket import AF_INET6 +from socket import getaddrinfo from time import strftime from vyos.remote import download @@ -31,65 +34,31 @@ from vyos.util import dict_search_args from vyos.util import dict_search_recursive from vyos.util import run +# Domain Resolver -# Functions for firewall group domain-groups -def get_ips_domains_dict(list_domains): - """ - Get list of IPv4 addresses by list of domains - Ex: get_ips_domains_dict(['ex1.com', 'ex2.com']) - {'ex1.com': ['192.0.2.1'], 'ex2.com': ['192.0.2.2', '192.0.2.3']} - """ - from socket import gethostbyname_ex - from socket import gaierror - - ip_dict = {} - for domain in list_domains: - try: - _, _, ips = gethostbyname_ex(domain) - ip_dict[domain] = ips - except gaierror: - pass - - return ip_dict - -def nft_init_set(group_name, table="vyos_filter", family="ip"): - """ - table ip vyos_filter { - set GROUP_NAME - type ipv4_addr - flags interval - } - """ - return call(f'nft add set ip {table} {group_name} {{ type ipv4_addr\\; flags interval\\; }}') - - -def nft_add_set_elements(group_name, elements, table="vyos_filter", family="ip"): - """ - table ip vyos_filter { - set GROUP_NAME { - type ipv4_addr - flags interval - elements = { 192.0.2.1, 192.0.2.2 } - } - """ - elements = ", ".join(elements) - return call(f'nft add element {family} {table} {group_name} {{ {elements} }} ') - -def nft_flush_set(group_name, table="vyos_filter", family="ip"): - """ - Flush elements of nft set - """ - return call(f'nft flush set {family} {table} {group_name}') - -def nft_update_set_elements(group_name, elements, table="vyos_filter", family="ip"): - """ - Update elements of nft set - """ - flush_set = nft_flush_set(group_name, table="vyos_filter", family="ip") - nft_add_set = nft_add_set_elements(group_name, elements, table="vyos_filter", family="ip") - return flush_set, nft_add_set - -# END firewall group domain-group (sets) +def fqdn_config_parse(firewall): + firewall['ip_fqdn'] = {} + firewall['ip6_fqdn'] = {} + + for domain, path in dict_search_recursive(firewall, 'fqdn'): + fw_name = path[1] # name/ipv6-name + rule = path[3] # rule id + suffix = path[4][0] # source/destination (1 char) + set_name = f'{fw_name}_{rule}_{suffix}' + + if path[0] == 'name': + firewall['ip_fqdn'][set_name] = domain + elif path[0] == 'ipv6_name': + firewall['ip6_fqdn'][set_name] = domain + +def fqdn_resolve(fqdn, ipv6=False): + try: + res = getaddrinfo(fqdn, None, AF_INET6 if ipv6 else AF_INET) + return set(item[4][0] for item in res) + except: + return None + +# End Domain Resolver def find_nftables_rule(table, chain, rule_matches=[]): # Find rule in table/chain that matches all criteria and return the handle @@ -144,12 +113,26 @@ 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'] + operator = '' + if fqdn[0] == '!': + operator = '!=' + output.append(f'{ip_name} {prefix}addr {operator} @FQDN_{fw_name}_{rule_id}_{prefix}') if dict_search_args(side_conf, 'geoip', 'country_code'): operator = '' @@ -192,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: @@ -249,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': '<'} @@ -327,6 +322,10 @@ def parse_rule(rule_conf, fw_name, rule_id, ip_name): if tcp_mss: output.append(f'tcp option maxseg size {tcp_mss}') + if 'connection_mark' in rule_conf: + conn_mark_str = ','.join(rule_conf['connection_mark']) + output.append(f'ct mark {{{conn_mark_str}}}') + output.append('counter') if 'set' in rule_conf: @@ -373,6 +372,9 @@ def parse_time(time): def parse_policy_set(set_conf, def_suffix): out = [] + if 'connection_mark' in set_conf: + conn_mark = set_conf['connection_mark'] + out.append(f'ct mark set {conn_mark}') if 'dscp' in set_conf: dscp = set_conf['dscp'] out.append(f'ip{def_suffix} dscp set {dscp}') diff --git a/python/vyos/formatversions.py b/python/vyos/formatversions.py deleted file mode 100644 index 29117a5d3..000000000 --- a/python/vyos/formatversions.py +++ /dev/null @@ -1,109 +0,0 @@ -# Copyright 2019 VyOS maintainers and contributors <maintainers@vyos.io> -# -# This library is free software; you can redistribute it and/or -# modify it under the terms of the GNU Lesser General Public -# License as published by the Free Software Foundation; either -# version 2.1 of the License, or (at your option) any later version. -# -# This library is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU -# Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with this library. If not, see <http://www.gnu.org/licenses/>. - -import sys -import os -import re -import fileinput - -def read_vyatta_versions(config_file): - config_file_versions = {} - - with open(config_file, 'r') as config_file_handle: - for config_line in config_file_handle: - if re.match(r'/\* === vyatta-config-version:.+=== \*/$', config_line): - if not re.match(r'/\* === vyatta-config-version:\s+"([\w,-]+@\d+:)+([\w,-]+@\d+)"\s+=== \*/$', config_line): - raise ValueError("malformed configuration string: " - "{}".format(config_line)) - - for pair in re.findall(r'([\w,-]+)@(\d+)', config_line): - config_file_versions[pair[0]] = int(pair[1]) - - - return config_file_versions - -def read_vyos_versions(config_file): - config_file_versions = {} - - with open(config_file, 'r') as config_file_handle: - for config_line in config_file_handle: - if re.match(r'// vyos-config-version:.+', config_line): - if not re.match(r'// vyos-config-version:\s+"([\w,-]+@\d+:)+([\w,-]+@\d+)"\s*', config_line): - raise ValueError("malformed configuration string: " - "{}".format(config_line)) - - for pair in re.findall(r'([\w,-]+)@(\d+)', config_line): - config_file_versions[pair[0]] = int(pair[1]) - - return config_file_versions - -def remove_versions(config_file): - """ - Remove old version string. - """ - for line in fileinput.input(config_file, inplace=True): - if re.match(r'/\* Warning:.+ \*/$', line): - continue - if re.match(r'/\* === vyatta-config-version:.+=== \*/$', line): - continue - if re.match(r'/\* Release version:.+ \*/$', line): - continue - if re.match('// vyos-config-version:.+', line): - continue - if re.match('// Warning:.+', line): - continue - if re.match('// Release version:.+', line): - continue - sys.stdout.write(line) - -def format_versions_string(config_versions): - cfg_keys = list(config_versions.keys()) - cfg_keys.sort() - - component_version_strings = [] - - for key in cfg_keys: - cfg_vers = config_versions[key] - component_version_strings.append('{}@{}'.format(key, cfg_vers)) - - separator = ":" - component_version_string = separator.join(component_version_strings) - - return component_version_string - -def write_vyatta_versions_foot(config_file, component_version_string, - os_version_string): - if config_file: - with open(config_file, 'a') as config_file_handle: - config_file_handle.write('/* Warning: Do not remove the following line. */\n') - config_file_handle.write('/* === vyatta-config-version: "{}" === */\n'.format(component_version_string)) - config_file_handle.write('/* Release version: {} */\n'.format(os_version_string)) - else: - sys.stdout.write('/* Warning: Do not remove the following line. */\n') - sys.stdout.write('/* === vyatta-config-version: "{}" === */\n'.format(component_version_string)) - sys.stdout.write('/* Release version: {} */\n'.format(os_version_string)) - -def write_vyos_versions_foot(config_file, component_version_string, - os_version_string): - if config_file: - with open(config_file, 'a') as config_file_handle: - config_file_handle.write('// Warning: Do not remove the following line.\n') - config_file_handle.write('// vyos-config-version: "{}"\n'.format(component_version_string)) - config_file_handle.write('// Release version: {}\n'.format(os_version_string)) - else: - sys.stdout.write('// Warning: Do not remove the following line.\n') - sys.stdout.write('// vyos-config-version: "{}"\n'.format(component_version_string)) - sys.stdout.write('// Release version: {}\n'.format(os_version_string)) - 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/ethernet.py b/python/vyos/ifconfig/ethernet.py index 519cfc58c..5080144ff 100644 --- a/python/vyos/ifconfig/ethernet.py +++ b/python/vyos/ifconfig/ethernet.py @@ -239,7 +239,7 @@ class EthernetIf(Interface): if not isinstance(state, bool): raise ValueError('Value out of range') - rps_cpus = '0' + rps_cpus = 0 queues = len(glob(f'/sys/class/net/{self.ifname}/queues/rx-*')) if state: # Enable RPS on all available CPUs except CPU0 which we will not @@ -248,10 +248,16 @@ class EthernetIf(Interface): # representation of the CPUs which should participate on RPS, we # can enable more CPUs that are physically present on the system, # Linux will clip that internally! - rps_cpus = 'ffffffff,ffffffff,ffffffff,fffffffe' + rps_cpus = (1 << os.cpu_count()) -1 + + # XXX: we should probably reserve one core when the system is under + # high preasure so we can still have a core left for housekeeping. + # This is done by masking out the lowst bit so CPU0 is spared from + # receive packet steering. + rps_cpus &= ~1 for i in range(0, queues): - self._write_sysfs(f'/sys/class/net/{self.ifname}/queues/rx-{i}/rps_cpus', rps_cpus) + self._write_sysfs(f'/sys/class/net/{self.ifname}/queues/rx-{i}/rps_cpus', f'{rps_cpus:x}') # send bitmask representation as hex string without leading '0x' return True diff --git a/python/vyos/ifconfig/input.py b/python/vyos/ifconfig/input.py index db7d2b6b4..3e5f5790d 100644 --- a/python/vyos/ifconfig/input.py +++ b/python/vyos/ifconfig/input.py @@ -1,4 +1,4 @@ -# Copyright 2020 VyOS maintainers and contributors <maintainers@vyos.io> +# Copyright 2023 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 @@ -17,6 +17,16 @@ from vyos.ifconfig.interface import Interface @Interface.register class InputIf(Interface): + """ + The Intermediate Functional Block (ifb) pseudo network interface acts as a + QoS concentrator for multiple different sources of traffic. Packets from + or to other interfaces have to be redirected to it using the mirred action + in order to be handled, regularly routed traffic will be dropped. This way, + a single stack of qdiscs, classes and filters can be shared between + multiple interfaces. + """ + + iftype = 'ifb' definition = { **Interface.definition, **{ diff --git a/python/vyos/ifconfig/macvlan.py b/python/vyos/ifconfig/macvlan.py index 776014bc3..2266879ec 100644 --- a/python/vyos/ifconfig/macvlan.py +++ b/python/vyos/ifconfig/macvlan.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 @@ -30,10 +30,17 @@ class MACVLANIf(Interface): } def _create(self): + """ + Create MACvlan interface in OS kernel. Interface is administrative + down by default. + """ # please do not change the order when assembling the command cmd = 'ip link add {ifname} link {source_interface} type {type} mode {mode}' self._cmd(cmd.format(**self.config)) + # interface is always A/D down. It needs to be enabled explicitly + self.set_admin_state('down') + def set_mode(self, mode): ifname = self.config['ifname'] cmd = f'ip link set dev {ifname} type macvlan mode {mode}' 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/migrator.py b/python/vyos/migrator.py index c6e3435ca..87c74e1ea 100644 --- a/python/vyos/migrator.py +++ b/python/vyos/migrator.py @@ -1,4 +1,4 @@ -# Copyright 2019 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 @@ -16,11 +16,13 @@ import sys import os import json -import subprocess -import vyos.version +import logging + import vyos.defaults -import vyos.systemversions as systemversions -import vyos.formatversions as formatversions +import vyos.component_version as component_version +from vyos.util import cmd + +log_file = os.path.join(vyos.defaults.directories['config'], 'vyos-migrate.log') class MigratorError(Exception): pass @@ -31,9 +33,21 @@ class Migrator(object): self._force = force self._set_vintage = set_vintage self._config_file_vintage = None - self._log_file = None self._changed = False + def init_logger(self): + self.logger = logging.getLogger(__name__) + self.logger.setLevel(logging.DEBUG) + + # on adding the file handler, allow write permission for cfg_group; + # restore original umask on exit + mask = os.umask(0o113) + fh = logging.FileHandler(log_file) + formatter = logging.Formatter('%(message)s') + fh.setFormatter(formatter) + self.logger.addHandler(fh) + os.umask(mask) + def read_config_file_versions(self): """ Get component versions from config file footer and set vintage; @@ -42,13 +56,13 @@ class Migrator(object): cfg_file = self._config_file component_versions = {} - cfg_versions = formatversions.read_vyatta_versions(cfg_file) + cfg_versions = component_version.from_file(cfg_file, vintage='vyatta') if cfg_versions: self._config_file_vintage = 'vyatta' component_versions = cfg_versions - cfg_versions = formatversions.read_vyos_versions(cfg_file) + cfg_versions = component_version.from_file(cfg_file, vintage='vyos') if cfg_versions: self._config_file_vintage = 'vyos' @@ -70,34 +84,15 @@ class Migrator(object): else: return True - def open_log_file(self): - """ - Open log file for migration, catching any error. - Note that, on boot, migration takes place before the canonical log - directory is created, hence write to the config file directory. - """ - self._log_file = os.path.join(vyos.defaults.directories['config'], - 'vyos-migrate.log') - # on creation, allow write permission for cfg_group; - # restore original umask on exit - mask = os.umask(0o113) - try: - log = open('{0}'.format(self._log_file), 'w') - log.write("List of executed migration scripts:\n") - except Exception as e: - os.umask(mask) - print("Logging error: {0}".format(e)) - return None - - os.umask(mask) - return log - def run_migration_scripts(self, config_file_versions, system_versions): """ Run migration scripts iteratively, until config file version equals system component version. """ - log = self.open_log_file() + os.environ['VYOS_MIGRATION'] = '1' + self.init_logger() + + self.logger.info("List of executed migration scripts:") cfg_versions = config_file_versions sys_versions = system_versions @@ -129,8 +124,9 @@ class Migrator(object): '{}-to-{}'.format(cfg_ver, next_ver)) try: - subprocess.check_call([migrate_script, - self._config_file]) + out = cmd([migrate_script, self._config_file]) + self.logger.info(f'{migrate_script}') + if out: self.logger.info(out) except FileNotFoundError: pass except Exception as err: @@ -138,38 +134,25 @@ class Migrator(object): "".format(migrate_script, err)) sys.exit(1) - if log: - try: - log.write('{0}\n'.format(migrate_script)) - except Exception as e: - print("Error writing log: {0}".format(e)) - cfg_ver = next_ver - rev_versions[key] = cfg_ver - if log: - log.close() - + del os.environ['VYOS_MIGRATION'] return rev_versions def write_config_file_versions(self, cfg_versions): """ Write new versions string. """ - versions_string = formatversions.format_versions_string(cfg_versions) - - os_version_string = vyos.version.get_version() - if self._config_file_vintage == 'vyatta': - formatversions.write_vyatta_versions_foot(self._config_file, - versions_string, - os_version_string) + component_version.write_version_footer(cfg_versions, + self._config_file, + vintage='vyatta') if self._config_file_vintage == 'vyos': - formatversions.write_vyos_versions_foot(self._config_file, - versions_string, - os_version_string) + component_version.write_version_footer(cfg_versions, + self._config_file, + vintage='vyos') def save_json_record(self, component_versions: dict): """ @@ -200,7 +183,7 @@ class Migrator(object): # This will force calling all migration scripts: cfg_versions = {} - sys_versions = systemversions.get_system_component_version() + sys_versions = component_version.from_system() # save system component versions in json file for easy reference self.save_json_record(sys_versions) @@ -216,7 +199,7 @@ class Migrator(object): if not self._changed: return - formatversions.remove_versions(cfg_file) + component_version.remove_footer(cfg_file) self.write_config_file_versions(rev_versions) @@ -237,7 +220,7 @@ class VirtualMigrator(Migrator): if not self._changed: return - formatversions.remove_versions(cfg_file) + component_version.remove_footer(cfg_file) self.write_config_file_versions(cfg_versions) diff --git a/python/vyos/nat.py b/python/vyos/nat.py index 31bbdc386..8a311045a 100644 --- a/python/vyos/nat.py +++ b/python/vyos/nat.py @@ -16,6 +16,8 @@ from vyos.template import is_ip_network from vyos.util import dict_search_args +from vyos.template import bracketize_ipv6 + def parse_nat_rule(rule_conf, rule_id, nat_type, ipv6=False): output = [] @@ -69,6 +71,7 @@ def parse_nat_rule(rule_conf, rule_id, nat_type, ipv6=False): else: translation_output.append('to') if addr: + addr = bracketize_ipv6(addr) translation_output.append(addr) options = [] @@ -85,8 +88,13 @@ def parse_nat_rule(rule_conf, rule_id, nat_type, ipv6=False): translation_str += f' {",".join(options)}' for target in ['source', 'destination']: + if target not in rule_conf: + continue + + side_conf = rule_conf[target] prefix = target[:1] - addr = dict_search_args(rule_conf, target, 'address') + + addr = dict_search_args(side_conf, 'address') if addr and not (ignore_type_addr and target == nat_type): operator = '' if addr[:1] == '!': @@ -94,7 +102,7 @@ def parse_nat_rule(rule_conf, rule_id, nat_type, ipv6=False): addr = addr[1:] output.append(f'{ip_prefix} {prefix}addr {operator} {addr}') - addr_prefix = dict_search_args(rule_conf, target, 'prefix') + addr_prefix = dict_search_args(side_conf, 'prefix') if addr_prefix and ipv6: operator = '' if addr_prefix[:1] == '!': @@ -102,7 +110,7 @@ def parse_nat_rule(rule_conf, rule_id, nat_type, ipv6=False): addr_prefix = addr[1:] output.append(f'ip6 {prefix}addr {operator} {addr_prefix}') - port = dict_search_args(rule_conf, target, 'port') + port = dict_search_args(side_conf, 'port') if port: protocol = rule_conf['protocol'] if protocol == 'tcp_udp': @@ -113,6 +121,51 @@ def parse_nat_rule(rule_conf, rule_id, nat_type, ipv6=False): port = port[1:] output.append(f'{protocol} {prefix}port {operator} {{ {port} }}') + if 'group' in side_conf: + group = side_conf['group'] + if 'address_group' in group and not (ignore_type_addr and target == nat_type): + group_name = group['address_group'] + operator = '' + if group_name[0] == '!': + operator = '!=' + group_name = group_name[1:] + output.append(f'{ip_prefix} {prefix}addr {operator} @A_{group_name}') + # Generate firewall group domain-group + elif 'domain_group' in group and not (ignore_type_addr and target == nat_type): + group_name = group['domain_group'] + operator = '' + if group_name[0] == '!': + operator = '!=' + group_name = group_name[1:] + output.append(f'{ip_prefix} {prefix}addr {operator} @D_{group_name}') + elif 'network_group' in group and not (ignore_type_addr and target == nat_type): + group_name = group['network_group'] + operator = '' + if group_name[0] == '!': + operator = '!=' + group_name = group_name[1:] + output.append(f'{ip_prefix} {prefix}addr {operator} @N_{group_name}') + if 'mac_group' in group: + group_name = group['mac_group'] + operator = '' + if group_name[0] == '!': + operator = '!=' + group_name = group_name[1:] + output.append(f'ether {prefix}addr {operator} @M_{group_name}') + if 'port_group' in group: + proto = rule_conf['protocol'] + group_name = group['port_group'] + + if proto == 'tcp_udp': + proto = 'th' + + operator = '' + if group_name[0] == '!': + operator = '!=' + group_name = group_name[1:] + + output.append(f'{proto} {prefix}port {operator} @P_{group_name}') + output.append('counter') if 'log' in rule_conf: diff --git a/python/vyos/opmode.py b/python/vyos/opmode.py index 7e3545c87..d02ad4de6 100644 --- a/python/vyos/opmode.py +++ b/python/vyos/opmode.py @@ -1,4 +1,4 @@ -# Copyright 2022 VyOS maintainers and contributors <maintainers@vyos.io> +# Copyright 2022-2023 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 @@ -16,11 +16,16 @@ import re import sys import typing +from humps import decamelize class Error(Exception): """ Any error that makes requested operation impossible to complete for reasons unrelated to the user input or script logic. + + This is the base class, scripts should not use it directly + and should raise more specific errors instead, + whenever possible. """ pass @@ -44,15 +49,45 @@ class PermissionDenied(Error): """ pass +class InsufficientResources(Error): + """ Requested operation and its arguments are valid but the system + does not have enough resources (such as drive space or memory) + to complete it. + """ + pass + +class UnsupportedOperation(Error): + """ Requested operation is technically valid but is not implemented yet. """ + pass + +class IncorrectValue(Error): + """ Requested operation is valid, but an argument provided has an + incorrect value, preventing successful completion. + """ + 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 + or errors in underlying software. + """ + pass + def _is_op_mode_function_name(name): - if re.match(r"^(show|clear|reset|restart)", name): + if re.match(r"^(show|clear|reset|restart|add|delete|generate|set)", name): return True else: return False -def _is_show(name): - if re.match(r"^show", name): +def _capture_output(name): + if re.match(r"^(show|generate)", name): return True else: return False @@ -93,6 +128,51 @@ def _get_arg_type(t): else: return t +def _normalize_field_name(name): + # Convert the name to string if it is not + # (in some cases they may be numbers) + name = str(name) + + # Replace all separators with underscores + name = re.sub(r'(\s|[\(\)\[\]\{\}\-\.\,:\"\'\`])+', '_', name) + + # Replace specific characters with textual descriptions + name = re.sub(r'@', '_at_', name) + name = re.sub(r'%', '_percentage_', name) + name = re.sub(r'~', '_tilde_', name) + + # Force all letters to lowercase + name = name.lower() + + # Remove leading and trailing underscores, if any + name = re.sub(r'(^(_+)(?=[^_])|_+$)', '', name) + + # Ensure there are only single underscores + name = re.sub(r'_+', '_', name) + + return name + +def _normalize_dict_field_names(old_dict): + new_dict = {} + + for key in old_dict: + new_key = _normalize_field_name(key) + new_dict[new_key] = _normalize_field_names(old_dict[key]) + + # Sanity check + if len(old_dict) != len(new_dict): + raise InternalError("Dictionary fields do not allow unique normalization") + else: + return new_dict + +def _normalize_field_names(value): + if isinstance(value, dict): + return _normalize_dict_field_names(value) + elif isinstance(value, list): + return list(map(lambda v: _normalize_field_names(v), value)) + else: + return value + def run(module): from argparse import ArgumentParser @@ -134,20 +214,25 @@ def run(module): # it would cause an extra argument error when we pass the dict to a function del args["subcommand"] - # Show commands must always get the "raw" argument, - # but other commands (clear/reset/restart) should not, + # Show and generate commands must always get the "raw" argument, + # but other commands (clear/reset/restart/add/delete) should not, # because they produce no output and it makes no sense for them. - if ("raw" not in args) and _is_show(function_name): + if ("raw" not in args) and _capture_output(function_name): args["raw"] = False - if re.match(r"^show", function_name): - # Show commands are slightly special: + if _capture_output(function_name): + # Show and generate commands are slightly special: # they may return human-formatted output # or a raw dict that we need to serialize in JSON for printing res = func(**args) if not args["raw"]: return res else: + if not isinstance(res, dict) and not isinstance(res, list): + raise InternalError(f"Bare literal is not an acceptable raw output, must be a list or an object.\ + The output was:{res}") + res = decamelize(res) + res = _normalize_field_names(res) from json import dumps return dumps(res, indent=4) else: diff --git a/python/vyos/qos/__init__.py b/python/vyos/qos/__init__.py new file mode 100644 index 000000000..a2980ccde --- /dev/null +++ b/python/vyos/qos/__init__.py @@ -0,0 +1,28 @@ +# 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.qos.base import QoSBase +from vyos.qos.cake import CAKE +from vyos.qos.droptail import DropTail +from vyos.qos.fairqueue import FairQueue +from vyos.qos.fqcodel import FQCodel +from vyos.qos.limiter import Limiter +from vyos.qos.netem import NetEm +from vyos.qos.priority import Priority +from vyos.qos.randomdetect import RandomDetect +from vyos.qos.ratelimiter import RateLimiter +from vyos.qos.roundrobin import RoundRobin +from vyos.qos.trafficshaper import TrafficShaper +from vyos.qos.trafficshaper import TrafficShaperHFSC diff --git a/python/vyos/qos/base.py b/python/vyos/qos/base.py new file mode 100644 index 000000000..28635b5e7 --- /dev/null +++ b/python/vyos/qos/base.py @@ -0,0 +1,282 @@ +# 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/>. + +import os + +from vyos.base import Warning +from vyos.util import cmd +from vyos.util import dict_search +from vyos.util import read_file + +class QoSBase: + _debug = False + _direction = ['egress'] + _parent = 0xffff + + def __init__(self, interface): + if os.path.exists('/tmp/vyos.qos.debug'): + self._debug = True + self._interface = interface + + def _cmd(self, command): + if self._debug: + print(f'DEBUG/QoS: {command}') + return cmd(command) + + def get_direction(self) -> list: + return self._direction + + def _get_class_max_id(self, config) -> int: + if 'class' in config: + tmp = list(config['class'].keys()) + tmp.sort(key=lambda ii: int(ii)) + return tmp[-1] + return None + + def _build_base_qdisc(self, config : dict, cls_id : int): + """ + Add/replace qdisc for every class (also default is a class). This is + a genetic method which need an implementation "per" queue-type. + + This matches the old mapping as defined in Perl here: + https://github.com/vyos/vyatta-cfg-qos/blob/equuleus/lib/Vyatta/Qos/ShaperClass.pm#L223-L229 + """ + queue_type = dict_search('queue_type', config) + default_tc = f'tc qdisc replace dev {self._interface} parent {self._parent}:{cls_id:x}' + + if queue_type == 'priority': + handle = 0x4000 + cls_id + default_tc += f' handle {handle:x}: prio' + self._cmd(default_tc) + + queue_limit = dict_search('queue_limit', config) + for ii in range(1, 4): + tmp = f'tc qdisc replace dev {self._interface} parent {handle:x}:{ii:x} pfifo limit {queue_limit}' + self._cmd(tmp) + + elif queue_type == 'fair-queue': + default_tc += f' sfq' + + tmp = dict_search('queue_limit', config) + if tmp: default_tc += f' limit {tmp}' + + self._cmd(default_tc) + + elif queue_type == 'fq-codel': + default_tc += f' fq_codel' + tmp = dict_search('codel_quantum', config) + if tmp: default_tc += f' quantum {tmp}' + + tmp = dict_search('flows', config) + if tmp: default_tc += f' flows {tmp}' + + tmp = dict_search('interval', config) + if tmp: default_tc += f' interval {tmp}' + + tmp = dict_search('interval', config) + if tmp: default_tc += f' interval {tmp}' + + tmp = dict_search('queue_limit', config) + if tmp: default_tc += f' limit {tmp}' + + tmp = dict_search('target', config) + if tmp: default_tc += f' target {tmp}' + + default_tc += f' noecn' + + self._cmd(default_tc) + + elif queue_type == 'random-detect': + default_tc += f' red' + + self._cmd(default_tc) + + elif queue_type == 'drop-tail': + default_tc += f' pfifo' + + tmp = dict_search('queue_limit', config) + if tmp: default_tc += f' limit {tmp}' + + self._cmd(default_tc) + + def _rate_convert(self, rate) -> int: + rates = { + 'bit' : 1, + 'kbit' : 1000, + 'mbit' : 1000000, + 'gbit' : 1000000000, + 'tbit' : 1000000000000, + } + + if rate == 'auto' or rate.endswith('%'): + speed = read_file(f'/sys/class/net/{self._interface}/speed') + if not speed.isnumeric(): + Warning('Interface speed cannot be determined (assuming 10 Mbit/s)') + speed = 10 + if rate.endswith('%'): + percent = rate.rstrip('%') + speed = int(speed) * int(percent) // 100 + return int(speed) *1000000 # convert to MBit/s + + rate_numeric = int(''.join([n for n in rate if n.isdigit()])) + rate_scale = ''.join([n for n in rate if not n.isdigit()]) + + if int(rate_numeric) <= 0: + raise ValueError(f'{rate_numeric} is not a valid bandwidth <= 0') + + if rate_scale: + return int(rate_numeric * rates[rate_scale]) + else: + # No suffix implies Kbps just as Cisco IOS + return int(rate_numeric * 1000) + + def update(self, config, direction, priority=None): + """ method must be called from derived class after it has completed qdisc setup """ + + if 'class' in config: + for cls, cls_config in config['class'].items(): + self._build_base_qdisc(cls_config, int(cls)) + + if 'match' in cls_config: + for match, match_config in cls_config['match'].items(): + for af in ['ip', 'ipv6']: + # every match criteria has it's tc instance + filter_cmd = f'tc filter replace dev {self._interface} parent {self._parent:x}:' + + if priority: + filter_cmd += f' prio {cls}' + elif 'priority' in cls_config: + prio = cls_config['priority'] + filter_cmd += f' prio {prio}' + + filter_cmd += ' protocol all u32' + + tc_af = af + if af == 'ipv6': + tc_af = 'ip6' + + if af in match_config: + tmp = dict_search(f'{af}.source.address', match_config) + if tmp: filter_cmd += f' match {tc_af} src {tmp}' + + tmp = dict_search(f'{af}.source.port', match_config) + if tmp: filter_cmd += f' match {tc_af} sport {tmp} 0xffff' + + tmp = dict_search(f'{af}.destination.address', match_config) + if tmp: filter_cmd += f' match {tc_af} dst {tmp}' + + tmp = dict_search(f'{af}.destination.port', match_config) + if tmp: filter_cmd += f' match {tc_af} dport {tmp} 0xffff' + + tmp = dict_search(f'{af}.protocol', match_config) + if tmp: filter_cmd += f' match {tc_af} protocol {tmp} 0xff' + + # Will match against total length of an IPv4 packet and + # payload length of an IPv6 packet. + # + # IPv4 : match u16 0x0000 ~MAXLEN at 2 + # IPv6 : match u16 0x0000 ~MAXLEN at 4 + tmp = dict_search(f'{af}.max_length', match_config) + if tmp: + # We need the 16 bit two's complement of the maximum + # packet length + tmp = hex(0xffff & ~int(tmp)) + + if af == 'ip': + filter_cmd += f' match u16 0x0000 {tmp} at 2' + elif af == 'ipv6': + filter_cmd += f' match u16 0x0000 {tmp} at 4' + + # We match against specific TCP flags - we assume the IPv4 + # header length is 20 bytes and assume the IPv6 packet is + # not using extension headers (hence a ip header length of 40 bytes) + # TCP Flags are set on byte 13 of the TCP header. + # IPv4 : match u8 X X at 33 + # IPv6 : match u8 X X at 53 + # with X = 0x02 for SYN and X = 0x10 for ACK + tmp = dict_search(f'{af}.tcp', match_config) + if tmp: + mask = 0 + if 'ack' in tmp: + mask |= 0x10 + if 'syn' in tmp: + mask |= 0x02 + mask = hex(mask) + + if af == 'ip': + filter_cmd += f' match u8 {mask} {mask} at 33' + elif af == 'ipv6': + filter_cmd += f' match u8 {mask} {mask} at 53' + + # The police block allows limiting of the byte or packet rate of + # traffic matched by the filter it is attached to. + # https://man7.org/linux/man-pages/man8/tc-police.8.html + if any(tmp in ['exceed', 'bandwidth', 'burst'] for tmp in cls_config): + filter_cmd += f' action police' + + if 'exceed' in cls_config: + action = cls_config['exceed'] + filter_cmd += f' conform-exceed {action}' + if 'not_exceed' in cls_config: + action = cls_config['not_exceed'] + filter_cmd += f'/{action}' + + if 'bandwidth' in cls_config: + rate = self._rate_convert(cls_config['bandwidth']) + filter_cmd += f' rate {rate}' + + if 'burst' in cls_config: + burst = cls_config['burst'] + filter_cmd += f' burst {burst}' + + cls = int(cls) + filter_cmd += f' flowid {self._parent:x}:{cls:x}' + self._cmd(filter_cmd) + + if 'default' in config: + if 'class' in config: + class_id_max = self._get_class_max_id(config) + default_cls_id = int(class_id_max) +1 + self._build_base_qdisc(config['default'], default_cls_id) + + filter_cmd = f'tc filter replace dev {self._interface} parent {self._parent:x}: ' + filter_cmd += 'prio 255 protocol all basic' + + # The police block allows limiting of the byte or packet rate of + # traffic matched by the filter it is attached to. + # https://man7.org/linux/man-pages/man8/tc-police.8.html + if any(tmp in ['exceed', 'bandwidth', 'burst'] for tmp in config['default']): + filter_cmd += f' action police' + + if 'exceed' in config['default']: + action = config['default']['exceed'] + filter_cmd += f' conform-exceed {action}' + if 'not_exceed' in config['default']: + action = config['default']['not_exceed'] + filter_cmd += f'/{action}' + + if 'bandwidth' in config['default']: + rate = self._rate_convert(config['default']['bandwidth']) + filter_cmd += f' rate {rate}' + + if 'burst' in config['default']: + burst = config['default']['burst'] + filter_cmd += f' burst {burst}' + + if 'class' in config: + filter_cmd += f' flowid {self._parent:x}:{default_cls_id:x}' + + self._cmd(filter_cmd) + diff --git a/python/vyos/qos/cake.py b/python/vyos/qos/cake.py new file mode 100644 index 000000000..a89b1de1e --- /dev/null +++ b/python/vyos/qos/cake.py @@ -0,0 +1,55 @@ +# 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.qos.base import QoSBase + +class CAKE(QoSBase): + _direction = ['egress'] + + # https://man7.org/linux/man-pages/man8/tc-cake.8.html + def update(self, config, direction): + tmp = f'tc qdisc add dev {self._interface} root handle 1: cake {direction}' + if 'bandwidth' in config: + bandwidth = self._rate_convert(config['bandwidth']) + tmp += f' bandwidth {bandwidth}' + + if 'rtt' in config: + rtt = config['rtt'] + tmp += f' rtt {rtt}ms' + + if 'flow_isolation' in config: + if 'blind' in config['flow_isolation']: + tmp += f' flowblind' + if 'dst_host' in config['flow_isolation']: + tmp += f' dsthost' + if 'dual_dst_host' in config['flow_isolation']: + tmp += f' dual-dsthost' + if 'dual_src_host' in config['flow_isolation']: + tmp += f' dual-srchost' + if 'flow' in config['flow_isolation']: + tmp += f' flows' + if 'host' in config['flow_isolation']: + tmp += f' hosts' + if 'nat' in config['flow_isolation']: + tmp += f' nat' + if 'src_host' in config['flow_isolation']: + tmp += f' srchost ' + else: + tmp += f' nonat' + + self._cmd(tmp) + + # call base class + super().update(config, direction) diff --git a/python/vyos/qos/droptail.py b/python/vyos/qos/droptail.py new file mode 100644 index 000000000..427d43d19 --- /dev/null +++ b/python/vyos/qos/droptail.py @@ -0,0 +1,28 @@ +# 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.qos.base import QoSBase + +class DropTail(QoSBase): + # https://man7.org/linux/man-pages/man8/tc-pfifo.8.html + def update(self, config, direction): + tmp = f'tc qdisc add dev {self._interface} root pfifo' + if 'queue_limit' in config: + limit = config["queue_limit"] + tmp += f' limit {limit}' + self._cmd(tmp) + + # call base class + super().update(config, direction) diff --git a/python/vyos/qos/fairqueue.py b/python/vyos/qos/fairqueue.py new file mode 100644 index 000000000..f41d098fb --- /dev/null +++ b/python/vyos/qos/fairqueue.py @@ -0,0 +1,31 @@ +# 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.qos.base import QoSBase + +class FairQueue(QoSBase): + # https://man7.org/linux/man-pages/man8/tc-sfq.8.html + def update(self, config, direction): + tmp = f'tc qdisc add dev {self._interface} root sfq' + + if 'hash_interval' in config: + tmp += f' perturb {config["hash_interval"]}' + if 'queue_limit' in config: + tmp += f' limit {config["queue_limit"]}' + + self._cmd(tmp) + + # call base class + super().update(config, direction) diff --git a/python/vyos/qos/fqcodel.py b/python/vyos/qos/fqcodel.py new file mode 100644 index 000000000..cd2340aa2 --- /dev/null +++ b/python/vyos/qos/fqcodel.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.qos.base import QoSBase + +class FQCodel(QoSBase): + # https://man7.org/linux/man-pages/man8/tc-fq_codel.8.html + def update(self, config, direction): + tmp = f'tc qdisc add dev {self._interface} root fq_codel' + + if 'codel_quantum' in config: + tmp += f' quantum {config["codel_quantum"]}' + if 'flows' in config: + tmp += f' flows {config["flows"]}' + if 'interval' in config: + interval = int(config['interval']) * 1000 + tmp += f' interval {interval}' + if 'queue_limit' in config: + tmp += f' limit {config["queue_limit"]}' + if 'target' in config: + target = int(config['target']) * 1000 + tmp += f' target {target}' + + tmp += f' noecn' + self._cmd(tmp) + + # call base class + super().update(config, direction) diff --git a/python/vyos/qos/limiter.py b/python/vyos/qos/limiter.py new file mode 100644 index 000000000..ace0c0b6c --- /dev/null +++ b/python/vyos/qos/limiter.py @@ -0,0 +1,27 @@ +# 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.qos.base import QoSBase + +class Limiter(QoSBase): + _direction = ['ingress'] + + def update(self, config, direction): + tmp = f'tc qdisc add dev {self._interface} handle {self._parent:x}: {direction}' + self._cmd(tmp) + + # base class must be called last + super().update(config, direction) + diff --git a/python/vyos/qos/netem.py b/python/vyos/qos/netem.py new file mode 100644 index 000000000..8bdef300b --- /dev/null +++ b/python/vyos/qos/netem.py @@ -0,0 +1,53 @@ +# 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.qos.base import QoSBase + +class NetEm(QoSBase): + # https://man7.org/linux/man-pages/man8/tc-netem.8.html + def update(self, config, direction): + tmp = f'tc qdisc add dev {self._interface} root netem' + if 'bandwidth' in config: + rate = self._rate_convert(config["bandwidth"]) + tmp += f' rate {rate}' + + if 'queue_limit' in config: + limit = config["queue_limit"] + tmp += f' limit {limit}' + + if 'delay' in config: + delay = config["delay"] + tmp += f' delay {delay}ms' + + if 'loss' in config: + drop = config["loss"] + tmp += f' drop {drop}%' + + if 'corruption' in config: + corrupt = config["corruption"] + tmp += f' corrupt {corrupt}%' + + if 'reordering' in config: + reorder = config["reordering"] + tmp += f' reorder {reorder}%' + + if 'duplicate' in config: + duplicate = config["duplicate"] + tmp += f' duplicate {duplicate}%' + + self._cmd(tmp) + + # call base class + super().update(config, direction) diff --git a/python/vyos/qos/priority.py b/python/vyos/qos/priority.py new file mode 100644 index 000000000..6d4a60a43 --- /dev/null +++ b/python/vyos/qos/priority.py @@ -0,0 +1,41 @@ +# 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.qos.base import QoSBase +from vyos.util import dict_search + +class Priority(QoSBase): + _parent = 1 + + # https://man7.org/linux/man-pages/man8/tc-prio.8.html + def update(self, config, direction): + if 'class' in config: + class_id_max = self._get_class_max_id(config) + bands = int(class_id_max) +1 + + tmp = f'tc qdisc add dev {self._interface} root handle {self._parent:x}: prio bands {bands} priomap ' \ + f'{class_id_max} {class_id_max} {class_id_max} {class_id_max} ' \ + f'{class_id_max} {class_id_max} {class_id_max} {class_id_max} ' \ + f'{class_id_max} {class_id_max} {class_id_max} {class_id_max} ' \ + f'{class_id_max} {class_id_max} {class_id_max} {class_id_max} ' + self._cmd(tmp) + + for cls in config['class']: + cls = int(cls) + tmp = f'tc qdisc add dev {self._interface} parent {self._parent:x}:{cls:x} pfifo' + self._cmd(tmp) + + # base class must be called last + super().update(config, direction, priority=True) diff --git a/python/vyos/qos/randomdetect.py b/python/vyos/qos/randomdetect.py new file mode 100644 index 000000000..d7d84260f --- /dev/null +++ b/python/vyos/qos/randomdetect.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.qos.base import QoSBase + +class RandomDetect(QoSBase): + _parent = 1 + + # https://man7.org/linux/man-pages/man8/tc.8.html + def update(self, config, direction): + + tmp = f'tc qdisc add dev {self._interface} root handle {self._parent}:0 dsmark indices 8 set_tc_index' + self._cmd(tmp) + + tmp = f'tc filter add dev {self._interface} parent {self._parent}:0 protocol ip prio 1 tcindex mask 0xe0 shift 5' + self._cmd(tmp) + + # Generalized Random Early Detection + handle = self._parent +1 + tmp = f'tc qdisc add dev {self._interface} parent {self._parent}:0 handle {handle}:0 gred setup DPs 8 default 0 grio' + self._cmd(tmp) + + bandwidth = self._rate_convert(config['bandwidth']) + + # set VQ (virtual queue) parameters + for precedence, precedence_config in config['precedence'].items(): + precedence = int(precedence) + avg_pkt = int(precedence_config['average_packet']) + limit = int(precedence_config['queue_limit']) * avg_pkt + min_val = int(precedence_config['minimum_threshold']) * avg_pkt + max_val = int(precedence_config['maximum_threshold']) * avg_pkt + + tmp = f'tc qdisc change dev {self._interface} handle {handle}:0 gred limit {limit} min {min_val} max {max_val} avpkt {avg_pkt} ' + + burst = (2 * int(precedence_config['minimum_threshold']) + int(precedence_config['maximum_threshold'])) // 3 + probability = 1 / int(precedence_config['mark_probability']) + tmp += f'burst {burst} bandwidth {bandwidth} probability {probability} DP {precedence} prio {8 - precedence:x}' + + self._cmd(tmp) + + # call base class + super().update(config, direction) diff --git a/python/vyos/qos/ratelimiter.py b/python/vyos/qos/ratelimiter.py new file mode 100644 index 000000000..a4f80a1be --- /dev/null +++ b/python/vyos/qos/ratelimiter.py @@ -0,0 +1,37 @@ +# 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.qos.base import QoSBase + +class RateLimiter(QoSBase): + # https://man7.org/linux/man-pages/man8/tc-tbf.8.html + def update(self, config, direction): + # call base class + super().update(config, direction) + + tmp = f'tc qdisc add dev {self._interface} root tbf' + if 'bandwidth' in config: + rate = self._rate_convert(config['bandwidth']) + tmp += f' rate {rate}' + + if 'burst' in config: + burst = config['burst'] + tmp += f' burst {burst}' + + if 'latency' in config: + latency = config['latency'] + tmp += f' latency {latency}ms' + + self._cmd(tmp) diff --git a/python/vyos/qos/roundrobin.py b/python/vyos/qos/roundrobin.py new file mode 100644 index 000000000..80814ddfb --- /dev/null +++ b/python/vyos/qos/roundrobin.py @@ -0,0 +1,44 @@ +# 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.qos.base import QoSBase + +class RoundRobin(QoSBase): + _parent = 1 + + # https://man7.org/linux/man-pages/man8/tc-drr.8.html + def update(self, config, direction): + tmp = f'tc qdisc add dev {self._interface} root handle 1: drr' + self._cmd(tmp) + + if 'class' in config: + for cls in config['class']: + cls = int(cls) + tmp = f'tc class replace dev {self._interface} parent 1:1 classid 1:{cls:x} drr' + self._cmd(tmp) + + tmp = f'tc qdisc replace dev {self._interface} parent 1:{cls:x} pfifo' + self._cmd(tmp) + + if 'default' in config: + class_id_max = self._get_class_max_id(config) + default_cls_id = int(class_id_max) +1 + + # class ID via CLI is in range 1-4095, thus 1000 hex = 4096 + tmp = f'tc class replace dev {self._interface} parent 1:1 classid 1:{default_cls_id:x} drr' + self._cmd(tmp) + + # call base class + super().update(config, direction, priority=True) diff --git a/python/vyos/qos/trafficshaper.py b/python/vyos/qos/trafficshaper.py new file mode 100644 index 000000000..f42f4d022 --- /dev/null +++ b/python/vyos/qos/trafficshaper.py @@ -0,0 +1,106 @@ +# 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 math import ceil +from vyos.qos.base import QoSBase + +# Kernel limits on quantum (bytes) +MAXQUANTUM = 200000 +MINQUANTUM = 1000 + +class TrafficShaper(QoSBase): + _parent = 1 + + # https://man7.org/linux/man-pages/man8/tc-htb.8.html + def update(self, config, direction): + class_id_max = 0 + if 'class' in config: + tmp = list(config['class']) + tmp.sort() + class_id_max = tmp[-1] + + r2q = 10 + # bandwidth is a mandatory CLI node + speed = self._rate_convert(config['bandwidth']) + speed_bps = int(speed) // 8 + + # need a bigger r2q if going fast than 16 mbits/sec + if (speed_bps // r2q) >= MAXQUANTUM: # integer division + r2q = ceil(speed_bps // MAXQUANTUM) + else: + # if there is a slow class then may need smaller value + if 'class' in config: + min_speed = speed_bps + for cls, cls_options in config['class'].items(): + # find class with the lowest bandwidth used + if 'bandwidth' in cls_options: + bw_bps = int(self._rate_convert(cls_options['bandwidth'])) // 8 # bandwidth in bytes per second + if bw_bps < min_speed: + min_speed = bw_bps + + while (r2q > 1) and (min_speed // r2q) < MINQUANTUM: + tmp = r2q -1 + if (speed_bps // tmp) >= MAXQUANTUM: + break + r2q = tmp + + + default_minor_id = int(class_id_max) +1 + tmp = f'tc qdisc replace dev {self._interface} root handle {self._parent:x}: htb r2q {r2q} default {default_minor_id:x}' # default is in hex + self._cmd(tmp) + + tmp = f'tc class replace dev {self._interface} parent {self._parent:x}: classid {self._parent:x}:1 htb rate {speed}' + self._cmd(tmp) + + if 'class' in config: + for cls, cls_config in config['class'].items(): + # class id is used later on and passed as hex, thus this needs to be an int + cls = int(cls) + + # bandwidth is a mandatory CLI node + rate = self._rate_convert(cls_config['bandwidth']) + burst = cls_config['burst'] + quantum = cls_config['codel_quantum'] + + tmp = f'tc class replace dev {self._interface} parent {self._parent:x}:1 classid {self._parent:x}:{cls:x} htb rate {rate} burst {burst} quantum {quantum}' + if 'priority' in cls_config: + priority = cls_config['priority'] + tmp += f' prio {priority}' + self._cmd(tmp) + + tmp = f'tc qdisc replace dev {self._interface} parent {self._parent:x}:{cls:x} sfq' + self._cmd(tmp) + + if 'default' in config: + rate = self._rate_convert(config['default']['bandwidth']) + burst = config['default']['burst'] + quantum = config['default']['codel_quantum'] + tmp = f'tc class replace dev {self._interface} parent {self._parent:x}:1 classid {self._parent:x}:{default_minor_id:x} htb rate {rate} burst {burst} quantum {quantum}' + if 'priority' in config['default']: + priority = config['default']['priority'] + tmp += f' prio {priority}' + self._cmd(tmp) + + tmp = f'tc qdisc replace dev {self._interface} parent {self._parent:x}:{default_minor_id:x} sfq' + self._cmd(tmp) + + # call base class + super().update(config, direction) + +class TrafficShaperHFSC(TrafficShaper): + def update(self, config, direction): + # call base class + super().update(config, direction) + diff --git a/python/vyos/systemversions.py b/python/vyos/systemversions.py deleted file mode 100644 index f2da76d4f..000000000 --- a/python/vyos/systemversions.py +++ /dev/null @@ -1,46 +0,0 @@ -# Copyright 2019 VyOS maintainers and contributors <maintainers@vyos.io> -# -# This library is free software; you can redistribute it and/or -# modify it under the terms of the GNU Lesser General Public -# License as published by the Free Software Foundation; either -# version 2.1 of the License, or (at your option) any later version. -# -# This library is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU -# Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with this library. If not, see <http://www.gnu.org/licenses/>. - -import os -import re -import sys -import vyos.defaults -from vyos.xml import component_version - -# legacy version, reading from the file names in -# /opt/vyatta/etc/config-migrate/current -def get_system_versions(): - """ - Get component versions from running system; critical failure if - unable to read migration directory. - """ - system_versions = {} - - try: - version_info = os.listdir(vyos.defaults.directories['current']) - except OSError as err: - print("OS error: {}".format(err)) - sys.exit(1) - - for info in version_info: - if re.match(r'[\w,-]+@\d+', info): - pair = info.split('@') - system_versions[pair[0]] = int(pair[1]) - - return system_versions - -# read from xml cache -def get_system_component_version(): - return component_version() diff --git a/python/vyos/template.py b/python/vyos/template.py index 2a4135f9e..15240f815 100644 --- a/python/vyos/template.py +++ b/python/vyos/template.py @@ -193,6 +193,16 @@ def dot_colon_to_dash(text): text = text.replace(".", "-") return text +@register_filter('generate_uuid4') +def generate_uuid4(text): + """ Generate random unique ID + Example: + % uuid4() + UUID('958ddf6a-ef14-4e81-8cfb-afb12456d1c5') + """ + from uuid import uuid4 + return uuid4() + @register_filter('netmask_from_cidr') def netmask_from_cidr(prefix): """ Take CIDR prefix and convert the prefix length to a "subnet mask". @@ -476,6 +486,8 @@ def get_esp_ike_cipher(group_config, ike_group=None): continue tmp = '{encryption}-{hash}'.format(**proposal) + if 'prf' in proposal: + tmp += '-' + proposal['prf'] if 'dh_group' in proposal: tmp += '-' + pfs_lut[ 'dh-group' + proposal['dh_group'] ] elif 'pfs' in group_config and group_config['pfs'] != 'disable': diff --git a/python/vyos/util.py b/python/vyos/util.py index 461df9a6e..66ded464d 100644 --- a/python/vyos/util.py +++ b/python/vyos/util.py @@ -348,9 +348,11 @@ def colon_separated_to_dict(data_string, uniquekeys=False): l = l.strip() if l: match = re.match(key_value_re, l) - if match: + if match and (len(match.groups()) == 2): key = match.groups()[0].strip() value = match.groups()[1].strip() + else: + raise ValueError(f"""Line "{l}" could not be parsed a colon-separated pair """, l) if key in data.keys(): if uniquekeys: raise ValueError("Data string has duplicate keys: {0}".format(key)) @@ -486,7 +488,7 @@ def is_listen_port_bind_service(port: int, service: str) -> bool: Example: % is_listen_port_bind_service(443, 'nginx') True - % is_listen_port_bind_service(443, 'ocservr-main') + % is_listen_port_bind_service(443, 'ocserv-main') False """ from psutil import net_connections as connections @@ -539,13 +541,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,9 +576,40 @@ 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): + """ Converts a data amount with a unit suffix to bytes, like 2K to 2048 """ + + from re import match as re_match + + res = re_match(r'^\s*(\d+(?:\.\d+)?)\s*([a-zA-Z]+)\s*$', value) + + if not res: + raise ValueError(f"'{value}' is not a valid data amount") + else: + amount = float(res.group(1)) + unit = res.group(2).lower() + + if unit == 'b': + res = amount + elif (unit == 'k') or (unit == 'kb'): + res = amount * 1024 + elif (unit == 'm') or (unit == 'mb'): + res = amount * 1024**2 + elif (unit == 'g') or (unit == 'gb'): + res = amount * 1024**3 + elif (unit == 't') or (unit == 'tb'): + res = amount * 1024**4 + else: + raise ValueError(f"Unsupported data unit '{unit}'") + + # There cannot be fractional bytes, so we convert them to integer. + # However, truncating causes problems with conversion back to human unit, + # so we round instead -- that seems to work well enough. + return round(res) + def get_cfg_group_id(): from grp import getgrnam from vyos.defaults import cfg_group @@ -1105,3 +1141,18 @@ def sysctl_write(name, value): call(f'sysctl -wq {name}={value}') return True return False + +# approach follows a discussion in: +# https://stackoverflow.com/questions/1175208/elegant-python-function-to-convert-camelcase-to-snake-case +def camel_to_snake_case(name: str) -> str: + pattern = r'\d+|[A-Z]?[a-z]+|\W|[A-Z]{2,}(?=[A-Z][a-z]|\d|\W|$)' + words = re.findall(pattern, name) + return '_'.join(map(str.lower, words)) + +def load_as_module(name: str, path: str): + import importlib.util + + spec = importlib.util.spec_from_file_location(name, path) + mod = importlib.util.module_from_spec(spec) + spec.loader.exec_module(mod) + return mod diff --git a/scripts/check-pr-title-and-commit-messages.py b/scripts/check-pr-title-and-commit-messages.py index c30c3ef1f..1d4a3cfe3 100755 --- a/scripts/check-pr-title-and-commit-messages.py +++ b/scripts/check-pr-title-and-commit-messages.py @@ -2,12 +2,13 @@ import re import sys +import time 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): @@ -27,9 +28,17 @@ if __name__ == '__main__': print("Please specify pull request URL!") sys.exit(1) + # There seems to be a race condition that causes this scripts to receive + # an incomplete PR object that is missing certain fields, + # which causes temporary CI failures that require re-running the script + # + # It's probably better to add a small delay to prevent that + time.sleep(5) + # Get the pull request object pr = requests.get(sys.argv[1]).json() if "title" not in pr: + print("The PR object does not have a title field!") print("Did not receive a valid pull request object, please check the URL!") sys.exit(1) diff --git a/smoketest/configs/dialup-router-medium-vpn b/smoketest/configs/dialup-router-medium-vpn index 56722d222..503280017 100644 --- a/smoketest/configs/dialup-router-medium-vpn +++ b/smoketest/configs/dialup-router-medium-vpn @@ -68,9 +68,6 @@ interfaces { mtu 1500 name-server auto password password - traffic-policy { - out shape-17mbit - } user-id vyos password vyos } @@ -96,9 +93,6 @@ interfaces { } smp-affinity auto speed auto - traffic-policy { - out shape-94mbit - } } loopback lo { } @@ -719,24 +713,6 @@ system { } time-zone Pacific/Auckland } -traffic-policy { - shaper shape-17mbit { - bandwidth 17mbit - default { - bandwidth 100% - burst 15k - queue-type fq-codel - } - } - shaper shape-94mbit { - bandwidth 94mbit - default { - bandwidth 100% - burst 15k - queue-type fq-codel - } - } -} /* Warning: Do not remove the following line. */ /* === vyatta-config-version: "broadcast-relay@1:cluster@1:config-management@1:conntrack-sync@1:conntrack@1:dhcp-relay@2:dhcp-server@5:dns-forwarding@1:firewall@5:ipsec@5:l2tp@1:mdns@1:nat@4:ntp@1:pptp@1:qos@1:quagga@6:snmp@1:ssh@1:system@9:vrrp@2:wanloadbalance@3:webgui@1:webproxy@1:webproxy@2:zone-policy@1" === */ /* Release version: 1.2.6 */ diff --git a/smoketest/configs/qos-basic b/smoketest/configs/qos-basic index f94a5650d..c279cbf67 100644 --- a/smoketest/configs/qos-basic +++ b/smoketest/configs/qos-basic @@ -16,19 +16,14 @@ interfaces { traffic-policy { out MY-HTB } - } - loopback lo { - } -} -protocols { - static { - route 0.0.0.0/0 { - next-hop 10.9.9.2 { - } - next-hop 10.1.1.1 { + vif 200 { + traffic-policy { + out foo-emulate } } } + loopback lo { + } } system { config-management { @@ -84,13 +79,14 @@ traffic-policy { class 10 { bandwidth 100% burst 15k - match ADDRESS10 { + match ssh4 { ip { - dscp CS4 + destination { + port 22 + } } } queue-type fair-queue - set-dscp CS5 } default { bandwidth 10mbit @@ -120,7 +116,6 @@ traffic-policy { ceiling 100% match ADDRESS40 { ip { - dscp CS4 source { address 10.2.1.0/24 } @@ -133,12 +128,13 @@ traffic-policy { bandwidth 100% burst 15k match ADDRESS50 { - ip { - dscp CS5 + ipv6 { + source { + address "2001:db8::1/64" + } } } queue-type fair-queue - set-dscp CS7 } default { bandwidth 10% @@ -146,7 +142,6 @@ traffic-policy { ceiling 100% priority 7 queue-type fair-queue - set-dscp CS1 } } shaper FS { @@ -162,7 +157,6 @@ traffic-policy { } } queue-type fair-queue - set-dscp CS4 } class 20 { bandwidth 100% @@ -175,7 +169,6 @@ traffic-policy { } } queue-type fair-queue - set-dscp CS5 } class 30 { bandwidth 100% @@ -188,7 +181,6 @@ traffic-policy { } } queue-type fair-queue - set-dscp CS6 } default { bandwidth 10% @@ -198,6 +190,10 @@ traffic-policy { queue-type fair-queue } } + network-emulator foo-emulate { + bandwidth 300mbit + burst 20000 + } } // Warning: Do not remove the following line. // vyos-config-version: "broadcast-relay@1:cluster@1:config-management@1:conntrack@3:conntrack-sync@2:dhcp-relay@2:dhcp-server@6:dhcpv6-server@1:dns-forwarding@3:firewall@5:https@2:interfaces@22:ipoe-server@1:ipsec@5:isis@1:l2tp@3:lldp@1:mdns@1:nat@5:ntp@1:pppoe-server@5:pptp@2:qos@1:quagga@8:rpki@1:salt@1:snmp@2:ssh@2:sstp@3:system@21:vrrp@2:vyos-accel-ppp@2:wanloadbalance@3:webproxy@2:zone-policy@1" diff --git a/smoketest/scripts/cli/test_component_version.py b/smoketest/scripts/cli/test_component_version.py index 1355c1f94..7b1b12c53 100755 --- a/smoketest/scripts/cli/test_component_version.py +++ b/smoketest/scripts/cli/test_component_version.py @@ -16,7 +16,7 @@ import unittest -from vyos.systemversions import get_system_versions, get_system_component_version +import vyos.component_version as component_version # After T3474, component versions should be updated in the files in # vyos-1x/interface-definitions/include/version/ @@ -24,8 +24,8 @@ from vyos.systemversions import get_system_versions, get_system_component_versio # that in the xml cache. class TestComponentVersion(unittest.TestCase): def setUp(self): - self.legacy_d = get_system_versions() - self.xml_d = get_system_component_version() + self.legacy_d = component_version.legacy_from_system() + self.xml_d = component_version.from_system() self.set_legacy_d = set(self.legacy_d) self.set_xml_d = set(self.xml_d) diff --git a/smoketest/scripts/cli/test_container.py b/smoketest/scripts/cli/test_container.py index cc0cdaec0..902156ee6 100644..100755 --- a/smoketest/scripts/cli/test_container.py +++ b/smoketest/scripts/cli/test_container.py @@ -15,6 +15,7 @@ # along with this program. If not, see <http://www.gnu.org/licenses/>. import unittest +import glob import json from base_vyostest_shim import VyOSUnitTestSHIM @@ -25,10 +26,13 @@ from vyos.util import process_named_running from vyos.util import read_file base_path = ['container'] -cont_image = 'busybox' +cont_image = 'busybox:stable' # busybox is included in vyos-build prefix = '192.168.205.0/24' net_name = 'NET01' -PROCESS_NAME = 'podman' +PROCESS_NAME = 'conmon' +PROCESS_PIDFILE = '/run/vyos-container-{0}.service.pid' + +busybox_image_path = '/usr/share/vyos/busybox-stable.tar' def cmd_to_json(command): c = cmd(command + ' --format=json') @@ -37,7 +41,34 @@ def cmd_to_json(command): return data -class TesContainer(VyOSUnitTestSHIM.TestCase): +class TestContainer(VyOSUnitTestSHIM.TestCase): + @classmethod + def setUpClass(cls): + super(TestContainer, cls).setUpClass() + + # Load image for smoketest provided in vyos-build + try: + cmd(f'cat {busybox_image_path} | sudo podman load') + except: + cls.skipTest(cls, reason='busybox image not available') + + @classmethod + def tearDownClass(cls): + super(TestContainer, cls).tearDownClass() + + # Cleanup podman image + cmd(f'sudo podman image rm -f {cont_image}') + + def tearDown(self): + self.cli_delete(base_path) + self.cli_commit() + + # Ensure no container process remains + self.assertIsNone(process_named_running(PROCESS_NAME)) + + # Ensure systemd units are removed + units = glob.glob('/run/systemd/system/vyos-container-*') + self.assertEqual(units, []) def test_01_basic_container(self): cont_name = 'c1' @@ -53,13 +84,17 @@ class TesContainer(VyOSUnitTestSHIM.TestCase): # commit changes self.cli_commit() + pid = 0 + with open(PROCESS_PIDFILE.format(cont_name), 'r') as f: + pid = int(f.read()) + # Check for running process - self.assertTrue(process_named_running(PROCESS_NAME)) + self.assertEqual(process_named_running(PROCESS_NAME), pid) def test_02_container_network(self): cont_name = 'c2' cont_ip = '192.168.205.25' - self.cli_set(base_path + ['network', net_name, 'ipv4-prefix', prefix]) + self.cli_set(base_path + ['network', net_name, 'prefix', prefix]) self.cli_set(base_path + ['name', cont_name, 'image', cont_image]) self.cli_set(base_path + ['name', cont_name, 'network', net_name, 'address', cont_ip]) @@ -67,7 +102,7 @@ class TesContainer(VyOSUnitTestSHIM.TestCase): self.cli_commit() n = cmd_to_json(f'sudo podman network inspect {net_name}') - json_subnet = n['plugins'][0]['ipam']['ranges'][0][0]['subnet'] + json_subnet = n['subnets'][0]['subnet'] c = cmd_to_json(f'sudo podman container inspect {cont_name}') json_ip = c['NetworkSettings']['Networks'][net_name]['IPAddress'] 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 821925bcd..f1c18d761 100755 --- a/smoketest/scripts/cli/test_firewall.py +++ b/smoketest/scripts/cli/test_firewall.py @@ -17,11 +17,13 @@ import unittest from glob import glob +from time import sleep from base_vyostest_shim import VyOSUnitTestSHIM from vyos.configsession import ConfigSessionError from vyos.util import cmd +from vyos.util import run sysfs_config = { 'all_ping': {'sysfs': '/proc/sys/net/ipv4/icmp_echo_ignore_all', 'default': '0', 'test_value': 'disable'}, @@ -76,6 +78,17 @@ class TestFirewall(VyOSUnitTestSHIM.TestCase): break self.assertTrue(not matched if inverse else matched, msg=search) + def wait_for_domain_resolver(self, table, set_name, element, max_wait=10): + # Resolver no longer blocks commit, need to wait for daemon to populate set + count = 0 + while count < max_wait: + code = run(f'sudo nft get element {table} {set_name} {{ {element} }}') + if code == 0: + return True + count += 1 + sleep(1) + return False + def test_geoip(self): self.cli_set(['firewall', 'name', 'smoketest', 'rule', '1', 'action', 'drop']) self.cli_set(['firewall', 'name', 'smoketest', 'rule', '1', 'source', 'geoip', 'country-code', 'se']) @@ -111,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']) @@ -121,10 +136,15 @@ 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']) self.cli_commit() + + self.wait_for_domain_resolver('ip vyos_filter', 'D_smoketest_domain', '192.0.2.5') + nftables_search = [ ['iifname "eth0"', 'jump NAME_smoketest'], ['ip saddr @N_smoketest_network', 'ip daddr 172.16.10.10', 'th dport @P_smoketest_port', 'return'], @@ -135,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') @@ -178,6 +199,7 @@ class TestFirewall(VyOSUnitTestSHIM.TestCase): name = 'smoketest' interface = 'eth0' mss_range = '501-1460' + conn_mark = '555' self.cli_set(['firewall', 'name', name, 'default-action', 'drop']) self.cli_set(['firewall', 'name', name, 'enable-default-log']) @@ -209,15 +231,18 @@ 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', 'name', name, 'rule', '6', 'connection-mark', conn_mark]) self.cli_set(['firewall', 'interface', interface, 'in', 'name', name]) self.cli_commit() + mark_hex = "{0:#010x}".format(int(conn_mark)) + nftables_search = [ [f'iifname "{interface}"', f'jump NAME_{name}'], ['saddr 172.16.20.10', 'daddr 172.16.10.10', 'log prefix "[smoketest-1-A]" level debug', 'ip ttl 15', 'return'], @@ -226,7 +251,7 @@ class TestFirewall(VyOSUnitTestSHIM.TestCase): ['log prefix "[smoketest-default-D]"','smoketest default-action', 'drop'], ['tcp dport 22', 'add @RECENT_smoketest_4 { ip saddr limit rate over 10/minute burst 10 packets }', 'drop'], ['tcp flags & syn == syn', f'tcp option maxseg size {mss_range}', f'iifname "{interface}"'], - ['meta l4proto gre', f'oifname "{interface}"', 'return'] + ['meta l4proto gre', f'oifname "{interface}"', f'ct mark {mark_hex}', 'return'] ] self.verify_nftables(nftables_search, 'ip vyos_filter') @@ -274,6 +299,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' @@ -290,11 +349,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]) @@ -353,6 +412,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/smoketest/scripts/cli/test_ha_vrrp.py b/smoketest/scripts/cli/test_ha_vrrp.py index 68905e447..3a4de2d8d 100755 --- a/smoketest/scripts/cli/test_ha_vrrp.py +++ b/smoketest/scripts/cli/test_ha_vrrp.py @@ -87,11 +87,21 @@ class TestVRRP(VyOSUnitTestSHIM.TestCase): advertise_interval = '77' priority = '123' preempt_delay = '400' + startup_delay = '120' + garp_master_delay = '2' + garp_master_repeat = '3' + garp_master_refresh = '4' + garp_master_refresh_repeat = '5' + garp_interval = '1.5' + group_garp_master_delay = '12' + group_garp_master_repeat = '13' + group_garp_master_refresh = '14' for group in groups: vlan_id = group.lstrip('VLAN') vip = f'100.64.{vlan_id}.1/24' group_base = base_path + ['vrrp', 'group', group] + global_param_base = base_path + ['vrrp', 'global-parameters'] self.cli_set(['interfaces', 'ethernet', vrrp_interface, 'vif', vlan_id, 'address', inc_ip(vip, 1) + '/' + vip.split('/')[-1]]) @@ -110,9 +120,32 @@ class TestVRRP(VyOSUnitTestSHIM.TestCase): self.cli_set(group_base + ['authentication', 'type', 'plaintext-password']) self.cli_set(group_base + ['authentication', 'password', f'{group}']) + # GARP + self.cli_set(group_base + ['garp', 'master-delay', group_garp_master_delay]) + self.cli_set(group_base + ['garp', 'master-repeat', group_garp_master_repeat]) + self.cli_set(group_base + ['garp', 'master-refresh', group_garp_master_refresh]) + + # Global parameters + #config = getConfig(f'global_defs') + self.cli_set(global_param_base + ['startup-delay', f'{startup_delay}']) + self.cli_set(global_param_base + ['garp', 'interval', f'{garp_interval}']) + self.cli_set(global_param_base + ['garp', 'master-delay', f'{garp_master_delay}']) + self.cli_set(global_param_base + ['garp', 'master-repeat', f'{garp_master_repeat}']) + self.cli_set(global_param_base + ['garp', 'master-refresh', f'{garp_master_refresh}']) + self.cli_set(global_param_base + ['garp', 'master-refresh-repeat', f'{garp_master_refresh_repeat}']) + # commit changes self.cli_commit() + # Check Global parameters + config = getConfig(f'global_defs') + self.assertIn(f'vrrp_startup_delay {startup_delay}', config) + self.assertIn(f'vrrp_garp_interval {garp_interval}', config) + self.assertIn(f'vrrp_garp_master_delay {garp_master_delay}', config) + self.assertIn(f'vrrp_garp_master_repeat {garp_master_repeat}', config) + self.assertIn(f'vrrp_garp_master_refresh {garp_master_refresh}', config) + self.assertIn(f'vrrp_garp_master_refresh_repeat {garp_master_refresh_repeat}', config) + for group in groups: vlan_id = group.lstrip('VLAN') vip = f'100.64.{vlan_id}.1/24' @@ -132,6 +165,11 @@ class TestVRRP(VyOSUnitTestSHIM.TestCase): self.assertIn(f'auth_pass "{group}"', config) self.assertIn(f'auth_type PASS', config) + #GARP + self.assertIn(f'garp_master_delay {group_garp_master_delay}', config) + self.assertIn(f'garp_master_refresh {group_garp_master_refresh}', config) + self.assertIn(f'garp_master_repeat {group_garp_master_repeat}', config) + def test_03_sync_group(self): sync_group = 'VyOS' diff --git a/smoketest/scripts/cli/test_interfaces_dummy.py b/smoketest/scripts/cli/test_interfaces_dummy.py index d96ec2c5d..a79e4cb1b 100755 --- a/smoketest/scripts/cli/test_interfaces_dummy.py +++ b/smoketest/scripts/cli/test_interfaces_dummy.py @@ -21,6 +21,7 @@ from base_interfaces_test import BasicInterfaceTest class DummyInterfaceTest(BasicInterfaceTest.TestCase): @classmethod def setUpClass(cls): + cls._test_mtu = True cls._base_path = ['interfaces', 'dummy'] cls._interfaces = ['dum435', 'dum8677', 'dum0931', 'dum089'] # call base-classes classmethod diff --git a/smoketest/scripts/cli/test_interfaces_ethernet.py b/smoketest/scripts/cli/test_interfaces_ethernet.py index ed611062a..e53413f0d 100755 --- a/smoketest/scripts/cli/test_interfaces_ethernet.py +++ b/smoketest/scripts/cli/test_interfaces_ethernet.py @@ -160,7 +160,7 @@ class EthernetInterfaceTest(BasicInterfaceTest.TestCase): self.assertFalse(is_intf_addr_assigned(intf, addr['addr'])) def test_offloading_rps(self): - # enable RPS on all available CPUs, RPS works woth a CPU bitmask, + # enable RPS on all available CPUs, RPS works with a CPU bitmask, # where each bit represents a CPU (core/thread). The formula below # expands to rps_cpus = 255 for a 8 core system rps_cpus = (1 << os.cpu_count()) -1 diff --git a/smoketest/scripts/cli/test_interfaces_input.py b/smoketest/scripts/cli/test_interfaces_input.py new file mode 100755 index 000000000..c6d7febec --- /dev/null +++ b/smoketest/scripts/cli/test_interfaces_input.py @@ -0,0 +1,52 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2023 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +import unittest + +from vyos.util import read_file +from vyos.ifconfig import Interface +from base_vyostest_shim import VyOSUnitTestSHIM + +base_path = ['interfaces', 'input'] + +# add a classmethod to setup a temporaray PPPoE server for "proper" validation +class InputInterfaceTest(VyOSUnitTestSHIM.TestCase): + @classmethod + def setUpClass(cls): + super(InputInterfaceTest, cls).setUpClass() + + cls._interfaces = ['ifb10', 'ifb20', 'ifb30'] + + def tearDown(self): + self.cli_delete(base_path) + self.cli_commit() + + def test_01_description(self): + # Check if PPPoE dialer can be configured and runs + for interface in self._interfaces: + self.cli_set(base_path + [interface, 'description', f'foo-{interface}']) + + # commit changes + self.cli_commit() + + # Validate remove interface description "empty" + for interface in self._interfaces: + tmp = read_file(f'/sys/class/net/{interface}/ifalias') + self.assertEqual(tmp, f'foo-{interface}') + self.assertEqual(Interface(interface).get_alias(), f'foo-{interface}') + +if __name__ == '__main__': + unittest.main(verbosity=2) diff --git a/smoketest/scripts/cli/test_interfaces_pppoe.py b/smoketest/scripts/cli/test_interfaces_pppoe.py index 8927121a8..08b7f2f46 100755 --- a/smoketest/scripts/cli/test_interfaces_pppoe.py +++ b/smoketest/scripts/cli/test_interfaces_pppoe.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2019-2022 VyOS maintainers and contributors +# Copyright (C) 2019-2023 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 @@ -57,8 +57,8 @@ class PPPoEInterfaceTest(VyOSUnitTestSHIM.TestCase): def test_01_pppoe_client(self): # Check if PPPoE dialer can be configured and runs for interface in self._interfaces: - user = 'VyOS-user-' + interface - passwd = 'VyOS-passwd-' + interface + user = f'VyOS-user-{interface}' + passwd = f'VyOS-passwd-{interface}' mtu = '1400' self.cli_set(base_path + [interface, 'authentication', 'user', user]) @@ -76,23 +76,26 @@ class PPPoEInterfaceTest(VyOSUnitTestSHIM.TestCase): # verify configuration file(s) for interface in self._interfaces: - user = 'VyOS-user-' + interface - password = 'VyOS-passwd-' + interface + user = f'VyOS-user-{interface}' + passwd = f'VyOS-passwd-{interface}' tmp = get_config_value(interface, 'mtu')[1] self.assertEqual(tmp, mtu) tmp = get_config_value(interface, 'user')[1].replace('"', '') self.assertEqual(tmp, user) tmp = get_config_value(interface, 'password')[1].replace('"', '') - self.assertEqual(tmp, password) + self.assertEqual(tmp, passwd) tmp = get_config_value(interface, 'ifname')[1] self.assertEqual(tmp, interface) def test_02_pppoe_client_disabled_interface(self): # Check if PPPoE Client can be disabled for interface in self._interfaces: - self.cli_set(base_path + [interface, 'authentication', 'user', 'vyos']) - self.cli_set(base_path + [interface, 'authentication', 'password', 'vyos']) + user = f'VyOS-user-{interface}' + passwd = f'VyOS-passwd-{interface}' + + self.cli_set(base_path + [interface, 'authentication', 'user', user]) + self.cli_set(base_path + [interface, 'authentication', 'password', passwd]) self.cli_set(base_path + [interface, 'source-interface', self._source_interface]) self.cli_set(base_path + [interface, 'disable']) @@ -117,7 +120,10 @@ class PPPoEInterfaceTest(VyOSUnitTestSHIM.TestCase): def test_03_pppoe_authentication(self): # When username or password is set - so must be the other for interface in self._interfaces: - self.cli_set(base_path + [interface, 'authentication', 'user', 'vyos']) + user = f'VyOS-user-{interface}' + passwd = f'VyOS-passwd-{interface}' + + self.cli_set(base_path + [interface, 'authentication', 'user', user]) self.cli_set(base_path + [interface, 'source-interface', self._source_interface]) self.cli_set(base_path + [interface, 'ipv6', 'address', 'autoconf']) @@ -125,7 +131,7 @@ class PPPoEInterfaceTest(VyOSUnitTestSHIM.TestCase): with self.assertRaises(ConfigSessionError): self.cli_commit() - self.cli_set(base_path + [interface, 'authentication', 'password', 'vyos']) + self.cli_set(base_path + [interface, 'authentication', 'password', passwd]) self.cli_commit() @@ -136,8 +142,11 @@ class PPPoEInterfaceTest(VyOSUnitTestSHIM.TestCase): sla_len = '8' for interface in self._interfaces: - self.cli_set(base_path + [interface, 'authentication', 'user', 'vyos']) - self.cli_set(base_path + [interface, 'authentication', 'password', 'vyos']) + user = f'VyOS-user-{interface}' + passwd = f'VyOS-passwd-{interface}' + + self.cli_set(base_path + [interface, 'authentication', 'user', user]) + self.cli_set(base_path + [interface, 'authentication', 'password', passwd]) self.cli_set(base_path + [interface, 'no-default-route']) self.cli_set(base_path + [interface, 'no-peer-dns']) self.cli_set(base_path + [interface, 'source-interface', self._source_interface]) @@ -149,18 +158,54 @@ class PPPoEInterfaceTest(VyOSUnitTestSHIM.TestCase): self.cli_set(dhcpv6_pd_base + ['interface', self._source_interface, 'address', address]) self.cli_set(dhcpv6_pd_base + ['interface', self._source_interface, 'sla-id', sla_id]) - # commit changes - self.cli_commit() + # commit changes + self.cli_commit() + + for interface in self._interfaces: + user = f'VyOS-user-{interface}' + passwd = f'VyOS-passwd-{interface}' # verify "normal" PPPoE value - 1492 is default MTU tmp = get_config_value(interface, 'mtu')[1] self.assertEqual(tmp, '1492') tmp = get_config_value(interface, 'user')[1].replace('"', '') - self.assertEqual(tmp, 'vyos') + self.assertEqual(tmp, user) tmp = get_config_value(interface, 'password')[1].replace('"', '') - self.assertEqual(tmp, 'vyos') + self.assertEqual(tmp, passwd) tmp = get_config_value(interface, '+ipv6 ipv6cp-use-ipaddr') self.assertListEqual(tmp, ['+ipv6', 'ipv6cp-use-ipaddr']) + def test_05_pppoe_options(self): + # Check if PPPoE dialer can be configured with DHCPv6-PD + for interface in self._interfaces: + user = f'VyOS-user-{interface}' + passwd = f'VyOS-passwd-{interface}' + ac_name = f'AC{interface}' + service_name = f'SRV{interface}' + host_uniq = 'cafebeefBABE123456' + + self.cli_set(base_path + [interface, 'authentication', 'user', user]) + self.cli_set(base_path + [interface, 'authentication', 'password', passwd]) + self.cli_set(base_path + [interface, 'source-interface', self._source_interface]) + + self.cli_set(base_path + [interface, 'access-concentrator', ac_name]) + self.cli_set(base_path + [interface, 'service-name', service_name]) + self.cli_set(base_path + [interface, 'host-uniq', host_uniq]) + + # commit changes + self.cli_commit() + + for interface in self._interfaces: + ac_name = f'AC{interface}' + service_name = f'SRV{interface}' + host_uniq = 'cafebeefBABE123456' + + tmp = get_config_value(interface, 'pppoe-ac')[1] + self.assertEqual(tmp, f'"{ac_name}"') + tmp = get_config_value(interface, 'pppoe-service')[1] + self.assertEqual(tmp, f'"{service_name}"') + tmp = get_config_value(interface, 'pppoe-host-uniq')[1] + self.assertEqual(tmp, f'"{host_uniq}"') + if __name__ == '__main__': unittest.main(verbosity=2) 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_interfaces_wireguard.py b/smoketest/scripts/cli/test_interfaces_wireguard.py index f3e9670f7..14fc8d109 100755 --- a/smoketest/scripts/cli/test_interfaces_wireguard.py +++ b/smoketest/scripts/cli/test_interfaces_wireguard.py @@ -62,10 +62,10 @@ class WireGuardInterfaceTest(VyOSUnitTestSHIM.TestCase): self.assertTrue(os.path.isdir(f'/sys/class/net/{intf}')) - def test_wireguard_add_remove_peer(self): # T2939: Create WireGuard interfaces with associated peers. # Remove one of the configured peers. + # T4774: Test prevention of duplicate peer public keys interface = 'wg0' port = '12345' privkey = '6ISOkASm6VhHOOSz/5iIxw+Q9adq9zA17iMM4X40dlc=' @@ -80,11 +80,17 @@ class WireGuardInterfaceTest(VyOSUnitTestSHIM.TestCase): self.cli_set(base_path + [interface, 'peer', 'PEER01', 'allowed-ips', '10.205.212.10/32']) self.cli_set(base_path + [interface, 'peer', 'PEER01', 'address', '192.0.2.1']) - self.cli_set(base_path + [interface, 'peer', 'PEER02', 'public-key', pubkey_2]) + self.cli_set(base_path + [interface, 'peer', 'PEER02', 'public-key', pubkey_1]) self.cli_set(base_path + [interface, 'peer', 'PEER02', 'port', port]) self.cli_set(base_path + [interface, 'peer', 'PEER02', 'allowed-ips', '10.205.212.11/32']) self.cli_set(base_path + [interface, 'peer', 'PEER02', 'address', '192.0.2.2']) + # Duplicate pubkey_1 + with self.assertRaises(ConfigSessionError): + self.cli_commit() + + self.cli_set(base_path + [interface, 'peer', 'PEER02', 'public-key', pubkey_2]) + # Commit peers self.cli_commit() diff --git a/smoketest/scripts/cli/test_load_balancning_wan.py b/smoketest/scripts/cli/test_load_balancing_wan.py index 23020b9b1..33c69c595 100755 --- a/smoketest/scripts/cli/test_load_balancning_wan.py +++ b/smoketest/scripts/cli/test_load_balancing_wan.py @@ -46,7 +46,6 @@ def cmd_in_netns(netns, cmd): def delete_netns(name): return call(f'sudo ip netns del {name}') - class TestLoadBalancingWan(VyOSUnitTestSHIM.TestCase): @classmethod def setUpClass(cls): @@ -61,7 +60,6 @@ class TestLoadBalancingWan(VyOSUnitTestSHIM.TestCase): self.cli_commit() def test_table_routes(self): - ns1 = 'ns201' ns2 = 'ns202' ns3 = 'ns203' @@ -79,6 +77,7 @@ class TestLoadBalancingWan(VyOSUnitTestSHIM.TestCase): create_veth_pair(iface1, container_iface1) create_veth_pair(iface2, container_iface2) create_veth_pair(iface3, container_iface3) + move_interface_to_netns(container_iface1, ns1) move_interface_to_netns(container_iface2, ns2) move_interface_to_netns(container_iface3, ns3) @@ -125,7 +124,7 @@ class TestLoadBalancingWan(VyOSUnitTestSHIM.TestCase): self.assertEqual(tmp, original) # Delete veth interfaces and netns - for iface in [iface1, iface2]: + for iface in [iface1, iface2, iface3, container_iface1, container_iface2, container_iface3]: call(f'sudo ip link del dev {iface}') delete_netns(ns1) @@ -196,9 +195,10 @@ class TestLoadBalancingWan(VyOSUnitTestSHIM.TestCase): call(f'sudo ip address add 203.0.113.10/24 dev {iface1}') call(f'sudo ip address add 192.0.2.10/24 dev {iface2}') call(f'sudo ip address add 198.51.100.10/24 dev {iface3}') - call(f'sudo ip link set dev {iface1} up') - call(f'sudo ip link set dev {iface2} up') - call(f'sudo ip link set dev {iface3} up') + + for iface in [iface1, iface2, iface3]: + call(f'sudo ip link set dev {iface} up') + cmd_in_netns(ns1, f'ip link set {container_iface1} name eth0') cmd_in_netns(ns2, f'ip link set {container_iface2} name eth0') cmd_in_netns(ns3, f'ip link set {container_iface3} name eth0') @@ -247,12 +247,11 @@ class TestLoadBalancingWan(VyOSUnitTestSHIM.TestCase): self.assertEqual(tmp, nat_vyos_pre_snat_hook) # Delete veth interfaces and netns - for iface in [iface1, iface2]: + for iface in [iface1, iface2, iface3, container_iface1, container_iface2, container_iface3]: call(f'sudo ip link del dev {iface}') delete_netns(ns1) delete_netns(ns2) - if __name__ == '__main__': unittest.main(verbosity=2) diff --git a/smoketest/scripts/cli/test_nat.py b/smoketest/scripts/cli/test_nat.py index f824838c0..9f4e3b831 100755 --- a/smoketest/scripts/cli/test_nat.py +++ b/smoketest/scripts/cli/test_nat.py @@ -16,6 +16,7 @@ import jmespath import json +import os import unittest from base_vyostest_shim import VyOSUnitTestSHIM @@ -28,6 +29,9 @@ src_path = base_path + ['source'] dst_path = base_path + ['destination'] static_path = base_path + ['static'] +nftables_nat_config = '/run/nftables_nat.conf' +nftables_static_nat_conf = '/run/nftables_static-nat-rules.nft' + class TestNAT(VyOSUnitTestSHIM.TestCase): @classmethod def setUpClass(cls): @@ -40,6 +44,8 @@ class TestNAT(VyOSUnitTestSHIM.TestCase): def tearDown(self): self.cli_delete(base_path) self.cli_commit() + self.assertFalse(os.path.exists(nftables_nat_config)) + self.assertFalse(os.path.exists(nftables_static_nat_conf)) def verify_nftables(self, nftables_search, table, inverse=False, args=''): nftables_output = cmd(f'sudo nft {args} list table {table}') @@ -52,6 +58,17 @@ class TestNAT(VyOSUnitTestSHIM.TestCase): break self.assertTrue(not matched if inverse else matched, msg=search) + def wait_for_domain_resolver(self, table, set_name, element, max_wait=10): + # Resolver no longer blocks commit, need to wait for daemon to populate set + count = 0 + while count < max_wait: + code = run(f'sudo nft get element {table} {set_name} {{ {element} }}') + if code == 0: + return True + count += 1 + sleep(1) + return False + def test_snat(self): rules = ['100', '110', '120', '130', '200', '210', '220', '230'] outbound_iface_100 = 'eth0' @@ -78,6 +95,30 @@ class TestNAT(VyOSUnitTestSHIM.TestCase): self.verify_nftables(nftables_search, 'ip vyos_nat') + def test_snat_groups(self): + address_group = 'smoketest_addr' + address_group_member = '192.0.2.1' + rule = '100' + outbound_iface = 'eth0' + + self.cli_set(['firewall', 'group', 'address-group', address_group, 'address', address_group_member]) + + self.cli_set(src_path + ['rule', rule, 'source', 'group', 'address-group', address_group]) + self.cli_set(src_path + ['rule', rule, 'outbound-interface', outbound_iface]) + self.cli_set(src_path + ['rule', rule, 'translation', 'address', 'masquerade']) + + self.cli_commit() + + nftables_search = [ + [f'set A_{address_group}'], + [f'elements = {{ {address_group_member} }}'], + [f'ip saddr @A_{address_group}', f'oifname "{outbound_iface}"', 'masquerade'] + ] + + self.verify_nftables(nftables_search, 'ip vyos_nat') + + self.cli_delete(['firewall']) + def test_dnat(self): rules = ['100', '110', '120', '130', '200', '210', '220', '230'] inbound_iface_100 = 'eth0' diff --git a/smoketest/scripts/cli/test_nat66.py b/smoketest/scripts/cli/test_nat66.py index 6cf7ca0a1..50806b3e8 100755 --- a/smoketest/scripts/cli/test_nat66.py +++ b/smoketest/scripts/cli/test_nat66.py @@ -136,7 +136,7 @@ class TestNAT66(VyOSUnitTestSHIM.TestCase): self.cli_commit() nftables_search = [ - ['iifname "eth1"', 'tcp dport 4545', 'ip6 saddr 2001:db8:2222::/64', 'tcp sport 8080', 'dnat to 2001:db8:1111::1:5555'] + ['iifname "eth1"', 'tcp dport 4545', 'ip6 saddr 2001:db8:2222::/64', 'tcp sport 8080', 'dnat to [2001:db8:1111::1]:5555'] ] self.verify_nftables(nftables_search, 'ip6 vyos_nat') @@ -208,7 +208,7 @@ class TestNAT66(VyOSUnitTestSHIM.TestCase): self.cli_commit() nftables_search = [ - ['oifname "eth1"', 'ip6 saddr 2001:db8:2222::/64', 'tcp dport 9999', 'tcp sport 8080', 'snat to 2001:db8:1111::1:80'] + ['oifname "eth1"', 'ip6 saddr 2001:db8:2222::/64', 'tcp dport 9999', 'tcp sport 8080', 'snat to [2001:db8:1111::1]:80'] ] self.verify_nftables(nftables_search, 'ip6 vyos_nat') 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_policy.py b/smoketest/scripts/cli/test_policy.py index 2166e63ec..3a4ef666a 100755 --- a/smoketest/scripts/cli/test_policy.py +++ b/smoketest/scripts/cli/test_policy.py @@ -1030,6 +1030,7 @@ class TestPolicy(VyOSUnitTestSHIM.TestCase): 'metric' : '150', 'metric-type' : 'type-1', 'origin' : 'incomplete', + 'l3vpn' : '', 'originator-id' : '172.16.10.1', 'src' : '100.0.0.1', 'tag' : '65530', @@ -1229,6 +1230,8 @@ class TestPolicy(VyOSUnitTestSHIM.TestCase): self.cli_set(path + ['rule', rule, 'set', 'ipv6-next-hop', 'local', rule_config['set']['ipv6-next-hop-local']]) if 'ip-next-hop' in rule_config['set']: self.cli_set(path + ['rule', rule, 'set', 'ip-next-hop', rule_config['set']['ip-next-hop']]) + if 'l3vpn' in rule_config['set']: + self.cli_set(path + ['rule', rule, 'set', 'l3vpn-nexthop', 'encapsulation', 'gre']) if 'local-preference' in rule_config['set']: self.cli_set(path + ['rule', rule, 'set', 'local-preference', rule_config['set']['local-preference']]) if 'metric' in rule_config['set']: @@ -1408,6 +1411,8 @@ class TestPolicy(VyOSUnitTestSHIM.TestCase): tmp += 'ipv6 next-hop global ' + rule_config['set']['ipv6-next-hop-global'] elif 'ipv6-next-hop-local' in rule_config['set']: tmp += 'ipv6 next-hop local ' + rule_config['set']['ipv6-next-hop-local'] + elif 'l3vpn' in rule_config['set']: + tmp += 'l3vpn next-hop encapsulation gre' elif 'local-preference' in rule_config['set']: tmp += 'local-preference ' + rule_config['set']['local-preference'] elif 'metric' in rule_config['set']: diff --git a/smoketest/scripts/cli/test_policy_route.py b/smoketest/scripts/cli/test_policy_route.py index 046e385bb..cb48a84ff 100755 --- a/smoketest/scripts/cli/test_policy_route.py +++ b/smoketest/scripts/cli/test_policy_route.py @@ -21,6 +21,8 @@ from base_vyostest_shim import VyOSUnitTestSHIM from vyos.util import cmd mark = '100' +conn_mark = '555' +conn_mark_set = '111' table_mark_offset = 0x7fffffff table_id = '101' interface = 'eth0' @@ -42,18 +44,25 @@ class TestPolicyRoute(VyOSUnitTestSHIM.TestCase): super(TestPolicyRoute, cls).tearDownClass() def tearDown(self): - self.cli_delete(['interfaces', 'ethernet', interface, 'policy']) self.cli_delete(['policy', 'route']) self.cli_delete(['policy', 'route6']) self.cli_commit() + # Verify nftables cleanup nftables_search = [ ['set N_smoketest_network'], ['set N_smoketest_network1'], ['chain VYOS_PBR_smoketest'] ] - self.verify_nftables(nftables_search, 'ip mangle', inverse=True) + self.verify_nftables(nftables_search, 'ip vyos_mangle', inverse=True) + + # Verify ip rule cleanup + ip_rule_search = [ + ['fwmark ' + hex(table_mark_offset - int(table_id)), 'lookup ' + table_id] + ] + + self.verify_rules(ip_rule_search, inverse=True) def verify_nftables(self, nftables_search, table, inverse=False): nftables_output = cmd(f'sudo nft list table {table}') @@ -66,6 +75,17 @@ class TestPolicyRoute(VyOSUnitTestSHIM.TestCase): break self.assertTrue(not matched if inverse else matched, msg=search) + def verify_rules(self, rules_search, inverse=False): + rule_output = cmd('ip rule show') + + for search in rules_search: + matched = False + for line in rule_output.split("\n"): + if all(item in line for item in search): + matched = True + break + self.assertTrue(not matched if inverse else matched, msg=search) + def test_pbr_group(self): self.cli_set(['firewall', 'group', 'network-group', 'smoketest_network', 'network', '172.16.99.0/24']) self.cli_set(['firewall', 'group', 'network-group', 'smoketest_network1', 'network', '172.16.101.0/24']) @@ -74,8 +94,7 @@ class TestPolicyRoute(VyOSUnitTestSHIM.TestCase): self.cli_set(['policy', 'route', 'smoketest', 'rule', '1', 'source', 'group', 'network-group', 'smoketest_network']) self.cli_set(['policy', 'route', 'smoketest', 'rule', '1', 'destination', 'group', 'network-group', 'smoketest_network1']) self.cli_set(['policy', 'route', 'smoketest', 'rule', '1', 'set', 'mark', mark]) - - self.cli_set(['interfaces', 'ethernet', interface, 'policy', 'route', 'smoketest']) + self.cli_set(['policy', 'route', 'smoketest', 'interface', interface]) self.cli_commit() @@ -84,7 +103,7 @@ class TestPolicyRoute(VyOSUnitTestSHIM.TestCase): ['ip daddr @N_smoketest_network1', 'ip saddr @N_smoketest_network'], ] - self.verify_nftables(nftables_search, 'ip mangle') + self.verify_nftables(nftables_search, 'ip vyos_mangle') self.cli_delete(['firewall']) @@ -92,8 +111,7 @@ class TestPolicyRoute(VyOSUnitTestSHIM.TestCase): self.cli_set(['policy', 'route', 'smoketest', 'rule', '1', 'source', 'address', '172.16.20.10']) self.cli_set(['policy', 'route', 'smoketest', 'rule', '1', 'destination', 'address', '172.16.10.10']) self.cli_set(['policy', 'route', 'smoketest', 'rule', '1', 'set', 'mark', mark]) - - self.cli_set(['interfaces', 'ethernet', interface, 'policy', 'route', 'smoketest']) + self.cli_set(['policy', 'route', 'smoketest', 'interface', interface]) self.cli_commit() @@ -104,7 +122,26 @@ class TestPolicyRoute(VyOSUnitTestSHIM.TestCase): ['ip daddr 172.16.10.10', 'ip saddr 172.16.20.10', 'meta mark set ' + mark_hex], ] - self.verify_nftables(nftables_search, 'ip mangle') + self.verify_nftables(nftables_search, 'ip vyos_mangle') + + def test_pbr_mark_connection(self): + self.cli_set(['policy', 'route', 'smoketest', 'rule', '1', 'source', 'address', '172.16.20.10']) + self.cli_set(['policy', 'route', 'smoketest', 'rule', '1', 'destination', 'address', '172.16.10.10']) + self.cli_set(['policy', 'route', 'smoketest', 'rule', '1', 'connection-mark', conn_mark]) + self.cli_set(['policy', 'route', 'smoketest', 'rule', '1', 'set', 'connection-mark', conn_mark_set]) + self.cli_set(['policy', 'route', 'smoketest', 'interface', interface]) + + self.cli_commit() + + mark_hex = "{0:#010x}".format(int(conn_mark)) + mark_hex_set = "{0:#010x}".format(int(conn_mark_set)) + + nftables_search = [ + [f'iifname "{interface}"','jump VYOS_PBR_smoketest'], + ['ip daddr 172.16.10.10', 'ip saddr 172.16.20.10', 'ct mark ' + mark_hex, 'ct mark set ' + mark_hex_set], + ] + + self.verify_nftables(nftables_search, 'ip vyos_mangle') def test_pbr_table(self): self.cli_set(['policy', 'route', 'smoketest', 'rule', '1', 'protocol', 'tcp']) @@ -116,8 +153,8 @@ class TestPolicyRoute(VyOSUnitTestSHIM.TestCase): self.cli_set(['policy', 'route6', 'smoketest6', 'rule', '1', 'destination', 'port', '8888']) self.cli_set(['policy', 'route6', 'smoketest6', 'rule', '1', 'set', 'table', table_id]) - self.cli_set(['interfaces', 'ethernet', interface, 'policy', 'route', 'smoketest']) - self.cli_set(['interfaces', 'ethernet', interface, 'policy', 'route6', 'smoketest6']) + self.cli_set(['policy', 'route', 'smoketest', 'interface', interface]) + self.cli_set(['policy', 'route6', 'smoketest6', 'interface', interface]) self.cli_commit() @@ -130,7 +167,7 @@ class TestPolicyRoute(VyOSUnitTestSHIM.TestCase): ['tcp flags syn / syn,ack', 'tcp dport 8888', 'meta mark set ' + mark_hex] ] - self.verify_nftables(nftables_search, 'ip mangle') + self.verify_nftables(nftables_search, 'ip vyos_mangle') # IPv6 @@ -139,7 +176,7 @@ class TestPolicyRoute(VyOSUnitTestSHIM.TestCase): ['meta l4proto { tcp, udp }', 'th dport 8888', 'meta mark set ' + mark_hex] ] - self.verify_nftables(nftables6_search, 'ip6 mangle') + self.verify_nftables(nftables6_search, 'ip6 vyos_mangle') # IP rule fwmark -> table @@ -147,15 +184,7 @@ class TestPolicyRoute(VyOSUnitTestSHIM.TestCase): ['fwmark ' + hex(table_mark_offset - int(table_id)), 'lookup ' + table_id] ] - ip_rule_output = cmd('ip rule show') - - for search in ip_rule_search: - matched = False - for line in ip_rule_output.split("\n"): - if all(item in line for item in search): - matched = True - break - self.assertTrue(matched) + self.verify_rules(ip_rule_search) def test_pbr_matching_criteria(self): @@ -203,8 +232,8 @@ class TestPolicyRoute(VyOSUnitTestSHIM.TestCase): self.cli_set(['policy', 'route6', 'smoketest6', 'rule', '5', 'dscp-exclude', '14-19']) self.cli_set(['policy', 'route6', 'smoketest6', 'rule', '5', 'set', 'table', table_id]) - self.cli_set(['interfaces', 'ethernet', interface, 'policy', 'route', 'smoketest']) - self.cli_set(['interfaces', 'ethernet', interface, 'policy', 'route6', 'smoketest6']) + self.cli_set(['policy', 'route', 'smoketest', 'interface', interface]) + self.cli_set(['policy', 'route6', 'smoketest6', 'interface', interface]) self.cli_commit() @@ -220,7 +249,7 @@ class TestPolicyRoute(VyOSUnitTestSHIM.TestCase): ['ip dscp { 0x29, 0x39-0x3b }', 'meta mark set ' + mark_hex] ] - self.verify_nftables(nftables_search, 'ip mangle') + self.verify_nftables(nftables_search, 'ip vyos_mangle') # IPv6 nftables6_search = [ @@ -232,7 +261,7 @@ class TestPolicyRoute(VyOSUnitTestSHIM.TestCase): ['ip6 dscp != { 0x0e-0x13, 0x3d }', 'meta mark set ' + mark_hex] ] - self.verify_nftables(nftables6_search, 'ip6 mangle') + self.verify_nftables(nftables6_search, 'ip6 vyos_mangle') 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..bf5b2e0f3 100755 --- a/smoketest/scripts/cli/test_protocols_bgp.py +++ b/smoketest/scripts/cli/test_protocols_bgp.py @@ -34,6 +34,10 @@ prefix_list_in6 = 'pfx-foo-in6' prefix_list_out6 = 'pfx-foo-out6' bfd_profile = 'foo-bar-baz' +import_afi = 'ipv4-unicast' +import_vrf = 'red' +import_rd = ASN + ':100' +import_vrf_base = ['vrf', 'name'] neighbor_config = { '192.0.2.1' : { 'bfd' : '', @@ -64,6 +68,7 @@ neighbor_config = { 'pfx_list_in' : prefix_list_in, 'pfx_list_out' : prefix_list_out, 'no_send_comm_std' : '', + 'local_role' : 'rs-client', }, '192.0.2.3' : { 'advertise_map' : route_map_in, @@ -94,6 +99,8 @@ neighbor_config = { 'no_send_comm_std' : '', 'addpath_per_as' : '', 'peer_group' : 'foo-bar', + 'local_role' : 'customer', + 'local_role_strict': '', }, '2001:db8::2' : { 'remote_as' : '456', @@ -150,6 +157,8 @@ peer_group_config = { 'update_src' : 'lo', 'route_map_in' : route_map_in, 'route_map_out' : route_map_out, + 'local_role' : 'peer', + 'local_role_strict': '', }, } @@ -189,6 +198,15 @@ class TestProtocolsBGP(VyOSUnitTestSHIM.TestCase): # Check for running process self.assertTrue(process_named_running(PROCESS_NAME)) + + def create_bgp_instances_for_import_test(self): + table = '1000' + self.cli_set(base_path + ['system-as', ASN]) + # testing only one AFI is sufficient as it's generic code + + self.cli_set(import_vrf_base + [import_vrf, 'table', table]) + self.cli_set(import_vrf_base + [import_vrf, 'protocols', 'bgp', 'system-as', ASN]) + def verify_frr_config(self, peer, peer_config, frrconfig): # recurring patterns to verify for both a simple neighbor and a peer-group if 'bfd' in peer_config: @@ -208,6 +226,11 @@ class TestProtocolsBGP(VyOSUnitTestSHIM.TestCase): self.assertIn(f' neighbor {peer} ebgp-multihop {peer_config["multi_hop"]}', frrconfig) if 'local_as' in peer_config: self.assertIn(f' neighbor {peer} local-as {peer_config["local_as"]} no-prepend replace-as', frrconfig) + if 'local_role' in peer_config: + tmp = f' neighbor {peer} local-role {peer_config["local_role"]}' + if 'local_role_strict' in peer_config: + tmp += ' strict' + self.assertIn(tmp, frrconfig) if 'cap_over' in peer_config: self.assertIn(f' neighbor {peer} override-capability', frrconfig) if 'passive' in peer_config: @@ -294,6 +317,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 +346,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) @@ -365,6 +390,10 @@ class TestProtocolsBGP(VyOSUnitTestSHIM.TestCase): self.cli_set(base_path + ['neighbor', peer, 'ebgp-multihop', peer_config["multi_hop"]]) if 'local_as' in peer_config: self.cli_set(base_path + ['neighbor', peer, 'local-as', peer_config["local_as"], 'no-prepend', 'replace-as']) + if 'local_role' in peer_config: + self.cli_set(base_path + ['neighbor', peer, 'local-role', peer_config["local_role"]]) + if 'local_role_strict' in peer_config: + self.cli_set(base_path + ['neighbor', peer, 'local-role', peer_config["local_role"], 'strict']) if 'cap_over' in peer_config: self.cli_set(base_path + ['neighbor', peer, 'override-capability']) if 'passive' in peer_config: @@ -461,6 +490,10 @@ class TestProtocolsBGP(VyOSUnitTestSHIM.TestCase): self.cli_set(base_path + ['peer-group', peer_group, 'ebgp-multihop', config["multi_hop"]]) if 'local_as' in config: self.cli_set(base_path + ['peer-group', peer_group, 'local-as', config["local_as"], 'no-prepend', 'replace-as']) + if 'local_role' in config: + self.cli_set(base_path + ['peer-group', peer_group, 'local-role', config["local_role"]]) + if 'local_role_strict' in config: + self.cli_set(base_path + ['peer-group', peer_group, 'local-role', config["local_role"], 'strict']) if 'cap_over' in config: self.cli_set(base_path + ['peer-group', peer_group, 'override-capability']) if 'passive' in config: @@ -957,6 +990,101 @@ class TestProtocolsBGP(VyOSUnitTestSHIM.TestCase): self.assertIn(f' neighbor {neighbor} remote-as {remote_asn}', frrconfig) self.assertIn(f' neighbor {neighbor} local-as {local_asn}', frrconfig) + def test_bgp_16_import_rd_rt_compatibility(self): + # Verify if import vrf and rd vpn export + # exist in the same address family + self.create_bgp_instances_for_import_test() + self.cli_set( + base_path + ['address-family', import_afi, 'import', 'vrf', + import_vrf]) + self.cli_set( + base_path + ['address-family', import_afi, 'rd', 'vpn', 'export', + import_rd]) + with self.assertRaises(ConfigSessionError): + self.cli_commit() + + def test_bgp_17_import_rd_rt_compatibility(self): + # Verify if vrf that is in import vrf list contains rd vpn export + self.create_bgp_instances_for_import_test() + self.cli_set( + base_path + ['address-family', import_afi, 'import', 'vrf', + import_vrf]) + self.cli_commit() + frrconfig = self.getFRRconfig(f'router bgp {ASN}') + frrconfig_vrf = self.getFRRconfig(f'router bgp {ASN} vrf {import_vrf}') + + self.assertIn(f'router bgp {ASN}', frrconfig) + self.assertIn(f'address-family ipv4 unicast', frrconfig) + self.assertIn(f' import vrf {import_vrf}', frrconfig) + self.assertIn(f'router bgp {ASN} vrf {import_vrf}', frrconfig_vrf) + + self.cli_set( + import_vrf_base + [import_vrf] + base_path + ['address-family', + import_afi, 'rd', + 'vpn', 'export', + import_rd]) + with self.assertRaises(ConfigSessionError): + self.cli_commit() + + def test_bgp_18_deleting_import_vrf(self): + # Verify deleting vrf that is in import vrf list + self.create_bgp_instances_for_import_test() + self.cli_set( + base_path + ['address-family', import_afi, 'import', 'vrf', + import_vrf]) + self.cli_commit() + frrconfig = self.getFRRconfig(f'router bgp {ASN}') + frrconfig_vrf = self.getFRRconfig(f'router bgp {ASN} vrf {import_vrf}') + self.assertIn(f'router bgp {ASN}', frrconfig) + self.assertIn(f'address-family ipv4 unicast', frrconfig) + self.assertIn(f' import vrf {import_vrf}', frrconfig) + self.assertIn(f'router bgp {ASN} vrf {import_vrf}', frrconfig_vrf) + self.cli_delete(import_vrf_base + [import_vrf]) + with self.assertRaises(ConfigSessionError): + self.cli_commit() + + def test_bgp_19_deleting_default_vrf(self): + # Verify deleting existent vrf default if other vrfs were created + self.create_bgp_instances_for_import_test() + self.cli_commit() + frrconfig = self.getFRRconfig(f'router bgp {ASN}') + frrconfig_vrf = self.getFRRconfig(f'router bgp {ASN} vrf {import_vrf}') + self.assertIn(f'router bgp {ASN}', frrconfig) + self.assertIn(f'router bgp {ASN} vrf {import_vrf}', frrconfig_vrf) + self.cli_delete(base_path) + with self.assertRaises(ConfigSessionError): + self.cli_commit() + + def test_bgp_20_import_rd_rt_compatibility(self): + # Verify if vrf that has rd vpn export is in import vrf of other vrfs + self.create_bgp_instances_for_import_test() + self.cli_set( + import_vrf_base + [import_vrf] + base_path + ['address-family', + import_afi, 'rd', + 'vpn', 'export', + import_rd]) + self.cli_commit() + frrconfig = self.getFRRconfig(f'router bgp {ASN}') + frrconfig_vrf = self.getFRRconfig(f'router bgp {ASN} vrf {import_vrf}') + self.assertIn(f'router bgp {ASN}', frrconfig) + self.assertIn(f'router bgp {ASN} vrf {import_vrf}', frrconfig_vrf) + self.assertIn(f'address-family ipv4 unicast', frrconfig_vrf) + self.assertIn(f' rd vpn export {import_rd}', frrconfig_vrf) + + self.cli_set( + base_path + ['address-family', import_afi, 'import', 'vrf', + import_vrf]) + with self.assertRaises(ConfigSessionError): + self.cli_commit() + + def test_bgp_21_import_unspecified_vrf(self): + # Verify if vrf that is in import is unspecified + self.create_bgp_instances_for_import_test() + self.cli_set( + base_path + ['address-family', import_afi, 'import', 'vrf', + 'test']) + with self.assertRaises(ConfigSessionError): + self.cli_commit() if __name__ == '__main__': unittest.main(verbosity=2) diff --git a/smoketest/scripts/cli/test_protocols_ospf.py b/smoketest/scripts/cli/test_protocols_ospf.py index 51c947537..581959b15 100755 --- a/smoketest/scripts/cli/test_protocols_ospf.py +++ b/smoketest/scripts/cli/test_protocols_ospf.py @@ -70,8 +70,15 @@ 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]) + self.cli_set(base_path + ['passive-interface', 'default']) + self.cli_set(base_path + ['area', '10', 'area-type', 'stub']) + self.cli_set(base_path + ['area', '10', 'network', '10.0.0.0/16']) + self.cli_set(base_path + ['area', '10', 'range', '10.0.1.0/24']) + self.cli_set(base_path + ['area', '10', 'range', '10.0.2.0/24', 'not-advertise']) # commit changes self.cli_commit() @@ -79,11 +86,19 @@ 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) + self.assertIn(f' passive-interface default', frrconfig) + self.assertIn(f' area 10 stub', frrconfig) + self.assertIn(f' network 10.0.0.0/16 area 10', frrconfig) + self.assertIn(f' area 10 range 10.0.1.0/24', frrconfig) + self.assertNotIn(f' area 10 range 10.0.1.0/24 not-advertise', frrconfig) + self.assertIn(f' area 10 range 10.0.2.0/24 not-advertise', frrconfig) def test_ospf_03_access_list(self): @@ -268,6 +283,10 @@ class TestProtocolsOSPF(VyOSUnitTestSHIM.TestCase): # commit changes self.cli_commit() + frrconfig = self.getFRRconfig('router ospf') + self.assertIn(f'router ospf', frrconfig) + self.assertIn(f' passive-interface default', frrconfig) + for interface in interfaces: config = self.getFRRconfig(f'interface {interface}') self.assertIn(f'interface {interface}', config) diff --git a/smoketest/scripts/cli/test_qos.py b/smoketest/scripts/cli/test_qos.py new file mode 100755 index 000000000..0092473d6 --- /dev/null +++ b/smoketest/scripts/cli/test_qos.py @@ -0,0 +1,547 @@ +#!/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 unittest + +from json import loads +from base_vyostest_shim import VyOSUnitTestSHIM + +from vyos.configsession import ConfigSessionError +from vyos.ifconfig import Section +from vyos.util import cmd + +base_path = ['qos'] + +def get_tc_qdisc_json(interface) -> dict: + tmp = cmd(f'tc -detail -json qdisc show dev {interface}') + tmp = loads(tmp) + return next(iter(tmp)) + +def get_tc_filter_json(interface, direction) -> list: + if direction not in ['ingress', 'egress']: + raise ValueError() + tmp = cmd(f'tc -detail -json filter show dev {interface} {direction}') + tmp = loads(tmp) + return tmp + +class TestQoS(VyOSUnitTestSHIM.TestCase): + @classmethod + def setUpClass(cls): + super(TestQoS, cls).setUpClass() + + # ensure we can also run this test on a live system - so lets clean + # out the current configuration :) + cls.cli_delete(cls, base_path) + + # We only test on physical interfaces and not VLAN (sub-)interfaces + cls._interfaces = [] + if 'TEST_ETH' in os.environ: + tmp = os.environ['TEST_ETH'].split() + cls._interfaces = tmp + else: + for tmp in Section.interfaces('ethernet', vlan=False): + cls._interfaces.append(tmp) + + def tearDown(self): + # delete testing SSH config + self.cli_delete(base_path) + self.cli_commit() + + def test_01_cake(self): + bandwidth = 1000000 + rtt = 200 + + for interface in self._interfaces: + policy_name = f'qos-policy-{interface}' + self.cli_set(base_path + ['interface', interface, 'egress', policy_name]) + self.cli_set(base_path + ['policy', 'cake', policy_name, 'bandwidth', str(bandwidth)]) + self.cli_set(base_path + ['policy', 'cake', policy_name, 'rtt', str(rtt)]) + self.cli_set(base_path + ['policy', 'cake', policy_name, 'flow-isolation', 'dual-src-host']) + + bandwidth += 1000000 + rtt += 20 + + # commit changes + self.cli_commit() + + bandwidth = 1000000 + rtt = 200 + for interface in self._interfaces: + tmp = get_tc_qdisc_json(interface) + + self.assertEqual('cake', tmp['kind']) + # TC store rates as a 32-bit unsigned integer in bps (Bytes per second) + self.assertEqual(int(bandwidth *125), tmp['options']['bandwidth']) + # RTT internally is in us + self.assertEqual(int(rtt *1000), tmp['options']['rtt']) + self.assertEqual('dual-srchost', tmp['options']['flowmode']) + self.assertFalse(tmp['options']['ingress']) + self.assertFalse(tmp['options']['nat']) + self.assertTrue(tmp['options']['raw']) + + bandwidth += 1000000 + rtt += 20 + + def test_02_drop_tail(self): + queue_limit = 50 + + first = True + for interface in self._interfaces: + policy_name = f'qos-policy-{interface}' + + if first: + self.cli_set(base_path + ['interface', interface, 'ingress', policy_name]) + # verify() - selected QoS policy on interface only supports egress + with self.assertRaises(ConfigSessionError): + self.cli_commit() + self.cli_delete(base_path + ['interface', interface, 'ingress', policy_name]) + first = False + + self.cli_set(base_path + ['interface', interface, 'egress', policy_name]) + self.cli_set(base_path + ['policy', 'drop-tail', policy_name, 'queue-limit', str(queue_limit)]) + + queue_limit += 10 + + # commit changes + self.cli_commit() + + queue_limit = 50 + for interface in self._interfaces: + tmp = get_tc_qdisc_json(interface) + + self.assertEqual('pfifo', tmp['kind']) + self.assertEqual(queue_limit, tmp['options']['limit']) + + queue_limit += 10 + + def test_03_fair_queue(self): + hash_interval = 10 + queue_limit = 5 + policy_type = 'fair-queue' + + first = True + for interface in self._interfaces: + policy_name = f'qos-policy-{interface}' + + if first: + self.cli_set(base_path + ['interface', interface, 'ingress', policy_name]) + # verify() - selected QoS policy on interface only supports egress + with self.assertRaises(ConfigSessionError): + self.cli_commit() + self.cli_delete(base_path + ['interface', interface, 'ingress', policy_name]) + first = False + + self.cli_set(base_path + ['interface', interface, 'egress', policy_name]) + self.cli_set(base_path + ['policy', policy_type, policy_name, 'hash-interval', str(hash_interval)]) + self.cli_set(base_path + ['policy', policy_type, policy_name, 'queue-limit', str(queue_limit)]) + + hash_interval += 1 + queue_limit += 1 + + # commit changes + self.cli_commit() + + hash_interval = 10 + queue_limit = 5 + for interface in self._interfaces: + tmp = get_tc_qdisc_json(interface) + + self.assertEqual('sfq', tmp['kind']) + self.assertEqual(hash_interval, tmp['options']['perturb']) + self.assertEqual(queue_limit, tmp['options']['limit']) + + hash_interval += 1 + queue_limit += 1 + + def test_04_fq_codel(self): + policy_type = 'fq-codel' + codel_quantum = 1500 + flows = 512 + interval = 100 + queue_limit = 2048 + target = 5 + + first = True + for interface in self._interfaces: + policy_name = f'qos-policy-{interface}' + + if first: + self.cli_set(base_path + ['interface', interface, 'ingress', policy_name]) + # verify() - selected QoS policy on interface only supports egress + with self.assertRaises(ConfigSessionError): + self.cli_commit() + self.cli_delete(base_path + ['interface', interface, 'ingress', policy_name]) + first = False + + self.cli_set(base_path + ['interface', interface, 'egress', policy_name]) + self.cli_set(base_path + ['policy', policy_type, policy_name, 'codel-quantum', str(codel_quantum)]) + self.cli_set(base_path + ['policy', policy_type, policy_name, 'flows', str(flows)]) + self.cli_set(base_path + ['policy', policy_type, policy_name, 'interval', str(interval)]) + self.cli_set(base_path + ['policy', policy_type, policy_name, 'queue-limit', str(queue_limit)]) + self.cli_set(base_path + ['policy', policy_type, policy_name, 'target', str(target)]) + + codel_quantum += 10 + flows += 2 + interval += 10 + queue_limit += 512 + target += 1 + + # commit changes + self.cli_commit() + + codel_quantum = 1500 + flows = 512 + interval = 100 + queue_limit = 2048 + target = 5 + for interface in self._interfaces: + tmp = get_tc_qdisc_json(interface) + + self.assertEqual('fq_codel', tmp['kind']) + self.assertEqual(codel_quantum, tmp['options']['quantum']) + self.assertEqual(flows, tmp['options']['flows']) + self.assertEqual(queue_limit, tmp['options']['limit']) + + # due to internal rounding we need to substract 1 from interval and target after converting to milliseconds + # configuration of: + # tc qdisc add dev eth0 root fq_codel quantum 1500 flows 512 interval 100ms limit 2048 target 5ms noecn + # results in: tc -j qdisc show dev eth0 + # [{"kind":"fq_codel","handle":"8046:","root":true,"refcnt":3,"options":{"limit":2048,"flows":512, + # "quantum":1500,"target":4999,"interval":99999,"memory_limit":33554432,"drop_batch":64}}] + self.assertAlmostEqual(tmp['options']['interval'], interval *1000, delta=1) + self.assertAlmostEqual(tmp['options']['target'], target *1000 -1, delta=1) + + codel_quantum += 10 + flows += 2 + interval += 10 + queue_limit += 512 + target += 1 + + def test_05_limiter(self): + qos_config = { + '1' : { + 'bandwidth' : '1000000', + 'match4' : { + 'ssh' : { 'dport' : '22', }, + }, + }, + '2' : { + 'bandwidth' : '1000000', + 'match6' : { + 'ssh' : { 'dport' : '22', }, + }, + }, + } + + first = True + for interface in self._interfaces: + policy_name = f'qos-policy-{interface}' + + if first: + self.cli_set(base_path + ['interface', interface, 'egress', policy_name]) + # verify() - selected QoS policy on interface only supports egress + with self.assertRaises(ConfigSessionError): + self.cli_commit() + self.cli_delete(base_path + ['interface', interface, 'egress', policy_name]) + first = False + + self.cli_set(base_path + ['interface', interface, 'ingress', policy_name]) + # set default bandwidth parameter for all remaining connections + self.cli_set(base_path + ['policy', 'limiter', policy_name, 'default', 'bandwidth', '500000']) + + for qos_class, qos_class_config in qos_config.items(): + qos_class_base = base_path + ['policy', 'limiter', policy_name, 'class', qos_class] + + if 'match4' in qos_class_config: + for match, match_config in qos_class_config['match4'].items(): + if 'dport' in match_config: + self.cli_set(qos_class_base + ['match', match, 'ip', 'destination', 'port', match_config['dport']]) + + if 'match6' in qos_class_config: + for match, match_config in qos_class_config['match6'].items(): + if 'dport' in match_config: + self.cli_set(qos_class_base + ['match', match, 'ipv6', 'destination', 'port', match_config['dport']]) + + if 'bandwidth' in qos_class_config: + self.cli_set(qos_class_base + ['bandwidth', qos_class_config['bandwidth']]) + + + # commit changes + self.cli_commit() + + for interface in self._interfaces: + for filter in get_tc_filter_json(interface, 'ingress'): + # bail out early if filter has no attached action + if 'options' not in filter or 'actions' not in filter['options']: + continue + + for qos_class, qos_class_config in qos_config.items(): + # Every flowid starts with ffff and we encopde the class number after the colon + if 'flowid' not in filter['options'] or filter['options']['flowid'] != f'ffff:{qos_class}': + continue + + ip_hdr_offset = 20 + if 'match6' in qos_class_config: + ip_hdr_offset = 40 + + self.assertEqual(ip_hdr_offset, filter['options']['match']['off']) + if 'dport' in match_config: + dport = int(match_config['dport']) + self.assertEqual(f'{dport:x}', filter['options']['match']['value']) + + def test_06_network_emulator(self): + policy_type = 'network-emulator' + + bandwidth = 1000000 + corruption = 1 + delay = 2 + duplicate = 3 + loss = 4 + queue_limit = 5 + reordering = 6 + + first = True + for interface in self._interfaces: + policy_name = f'qos-policy-{interface}' + + if first: + self.cli_set(base_path + ['interface', interface, 'ingress', policy_name]) + # verify() - selected QoS policy on interface only supports egress + with self.assertRaises(ConfigSessionError): + self.cli_commit() + self.cli_delete(base_path + ['interface', interface, 'ingress', policy_name]) + first = False + + self.cli_set(base_path + ['interface', interface, 'egress', policy_name]) + + self.cli_set(base_path + ['policy', policy_type, policy_name, 'bandwidth', str(bandwidth)]) + self.cli_set(base_path + ['policy', policy_type, policy_name, 'corruption', str(corruption)]) + self.cli_set(base_path + ['policy', policy_type, policy_name, 'delay', str(delay)]) + self.cli_set(base_path + ['policy', policy_type, policy_name, 'duplicate', str(duplicate)]) + self.cli_set(base_path + ['policy', policy_type, policy_name, 'loss', str(loss)]) + self.cli_set(base_path + ['policy', policy_type, policy_name, 'queue-limit', str(queue_limit)]) + self.cli_set(base_path + ['policy', policy_type, policy_name, 'reordering', str(reordering)]) + + bandwidth += 1000000 + corruption += 1 + delay += 1 + duplicate +=1 + loss += 1 + queue_limit += 1 + reordering += 1 + + # commit changes + self.cli_commit() + + bandwidth = 1000000 + corruption = 1 + delay = 2 + duplicate = 3 + loss = 4 + queue_limit = 5 + reordering = 6 + for interface in self._interfaces: + tmp = get_tc_qdisc_json(interface) + self.assertEqual('netem', tmp['kind']) + + self.assertEqual(int(bandwidth *125), tmp['options']['rate']['rate']) + # values are in % + self.assertEqual(corruption/100, tmp['options']['corrupt']['corrupt']) + self.assertEqual(duplicate/100, tmp['options']['duplicate']['duplicate']) + self.assertEqual(loss/100, tmp['options']['loss-random']['loss']) + self.assertEqual(reordering/100, tmp['options']['reorder']['reorder']) + self.assertEqual(delay/1000, tmp['options']['delay']['delay']) + + self.assertEqual(queue_limit, tmp['options']['limit']) + + bandwidth += 1000000 + corruption += 1 + delay += 1 + duplicate += 1 + loss += 1 + queue_limit += 1 + reordering += 1 + + def test_07_priority_queue(self): + priorities = ['1', '2', '3', '4', '5'] + + first = True + for interface in self._interfaces: + policy_name = f'qos-policy-{interface}' + + if first: + self.cli_set(base_path + ['interface', interface, 'ingress', policy_name]) + # verify() - selected QoS policy on interface only supports egress + with self.assertRaises(ConfigSessionError): + self.cli_commit() + self.cli_delete(base_path + ['interface', interface, 'ingress', policy_name]) + first = False + + self.cli_set(base_path + ['interface', interface, 'egress', policy_name]) + self.cli_set(base_path + ['policy', 'priority-queue', policy_name, 'default', 'queue-limit', '10']) + + for priority in priorities: + prio_base = base_path + ['policy', 'priority-queue', policy_name, 'class', priority] + self.cli_set(prio_base + ['match', f'prio-{priority}', 'ip', 'destination', 'port', str(1000 + int(priority))]) + + # commit changes + self.cli_commit() + + def test_08_random_detect(self): + self.skipTest('tc returns invalid JSON here - needs iproute2 fix') + bandwidth = 5000 + + first = True + for interface in self._interfaces: + policy_name = f'qos-policy-{interface}' + + if first: + self.cli_set(base_path + ['interface', interface, 'ingress', policy_name]) + # verify() - selected QoS policy on interface only supports egress + with self.assertRaises(ConfigSessionError): + self.cli_commit() + self.cli_delete(base_path + ['interface', interface, 'ingress', policy_name]) + first = False + + self.cli_set(base_path + ['interface', interface, 'egress', policy_name]) + self.cli_set(base_path + ['policy', 'random-detect', policy_name, 'bandwidth', str(bandwidth)]) + + bandwidth += 1000 + + # commit changes + self.cli_commit() + + bandwidth = 5000 + for interface in self._interfaces: + tmp = get_tc_qdisc_json(interface) + import pprint + pprint.pprint(tmp) + + def test_09_rate_control(self): + bandwidth = 5000 + burst = 20 + latency = 5 + + first = True + for interface in self._interfaces: + policy_name = f'qos-policy-{interface}' + + if first: + self.cli_set(base_path + ['interface', interface, 'ingress', policy_name]) + # verify() - selected QoS policy on interface only supports egress + with self.assertRaises(ConfigSessionError): + self.cli_commit() + self.cli_delete(base_path + ['interface', interface, 'ingress', policy_name]) + first = False + + self.cli_set(base_path + ['interface', interface, 'egress', policy_name]) + self.cli_set(base_path + ['policy', 'rate-control', policy_name, 'bandwidth', str(bandwidth)]) + self.cli_set(base_path + ['policy', 'rate-control', policy_name, 'burst', str(burst)]) + self.cli_set(base_path + ['policy', 'rate-control', policy_name, 'latency', str(latency)]) + + bandwidth += 1000 + burst += 5 + latency += 1 + # commit changes + self.cli_commit() + + bandwidth = 5000 + burst = 20 + latency = 5 + for interface in self._interfaces: + tmp = get_tc_qdisc_json(interface) + + self.assertEqual('tbf', tmp['kind']) + self.assertEqual(0, tmp['options']['mpu']) + # TC store rates as a 32-bit unsigned integer in bps (Bytes per second) + self.assertEqual(int(bandwidth * 125), tmp['options']['rate']) + + bandwidth += 1000 + burst += 5 + latency += 1 + + def test_10_round_robin(self): + qos_config = { + '1' : { + 'match4' : { + 'ssh' : { 'dport' : '22', }, + }, + }, + '2' : { + 'match6' : { + 'ssh' : { 'dport' : '22', }, + }, + }, + } + + first = True + for interface in self._interfaces: + policy_name = f'qos-policy-{interface}' + + if first: + self.cli_set(base_path + ['interface', interface, 'ingress', policy_name]) + # verify() - selected QoS policy on interface only supports egress + with self.assertRaises(ConfigSessionError): + self.cli_commit() + self.cli_delete(base_path + ['interface', interface, 'ingress', policy_name]) + first = False + + self.cli_set(base_path + ['interface', interface, 'egress', policy_name]) + + for qos_class, qos_class_config in qos_config.items(): + qos_class_base = base_path + ['policy', 'round-robin', policy_name, 'class', qos_class] + + if 'match4' in qos_class_config: + for match, match_config in qos_class_config['match4'].items(): + if 'dport' in match_config: + self.cli_set(qos_class_base + ['match', match, 'ip', 'destination', 'port', match_config['dport']]) + + if 'match6' in qos_class_config: + for match, match_config in qos_class_config['match6'].items(): + if 'dport' in match_config: + self.cli_set(qos_class_base + ['match', match, 'ipv6', 'destination', 'port', match_config['dport']]) + + + # commit changes + self.cli_commit() + + for interface in self._interfaces: + import pprint + tmp = get_tc_qdisc_json(interface) + self.assertEqual('drr', tmp['kind']) + + for filter in get_tc_filter_json(interface, 'ingress'): + # bail out early if filter has no attached action + if 'options' not in filter or 'actions' not in filter['options']: + continue + + for qos_class, qos_class_config in qos_config.items(): + # Every flowid starts with ffff and we encopde the class number after the colon + if 'flowid' not in filter['options'] or filter['options']['flowid'] != f'ffff:{qos_class}': + continue + + ip_hdr_offset = 20 + if 'match6' in qos_class_config: + ip_hdr_offset = 40 + + self.assertEqual(ip_hdr_offset, filter['options']['match']['off']) + if 'dport' in match_config: + dport = int(match_config['dport']) + self.assertEqual(f'{dport:x}', filter['options']['match']['value']) + +if __name__ == '__main__': + unittest.main(verbosity=2, failfast=True) diff --git a/smoketest/scripts/cli/test_service_dhcp-server.py b/smoketest/scripts/cli/test_service_dhcp-server.py index 9c9d6d9f1..64ba100a2 100755 --- a/smoketest/scripts/cli/test_service_dhcp-server.py +++ b/smoketest/scripts/cli/test_service_dhcp-server.py @@ -102,16 +102,17 @@ class TestServiceDHCPServer(VyOSUnitTestSHIM.TestCase): def test_dhcp_single_pool_options(self): shared_net_name = 'SMOKE-0815' - range_0_start = inc_ip(subnet, 10) - range_0_stop = inc_ip(subnet, 20) - smtp_server = '1.2.3.4' - time_server = '4.3.2.1' - tftp_server = 'tftp.vyos.io' - search_domains = ['foo.vyos.net', 'bar.vyos.net'] - bootfile_name = 'vyos' - bootfile_server = '192.0.2.1' - wpad = 'http://wpad.vyos.io/foo/bar' - server_identifier = bootfile_server + range_0_start = inc_ip(subnet, 10) + range_0_stop = inc_ip(subnet, 20) + smtp_server = '1.2.3.4' + time_server = '4.3.2.1' + tftp_server = 'tftp.vyos.io' + search_domains = ['foo.vyos.net', 'bar.vyos.net'] + bootfile_name = 'vyos' + bootfile_server = '192.0.2.1' + wpad = 'http://wpad.vyos.io/foo/bar' + server_identifier = bootfile_server + ipv6_only_preferred = '300' pool = base_path + ['shared-network-name', shared_net_name, 'subnet', subnet] # we use the first subnet IP address as default gateway @@ -132,6 +133,7 @@ class TestServiceDHCPServer(VyOSUnitTestSHIM.TestCase): self.cli_set(pool + ['server-identifier', server_identifier]) self.cli_set(pool + ['static-route', '10.0.0.0/24', 'next-hop', '192.0.2.1']) + self.cli_set(pool + ['ipv6-only-preferred', ipv6_only_preferred]) # check validate() - No DHCP address range or active static-mapping set with self.assertRaises(ConfigSessionError): @@ -169,6 +171,7 @@ class TestServiceDHCPServer(VyOSUnitTestSHIM.TestCase): self.assertIn(f'max-lease-time 86400;', config) self.assertIn(f'range {range_0_start} {range_0_stop};', config) self.assertIn(f'set shared-networkname = "{shared_net_name}";', config) + self.assertIn(f'option rfc8925-ipv6-only-preferred {ipv6_only_preferred};', config) # weird syntax for those static routes self.assertIn(f'option rfc3442-static-route 24,10,0,0,192,0,2,1, 0,192,0,2,1;', config) diff --git a/smoketest/scripts/cli/test_service_dhcpv6-relay.py b/smoketest/scripts/cli/test_service_dhcpv6-relay.py index fc206435b..8bb58d296 100755 --- a/smoketest/scripts/cli/test_service_dhcpv6-relay.py +++ b/smoketest/scripts/cli/test_service_dhcpv6-relay.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2020 VyOS maintainers and contributors +# Copyright (C) 2020-2023 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 @@ -34,22 +34,30 @@ listen_addr = '2001:db8:ffff::1/64' interfaces = [] class TestServiceDHCPv6Relay(VyOSUnitTestSHIM.TestCase): - def setUp(self): - for tmp in interfaces: + @classmethod + def setUpClass(cls): + super(TestServiceDHCPv6Relay, cls).setUpClass() + + # ensure we can also run this test on a live system - so lets clean + # out the current configuration :) + cls.cli_delete(cls, base_path) + + for tmp in Section.interfaces('ethernet', vlan=False): + interfaces.append(tmp) listen = listen_addr if tmp == upstream_if: listen = upstream_if_addr - self.cli_set(['interfaces', 'ethernet', tmp, 'address', listen]) + cls.cli_set(cls, ['interfaces', 'ethernet', tmp, 'address', listen]) - def tearDown(self): - self.cli_delete(base_path) + @classmethod + def tearDownClass(cls): for tmp in interfaces: listen = listen_addr if tmp == upstream_if: listen = upstream_if_addr - self.cli_delete(['interfaces', 'ethernet', tmp, 'address', listen]) + cls.cli_delete(cls, ['interfaces', 'ethernet', tmp, 'address', listen]) - self.cli_commit() + super(TestServiceDHCPv6Relay, cls).tearDownClass() def test_relay_default(self): dhcpv6_server = '2001:db8::ffff' @@ -100,9 +108,5 @@ class TestServiceDHCPv6Relay(VyOSUnitTestSHIM.TestCase): self.assertTrue(process_named_running(PROCESS_NAME)) if __name__ == '__main__': - for tmp in Section.interfaces('ethernet'): - if '.' not in tmp: - interfaces.append(tmp) - unittest.main(verbosity=2) diff --git a/smoketest/scripts/cli/test_service_dns_forwarding.py b/smoketest/scripts/cli/test_service_dns_forwarding.py index fe2682d50..94e0597ad 100755 --- a/smoketest/scripts/cli/test_service_dns_forwarding.py +++ b/smoketest/scripts/cli/test_service_dns_forwarding.py @@ -111,6 +111,10 @@ class TestServicePowerDNS(VyOSUnitTestSHIM.TestCase): tmp = get_config_value('serve-rfc1918') self.assertEqual(tmp, 'yes') + # verify default port configuration + tmp = get_config_value('local-port') + self.assertEqual(tmp, '53') + def test_dnssec(self): # DNSSEC option testing @@ -224,5 +228,21 @@ class TestServicePowerDNS(VyOSUnitTestSHIM.TestCase): tmp = get_config_value('dns64-prefix') self.assertEqual(tmp, dns_prefix) + def test_listening_port(self): + # We can listen on a different port compared to '53' but only one at a time + for port in ['1053', '5353']: + self.cli_set(base_path + ['port', port]) + for network in allow_from: + self.cli_set(base_path + ['allow-from', network]) + for address in listen_adress: + self.cli_set(base_path + ['listen-address', address]) + + # commit changes + self.cli_commit() + + # verify local-port configuration + tmp = get_config_value('local-port') + self.assertEqual(tmp, port) + if __name__ == '__main__': - unittest.main(verbosity=2) + unittest.main(verbosity=2, failfast=True) diff --git a/smoketest/scripts/cli/test_service_https.py b/smoketest/scripts/cli/test_service_https.py index 72c1d4e43..0f4b1393c 100755 --- a/smoketest/scripts/cli/test_service_https.py +++ b/smoketest/scripts/cli/test_service_https.py @@ -143,10 +143,10 @@ class TestHTTPSService(VyOSUnitTestSHIM.TestCase): # caught by the resolver, and returns success 'False', so one must # check the return value. - self.cli_set(base_path + ['api', 'gql']) + self.cli_set(base_path + ['api', 'graphql']) self.cli_commit() - gql_url = f'https://{address}/graphql' + graphql_url = f'https://{address}/graphql' query_valid_key = f""" {{ @@ -160,7 +160,7 @@ class TestHTTPSService(VyOSUnitTestSHIM.TestCase): }} """ - r = request('POST', gql_url, verify=False, headers=headers, json={'query': query_valid_key}) + r = request('POST', graphql_url, verify=False, headers=headers, json={'query': query_valid_key}) success = r.json()['data']['SystemStatus']['success'] self.assertTrue(success) @@ -176,7 +176,7 @@ class TestHTTPSService(VyOSUnitTestSHIM.TestCase): } """ - r = request('POST', gql_url, verify=False, headers=headers, json={'query': query_invalid_key}) + r = request('POST', graphql_url, verify=False, headers=headers, json={'query': query_invalid_key}) success = r.json()['data']['SystemStatus']['success'] self.assertFalse(success) @@ -192,8 +192,52 @@ class TestHTTPSService(VyOSUnitTestSHIM.TestCase): } """ - r = request('POST', gql_url, verify=False, headers=headers, json={'query': query_no_key}) + r = request('POST', graphql_url, verify=False, headers=headers, json={'query': query_no_key}) self.assertEqual(r.status_code, 400) + # GraphQL token authentication test: request token; pass in header + # of query. + + self.cli_set(base_path + ['api', 'graphql', 'authentication', 'type', 'token']) + self.cli_commit() + + mutation = """ + mutation { + AuthToken (data: {username: "vyos", password: "vyos"}) { + success + errors + data { + result + } + } + } + """ + r = request('POST', graphql_url, verify=False, headers=headers, json={'query': mutation}) + + token = r.json()['data']['AuthToken']['data']['result']['token'] + + headers = {'Authorization': f'Bearer {token}'} + + query = """ + { + ShowVersion (data: {}) { + success + errors + op_mode_error { + name + message + vyos_code + } + data { + result + } + } + } + """ + + r = request('POST', graphql_url, verify=False, headers=headers, json={'query': query}) + success = r.json()['data']['ShowVersion']['success'] + self.assertTrue(success) + if __name__ == '__main__': unittest.main(verbosity=2) diff --git a/smoketest/scripts/cli/test_system_ntp.py b/smoketest/scripts/cli/test_service_ntp.py index a0806acf0..3ccd19a31 100755 --- a/smoketest/scripts/cli/test_system_ntp.py +++ b/smoketest/scripts/cli/test_service_ntp.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2019-2022 VyOS maintainers and contributors +# Copyright (C) 2019-2023 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 @@ -19,14 +19,12 @@ import unittest from base_vyostest_shim import VyOSUnitTestSHIM from vyos.configsession import ConfigSessionError -from vyos.template import address_from_cidr -from vyos.template import netmask_from_cidr -from vyos.util import read_file +from vyos.util import cmd from vyos.util import process_named_running -PROCESS_NAME = 'ntpd' -NTP_CONF = '/run/ntpd/ntpd.conf' -base_path = ['system', 'ntp'] +PROCESS_NAME = 'chronyd' +NTP_CONF = '/run/chrony/chrony.conf' +base_path = ['service', 'ntp'] class TestSystemNTP(VyOSUnitTestSHIM.TestCase): @classmethod @@ -38,6 +36,8 @@ class TestSystemNTP(VyOSUnitTestSHIM.TestCase): cls.cli_delete(cls, base_path) def tearDown(self): + self.assertTrue(process_named_running(PROCESS_NAME)) + self.cli_delete(base_path) self.cli_commit() @@ -46,7 +46,7 @@ class TestSystemNTP(VyOSUnitTestSHIM.TestCase): def test_01_ntp_options(self): # Test basic NTP support with multiple servers and their options servers = ['192.0.2.1', '192.0.2.2'] - options = ['noselect', 'preempt', 'prefer'] + options = ['noselect', 'prefer'] pools = ['pool.vyos.io'] for server in servers: @@ -61,12 +61,14 @@ class TestSystemNTP(VyOSUnitTestSHIM.TestCase): self.cli_commit() # Check generated configuration - config = read_file(NTP_CONF) - self.assertIn('driftfile /var/lib/ntp/ntp.drift', config) - self.assertIn('restrict default noquery nopeer notrap nomodify', config) - self.assertIn('restrict source nomodify notrap noquery', config) - self.assertIn('restrict 127.0.0.1', config) - self.assertIn('restrict -6 ::1', config) + # this file must be read with higher permissions + config = cmd(f'sudo cat {NTP_CONF}') + self.assertIn('driftfile /run/chrony/drift', config) + self.assertIn('dumpdir /run/chrony', config) + self.assertIn('clientloglimit 1048576', config) + self.assertIn('rtcsync', config) + self.assertIn('makestep 1.0 3', config) + self.assertIn('leapsectz right/UTC', config) for server in servers: self.assertIn(f'server {server} iburst ' + ' '.join(options), config) @@ -80,9 +82,9 @@ class TestSystemNTP(VyOSUnitTestSHIM.TestCase): for listen in listen_address: self.cli_set(base_path + ['listen-address', listen]) - networks = ['192.0.2.0/24', '2001:db8:1000::/64'] + networks = ['192.0.2.0/24', '2001:db8:1000::/64', '100.64.0.0', '2001:db8::ffff'] for network in networks: - self.cli_set(base_path + ['allow-clients', 'address', network]) + self.cli_set(base_path + ['allow-client', 'address', network]) # Verify "NTP server not configured" verify() statement with self.assertRaises(ConfigSessionError): @@ -95,18 +97,14 @@ class TestSystemNTP(VyOSUnitTestSHIM.TestCase): self.cli_commit() # Check generated client address configuration - config = read_file(NTP_CONF) - self.assertIn('restrict default ignore', config) - + # this file must be read with higher permissions + config = cmd(f'sudo cat {NTP_CONF}') for network in networks: - network_address = address_from_cidr(network) - network_netmask = netmask_from_cidr(network) - self.assertIn(f'restrict {network_address} mask {network_netmask} nomodify notrap nopeer', config) + self.assertIn(f'allow {network}', config) # Check listen address - self.assertIn('interface ignore wildcard', config) for listen in listen_address: - self.assertIn(f'interface listen {listen}', config) + self.assertIn(f'bindaddress {listen}', config) def test_03_ntp_interface(self): interfaces = ['eth0', 'eth1'] @@ -120,10 +118,28 @@ class TestSystemNTP(VyOSUnitTestSHIM.TestCase): self.cli_commit() # Check generated client address configuration - config = read_file(NTP_CONF) - self.assertIn('interface ignore wildcard', config) + # this file must be read with higher permissions + config = cmd(f'sudo cat {NTP_CONF}') for interface in interfaces: - self.assertIn(f'interface listen {interface}', config) + self.assertIn(f'binddevice {interface}', config) + + def test_04_ntp_vrf(self): + vrf_name = 'vyos-mgmt' + + self.cli_set(['vrf', 'name', vrf_name, 'table', '12345']) + self.cli_set(base_path + ['vrf', vrf_name]) + + servers = ['time1.vyos.net', 'time2.vyos.net'] + for server in servers: + self.cli_set(base_path + ['server', server]) + + self.cli_commit() + + # Check for process in VRF + tmp = cmd(f'ip vrf pids {vrf_name}') + self.assertIn(PROCESS_NAME, tmp) + + self.cli_delete(['vrf', 'name', vrf_name]) if __name__ == '__main__': unittest.main(verbosity=2) diff --git a/smoketest/scripts/cli/test_service_router-advert.py b/smoketest/scripts/cli/test_service_router-advert.py index 873be7df0..0169b7934 100755 --- a/smoketest/scripts/cli/test_service_router-advert.py +++ b/smoketest/scripts/cli/test_service_router-advert.py @@ -37,7 +37,6 @@ def get_config_value(key): return tmp[0].split()[0].replace(';','') class TestServiceRADVD(VyOSUnitTestSHIM.TestCase): - @classmethod def setUpClass(cls): super(TestServiceRADVD, cls).setUpClass() @@ -114,7 +113,6 @@ class TestServiceRADVD(VyOSUnitTestSHIM.TestCase): tmp = get_config_value('DecrementLifetimes') self.assertEqual(tmp, 'off') - def test_dns(self): nameserver = ['2001:db8::1', '2001:db8::2'] dnssl = ['vyos.net', 'vyos.io'] @@ -150,7 +148,6 @@ class TestServiceRADVD(VyOSUnitTestSHIM.TestCase): tmp = 'DNSSL ' + ' '.join(dnssl) + ' {' self.assertIn(tmp, config) - def test_deprecate_prefix(self): self.cli_set(base_path + ['prefix', prefix, 'valid-lifetime', 'infinity']) self.cli_set(base_path + ['prefix', prefix, 'deprecate-prefix']) @@ -159,13 +156,45 @@ class TestServiceRADVD(VyOSUnitTestSHIM.TestCase): # commit changes self.cli_commit() - config = read_file(RADVD_CONF) - tmp = get_config_value('DeprecatePrefix') self.assertEqual(tmp, 'on') tmp = get_config_value('DecrementLifetimes') self.assertEqual(tmp, 'on') + def test_route(self): + route = '2001:db8:1000::/64' + + self.cli_set(base_path + ['prefix', prefix]) + self.cli_set(base_path + ['route', route]) + + # commit changes + self.cli_commit() + + config = read_file(RADVD_CONF) + + tmp = f'route {route}' + ' {' + self.assertIn(tmp, config) + + self.assertIn('AdvRouteLifetime 1800;', config) + self.assertIn('AdvRoutePreference medium;', config) + self.assertIn('RemoveRoute on;', config) + + def test_rasrcaddress(self): + ra_src = ['fe80::1', 'fe80::2'] + + self.cli_set(base_path + ['prefix', prefix]) + for src in ra_src: + self.cli_set(base_path + ['source-address', src]) + + # commit changes + self.cli_commit() + + config = read_file(RADVD_CONF) + self.assertIn('AdvRASrcAddress {', config) + for src in ra_src: + self.assertIn(f' {src};', config) + + if __name__ == '__main__': unittest.main(verbosity=2) diff --git a/smoketest/scripts/cli/test_service_snmp.py b/smoketest/scripts/cli/test_service_snmp.py index e80c689cc..b18b9e7a1 100755 --- a/smoketest/scripts/cli/test_service_snmp.py +++ b/smoketest/scripts/cli/test_service_snmp.py @@ -123,6 +123,28 @@ class TestSNMPService(VyOSUnitTestSHIM.TestCase): self.assertTrue(process_named_running(PROCESS_NAME)) self.cli_delete(['interfaces', 'dummy', dummy_if]) + ## Check communities and default view RESTRICTED + for auth in ['ro', 'rw']: + community = 'VyOS' + auth + for addr in clients: + if is_ipv4(addr): + entry = auth + 'community ' + community + ' ' + addr + ' -V' + else: + entry = auth + 'community6 ' + community + ' ' + addr + ' -V' + config = get_config_value(entry) + expected = 'RESTRICTED' + self.assertIn(expected, config) + for addr in networks: + if is_ipv4(addr): + entry = auth + 'community ' + community + ' ' + addr + ' -V' + else: + entry = auth + 'community6 ' + community + ' ' + addr + ' -V' + config = get_config_value(entry) + expected = 'RESTRICTED' + self.assertIn(expected, config) + # And finally check global entry for RESTRICTED view + config = get_config_value('view RESTRICTED included .1') + self.assertIn('80', config) def test_snmpv3_sha(self): # Check if SNMPv3 can be configured with SHA authentication diff --git a/smoketest/scripts/cli/test_service_ssh.py b/smoketest/scripts/cli/test_service_ssh.py index 0b029dd00..8de98f34f 100755 --- a/smoketest/scripts/cli/test_service_ssh.py +++ b/smoketest/scripts/cli/test_service_ssh.py @@ -262,5 +262,42 @@ class TestServiceSSH(VyOSUnitTestSHIM.TestCase): self.assertFalse(process_named_running(SSHGUARD_PROCESS)) + + # Network Device Collaborative Protection Profile + def test_ssh_ndcpp(self): + ciphers = ['aes128-cbc', 'aes128-ctr', 'aes256-cbc', 'aes256-ctr'] + host_key_algs = ['sk-ssh-ed25519@openssh.com', 'ssh-rsa', 'ssh-ed25519'] + kexes = ['diffie-hellman-group14-sha1', 'ecdh-sha2-nistp256', 'ecdh-sha2-nistp384', 'ecdh-sha2-nistp521'] + macs = ['hmac-sha1', 'hmac-sha2-256', 'hmac-sha2-512'] + rekey_time = '60' + rekey_data = '1024' + + for cipher in ciphers: + self.cli_set(base_path + ['ciphers', cipher]) + for host_key in host_key_algs: + self.cli_set(base_path + ['hostkey-algorithm', host_key]) + for kex in kexes: + self.cli_set(base_path + ['key-exchange', kex]) + for mac in macs: + self.cli_set(base_path + ['mac', mac]) + # Optional rekey parameters + self.cli_set(base_path + ['rekey', 'data', rekey_data]) + self.cli_set(base_path + ['rekey', 'time', rekey_time]) + + # commit changes + self.cli_commit() + + ssh_lines = ['Ciphers aes128-cbc,aes128-ctr,aes256-cbc,aes256-ctr', + 'HostKeyAlgorithms sk-ssh-ed25519@openssh.com,ssh-rsa,ssh-ed25519', + 'MACs hmac-sha1,hmac-sha2-256,hmac-sha2-512', + 'KexAlgorithms diffie-hellman-group14-sha1,ecdh-sha2-nistp256,ecdh-sha2-nistp384,ecdh-sha2-nistp521', + 'RekeyLimit 1024M 60M' + ] + tmp_sshd_conf = read_file(SSHD_CONF) + + for line in ssh_lines: + self.assertIn(line, tmp_sshd_conf) + + if __name__ == '__main__': unittest.main(verbosity=2) diff --git a/smoketest/scripts/cli/test_service_tftp-server.py b/smoketest/scripts/cli/test_service_tftp-server.py index b57c33f26..99d81e203 100755 --- a/smoketest/scripts/cli/test_service_tftp-server.py +++ b/smoketest/scripts/cli/test_service_tftp-server.py @@ -33,15 +33,32 @@ address_ipv6 = '2001:db8::1' vrf = 'mgmt' class TestServiceTFTPD(VyOSUnitTestSHIM.TestCase): - def setUp(self): - self.cli_set(dummy_if_path + ['address', address_ipv4 + '/32']) - self.cli_set(dummy_if_path + ['address', address_ipv6 + '/128']) + @classmethod + def setUpClass(cls): + super(TestServiceTFTPD, cls).setUpClass() + + # ensure we can also run this test on a live system - so lets clean + # out the current configuration :) + cls.cli_delete(cls, base_path) + + cls.cli_set(cls, dummy_if_path + ['address', address_ipv4 + '/32']) + cls.cli_set(cls, dummy_if_path + ['address', address_ipv6 + '/128']) + + @classmethod + def tearDownClass(cls): + cls.cli_delete(cls, dummy_if_path) + super(TestServiceTFTPD, cls).tearDownClass() def tearDown(self): + # Check for running process + self.assertTrue(process_named_running(PROCESS_NAME)) + self.cli_delete(base_path) - self.cli_delete(dummy_if_path) self.cli_commit() + # Check for no longer running process + self.assertFalse(process_named_running(PROCESS_NAME)) + def test_01_tftpd_single(self): directory = '/tmp' port = '69' # default port @@ -61,9 +78,6 @@ class TestServiceTFTPD(VyOSUnitTestSHIM.TestCase): # verify upload self.assertIn('--create --umask 000', config) - # Check for running process - self.assertTrue(process_named_running(PROCESS_NAME)) - def test_02_tftpd_multi(self): directory = '/tmp' address = [address_ipv4, address_ipv6] @@ -125,9 +139,6 @@ class TestServiceTFTPD(VyOSUnitTestSHIM.TestCase): # verify upload self.assertIn('--create --umask 000', config) - # Check for running process - self.assertTrue(process_named_running(PROCESS_NAME)) - # Check for process in VRF tmp = cmd(f'ip vrf pids {vrf}') self.assertIn(PROCESS_NAME, tmp) diff --git a/smoketest/scripts/cli/test_vpn_ipsec.py b/smoketest/scripts/cli/test_vpn_ipsec.py index bd242104f..c8634dd57 100755 --- a/smoketest/scripts/cli/test_vpn_ipsec.py +++ b/smoketest/scripts/cli/test_vpn_ipsec.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2021-2022 VyOS maintainers and contributors +# Copyright (C) 2021-2023 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 @@ -34,11 +34,15 @@ swanctl_file = '/etc/swanctl/swanctl.conf' peer_ip = '203.0.113.45' connection_name = 'main-branch' +local_id = 'left' +remote_id = 'right' interface = 'eth1' vif = '100' esp_group = 'MyESPGroup' ike_group = 'MyIKEGroup' secret = 'MYSECRETKEY' +PROCESS_NAME = 'charon' +regex_uuid4 = '[0-9a-fA-F]{8}\-[0-9a-fA-F]{4}\-[0-9a-fA-F]{4}\-[0-9a-fA-F]{4}\-[0-9a-fA-F]{12}' ca_pem = """ MIIDSzCCAjOgAwIBAgIUQHK+ZgTUYZksvXY2/MyW+Jiels4wDQYJKoZIhvcNAQEL @@ -137,23 +141,28 @@ class TestVPNIPsec(VyOSUnitTestSHIM.TestCase): def tearDown(self): # Check for running process - self.assertTrue(process_named_running('charon')) + self.assertTrue(process_named_running(PROCESS_NAME)) self.cli_delete(base_path) self.cli_delete(tunnel_path) self.cli_commit() # Check for no longer running process - self.assertFalse(process_named_running('charon')) + self.assertFalse(process_named_running(PROCESS_NAME)) def test_01_dhcp_fail_handling(self): # Interface for dhcp-interface self.cli_set(ethernet_path + [interface, 'vif', vif, 'address', 'dhcp']) # Use VLAN to avoid getting IP from qemu dhcp server + # vpn ipsec auth psk <tag> id <x.x.x.x> + self.cli_set(base_path + ['authentication', 'psk', connection_name, 'id', local_id]) + self.cli_set(base_path + ['authentication', 'psk', connection_name, 'id', remote_id]) + self.cli_set(base_path + ['authentication', 'psk', connection_name, 'id', peer_ip]) + self.cli_set(base_path + ['authentication', 'psk', connection_name, 'secret', secret]) + # Site to site peer_base_path = base_path + ['site-to-site', 'peer', connection_name] self.cli_set(peer_base_path + ['authentication', 'mode', 'pre-shared-secret']) - self.cli_set(peer_base_path + ['authentication', 'pre-shared-secret', secret]) self.cli_set(peer_base_path + ['ike-group', ike_group]) self.cli_set(peer_base_path + ['default-esp-group', esp_group]) self.cli_set(peer_base_path + ['dhcp-interface', f'{interface}.{vif}']) @@ -166,21 +175,30 @@ class TestVPNIPsec(VyOSUnitTestSHIM.TestCase): dhcp_waiting = read_file(dhcp_waiting_file) self.assertIn(f'{interface}.{vif}', dhcp_waiting) # Ensure dhcp-failed interface was added for dhclient hook + self.cli_delete(ethernet_path + [interface, 'vif', vif, 'address']) + def test_02_site_to_site(self): self.cli_set(base_path + ['ike-group', ike_group, 'key-exchange', 'ikev2']) - # Site to site local_address = '192.0.2.10' priority = '20' life_bytes = '100000' life_packets = '2000000' + + # vpn ipsec auth psk <tag> id <x.x.x.x> + self.cli_set(base_path + ['authentication', 'psk', connection_name, 'id', local_id]) + self.cli_set(base_path + ['authentication', 'psk', connection_name, 'id', remote_id]) + self.cli_set(base_path + ['authentication', 'psk', connection_name, 'id', local_address]) + self.cli_set(base_path + ['authentication', 'psk', connection_name, 'id', peer_ip]) + self.cli_set(base_path + ['authentication', 'psk', connection_name, 'secret', secret]) + + # Site to site peer_base_path = base_path + ['site-to-site', 'peer', connection_name] self.cli_set(base_path + ['esp-group', esp_group, 'life-bytes', life_bytes]) self.cli_set(base_path + ['esp-group', esp_group, 'life-packets', life_packets]) self.cli_set(peer_base_path + ['authentication', 'mode', 'pre-shared-secret']) - self.cli_set(peer_base_path + ['authentication', 'pre-shared-secret', secret]) self.cli_set(peer_base_path + ['ike-group', ike_group]) self.cli_set(peer_base_path + ['default-esp-group', esp_group]) self.cli_set(peer_base_path + ['local-address', local_address]) @@ -227,12 +245,14 @@ class TestVPNIPsec(VyOSUnitTestSHIM.TestCase): self.assertIn(line, swanctl_conf) swanctl_secrets_lines = [ - f'id-local = {local_address} # dhcp:no', - f'id-remote_{peer_ip.replace(".","-")} = {peer_ip}', + f'id-{regex_uuid4} = "{local_id}"', + f'id-{regex_uuid4} = "{remote_id}"', + f'id-{regex_uuid4} = "{local_address}"', + f'id-{regex_uuid4} = "{peer_ip}"', f'secret = "{secret}"' ] for line in swanctl_secrets_lines: - self.assertIn(line, swanctl_conf) + self.assertRegex(swanctl_conf, fr'{line}') def test_03_site_to_site_vti(self): @@ -246,10 +266,15 @@ class TestVPNIPsec(VyOSUnitTestSHIM.TestCase): # VTI interface self.cli_set(vti_path + [vti, 'address', '10.1.1.1/24']) + # vpn ipsec auth psk <tag> id <x.x.x.x> + self.cli_set(base_path + ['authentication', 'psk', connection_name, 'id', local_id]) + self.cli_set(base_path + ['authentication', 'psk', connection_name, 'id', remote_id]) + self.cli_set(base_path + ['authentication', 'psk', connection_name, 'id', peer_ip]) + self.cli_set(base_path + ['authentication', 'psk', connection_name, 'secret', secret]) + # Site to site peer_base_path = base_path + ['site-to-site', 'peer', connection_name] self.cli_set(peer_base_path + ['authentication', 'mode', 'pre-shared-secret']) - self.cli_set(peer_base_path + ['authentication', 'pre-shared-secret', secret]) self.cli_set(peer_base_path + ['connection-type', 'none']) self.cli_set(peer_base_path + ['force-udp-encapsulation']) self.cli_set(peer_base_path + ['ike-group', ike_group]) @@ -292,12 +317,12 @@ class TestVPNIPsec(VyOSUnitTestSHIM.TestCase): self.assertIn(line, swanctl_conf) swanctl_secrets_lines = [ - f'id-local = {local_address} # dhcp:no', - f'id-remote_{peer_ip.replace(".","-")} = {peer_ip}', + f'id-{regex_uuid4} = "{local_id}"', + f'id-{regex_uuid4} = "{remote_id}"', f'secret = "{secret}"' ] for line in swanctl_secrets_lines: - self.assertIn(line, swanctl_conf) + self.assertRegex(swanctl_conf, fr'{line}') def test_04_dmvpn(self): @@ -334,6 +359,7 @@ class TestVPNIPsec(VyOSUnitTestSHIM.TestCase): self.cli_set(base_path + ['ike-group', ike_group, 'proposal', '2', 'dh-group', '2']) self.cli_set(base_path + ['ike-group', ike_group, 'proposal', '2', 'encryption', 'aes256']) self.cli_set(base_path + ['ike-group', ike_group, 'proposal', '2', 'hash', 'sha1']) + self.cli_set(base_path + ['ike-group', ike_group, 'proposal', '2', 'prf', 'prfsha1']) # Profile self.cli_set(base_path + ['profile', 'NHRPVPN', 'authentication', 'mode', 'pre-shared-secret']) @@ -346,7 +372,7 @@ class TestVPNIPsec(VyOSUnitTestSHIM.TestCase): swanctl_conf = read_file(swanctl_file) swanctl_lines = [ - f'proposals = aes128-sha1-modp1024,aes256-sha1-modp1024', + f'proposals = aes128-sha1-modp1024,aes256-sha1-prfsha1-modp1024', f'version = 1', f'rekey_time = {ike_lifetime}s', f'rekey_time = {esp_lifetime}s', @@ -450,9 +476,15 @@ class TestVPNIPsec(VyOSUnitTestSHIM.TestCase): self.cli_set(base_path + ['options', 'interface', 'tun1']) self.cli_set(base_path + ['ike-group', ike_group, 'key-exchange', 'ikev2']) + # vpn ipsec auth psk <tag> id <x.x.x.x> + self.cli_set(base_path + ['authentication', 'psk', connection_name, 'id', local_id]) + self.cli_set(base_path + ['authentication', 'psk', connection_name, 'id', remote_id]) + self.cli_set(base_path + ['authentication', 'psk', connection_name, 'id', local_address]) + self.cli_set(base_path + ['authentication', 'psk', connection_name, 'id', peer_ip]) + self.cli_set(base_path + ['authentication', 'psk', connection_name, 'secret', secret]) + self.cli_set(peer_base_path + ['authentication', 'local-id', local_id]) self.cli_set(peer_base_path + ['authentication', 'mode', 'pre-shared-secret']) - self.cli_set(peer_base_path + ['authentication', 'pre-shared-secret', secret]) self.cli_set(peer_base_path + ['authentication', 'remote-id', remote_id]) self.cli_set(peer_base_path + ['connection-type', 'initiate']) self.cli_set(peer_base_path + ['ike-group', ike_group]) @@ -482,15 +514,15 @@ class TestVPNIPsec(VyOSUnitTestSHIM.TestCase): self.assertIn(line, swanctl_conf) swanctl_secrets_lines = [ - f'id-local = {local_address} # dhcp:no', - f'id-remote_{peer_ip.replace(".","-")} = {peer_ip}', - f'id-localid = {local_id}', - f'id-remoteid = {remote_id}', + f'id-{regex_uuid4} = "{local_id}"', + f'id-{regex_uuid4} = "{remote_id}"', + f'id-{regex_uuid4} = "{peer_ip}"', + f'id-{regex_uuid4} = "{local_address}"', f'secret = "{secret}"', ] for line in swanctl_secrets_lines: - self.assertIn(line, swanctl_conf) + self.assertRegex(swanctl_conf, fr'{line}') # Verify charon configuration charon_conf = read_file(charon_file) diff --git a/smoketest/scripts/cli/test_vpn_openconnect.py b/smoketest/scripts/cli/test_vpn_openconnect.py index 8572d6d66..ec8ecacb9 100755 --- a/smoketest/scripts/cli/test_vpn_openconnect.py +++ b/smoketest/scripts/cli/test_vpn_openconnect.py @@ -18,6 +18,7 @@ import unittest from base_vyostest_shim import VyOSUnitTestSHIM +from vyos.template import ip_from_cidr from vyos.util import process_named_running from vyos.util import read_file @@ -52,6 +53,9 @@ config_file = '/run/ocserv/ocserv.conf' auth_file = '/run/ocserv/ocpasswd' otp_file = '/run/ocserv/users.oath' +listen_if = 'dum116' +listen_address = '100.64.0.1/32' + class TestVPNOpenConnect(VyOSUnitTestSHIM.TestCase): @classmethod def setUpClass(cls): @@ -61,6 +65,8 @@ class TestVPNOpenConnect(VyOSUnitTestSHIM.TestCase): # out the current configuration :) cls.cli_delete(cls, base_path) + cls.cli_set(cls, ['interfaces', 'dummy', listen_if, 'address', listen_address]) + cls.cli_set(cls, pki_path + ['ca', 'openconnect', 'certificate', cert_data.replace('\n','')]) cls.cli_set(cls, pki_path + ['certificate', 'openconnect', 'certificate', cert_data.replace('\n','')]) cls.cli_set(cls, pki_path + ['certificate', 'openconnect', 'private', 'key', key_data.replace('\n','')]) @@ -68,6 +74,7 @@ class TestVPNOpenConnect(VyOSUnitTestSHIM.TestCase): @classmethod def tearDownClass(cls): cls.cli_delete(cls, pki_path) + cls.cli_delete(cls, ['interfaces', 'dummy', listen_if]) super(TestVPNOpenConnect, cls).tearDownClass() def tearDown(self): @@ -104,6 +111,9 @@ class TestVPNOpenConnect(VyOSUnitTestSHIM.TestCase): self.cli_set(base_path + ['ssl', 'ca-certificate', 'openconnect']) self.cli_set(base_path + ['ssl', 'certificate', 'openconnect']) + listen_ip_no_cidr = ip_from_cidr(listen_address) + self.cli_set(base_path + ['listen-address', listen_ip_no_cidr]) + self.cli_commit() # Verify configuration @@ -111,10 +121,15 @@ class TestVPNOpenConnect(VyOSUnitTestSHIM.TestCase): # authentication mode local password-otp self.assertIn(f'auth = "plain[passwd=/run/ocserv/ocpasswd,otp=/run/ocserv/users.oath]"', daemon_config) + self.assertIn(f'listen-host = {listen_ip_no_cidr}', daemon_config) self.assertIn(f'ipv4-network = {v4_subnet}', daemon_config) self.assertIn(f'ipv6-network = {v6_prefix}', daemon_config) self.assertIn(f'ipv6-subnet-prefix = {v6_len}', daemon_config) + # defaults + self.assertIn(f'tcp-port = 443', daemon_config) + self.assertIn(f'udp-port = 443', daemon_config) + for ns in name_server: self.assertIn(f'dns = {ns}', daemon_config) for domain in split_dns: diff --git a/smoketest/scripts/system/test_module_load.py b/smoketest/scripts/system/test_module_load.py index 76a41ac4d..bd30c57ec 100755 --- a/smoketest/scripts/system/test_module_load.py +++ b/smoketest/scripts/system/test_module_load.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2019-2020 VyOS maintainers and contributors +# Copyright (C) 2019-2023 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 @@ -23,8 +23,7 @@ modules = { "intel_qat": ["qat_200xx", "qat_200xxvf", "qat_c3xxx", "qat_c3xxxvf", "qat_c62x", "qat_c62xvf", "qat_d15xx", "qat_d15xxvf", "qat_dh895xcc", "qat_dh895xccvf"], - "accel_ppp": ["ipoe", "vlan_mon"], - "misc": ["wireguard"] + "accel_ppp": ["ipoe", "vlan_mon"] } class TestKernelModules(unittest.TestCase): diff --git a/src/conf_mode/config_mgmt.py b/src/conf_mode/config_mgmt.py new file mode 100755 index 000000000..c681a8405 --- /dev/null +++ b/src/conf_mode/config_mgmt.py @@ -0,0 +1,96 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2023 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 vyos import ConfigError +from vyos.config import Config +from vyos.config_mgmt import ConfigMgmt +from vyos.config_mgmt import commit_post_hook_dir, commit_hooks + +def get_config(config=None): + if config: + conf = config + else: + conf = Config() + + base = ['system', 'config-management'] + if not conf.exists(base): + return None + + mgmt = ConfigMgmt(config=conf) + + return mgmt + +def verify(_mgmt): + return + +def generate(mgmt): + if mgmt is None: + return + + mgmt.initialize_revision() + +def apply(mgmt): + if mgmt is None: + return + + locations = mgmt.locations + archive_target = os.path.join(commit_post_hook_dir, + commit_hooks['commit_archive']) + if locations: + try: + os.symlink('/usr/bin/config-mgmt', archive_target) + except FileExistsError: + pass + except OSError as exc: + raise ConfigError from exc + else: + try: + os.unlink(archive_target) + except FileNotFoundError: + pass + except OSError as exc: + raise ConfigError from exc + + revisions = mgmt.max_revisions + revision_target = os.path.join(commit_post_hook_dir, + commit_hooks['commit_revision']) + if revisions > 0: + try: + os.symlink('/usr/bin/config-mgmt', revision_target) + except FileExistsError: + pass + except OSError as exc: + raise ConfigError from exc + else: + try: + os.unlink(revision_target) + except FileNotFoundError: + pass + except OSError as exc: + raise ConfigError from exc + +if __name__ == '__main__': + try: + c = get_config() + verify(c) + generate(c) + apply(c) + except ConfigError as e: + print(e) + sys.exit(1) diff --git a/src/conf_mode/container.py b/src/conf_mode/container.py index ac3dc536b..08861053d 100755 --- a/src/conf_mode/container.py +++ b/src/conf_mode/container.py @@ -40,20 +40,7 @@ airbag.enable() config_containers_registry = '/etc/containers/registries.conf' config_containers_storage = '/etc/containers/storage.conf' - -def _run_rerun(container_cmd): - counter = 0 - while True: - if counter >= 10: - break - try: - _cmd(container_cmd) - break - except: - counter = counter +1 - sleep(0.5) - - return None +systemd_unit_path = '/run/systemd/system' def _cmd(command): if os.path.exists('/tmp/vyos.container.debug'): @@ -86,9 +73,28 @@ def get_config(config=None): # Merge per-container default values if 'name' in container: default_values = defaults(base + ['name']) + if 'port' in default_values: + del default_values['port'] + if 'volume' in default_values: + del default_values['volume'] for name in container['name']: container['name'][name] = dict_merge(default_values, container['name'][name]) + # XXX: T2665: we can not safely rely on the defaults() when there are + # tagNodes in place, it is better to blend in the defaults manually. + if 'port' in container['name'][name]: + for port in container['name'][name]['port']: + default_values = defaults(base + ['name', 'port']) + container['name'][name]['port'][port] = dict_merge( + default_values, container['name'][name]['port'][port]) + # XXX: T2665: we can not safely rely on the defaults() when there are + # tagNodes in place, it is better to blend in the defaults manually. + if 'volume' in container['name'][name]: + for volume in container['name'][name]['volume']: + default_values = defaults(base + ['name', 'volume']) + container['name'][name]['volume'][volume] = dict_merge( + default_values, container['name'][name]['volume'][volume]) + # Delete container network, delete containers tmp = node_changed(conf, base + ['network']) if tmp: container.update({'network_remove' : tmp}) @@ -122,7 +128,7 @@ def verify(container): # of image upgrade and deletion. image = container_config['image'] if run(f'podman image exists {image}') != 0: - Warning(f'Image "{image}" used in contianer "{name}" does not exist '\ + Warning(f'Image "{image}" used in container "{name}" does not exist '\ f'locally. Please use "add container image {image}" to add it '\ f'to the system! Container "{name}" will not be started!') @@ -136,9 +142,6 @@ def verify(container): raise ConfigError(f'Container network "{network_name}" does not exist!') if 'address' in container_config['network'][network_name]: - if 'network' not in container_config: - raise ConfigError(f'Can not use "address" without "network" for container "{name}"!') - address = container_config['network'][network_name]['address'] network = None if is_ipv4(address): @@ -184,6 +187,11 @@ def verify(container): if not os.path.exists(source): raise ConfigError(f'Volume "{volume}" source path "{source}" does not exist!') + if 'port' in container_config: + for tmp in container_config['port']: + if not {'source', 'destination'} <= set(container_config['port'][tmp]): + raise ConfigError(f'Both "source" and "destination" must be specified for a port mapping!') + # If 'allow-host-networks' or 'network' not set. if 'allow_host_networks' not in container_config and 'network' not in container_config: raise ConfigError(f'Must either set "network" or "allow-host-networks" for container "{name}"!') @@ -220,6 +228,69 @@ def verify(container): return None +def generate_run_arguments(name, container_config): + image = container_config['image'] + memory = container_config['memory'] + shared_memory = container_config['shared_memory'] + restart = container_config['restart'] + + # Add capability options. Should be in uppercase + cap_add = '' + if 'cap_add' in container_config: + for c in container_config['cap_add']: + c = c.upper() + c = c.replace('-', '_') + cap_add += f' --cap-add={c}' + + # Add a host device to the container /dev/x:/dev/x + device = '' + if 'device' in container_config: + for dev, dev_config in container_config['device'].items(): + source_dev = dev_config['source'] + dest_dev = dev_config['destination'] + device += f' --device={source_dev}:{dest_dev}' + + # Check/set environment options "-e foo=bar" + env_opt = '' + if 'environment' in container_config: + for k, v in container_config['environment'].items(): + env_opt += f" --env \"{k}={v['value']}\"" + + # Publish ports + port = '' + if 'port' in container_config: + protocol = '' + for portmap in container_config['port']: + protocol = container_config['port'][portmap]['protocol'] + sport = container_config['port'][portmap]['source'] + dport = container_config['port'][portmap]['destination'] + port += f' --publish {sport}:{dport}/{protocol}' + + # Bind volume + volume = '' + if 'volume' in container_config: + for vol, vol_config in container_config['volume'].items(): + svol = vol_config['source'] + dvol = vol_config['destination'] + mode = vol_config['mode'] + volume += f' --volume {svol}:{dvol}:{mode}' + + container_base_cmd = f'--detach --interactive --tty --replace {cap_add} ' \ + f'--memory {memory}m --shm-size {shared_memory}m --memory-swap 0 --restart {restart} ' \ + f'--name {name} {device} {port} {volume} {env_opt}' + + if 'allow_host_networks' in container_config: + return f'{container_base_cmd} --net host {image}' + + ip_param = '' + networks = ",".join(container_config['network']) + for network in container_config['network']: + if 'address' in container_config['network'][network]: + address = container_config['network'][network]['address'] + ip_param = f'--ip {address}' + + return f'{container_base_cmd} --net {networks} {ip_param} {image}' + def generate(container): # bail out early - looks like removal from running config if not container: @@ -263,6 +334,15 @@ def generate(container): render(config_containers_registry, 'container/registries.conf.j2', container) render(config_containers_storage, 'container/storage.conf.j2', container) + if 'name' in container: + for name, container_config in container['name'].items(): + if 'disable' in container_config: + continue + + file_path = os.path.join(systemd_unit_path, f'vyos-container-{name}.service') + run_args = generate_run_arguments(name, container_config) + render(file_path, 'container/systemd-unit.j2', {'name': name, 'run_args': run_args}) + return None def apply(container): @@ -270,8 +350,12 @@ def apply(container): # Option "--force" allows to delete containers with any status if 'container_remove' in container: for name in container['container_remove']: - call(f'podman stop --time 3 {name}') - call(f'podman rm --force {name}') + file_path = os.path.join(systemd_unit_path, f'vyos-container-{name}.service') + call(f'systemctl stop vyos-container-{name}.service') + if os.path.exists(file_path): + os.unlink(file_path) + + call('systemctl daemon-reload') # Delete old networks if needed if 'network_remove' in container: @@ -282,6 +366,7 @@ def apply(container): os.unlink(tmp) # Add container + disabled_new = False if 'name' in container: for name, container_config in container['name'].items(): image = container_config['image'] @@ -295,70 +380,17 @@ def apply(container): # check if there is a container by that name running tmp = _cmd('podman ps -a --format "{{.Names}}"') if name in tmp: - _cmd(f'podman stop --time 3 {name}') - _cmd(f'podman rm --force {name}') + file_path = os.path.join(systemd_unit_path, f'vyos-container-{name}.service') + call(f'systemctl stop vyos-container-{name}.service') + if os.path.exists(file_path): + disabled_new = True + os.unlink(file_path) continue - memory = container_config['memory'] - restart = container_config['restart'] + cmd(f'systemctl restart vyos-container-{name}.service') - # Add capability options. Should be in uppercase - cap_add = '' - if 'cap_add' in container_config: - for c in container_config['cap_add']: - c = c.upper() - c = c.replace('-', '_') - cap_add += f' --cap-add={c}' - - # Add a host device to the container /dev/x:/dev/x - device = '' - if 'device' in container_config: - for dev, dev_config in container_config['device'].items(): - source_dev = dev_config['source'] - dest_dev = dev_config['destination'] - device += f' --device={source_dev}:{dest_dev}' - - # Check/set environment options "-e foo=bar" - env_opt = '' - if 'environment' in container_config: - for k, v in container_config['environment'].items(): - env_opt += f" -e \"{k}={v['value']}\"" - - # Publish ports - port = '' - if 'port' in container_config: - protocol = '' - for portmap in container_config['port']: - if 'protocol' in container_config['port'][portmap]: - protocol = container_config['port'][portmap]['protocol'] - protocol = f'/{protocol}' - else: - protocol = '/tcp' - sport = container_config['port'][portmap]['source'] - dport = container_config['port'][portmap]['destination'] - port += f' -p {sport}:{dport}{protocol}' - - # Bind volume - volume = '' - if 'volume' in container_config: - for vol, vol_config in container_config['volume'].items(): - svol = vol_config['source'] - dvol = vol_config['destination'] - volume += f' -v {svol}:{dvol}' - - container_base_cmd = f'podman run --detach --interactive --tty --replace {cap_add} ' \ - f'--memory {memory}m --memory-swap 0 --restart {restart} ' \ - f'--name {name} {device} {port} {volume} {env_opt}' - if 'allow_host_networks' in container_config: - _run_rerun(f'{container_base_cmd} --net host {image}') - else: - for network in container_config['network']: - ipparam = '' - if 'address' in container_config['network'][network]: - address = container_config['network'][network]['address'] - ipparam = f'--ip {address}' - - _run_rerun(f'{container_base_cmd} --net {network} {ipparam} {image}') + if disabled_new: + call('systemctl daemon-reload') return None diff --git a/src/conf_mode/firewall.py b/src/conf_mode/firewall.py index cbd9cbe90..20cf1ead1 100755 --- a/src/conf_mode/firewall.py +++ b/src/conf_mode/firewall.py @@ -26,13 +26,10 @@ 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_dependents, call_dependents # from vyos.configverify import verify_interface_exists +from vyos.firewall import fqdn_config_parse from vyos.firewall import geoip_update -from vyos.firewall import get_ips_domains_dict -from vyos.firewall import nft_add_set_elements -from vyos.firewall import nft_flush_set -from vyos.firewall import nft_init_set -from vyos.firewall import nft_update_set_elements from vyos.template import render from vyos.util import call from vyos.util import cmd @@ -45,7 +42,8 @@ from vyos import ConfigError from vyos import airbag airbag.enable() -policy_route_conf_script = '/usr/libexec/vyos/conf_mode/policy-route.py' +nat_conf_script = 'nat.py' +policy_route_conf_script = 'policy-route.py' nftables_conf = '/run/nftables.conf' @@ -67,7 +65,8 @@ valid_groups = [ 'address_group', 'domain_group', 'network_group', - 'port_group' + 'port_group', + 'interface_group' ] nested_group_types = [ @@ -162,7 +161,10 @@ def get_config(config=None): for zone in firewall['zone']: firewall['zone'][zone] = dict_merge(default_values, firewall['zone'][zone]) - firewall['policy_resync'] = bool('group' in firewall or node_changed(conf, base + ['group'])) + firewall['group_resync'] = bool('group' in firewall or node_changed(conf, base + ['group'])) + if firewall['group_resync']: + # 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) @@ -173,6 +175,8 @@ def get_config(config=None): firewall['geoip_updated'] = geoip_updated(conf, firewall) + fqdn_config_parse(firewall) + return firewall def verify_rule(firewall, rule_conf, ipv6): @@ -232,29 +236,28 @@ def verify_rule(firewall, rule_conf, ipv6): if side in rule_conf: side_conf = rule_conf[side] - if dict_search_args(side_conf, 'geoip', 'country_code'): - if 'address' in side_conf: - raise ConfigError('Address and GeoIP cannot both be defined') - - if dict_search_args(side_conf, 'group', 'address_group'): - raise ConfigError('Address-group and GeoIP cannot both be defined') - - if dict_search_args(side_conf, 'group', 'network_group'): - raise ConfigError('Network-group and GeoIP cannot both be defined') + if len({'address', 'fqdn', 'geoip'} & set(side_conf)) > 1: + raise ConfigError('Only one of address, fqdn or geoip can be specified') if 'group' in side_conf: - if {'address_group', 'network_group'} <= set(side_conf['group']): - raise ConfigError('Only one address-group or network-group can be specified') + if len({'address_group', 'network_group', 'domain_group'} & set(side_conf['group'])) > 1: + raise ConfigError('Only one address-group, network-group or domain-group can be specified') for group in valid_groups: if group in side_conf['group']: group_name = side_conf['group'][group] + fw_group = f'ipv6_{group}' if ipv6 and group in ['address_group', 'network_group'] else group + error_group = fw_group.replace("_", "-") + + if group in ['address_group', 'network_group', 'domain_group']: + types = [t for t in ['address', 'fqdn', 'geoip'] if t in side_conf] + if types: + raise ConfigError(f'{error_group} and {types[0]} cannot both be defined') + if group_name and group_name[0] == '!': group_name = group_name[1:] - fw_group = f'ipv6_{group}' if ipv6 and group in ['address_group', 'network_group'] else group - error_group = fw_group.replace("_", "-") group_obj = dict_search_args(firewall, 'group', fw_group, group_name) if group_obj is None: @@ -274,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') @@ -281,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) @@ -466,42 +469,23 @@ def post_apply_trap(firewall): cmd(base_cmd + ' '.join(objects)) -def resync_policy_route(): - # Update policy route as firewall groups were updated - tmp, out = rc_cmd(policy_route_conf_script) - if tmp > 0: - Warning(f'Failed to re-apply policy route configuration! {out}') - def apply(firewall): install_result, output = rc_cmd(f'nft -f {nftables_conf}') if install_result == 1: raise ConfigError(f'Failed to apply firewall: {output}') - # set firewall group domain-group xxx - if 'group' in firewall: - if 'domain_group' in firewall['group']: - # T970 Enable a resolver (systemd daemon) that checks - # domain-group addresses and update entries for domains by timeout - # If router loaded without internet connection or for synchronization - call('systemctl restart vyos-domain-group-resolve.service') - for group, group_config in firewall['group']['domain_group'].items(): - domains = [] - if group_config.get('address') is not None: - for address in group_config.get('address'): - domains.append(address) - # Add elements to domain-group, try to resolve domain => ip - # and add elements to nft set - ip_dict = get_ips_domains_dict(domains) - elements = sum(ip_dict.values(), []) - nft_init_set(f'D_{group}') - nft_add_set_elements(f'D_{group}', elements) - else: - call('systemctl stop vyos-domain-group-resolve.service') - apply_sysfs(firewall) - if firewall['policy_resync']: - resync_policy_route() + if firewall['group_resync']: + call_dependents() + + # T970 Enable a resolver (systemd daemon) that checks + # domain-group/fqdn addresses and update entries for domains by timeout + # If router loaded without internet connection or for synchronization + domain_action = 'stop' + if dict_search_args(firewall, 'group', 'domain_group') or firewall['ip_fqdn'] or firewall['ip6_fqdn']: + domain_action = 'restart' + call(f'systemctl {domain_action} vyos-domain-resolver.service') if firewall['geoip_updated']: # Call helper script to Update set contents diff --git a/src/conf_mode/flow_accounting_conf.py b/src/conf_mode/flow_accounting_conf.py index 7e16235c1..f67f1710e 100755 --- a/src/conf_mode/flow_accounting_conf.py +++ b/src/conf_mode/flow_accounting_conf.py @@ -38,7 +38,7 @@ airbag.enable() uacctd_conf_path = '/run/pmacct/uacctd.conf' systemd_service = 'uacctd.service' -systemd_override = f'/etc/systemd/system/{systemd_service}.d/override.conf' +systemd_override = f'/run/systemd/system/{systemd_service}.d/override.conf' nftables_nflog_table = 'raw' nftables_nflog_chain = 'VYOS_CT_PREROUTING_HOOK' egress_nftables_nflog_table = 'inet mangle' @@ -192,7 +192,7 @@ def verify(flow_config): raise ConfigError("All sFlow servers must use the same IP protocol") else: sflow_collector_ipver = ip_address(server).version - + # check if vrf is defined for Sflow sflow_vrf = None if 'vrf' in flow_config: diff --git a/src/conf_mode/high-availability.py b/src/conf_mode/high-availability.py index 8a959dc79..79e407efd 100755 --- a/src/conf_mode/high-availability.py +++ b/src/conf_mode/high-availability.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2018-2022 VyOS maintainers and contributors +# Copyright (C) 2018-2023 VyOS maintainers and contributors # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 or later as @@ -28,6 +28,7 @@ from vyos.template import render from vyos.template import is_ipv4 from vyos.template import is_ipv6 from vyos.util import call +from vyos.util import dict_search from vyos.xml import defaults from vyos import ConfigError from vyos import airbag @@ -49,10 +50,27 @@ def get_config(config=None): # We have gathered the dict representation of the CLI, but there are default # options which we need to update into the dictionary retrived. if 'vrrp' in ha: + if dict_search('vrrp.global_parameters.garp', ha) != None: + default_values = defaults(base_vrrp + ['global-parameters', 'garp']) + ha['vrrp']['global_parameters']['garp'] = dict_merge( + default_values, ha['vrrp']['global_parameters']['garp']) + if 'group' in ha['vrrp']: - default_values_vrrp = defaults(base_vrrp + ['group']) + default_values = defaults(base_vrrp + ['group']) + default_values_garp = defaults(base_vrrp + ['group', 'garp']) + + # XXX: T2665: we can not safely rely on the defaults() when there are + # tagNodes in place, it is better to blend in the defaults manually. + if 'garp' in default_values: + del default_values['garp'] for group in ha['vrrp']['group']: - ha['vrrp']['group'][group] = dict_merge(default_values_vrrp, ha['vrrp']['group'][group]) + ha['vrrp']['group'][group] = dict_merge(default_values, ha['vrrp']['group'][group]) + + # XXX: T2665: we can not safely rely on the defaults() when there are + # tagNodes in place, it is better to blend in the defaults manually. + if 'garp' in ha['vrrp']['group'][group]: + ha['vrrp']['group'][group]['garp'] = dict_merge( + default_values_garp, ha['vrrp']['group'][group]['garp']) # Merge per virtual-server default values if 'virtual_server' in ha: @@ -144,8 +162,10 @@ def verify(ha): # Virtual-server if 'virtual_server' in ha: for vs, vs_config in ha['virtual_server'].items(): - if 'port' not in vs_config: - raise ConfigError(f'Port is required but not set for virtual-server "{vs}"') + if 'port' not in vs_config and 'fwmark' not in vs_config: + raise ConfigError(f'Port or fwmark is required but not set for virtual-server "{vs}"') + if 'port' in vs_config and 'fwmark' in vs_config: + raise ConfigError(f'Cannot set both port and fwmark for virtual-server "{vs}"') if 'real_server' not in vs_config: raise ConfigError(f'Real-server ip is required but not set for virtual-server "{vs}"') # Real-server diff --git a/src/conf_mode/http-api.py b/src/conf_mode/http-api.py index c196e272b..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 @@ -86,7 +92,7 @@ def get_config(config=None): if 'api_keys' in api_dict: keys_added = True - if 'gql' in api_dict: + if 'graphql' in api_dict: api_dict = dict_merge(defaults(base), api_dict) http_api.update(api_dict) @@ -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/https.py b/src/conf_mode/https.py index 7cd7ea42e..ce5e63928 100755 --- a/src/conf_mode/https.py +++ b/src/conf_mode/https.py @@ -37,7 +37,7 @@ from vyos import airbag airbag.enable() config_file = '/etc/nginx/sites-available/default' -systemd_override = r'/etc/systemd/system/nginx.service.d/override.conf' +systemd_override = r'/run/systemd/system/nginx.service.d/override.conf' cert_dir = '/etc/ssl/certs' key_dir = '/etc/ssl/private' certbot_dir = vyos.defaults.directories['certbot'] 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-geneve.py b/src/conf_mode/interfaces-geneve.py index 08cc3a48d..f6694ddde 100755 --- a/src/conf_mode/interfaces-geneve.py +++ b/src/conf_mode/interfaces-geneve.py @@ -14,14 +14,11 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see <http://www.gnu.org/licenses/>. -import os - from sys import exit from netifaces import interfaces from vyos.config import Config from vyos.configdict import get_interface_dict -from vyos.configdict import leaf_node_changed from vyos.configdict import is_node_changed from vyos.configverify import verify_address from vyos.configverify import verify_mtu_ipv6 @@ -49,13 +46,10 @@ def get_config(config=None): # GENEVE interfaces are picky and require recreation if certain parameters # change. But a GENEVE interface should - of course - not be re-created if # it's description or IP address is adjusted. Feels somehow logic doesn't it? - for cli_option in ['remote', 'vni']: - if leaf_node_changed(conf, base + [ifname, cli_option]): + for cli_option in ['remote', 'vni', 'parameters']: + if is_node_changed(conf, base + [ifname, cli_option]): geneve.update({'rebuild_required': {}}) - if is_node_changed(conf, base + [ifname, 'parameters']): - geneve.update({'rebuild_required': {}}) - return geneve def verify(geneve): diff --git a/src/conf_mode/interfaces-input.py b/src/conf_mode/interfaces-input.py new file mode 100755 index 000000000..ad248843d --- /dev/null +++ b/src/conf_mode/interfaces-input.py @@ -0,0 +1,70 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2023 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 vyos.config import Config +from vyos.configdict import get_interface_dict +from vyos.configverify import verify_mirror_redirect +from vyos.ifconfig import InputIf +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', 'input'] + _, ifb = get_interface_dict(conf, base) + + return ifb + +def verify(ifb): + if 'deleted' in ifb: + return None + + verify_mirror_redirect(ifb) + return None + +def generate(ifb): + return None + +def apply(ifb): + d = InputIf(ifb['ifname']) + + # Remove input interface + if 'deleted' in ifb: + d.remove() + else: + d.update(ifb) + + 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-pppoe.py b/src/conf_mode/interfaces-pppoe.py index e2fdc7a42..5f0b76f90 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 @@ -55,7 +54,8 @@ def get_config(config=None): # All parameters that can be changed on-the-fly (like interface description) # should not lead to a reconnect! for options in ['access-concentrator', 'connect-on-demand', 'service-name', - 'source-interface', 'vrf', 'no-default-route', 'authentication']: + 'source-interface', 'vrf', 'no-default-route', + 'authentication', 'host_uniq']: if is_node_changed(conf, base + [ifname, options]): pppoe.update({'shutdown_required': {}}) # bail out early - no need to further process other nodes diff --git a/src/conf_mode/interfaces-pseudo-ethernet.py b/src/conf_mode/interfaces-pseudo-ethernet.py index 4c65bc0b6..dce5c2358 100755 --- a/src/conf_mode/interfaces-pseudo-ethernet.py +++ b/src/conf_mode/interfaces-pseudo-ethernet.py @@ -21,7 +21,7 @@ from vyos.config import Config from vyos.configdict import get_interface_dict from vyos.configdict import is_node_changed from vyos.configdict import is_source_interface -from vyos.configdict import leaf_node_changed +from vyos.configdict import is_node_changed from vyos.configverify import verify_vrf from vyos.configverify import verify_address from vyos.configverify import verify_bridge_delete @@ -51,7 +51,7 @@ def get_config(config=None): mode = is_node_changed(conf, ['mode']) if mode: peth.update({'shutdown_required' : {}}) - if leaf_node_changed(conf, base + [ifname, 'mode']): + if is_node_changed(conf, base + [ifname, 'mode']): peth.update({'rebuild_required': {}}) if 'source_interface' in peth: diff --git a/src/conf_mode/interfaces-sstpc.py b/src/conf_mode/interfaces-sstpc.py new file mode 100755 index 000000000..b5cc4cf4e --- /dev/null +++ b/src/conf_mode/interfaces-sstpc.py @@ -0,0 +1,145 @@ +#!/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 not dict_search('server', sstpc): + raise ConfigError('Remote SSTP server must be specified!') + + if not dict_search('ssl.ca_certificate', sstpc): + 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-tunnel.py b/src/conf_mode/interfaces-tunnel.py index acef1fda7..e2701d9d3 100755 --- a/src/conf_mode/interfaces-tunnel.py +++ b/src/conf_mode/interfaces-tunnel.py @@ -21,7 +21,7 @@ from netifaces import interfaces from vyos.config import Config from vyos.configdict import get_interface_dict -from vyos.configdict import leaf_node_changed +from vyos.configdict import is_node_changed from vyos.configverify import verify_address from vyos.configverify import verify_bridge_delete from vyos.configverify import verify_interface_exists @@ -52,7 +52,7 @@ def get_config(config=None): ifname, tunnel = get_interface_dict(conf, base) if 'deleted' not in tunnel: - tmp = leaf_node_changed(conf, base + [ifname, 'encapsulation']) + tmp = is_node_changed(conf, base + [ifname, 'encapsulation']) if tmp: tunnel.update({'encapsulation_changed': {}}) # We also need to inspect other configured tunnels as there are Kernel 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/interfaces-vxlan.py b/src/conf_mode/interfaces-vxlan.py index af2d0588d..b1536148c 100755 --- a/src/conf_mode/interfaces-vxlan.py +++ b/src/conf_mode/interfaces-vxlan.py @@ -52,13 +52,11 @@ def get_config(config=None): # VXLAN interfaces are picky and require recreation if certain parameters # change. But a VXLAN interface should - of course - not be re-created if # it's description or IP address is adjusted. Feels somehow logic doesn't it? - for cli_option in ['external', 'gpe', 'group', 'port', 'remote', + for cli_option in ['parameters', 'external', 'gpe', 'group', 'port', 'remote', 'source-address', 'source-interface', 'vni']: - if leaf_node_changed(conf, base + [ifname, cli_option]): + if is_node_changed(conf, base + [ifname, cli_option]): vxlan.update({'rebuild_required': {}}) - - if is_node_changed(conf, base + [ifname, 'parameters']): - vxlan.update({'rebuild_required': {}}) + break # We need to verify that no other VXLAN tunnel is configured when external # mode is in use - Linux Kernel limitation diff --git a/src/conf_mode/interfaces-wireguard.py b/src/conf_mode/interfaces-wireguard.py index 8d738f55e..762bad94f 100755 --- a/src/conf_mode/interfaces-wireguard.py +++ b/src/conf_mode/interfaces-wireguard.py @@ -87,6 +87,8 @@ def verify(wireguard): 'cannot be used for the interface!') # run checks on individual configured WireGuard peer + public_keys = [] + for tmp in wireguard['peer']: peer = wireguard['peer'][tmp] @@ -100,6 +102,11 @@ def verify(wireguard): raise ConfigError('Both Wireguard port and address must be defined ' f'for peer "{tmp}" if either one of them is set!') + if peer['public_key'] in public_keys: + raise ConfigError(f'Duplicate public-key defined on peer "{tmp}"') + + public_keys.append(peer['public_key']) + def apply(wireguard): tmp = WireGuardIf(wireguard['ifname']) if 'deleted' in wireguard: diff --git a/src/conf_mode/nat.py b/src/conf_mode/nat.py index 8b1a5a720..9f8221514 100755 --- a/src/conf_mode/nat.py +++ b/src/conf_mode/nat.py @@ -32,6 +32,7 @@ from vyos.util import cmd from vyos.util import run from vyos.util import check_kmod from vyos.util import dict_search +from vyos.util import dict_search_args from vyos.validate import is_addr_assigned from vyos.xml import defaults from vyos import ConfigError @@ -47,6 +48,13 @@ else: nftables_nat_config = '/run/nftables_nat.conf' nftables_static_nat_conf = '/run/nftables_static-nat-rules.nft' +valid_groups = [ + 'address_group', + 'domain_group', + 'network_group', + 'port_group' +] + def get_handler(json, chain, target): """ Get nftable rule handler number of given chain/target combination. Handler is required when adding NAT/Conntrack helper targets """ @@ -60,7 +68,7 @@ def get_handler(json, chain, target): return None -def verify_rule(config, err_msg): +def verify_rule(config, err_msg, groups_dict): """ Common verify steps used for both source and destination NAT """ if (dict_search('translation.port', config) != None or @@ -78,6 +86,45 @@ def verify_rule(config, err_msg): 'statically maps a whole network of addresses onto another\n' \ 'network of addresses') + for side in ['destination', 'source']: + if side in config: + side_conf = config[side] + + if len({'address', 'fqdn'} & set(side_conf)) > 1: + raise ConfigError('Only one of address, fqdn or geoip can be specified') + + if 'group' in side_conf: + if len({'address_group', 'network_group', 'domain_group'} & set(side_conf['group'])) > 1: + raise ConfigError('Only one address-group, network-group or domain-group can be specified') + + for group in valid_groups: + if group in side_conf['group']: + group_name = side_conf['group'][group] + error_group = group.replace("_", "-") + + if group in ['address_group', 'network_group', 'domain_group']: + types = [t for t in ['address', 'fqdn'] if t in side_conf] + if types: + raise ConfigError(f'{error_group} and {types[0]} cannot both be defined') + + if group_name and group_name[0] == '!': + group_name = group_name[1:] + + group_obj = dict_search_args(groups_dict, group, group_name) + + if group_obj is None: + raise ConfigError(f'Invalid {error_group} "{group_name}" on firewall rule') + + if not group_obj: + Warning(f'{error_group} "{group_name}" has no members!') + + if dict_search_args(side_conf, 'group', 'port_group'): + if 'protocol' not in config: + raise ConfigError('Protocol must be defined if specifying a port-group') + + if config['protocol'] not in ['tcp', 'udp', 'tcp_udp']: + raise ConfigError('Protocol must be tcp, udp, or tcp_udp when specifying a port-group') + def get_config(config=None): if config: conf = config @@ -105,16 +152,20 @@ def get_config(config=None): condensed_json = jmespath.search(pattern, nftable_json) if not conf.exists(base): - nat['helper_functions'] = 'remove' - - # Retrieve current table handler positions - nat['pre_ct_ignore'] = get_handler(condensed_json, 'PREROUTING', 'VYOS_CT_HELPER') - nat['pre_ct_conntrack'] = get_handler(condensed_json, 'PREROUTING', 'NAT_CONNTRACK') - nat['out_ct_ignore'] = get_handler(condensed_json, 'OUTPUT', 'VYOS_CT_HELPER') - nat['out_ct_conntrack'] = get_handler(condensed_json, 'OUTPUT', 'NAT_CONNTRACK') + if get_handler(condensed_json, 'PREROUTING', 'VYOS_CT_HELPER'): + nat['helper_functions'] = 'remove' + + # Retrieve current table handler positions + nat['pre_ct_ignore'] = get_handler(condensed_json, 'PREROUTING', 'VYOS_CT_HELPER') + nat['pre_ct_conntrack'] = get_handler(condensed_json, 'PREROUTING', 'NAT_CONNTRACK') + nat['out_ct_ignore'] = get_handler(condensed_json, 'OUTPUT', 'VYOS_CT_HELPER') + nat['out_ct_conntrack'] = get_handler(condensed_json, 'OUTPUT', 'NAT_CONNTRACK') nat['deleted'] = '' return nat + nat['firewall_group'] = conf.get_config_dict(['firewall', 'group'], key_mangling=('-', '_'), get_first_key=True, + no_tag_node_value_mangle=True) + # check if NAT connection tracking helpers need to be set up - this has to # be done only once if not get_handler(condensed_json, 'PREROUTING', 'NAT_CONNTRACK'): @@ -146,6 +197,10 @@ def verify(nat): if config['outbound_interface'] not in 'any' and config['outbound_interface'] not in interfaces(): Warning(f'rule "{rule}" interface "{config["outbound_interface"]}" does not exist on this system') + if not dict_search('translation.address', config) and not dict_search('translation.port', config): + if 'exclude' not in config: + raise ConfigError(f'{err_msg} translation requires address and/or port') + addr = dict_search('translation.address', config) if addr != None and addr != 'masquerade' and not is_ip_network(addr): for ip in addr.split('-'): @@ -153,7 +208,7 @@ def verify(nat): Warning(f'IP address {ip} does not exist on the system!') # common rule verification - verify_rule(config, err_msg) + verify_rule(config, err_msg, nat['firewall_group']) if dict_search('destination.rule', nat): @@ -166,8 +221,12 @@ def verify(nat): elif config['inbound_interface'] not in 'any' and config['inbound_interface'] not in interfaces(): Warning(f'rule "{rule}" interface "{config["inbound_interface"]}" does not exist on this system') + if not dict_search('translation.address', config) and not dict_search('translation.port', config): + if 'exclude' not in config: + raise ConfigError(f'{err_msg} translation requires address and/or port') + # common rule verification - verify_rule(config, err_msg) + verify_rule(config, err_msg, nat['firewall_group']) if dict_search('static.rule', nat): for rule, config in dict_search('static.rule', nat).items(): @@ -178,7 +237,7 @@ def verify(nat): 'inbound-interface not specified') # common rule verification - verify_rule(config, err_msg) + verify_rule(config, err_msg, nat['firewall_group']) return None @@ -204,6 +263,10 @@ def apply(nat): cmd(f'nft -f {nftables_nat_config}') cmd(f'nft -f {nftables_static_nat_conf}') + if not nat or 'deleted' in nat: + os.unlink(nftables_nat_config) + os.unlink(nftables_static_nat_conf) + return None if __name__ == '__main__': diff --git a/src/conf_mode/ntp.py b/src/conf_mode/ntp.py index 0ecb4d736..92cb73aab 100755 --- a/src/conf_mode/ntp.py +++ b/src/conf_mode/ntp.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2018-2022 VyOS maintainers and contributors +# Copyright (C) 2018-2023 VyOS maintainers and contributors # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 or later as @@ -21,26 +21,29 @@ from vyos.configdict import is_node_changed from vyos.configverify import verify_vrf from vyos.configverify import verify_interface_exists from vyos.util import call +from vyos.util import chmod_750 from vyos.util import get_interface_config from vyos.template import render from vyos import ConfigError from vyos import airbag airbag.enable() -config_file = r'/run/ntpd/ntpd.conf' -systemd_override = r'/etc/systemd/system/ntp.service.d/override.conf' +config_file = r'/run/chrony/chrony.conf' +systemd_override = r'/run/systemd/system/chrony.service.d/override.conf' +user_group = '_chrony' def get_config(config=None): if config: conf = config else: conf = Config() - base = ['system', 'ntp'] + base = ['service', 'ntp'] if not conf.exists(base): return None ntp = conf.get_config_dict(base, key_mangling=('-', '_'), get_first_key=True) ntp['config_file'] = config_file + ntp['user'] = user_group tmp = is_node_changed(conf, base + ['vrf']) if tmp: ntp.update({'restart_required': {}}) @@ -52,7 +55,7 @@ def verify(ntp): if not ntp: return None - if 'allow_clients' in ntp and 'server' not in ntp: + if 'server' not in ntp: raise ConfigError('NTP server not configured') verify_vrf(ntp) @@ -77,13 +80,17 @@ def generate(ntp): if not ntp: return None - render(config_file, 'ntp/ntpd.conf.j2', ntp) - render(systemd_override, 'ntp/override.conf.j2', ntp) + render(config_file, 'chrony/chrony.conf.j2', ntp, user=user_group, group=user_group) + render(systemd_override, 'chrony/override.conf.j2', ntp, user=user_group, group=user_group) + + # Ensure proper permission for chrony command socket + config_dir = os.path.dirname(config_file) + chmod_750(config_dir) return None def apply(ntp): - systemd_service = 'ntp.service' + systemd_service = 'chrony.service' # Reload systemd manager configuration call('systemctl daemon-reload') diff --git a/src/conf_mode/pki.py b/src/conf_mode/pki.py index 29ed7b1b7..54de467ca 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 @@ -55,6 +51,11 @@ sync_search = [ 'script': '/usr/libexec/vyos/conf_mode/interfaces-openvpn.py' }, { + 'keys': ['ca_certificate'], + 'path': ['interfaces', 'sstpc'], + 'script': '/usr/libexec/vyos/conf_mode/interfaces-sstpc.py' + }, + { 'keys': ['certificate', 'ca_certificate', 'local_key', 'remote_key'], 'path': ['vpn', 'ipsec'], 'script': '/usr/libexec/vyos/conf_mode/vpn_ipsec.py' @@ -121,6 +122,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 +293,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-interface.py b/src/conf_mode/policy-route-interface.py deleted file mode 100755 index 58c5fd93d..000000000 --- a/src/conf_mode/policy-route-interface.py +++ /dev/null @@ -1,132 +0,0 @@ -#!/usr/bin/env python3 -# -# Copyright (C) 2021 VyOS maintainers and contributors -# -# This program is free software; you can redistribute it and/or modify -# it under the terms of the GNU General Public License version 2 or later as -# published by the Free Software Foundation. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see <http://www.gnu.org/licenses/>. - -import os -import re - -from sys import argv -from sys import exit - -from vyos.config import Config -from vyos.ifconfig import Section -from vyos.template import render -from vyos.util import cmd -from vyos.util import run -from vyos import ConfigError -from vyos import airbag -airbag.enable() - -def get_config(config=None): - if config: - conf = config - else: - conf = Config() - - ifname = argv[1] - ifpath = Section.get_config_path(ifname) - if_policy_path = f'interfaces {ifpath} policy' - - if_policy = conf.get_config_dict(if_policy_path, key_mangling=('-', '_'), get_first_key=True, - no_tag_node_value_mangle=True) - - if_policy['ifname'] = ifname - if_policy['policy'] = conf.get_config_dict(['policy'], key_mangling=('-', '_'), get_first_key=True, - no_tag_node_value_mangle=True) - - return if_policy - -def verify_chain(table, chain): - # Verify policy route applied - code = run(f'nft list chain {table} {chain}') - return code == 0 - -def verify(if_policy): - # bail out early - looks like removal from running config - if not if_policy: - return None - - for route in ['route', 'route6']: - if route in if_policy: - if route not in if_policy['policy']: - raise ConfigError('Policy route not configured') - - route_name = if_policy[route] - - if route_name not in if_policy['policy'][route]: - raise ConfigError(f'Invalid policy route name "{name}"') - - nft_prefix = 'VYOS_PBR6_' if route == 'route6' else 'VYOS_PBR_' - nft_table = 'ip6 mangle' if route == 'route6' else 'ip mangle' - - if not verify_chain(nft_table, nft_prefix + route_name): - raise ConfigError('Policy route did not apply') - - return None - -def generate(if_policy): - return None - -def cleanup_rule(table, chain, ifname, new_name=None): - results = cmd(f'nft -a list chain {table} {chain}').split("\n") - retval = None - for line in results: - if f'ifname "{ifname}"' in line: - if new_name and f'jump {new_name}' in line: - # new_name is used to clear rules for any previously referenced chains - # returns true when rule exists and doesn't need to be created - retval = True - continue - - handle_search = re.search('handle (\d+)', line) - if handle_search: - cmd(f'nft delete rule {table} {chain} handle {handle_search[1]}') - return retval - -def apply(if_policy): - ifname = if_policy['ifname'] - - route_chain = 'VYOS_PBR_PREROUTING' - ipv6_route_chain = 'VYOS_PBR6_PREROUTING' - - if 'route' in if_policy: - name = 'VYOS_PBR_' + if_policy['route'] - rule_exists = cleanup_rule('ip mangle', route_chain, ifname, name) - - if not rule_exists: - cmd(f'nft insert rule ip mangle {route_chain} iifname {ifname} counter jump {name}') - else: - cleanup_rule('ip mangle', route_chain, ifname) - - if 'route6' in if_policy: - name = 'VYOS_PBR6_' + if_policy['route6'] - rule_exists = cleanup_rule('ip6 mangle', ipv6_route_chain, ifname, name) - - if not rule_exists: - cmd(f'nft insert rule ip6 mangle {ipv6_route_chain} iifname {ifname} counter jump {name}') - else: - cleanup_rule('ip6 mangle', ipv6_route_chain, ifname) - - 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/policy-route.py b/src/conf_mode/policy-route.py index 00539b9c7..40a32efb3 100755 --- a/src/conf_mode/policy-route.py +++ b/src/conf_mode/policy-route.py @@ -15,7 +15,6 @@ # along with this program. If not, see <http://www.gnu.org/licenses/>. import os -import re from json import loads from sys import exit @@ -25,7 +24,6 @@ from vyos.config import Config from vyos.template import render from vyos.util import cmd from vyos.util import dict_search_args -from vyos.util import dict_search_recursive from vyos.util import run from vyos import ConfigError from vyos import airbag @@ -34,48 +32,14 @@ airbag.enable() mark_offset = 0x7FFFFFFF nftables_conf = '/run/nftables_policy.conf' -ROUTE_PREFIX = 'VYOS_PBR_' -ROUTE6_PREFIX = 'VYOS_PBR6_' - -preserve_chains = [ - 'VYOS_PBR_PREROUTING', - 'VYOS_PBR_POSTROUTING', - 'VYOS_PBR6_PREROUTING', - 'VYOS_PBR6_POSTROUTING' -] - valid_groups = [ 'address_group', + 'domain_group', 'network_group', - 'port_group' + 'port_group', + 'interface_group' ] -group_set_prefix = { - 'A_': 'address_group', - 'A6_': 'ipv6_address_group', -# 'D_': 'domain_group', - 'M_': 'mac_group', - 'N_': 'network_group', - 'N6_': 'ipv6_network_group', - 'P_': 'port_group' -} - -def get_policy_interfaces(conf): - out = {} - interfaces = conf.get_config_dict(['interfaces'], key_mangling=('-', '_'), get_first_key=True, - no_tag_node_value_mangle=True) - def find_interfaces(iftype_conf, output={}, prefix=''): - for ifname, if_conf in iftype_conf.items(): - if 'policy' in if_conf: - output[prefix + ifname] = if_conf['policy'] - for vif in ['vif', 'vif_s', 'vif_c']: - if vif in if_conf: - output.update(find_interfaces(if_conf[vif], output, f'{prefix}{ifname}.')) - return output - for iftype, iftype_conf in interfaces.items(): - out.update(find_interfaces(iftype_conf)) - return out - def get_config(config=None): if config: conf = config @@ -88,7 +52,6 @@ def get_config(config=None): policy['firewall_group'] = conf.get_config_dict(['firewall', 'group'], key_mangling=('-', '_'), get_first_key=True, no_tag_node_value_mangle=True) - policy['interfaces'] = get_policy_interfaces(conf) return policy @@ -132,8 +95,8 @@ def verify_rule(policy, name, rule_conf, ipv6, rule_id): side_conf = rule_conf[side] if 'group' in side_conf: - if {'address_group', 'network_group'} <= set(side_conf['group']): - raise ConfigError('Only one address-group or network-group can be specified') + if len({'address_group', 'domain_group', 'network_group'} & set(side_conf['group'])) > 1: + raise ConfigError('Only one address-group, domain-group or network-group can be specified') for group in valid_groups: if group in side_conf['group']: @@ -168,73 +131,11 @@ def verify(policy): for rule_id, rule_conf in pol_conf['rule'].items(): verify_rule(policy, name, rule_conf, ipv6, rule_id) - for ifname, if_policy in policy['interfaces'].items(): - name = dict_search_args(if_policy, 'route') - ipv6_name = dict_search_args(if_policy, 'route6') - - if name and not dict_search_args(policy, 'route', name): - raise ConfigError(f'Policy route "{name}" is still referenced on interface {ifname}') - - if ipv6_name and not dict_search_args(policy, 'route6', ipv6_name): - raise ConfigError(f'Policy route6 "{ipv6_name}" is still referenced on interface {ifname}') - return None -def cleanup_commands(policy): - commands = [] - commands_chains = [] - commands_sets = [] - for table in ['ip mangle', 'ip6 mangle']: - route_node = 'route' if table == 'ip mangle' else 'route6' - chain_prefix = ROUTE_PREFIX if table == 'ip mangle' else ROUTE6_PREFIX - - json_str = cmd(f'nft -t -j list table {table}') - obj = loads(json_str) - if 'nftables' not in obj: - continue - for item in obj['nftables']: - if 'chain' in item: - chain = item['chain']['name'] - if chain in preserve_chains or not chain.startswith("VYOS_PBR"): - continue - - if dict_search_args(policy, route_node, chain.replace(chain_prefix, "", 1)) != None: - commands.append(f'flush chain {table} {chain}') - else: - commands_chains.append(f'delete chain {table} {chain}') - - if 'rule' in item: - rule = item['rule'] - chain = rule['chain'] - handle = rule['handle'] - - if chain not in preserve_chains: - continue - - target, _ = next(dict_search_recursive(rule['expr'], 'target')) - - if target.startswith(chain_prefix): - if dict_search_args(policy, route_node, target.replace(chain_prefix, "", 1)) == None: - commands.append(f'delete rule {table} {chain} handle {handle}') - - if 'set' in item: - set_name = item['set']['name'] - - for prefix, group_type in group_set_prefix.items(): - if set_name.startswith(prefix): - group_name = set_name.replace(prefix, "", 1) - if dict_search_args(policy, 'firewall_group', group_type, group_name) != None: - commands_sets.append(f'flush set {table} {set_name}') - else: - commands_sets.append(f'delete set {table} {set_name}') - - return commands + commands_chains + commands_sets - def generate(policy): if not os.path.exists(nftables_conf): policy['first_install'] = True - else: - policy['cleanup_commands'] = cleanup_commands(policy) render(nftables_conf, 'firewall/nftables-policy.j2', policy) return None diff --git a/src/conf_mode/policy.py b/src/conf_mode/policy.py index a0d288e91..331194fec 100755 --- a/src/conf_mode/policy.py +++ b/src/conf_mode/policy.py @@ -167,6 +167,11 @@ def verify(policy): continue for rule, rule_config in route_map_config['rule'].items(): + # Action 'deny' cannot be used with "continue" + # FRR does not validate it T4827 + if rule_config['action'] == 'deny' and 'continue' in rule_config: + raise ConfigError(f'rule {rule} "continue" cannot be used with action deny!') + # Specified community-list must exist tmp = dict_search('match.community.community_list', rule_config) diff --git a/src/conf_mode/protocols_bgp.py b/src/conf_mode/protocols_bgp.py index ff568d470..4f05957fa 100755 --- a/src/conf_mode/protocols_bgp.py +++ b/src/conf_mode/protocols_bgp.py @@ -14,8 +14,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 os - from sys import exit from sys import argv @@ -57,13 +55,18 @@ def get_config(config=None): # instead of the VRF instance. if vrf: bgp.update({'vrf' : vrf}) + bgp['dependent_vrfs'] = conf.get_config_dict(['vrf', 'name'], + key_mangling=('-', '_'), + get_first_key=True, + no_tag_node_value_mangle=True) + + bgp['dependent_vrfs'].update({'default': {'protocols': { + 'bgp': conf.get_config_dict(base_path, key_mangling=('-', '_'), + get_first_key=True, + no_tag_node_value_mangle=True)}}}) if not conf.exists(base): + # If bgp instance is deleted then mark it bgp.update({'deleted' : ''}) - if not vrf: - # We are running in the default VRF context, thus we can not delete - # our main BGP instance if there are dependent BGP VRF instances. - bgp['dependent_vrfs'] = conf.get_config_dict(['vrf', 'name'], - key_mangling=('-', '_'), get_first_key=True, no_tag_node_value_mangle=True) return bgp # We also need some additional information from the config, prefix-lists @@ -74,9 +77,91 @@ def get_config(config=None): tmp = conf.get_config_dict(['policy']) # Merge policy dict into "regular" config dict bgp = dict_merge(tmp, bgp) - return bgp + +def verify_vrf_as_import(search_vrf_name: str, afi_name: str, vrfs_config: dict) -> bool: + """ + :param search_vrf_name: search vrf name in import list + :type search_vrf_name: str + :param afi_name: afi/safi name + :type afi_name: str + :param vrfs_config: configuration dependents vrfs + :type vrfs_config: dict + :return: if vrf in import list retrun true else false + :rtype: bool + """ + for vrf_name, vrf_config in vrfs_config.items(): + import_list = dict_search( + f'protocols.bgp.address_family.{afi_name}.import.vrf', + vrf_config) + if import_list: + if search_vrf_name in import_list: + return True + return False + +def verify_vrf_import_options(afi_config: dict) -> bool: + """ + Search if afi contains one of options + :param afi_config: afi/safi + :type afi_config: dict + :return: if vrf contains rd and route-target options return true else false + :rtype: bool + """ + options = [ + f'rd.vpn.export', + f'route_target.vpn.import', + f'route_target.vpn.export', + f'route_target.vpn.both' + ] + for option in options: + if dict_search(option, afi_config): + return True + return False + +def verify_vrf_import(vrf_name: str, vrfs_config: dict, afi_name: str) -> bool: + """ + Verify if vrf exists and contain options + :param vrf_name: name of VRF + :type vrf_name: str + :param vrfs_config: dependent vrfs config + :type vrfs_config: dict + :param afi_name: afi/safi name + :type afi_name: str + :return: if vrf contains rd and route-target options return true else false + :rtype: bool + """ + if vrf_name != 'default': + verify_vrf({'vrf': vrf_name}) + if dict_search(f'{vrf_name}.protocols.bgp.address_family.{afi_name}', + vrfs_config): + afi_config = \ + vrfs_config[vrf_name]['protocols']['bgp']['address_family'][ + afi_name] + if verify_vrf_import_options(afi_config): + return True + return False + +def verify_vrflist_import(afi_name: str, afi_config: dict, vrfs_config: dict) -> bool: + """ + Call function to verify + if scpecific vrf contains rd and route-target + options return true else false + + :param afi_name: afi/safi name + :type afi_name: str + :param afi_config: afi/safi configuration + :type afi_config: dict + :param vrfs_config: dependent vrfs config + :type vrfs_config:dict + :return: if vrf contains rd and route-target options return true else false + :rtype: bool + """ + for vrf_name in afi_config['import']['vrf']: + if verify_vrf_import(vrf_name, vrfs_config, afi_name): + return True + return False + def verify_remote_as(peer_config, bgp_config): if 'remote_as' in peer_config: return peer_config['remote_as'] @@ -113,12 +198,22 @@ def verify_afi(peer_config, bgp_config): return False def verify(bgp): - if not bgp or 'deleted' in bgp: - if 'dependent_vrfs' in bgp: - for vrf, vrf_options in bgp['dependent_vrfs'].items(): - if dict_search('protocols.bgp', vrf_options) != None: - raise ConfigError('Cannot delete default BGP instance, ' \ - 'dependent VRF instance(s) exist!') + if 'deleted' in bgp: + if 'vrf' in bgp: + # Cannot delete vrf if it exists in import vrf list in other vrfs + for tmp_afi in ['ipv4_unicast', 'ipv6_unicast']: + if verify_vrf_as_import(bgp['vrf'],tmp_afi,bgp['dependent_vrfs']): + raise ConfigError(f'Cannot delete vrf {bgp["vrf"]} instance, ' \ + 'Please unconfigure import vrf commands!') + else: + # We are running in the default VRF context, thus we can not delete + # our main BGP instance if there are dependent BGP VRF instances. + if 'dependent_vrfs' in bgp: + for vrf, vrf_options in bgp['dependent_vrfs'].items(): + if vrf != 'default': + if dict_search('protocols.bgp', vrf_options): + raise ConfigError('Cannot delete default BGP instance, ' \ + 'dependent VRF instance(s) exist!') return None if 'system_as' not in bgp: @@ -140,6 +235,11 @@ def verify(bgp): raise ConfigError(f'Specified peer-group "{peer_group}" for '\ f'neighbor "{neighbor}" does not exist!') + if 'local_role' in peer_config: + #Ensure Local Role has only one value. + if len(peer_config['local_role']) > 1: + raise ConfigError(f'Only one local role can be specified for peer "{peer}"!') + if 'local_as' in peer_config: if len(peer_config['local_as']) > 1: raise ConfigError(f'Only one local-as number can be specified for peer "{peer}"!') @@ -324,9 +424,43 @@ def verify(bgp): f'{afi} administrative distance {key}!') if afi in ['ipv4_unicast', 'ipv6_unicast']: - if 'import' in afi_config and 'vrf' in afi_config['import']: - # Check if VRF exists - verify_vrf(afi_config['import']['vrf']) + + vrf_name = bgp['vrf'] if dict_search('vrf', bgp) else 'default' + # Verify if currant VRF contains rd and route-target options + # and does not exist in import list in other VRFs + if dict_search(f'rd.vpn.export', afi_config): + if verify_vrf_as_import(vrf_name, afi, bgp['dependent_vrfs']): + raise ConfigError( + 'Command "import vrf" conflicts with "rd vpn export" command!') + + if dict_search('route_target.vpn.both', afi_config): + if verify_vrf_as_import(vrf_name, afi, bgp['dependent_vrfs']): + raise ConfigError( + 'Command "import vrf" conflicts with "route-target vpn both" command!') + + if dict_search('route_target.vpn.import', afi_config): + if verify_vrf_as_import(vrf_name, afi, bgp['dependent_vrfs']): + raise ConfigError( + 'Command "import vrf conflicts" with "route-target vpn import" command!') + + if dict_search('route_target.vpn.export', afi_config): + if verify_vrf_as_import(vrf_name, afi, bgp['dependent_vrfs']): + raise ConfigError( + 'Command "import vrf" conflicts with "route-target vpn export" command!') + + # Verify if VRFs in import do not contain rd + # and route-target options + if dict_search('import.vrf', afi_config) is not None: + # Verify if VRF with import does not contain rd + # and route-target options + if verify_vrf_import_options(afi_config): + raise ConfigError( + 'Please unconfigure "import vrf" commands before using vpn commands in the same VRF!') + # Verify if VRFs in import list do not contain rd + # and route-target options + if verify_vrflist_import(afi, afi_config, bgp['dependent_vrfs']): + raise ConfigError( + 'Please unconfigure import vrf commands before using vpn commands in dependent VRFs!') # FRR error: please unconfigure vpn to vrf commands before # using import vrf commands @@ -339,7 +473,6 @@ def verify(bgp): tmp = dict_search(f'route_map.vpn.{export_import}', afi_config) if tmp: verify_route_map(tmp, bgp) - return None def generate(bgp): diff --git a/src/conf_mode/protocols_failover.py b/src/conf_mode/protocols_failover.py new file mode 100755 index 000000000..85e984afe --- /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 = '/run/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/protocols_ospfv3.py b/src/conf_mode/protocols_ospfv3.py index ee4eaf59d..ed0a8fba2 100755 --- a/src/conf_mode/protocols_ospfv3.py +++ b/src/conf_mode/protocols_ospfv3.py @@ -117,6 +117,10 @@ def verify(ospfv3): if 'area_type' in area_config: if len(area_config['area_type']) > 1: raise ConfigError(f'Can only configure one area-type for OSPFv3 area "{area}"!') + if 'range' in area_config: + for range, range_config in area_config['range'].items(): + if {'not_advertise', 'advertise'} <= range_config.keys(): + raise ConfigError(f'"not-advertise" and "advertise" for "range {range}" cannot be both configured at the same time!') if 'interface' in ospfv3: for interface, interface_config in ospfv3['interface'].items(): diff --git a/src/conf_mode/protocols_static.py b/src/conf_mode/protocols_static.py index 58e202928..3e5ebb805 100755 --- a/src/conf_mode/protocols_static.py +++ b/src/conf_mode/protocols_static.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2021 VyOS maintainers and contributors +# Copyright (C) 2021-2023 VyOS maintainers and contributors # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 or later as @@ -25,12 +25,15 @@ from vyos.configdict import get_dhcp_interfaces from vyos.configdict import get_pppoe_interfaces from vyos.configverify import verify_common_route_maps from vyos.configverify import verify_vrf +from vyos.template import render from vyos.template import render_to_string from vyos import ConfigError from vyos import frr from vyos import airbag airbag.enable() +config_file = '/etc/iproute2/rt_tables.d/vyos-static.conf' + def get_config(config=None): if config: conf = config @@ -94,6 +97,9 @@ def verify(static): def generate(static): if not static: return None + + # Put routing table names in /etc/iproute2/rt_tables + render(config_file, 'iproute2/static.conf.j2', static) static['new_frr_config'] = render_to_string('frr/staticd.frr.j2', static) return None diff --git a/src/conf_mode/qos.py b/src/conf_mode/qos.py index dbe3be225..dca713283 100755 --- a/src/conf_mode/qos.py +++ b/src/conf_mode/qos.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2022 VyOS maintainers and contributors +# Copyright (C) 2023 VyOS maintainers and contributors # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 or later as @@ -14,15 +14,62 @@ # 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 netifaces import interfaces +from vyos.base import Warning from vyos.config import Config from vyos.configdict import dict_merge +from vyos.qos import CAKE +from vyos.qos import DropTail +from vyos.qos import FairQueue +from vyos.qos import FQCodel +from vyos.qos import Limiter +from vyos.qos import NetEm +from vyos.qos import Priority +from vyos.qos import RandomDetect +from vyos.qos import RateLimiter +from vyos.qos import RoundRobin +from vyos.qos import TrafficShaper +from vyos.qos import TrafficShaperHFSC +from vyos.util import call +from vyos.util import dict_search_recursive from vyos.xml import defaults from vyos import ConfigError from vyos import airbag airbag.enable() +map_vyops_tc = { + 'cake' : CAKE, + 'drop_tail' : DropTail, + 'fair_queue' : FairQueue, + 'fq_codel' : FQCodel, + 'limiter' : Limiter, + 'network_emulator' : NetEm, + 'priority_queue' : Priority, + 'random_detect' : RandomDetect, + 'rate_control' : RateLimiter, + 'round_robin' : RoundRobin, + 'shaper' : TrafficShaper, + 'shaper_hfsc' : TrafficShaperHFSC, +} + +def get_shaper(qos, interface_config, direction): + policy_name = interface_config[direction] + # An interface might have a QoS configuration, search the used + # configuration referenced by this. Path will hold the dict element + # referenced by the config, as this will be of sort: + # + # ['policy', 'drop_tail', 'foo-dtail'] <- we are only interested in + # drop_tail as the policy/shaper type + _, path = next(dict_search_recursive(qos, policy_name)) + shaper_type = path[1] + shaper_config = qos['policy'][shaper_type][policy_name] + + return (map_vyops_tc[shaper_type], shaper_config) + def get_config(config=None): if config: conf = config @@ -32,48 +79,172 @@ def get_config(config=None): if not conf.exists(base): return None - qos = conf.get_config_dict(base, key_mangling=('-', '_'), get_first_key=True) + qos = conf.get_config_dict(base, key_mangling=('-', '_'), + get_first_key=True, + no_tag_node_value_mangle=True) if 'policy' in qos: for policy in qos['policy']: - # CLI mangles - to _ for better Jinja2 compatibility - do we need - # Jinja2 here? - policy = policy.replace('-','_') + # when calling defaults() we need to use the real CLI node, thus we + # need a hyphen + policy_hyphen = policy.replace('_', '-') - default_values = defaults(base + ['policy', policy]) - - # class is another tag node which requires individual handling - class_default_values = defaults(base + ['policy', policy, 'class']) - if 'class' in default_values: - del default_values['class'] + if policy in ['random_detect']: + for rd_name, rd_config in qos['policy'][policy].items(): + # There are eight precedence levels - ensure all are present + # to be filled later down with the appropriate default values + default_precedence = {'precedence' : { '0' : {}, '1' : {}, '2' : {}, '3' : {}, + '4' : {}, '5' : {}, '6' : {}, '7' : {} }} + qos['policy']['random_detect'][rd_name] = dict_merge( + default_precedence, qos['policy']['random_detect'][rd_name]) for p_name, p_config in qos['policy'][policy].items(): + default_values = defaults(base + ['policy', policy_hyphen]) + + if policy in ['priority_queue']: + if 'default' not in p_config: + raise ConfigError(f'QoS policy {p_name} misses "default" class!') + + # XXX: T2665: we can not safely rely on the defaults() when there are + # tagNodes in place, it is better to blend in the defaults manually. + if 'class' in default_values: + del default_values['class'] + if 'precedence' in default_values: + del default_values['precedence'] + qos['policy'][policy][p_name] = dict_merge( default_values, qos['policy'][policy][p_name]) + # class is another tag node which requires individual handling if 'class' in p_config: + default_values = defaults(base + ['policy', policy_hyphen, 'class']) for p_class in p_config['class']: qos['policy'][policy][p_name]['class'][p_class] = dict_merge( - class_default_values, qos['policy'][policy][p_name]['class'][p_class]) + default_values, qos['policy'][policy][p_name]['class'][p_class]) + + if 'precedence' in p_config: + default_values = defaults(base + ['policy', policy_hyphen, 'precedence']) + # precedence values are a bit more complex as they are calculated + # under specific circumstances - thus we need to iterate two times. + # first blend in the defaults from XML / CLI + for precedence in p_config['precedence']: + qos['policy'][policy][p_name]['precedence'][precedence] = dict_merge( + default_values, qos['policy'][policy][p_name]['precedence'][precedence]) + # second calculate defaults based on actual dictionary + for precedence in p_config['precedence']: + max_thr = int(qos['policy'][policy][p_name]['precedence'][precedence]['maximum_threshold']) + if 'minimum_threshold' not in qos['policy'][policy][p_name]['precedence'][precedence]: + qos['policy'][policy][p_name]['precedence'][precedence]['minimum_threshold'] = str( + int((9 + int(precedence)) * max_thr) // 18); + + if 'queue_limit' not in qos['policy'][policy][p_name]['precedence'][precedence]: + qos['policy'][policy][p_name]['precedence'][precedence]['queue_limit'] = \ + str(int(4 * max_thr)) - import pprint - pprint.pprint(qos) return qos def verify(qos): - if not qos: + if not qos or 'interface' not in qos: return None # network policy emulator # reorder rerquires delay to be set + if 'policy' in qos: + for policy_type in qos['policy']: + for policy, policy_config in qos['policy'][policy_type].items(): + # a policy with it's given name is only allowed to exist once + # on the system. This is because an interface selects a policy + # for ingress/egress traffic, and thus there can only be one + # policy with a given name. + # + # We check if the policy name occurs more then once - error out + # if this is true + counter = 0 + for _, path in dict_search_recursive(qos['policy'], policy): + counter += 1 + if counter > 1: + raise ConfigError(f'Conflicting policy name "{policy}", already in use!') + + if 'class' in policy_config: + for cls, cls_config in policy_config['class'].items(): + # bandwidth is not mandatory for priority-queue - that is why this is on the exception list + if 'bandwidth' not in cls_config and policy_type not in ['priority_queue', 'round_robin']: + raise ConfigError(f'Bandwidth must be defined for policy "{policy}" class "{cls}"!') + if 'match' in cls_config: + for match, match_config in cls_config['match'].items(): + if {'ip', 'ipv6'} <= set(match_config): + raise ConfigError(f'Can not use both IPv6 and IPv4 in one match ({match})!') + + if policy_type in ['random_detect']: + if 'precedence' in policy_config: + for precedence, precedence_config in policy_config['precedence'].items(): + max_tr = int(precedence_config['maximum_threshold']) + if {'maximum_threshold', 'minimum_threshold'} <= set(precedence_config): + min_tr = int(precedence_config['minimum_threshold']) + if min_tr >= max_tr: + raise ConfigError(f'Policy "{policy}" uses min-threshold "{min_tr}" >= max-threshold "{max_tr}"!') + + if {'maximum_threshold', 'queue_limit'} <= set(precedence_config): + queue_lim = int(precedence_config['queue_limit']) + if queue_lim < max_tr: + raise ConfigError(f'Policy "{policy}" uses queue-limit "{queue_lim}" < max-threshold "{max_tr}"!') + + if 'default' in policy_config: + if 'bandwidth' not in policy_config['default'] and policy_type not in ['priority_queue', 'round_robin']: + raise ConfigError('Bandwidth not defined for default traffic!') + + # we should check interface ingress/egress configuration after verifying that + # the policy name is used only once - this makes the logic easier! + for interface, interface_config in qos['interface'].items(): + for direction in ['egress', 'ingress']: + # bail out early if shaper for given direction is not used at all + if direction not in interface_config: + continue + + policy_name = interface_config[direction] + if 'policy' not in qos or list(dict_search_recursive(qos['policy'], policy_name)) == []: + raise ConfigError(f'Selected QoS policy "{policy_name}" does not exist!') + + shaper_type, shaper_config = get_shaper(qos, interface_config, direction) + tmp = shaper_type(interface).get_direction() + if direction not in tmp: + raise ConfigError(f'Selected QoS policy on interface "{interface}" only supports "{tmp}"!') - raise ConfigError('123') return None def generate(qos): + if not qos or 'interface' not in qos: + return None + return None def apply(qos): + # Always delete "old" shapers first + for interface in interfaces(): + # Ignore errors (may have no qdisc) + call(f'tc qdisc del dev {interface} parent ffff:') + call(f'tc qdisc del dev {interface} root') + + if not qos or 'interface' not in qos: + return None + + for interface, interface_config in qos['interface'].items(): + if not os.path.exists(f'/sys/class/net/{interface}'): + # When shaper is bound to a dialup (e.g. PPPoE) interface it is + # possible that it is yet not availbale when to QoS code runs. + # Skip the configuration and inform the user + Warning(f'Interface "{interface}" does not exist!') + continue + + for direction in ['egress', 'ingress']: + # bail out early if shaper for given direction is not used at all + if direction not in interface_config: + continue + + shaper_type, shaper_config = get_shaper(qos, interface_config, direction) + tmp = shaper_type(interface) + tmp.update(shaper_config, direction) + return None if __name__ == '__main__': diff --git a/src/conf_mode/service_console-server.py b/src/conf_mode/service_console-server.py index ee4fe42ab..60eff6543 100755 --- a/src/conf_mode/service_console-server.py +++ b/src/conf_mode/service_console-server.py @@ -27,7 +27,7 @@ from vyos.xml import defaults from vyos import ConfigError config_file = '/run/conserver/conserver.cf' -dropbear_systemd_file = '/etc/systemd/system/dropbear@{port}.service.d/override.conf' +dropbear_systemd_file = '/run/systemd/system/dropbear@{port}.service.d/override.conf' def get_config(config=None): if config: diff --git a/src/conf_mode/service_monitoring_telegraf.py b/src/conf_mode/service_monitoring_telegraf.py index aafece47a..363408679 100755 --- a/src/conf_mode/service_monitoring_telegraf.py +++ b/src/conf_mode/service_monitoring_telegraf.py @@ -38,7 +38,7 @@ cache_dir = f'/etc/telegraf/.cache' config_telegraf = f'/run/telegraf/telegraf.conf' custom_scripts_dir = '/etc/telegraf/custom_scripts' syslog_telegraf = '/etc/rsyslog.d/50-telegraf.conf' -systemd_override = '/etc/systemd/system/telegraf.service.d/10-override.conf' +systemd_override = '/run/systemd/system/telegraf.service.d/10-override.conf' def get_nft_filter_chains(): """ Get nft chains for table filter """ 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_sla.py b/src/conf_mode/service_sla.py index e7c3ca59c..b1e22f37b 100755 --- a/src/conf_mode/service_sla.py +++ b/src/conf_mode/service_sla.py @@ -27,15 +27,13 @@ from vyos import ConfigError from vyos import airbag airbag.enable() - owamp_config_dir = '/etc/owamp-server' owamp_config_file = f'{owamp_config_dir}/owamp-server.conf' -systemd_override_owamp = r'/etc/systemd/system/owamp-server.d/20-override.conf' +systemd_override_owamp = r'/run/systemd/system/owamp-server.d/20-override.conf' twamp_config_dir = '/etc/twamp-server' twamp_config_file = f'{twamp_config_dir}/twamp-server.conf' -systemd_override_twamp = r'/etc/systemd/system/twamp-server.d/20-override.conf' - +systemd_override_twamp = r'/run/systemd/system/twamp-server.d/20-override.conf' def get_config(config=None): if config: diff --git a/src/conf_mode/service_webproxy.py b/src/conf_mode/service_webproxy.py index 32af31bde..658e496a6 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 @@ -195,9 +246,10 @@ def apply(proxy): if os.path.exists(squidguard_db_dir): chmod_755(squidguard_db_dir) - call('systemctl restart squid.service') + call('systemctl reload-or-restart squid.service') return None + if __name__ == '__main__': try: c = get_config() diff --git a/src/conf_mode/snmp.py b/src/conf_mode/snmp.py index 5cd24db32..ab2ccf99e 100755 --- a/src/conf_mode/snmp.py +++ b/src/conf_mode/snmp.py @@ -40,7 +40,7 @@ config_file_client = r'/etc/snmp/snmp.conf' config_file_daemon = r'/etc/snmp/snmpd.conf' config_file_access = r'/usr/share/snmp/snmpd.conf' config_file_user = r'/var/lib/snmp/snmpd.conf' -systemd_override = r'/etc/systemd/system/snmpd.service.d/override.conf' +systemd_override = r'/run/systemd/system/snmpd.service.d/override.conf' systemd_service = 'snmpd.service' def get_config(config=None): @@ -166,6 +166,10 @@ def verify(snmp): if 'community' not in trap_config: raise ConfigError(f'Trap target "{trap}" requires a community to be set!') + if 'oid_enable' in snmp: + Warning(f'Custom OIDs are enabled and may lead to system instability and high resource consumption') + + verify_vrf(snmp) # bail out early if SNMP v3 is not configured diff --git a/src/conf_mode/ssh.py b/src/conf_mode/ssh.py index 8746cc701..8de0617af 100755 --- a/src/conf_mode/ssh.py +++ b/src/conf_mode/ssh.py @@ -32,7 +32,7 @@ from vyos import airbag airbag.enable() config_file = r'/run/sshd/sshd_config' -systemd_override = r'/etc/systemd/system/ssh.service.d/override.conf' +systemd_override = r'/run/systemd/system/ssh.service.d/override.conf' sshguard_config_file = '/etc/sshguard/sshguard.conf' sshguard_whitelist = '/etc/sshguard/whitelist' diff --git a/src/conf_mode/system-option.py b/src/conf_mode/system-option.py index 36dbf155b..e6c7a0ed2 100755 --- a/src/conf_mode/system-option.py +++ b/src/conf_mode/system-option.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2019-2020 VyOS maintainers and contributors +# Copyright (C) 2019-2022 VyOS maintainers and contributors # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 or later as @@ -22,17 +22,19 @@ from time import sleep from vyos.config import Config from vyos.configdict import dict_merge +from vyos.configverify import verify_source_interface from vyos.template import render from vyos.util import cmd from vyos.util import is_systemd_service_running from vyos.validate import is_addr_assigned +from vyos.validate import is_intf_addr_assigned from vyos.xml import defaults from vyos import ConfigError from vyos import airbag airbag.enable() curlrc_config = r'/etc/curlrc' -ssh_config = r'/etc/ssh/ssh_config' +ssh_config = r'/etc/ssh/ssh_config.d/91-vyos-ssh-client-options.conf' systemd_action_file = '/lib/systemd/system/ctrl-alt-del.target' def get_config(config=None): @@ -68,8 +70,17 @@ def verify(options): if 'ssh_client' in options: config = options['ssh_client'] if 'source_address' in config: + address = config['source_address'] if not is_addr_assigned(config['source_address']): - raise ConfigError('No interface with give address specified!') + raise ConfigError('No interface with address "{address}" configured!') + + if 'source_interface' in config: + verify_source_interface(config) + if 'source_address' in config: + address = config['source_address'] + interface = config['source_interface'] + if not is_intf_addr_assigned(interface, address): + raise ConfigError(f'Address "{address}" not assigned on interface "{interface}"!') return None diff --git a/src/conf_mode/vpn_ipsec.py b/src/conf_mode/vpn_ipsec.py index 77a425f8b..ce4f13d27 100755 --- a/src/conf_mode/vpn_ipsec.py +++ b/src/conf_mode/vpn_ipsec.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2021-2022 VyOS maintainers and contributors +# Copyright (C) 2021-2023 VyOS maintainers and contributors # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 or later as @@ -17,11 +17,13 @@ import ipaddress import os import re +import jmespath from sys import exit from time import sleep from time import time +from vyos.base import Warning from vyos.config import Config from vyos.configdict import leaf_node_changed from vyos.configverify import verify_interface_exists @@ -94,6 +96,7 @@ def get_config(config=None): del default_values['esp_group'] del default_values['ike_group'] del default_values['remote_access'] + del default_values['site_to_site'] ipsec = dict_merge(default_values, ipsec) if 'esp_group' in ipsec: @@ -117,18 +120,39 @@ def get_config(config=None): ipsec['ike_group'][group]['proposal'][proposal] = dict_merge(default_values, ipsec['ike_group'][group]['proposal'][proposal]) - if 'remote_access' in ipsec and 'connection' in ipsec['remote_access']: + # XXX: T2665: we can not safely rely on the defaults() when there are + # tagNodes in place, it is better to blend in the defaults manually. + if dict_search('remote_access.connection', ipsec): default_values = defaults(base + ['remote-access', 'connection']) for rw in ipsec['remote_access']['connection']: ipsec['remote_access']['connection'][rw] = dict_merge(default_values, ipsec['remote_access']['connection'][rw]) - if 'remote_access' in ipsec and 'radius' in ipsec['remote_access'] and 'server' in ipsec['remote_access']['radius']: + # XXX: T2665: we can not safely rely on the defaults() when there are + # tagNodes in place, it is better to blend in the defaults manually. + if dict_search('remote_access.radius.server', ipsec): + # Fist handle the "base" stuff like RADIUS timeout + default_values = defaults(base + ['remote-access', 'radius']) + if 'server' in default_values: + del default_values['server'] + ipsec['remote_access']['radius'] = dict_merge(default_values, + ipsec['remote_access']['radius']) + + # Take care about individual RADIUS servers implemented as tagNodes - this + # requires special treatment default_values = defaults(base + ['remote-access', 'radius', 'server']) for server in ipsec['remote_access']['radius']['server']: ipsec['remote_access']['radius']['server'][server] = dict_merge(default_values, ipsec['remote_access']['radius']['server'][server]) + # XXX: T2665: we can not safely rely on the defaults() when there are + # tagNodes in place, it is better to blend in the defaults manually. + if dict_search('site_to_site.peer', ipsec): + default_values = defaults(base + ['site-to-site', 'peer']) + for peer in ipsec['site_to_site']['peer']: + ipsec['site_to_site']['peer'][peer] = dict_merge(default_values, + ipsec['site_to_site']['peer'][peer]) + ipsec['dhcp_no_address'] = {} ipsec['install_routes'] = 'no' if conf.exists(base + ["options", "disable-route-autoinstall"]) else default_install_routes ipsec['interface_change'] = leaf_node_changed(conf, base + ['interface']) @@ -196,6 +220,12 @@ def verify(ipsec): if not ipsec: return None + if 'authentication' in ipsec: + if 'psk' in ipsec['authentication']: + for psk, psk_config in ipsec['authentication']['psk'].items(): + if 'id' not in psk_config or 'secret' not in psk_config: + raise ConfigError(f'Authentication psk "{psk}" missing "id" or "secret"') + if 'interfaces' in ipsec : for ifname in ipsec['interface']: verify_interface_exists(ifname) @@ -425,6 +455,10 @@ def verify(ipsec): if 'local_address' in peer_conf and 'dhcp_interface' in peer_conf: raise ConfigError(f"A single local-address or dhcp-interface is required when using VTI on site-to-site peer {peer}") + if dict_search('options.disable_route_autoinstall', + ipsec) == None: + Warning('It\'s recommended to use ipsec vty with the next command\n[set vpn ipsec option disable-route-autoinstall]') + if 'bind' in peer_conf['vti']: vti_interface = peer_conf['vti']['bind'] if not os.path.exists(f'/sys/class/net/{vti_interface}'): @@ -575,6 +609,14 @@ def generate(ipsec): ipsec['site_to_site']['peer'][peer]['tunnel'][tunnel]['passthrough'] = passthrough + # auth psk <tag> dhcp-interface <xxx> + if jmespath.search('authentication.psk.*.dhcp_interface', ipsec): + for psk, psk_config in ipsec['authentication']['psk'].items(): + if 'dhcp_interface' in psk_config: + for iface in psk_config['dhcp_interface']: + id = get_dhcp_address(iface) + if id: + ipsec['authentication']['psk'][psk]['id'].append(id) render(ipsec_conf, 'ipsec/ipsec.conf.j2', ipsec) render(ipsec_secrets, 'ipsec/ipsec.secrets.j2', ipsec) diff --git a/src/conf_mode/vpn_l2tp.py b/src/conf_mode/vpn_l2tp.py index fd5a4acd8..65623c2b1 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': [], @@ -54,6 +58,9 @@ default_config_data = { 'ppp_echo_failure' : '3', 'ppp_echo_interval' : '30', 'ppp_echo_timeout': '0', + 'ppp_ipv6_accept_peer_intf_id': False, + 'ppp_ipv6_intf_id': None, + 'ppp_ipv6_peer_intf_id': None, 'radius_server': [], 'radius_acct_inter_jitter': '', 'radius_acct_tmo': '3', @@ -64,7 +71,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 +212,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 +251,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 +314,18 @@ 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']) + + if conf.exists(['ppp-options', 'ipv6-accept-peer-intf-id']): + l2tp['ppp_ipv6_accept_peer_intf_id'] = True + + if conf.exists(['ppp-options', 'ipv6-intf-id']): + l2tp['ppp_ipv6_intf_id'] = conf.return_value(['ppp-options', 'ipv6-intf-id']) + + if conf.exists(['ppp-options', 'ipv6-peer-intf-id']): + l2tp['ppp_ipv6_peer_intf_id'] = conf.return_value(['ppp-options', 'ipv6-peer-intf-id']) + return l2tp @@ -329,6 +349,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..63ffe2a41 100755 --- a/src/conf_mode/vpn_openconnect.py +++ b/src/conf_mode/vpn_openconnect.py @@ -46,6 +46,65 @@ radius_servers = cfg_dir + '/radius_servers' def get_hash(password): return crypt(password, mksalt(METHOD_SHA512)) + +def T2665_default_dict_cleanup(origin: dict, default_values: dict) -> dict: + """ + https://phabricator.vyos.net/T2665 + Clear unnecessary key values in merged config by dict_merge function + :param origin: config + :type origin: dict + :param default_values: default values + :type default_values: dict + :return: merged dict + :rtype: dict + """ + if 'mode' in origin["authentication"] and "local" in \ + origin["authentication"]["mode"]: + del origin['authentication']['local_users']['username']['otp'] + if not origin["authentication"]["local_users"]["username"]: + raise ConfigError( + 'Openconnect mode local required at least one user') + default_ocserv_usr_values = \ + default_values['authentication']['local_users']['username']['otp'] + for user, params in origin['authentication']['local_users'][ + 'username'].items(): + # Not every configuration requires OTP settings + if origin['authentication']['local_users']['username'][user].get( + 'otp'): + origin['authentication']['local_users']['username'][user][ + 'otp'] = dict_merge(default_ocserv_usr_values, + origin['authentication'][ + 'local_users']['username'][user][ + 'otp']) + + if 'mode' in origin["authentication"] and "radius" in \ + origin["authentication"]["mode"]: + del origin['authentication']['radius']['server']['port'] + if not origin["authentication"]['radius']['server']: + raise ConfigError( + 'Openconnect authentication mode radius required at least one radius server') + default_values_radius_port = \ + default_values['authentication']['radius']['server']['port'] + for server, params in origin['authentication']['radius'][ + 'server'].items(): + if 'port' not in params: + params['port'] = default_values_radius_port + + if 'mode' in origin["accounting"] and "radius" in \ + origin["accounting"]["mode"]: + del origin['accounting']['radius']['server']['port'] + if not origin["accounting"]['radius']['server']: + raise ConfigError( + 'Openconnect accounting mode radius required at least one radius server') + default_values_radius_port = \ + default_values['accounting']['radius']['server']['port'] + for server, params in origin['accounting']['radius'][ + 'server'].items(): + if 'port' not in params: + params['port'] = default_values_radius_port + return origin + + def get_config(): conf = Config() base = ['vpn', 'openconnect'] @@ -57,18 +116,8 @@ def get_config(): # options which we need to update into the dictionary retrived. default_values = defaults(base) ocserv = dict_merge(default_values, ocserv) - - if "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"]: - raise ConfigError('openconnect mode local required at least one user') - default_ocserv_usr_values = default_values['authentication']['local_users']['username']['otp'] - for user, params in ocserv['authentication']['local_users']['username'].items(): - # Not every configuration requires OTP settings - if ocserv['authentication']['local_users']['username'][user].get('otp'): - ocserv['authentication']['local_users']['username'][user]['otp'] = dict_merge(default_ocserv_usr_values, ocserv['authentication']['local_users']['username'][user]['otp']) - + # workaround a "know limitation" - https://phabricator.vyos.net/T2665 + ocserv = T2665_default_dict_cleanup(ocserv, default_values) if ocserv: ocserv['pki'] = conf.get_config_dict(['pki'], key_mangling=('-', '_'), get_first_key=True, no_tag_node_value_mangle=True) @@ -85,6 +134,14 @@ def verify(ocserv): not is_listen_port_bind_service(int(port), 'ocserv-main'): raise ConfigError(f'"{proto}" port "{port}" is used by another service') + # Check accounting + if "accounting" in ocserv: + if "mode" in ocserv["accounting"] and "radius" in ocserv["accounting"]["mode"]: + if "authentication" not in ocserv or "mode" not in ocserv["authentication"]: + raise ConfigError('Accounting depends on OpenConnect authentication configuration') + elif "radius" not in ocserv["authentication"]["mode"]: + raise ConfigError('RADIUS accounting must be used with RADIUS authentication') + # Check authentication if "authentication" in ocserv: if "mode" in ocserv["authentication"]: @@ -157,7 +214,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') @@ -166,10 +223,18 @@ def generate(ocserv): return None if "radius" in ocserv["authentication"]["mode"]: - # Render radius client configuration - render(radius_cfg, 'ocserv/radius_conf.j2', ocserv["authentication"]["radius"]) - # Render radius servers - render(radius_servers, 'ocserv/radius_servers.j2', ocserv["authentication"]["radius"]) + if dict_search(ocserv, 'accounting.mode.radius'): + # Render radius client configuration + render(radius_cfg, 'ocserv/radius_conf.j2', ocserv) + merged_servers = ocserv["accounting"]["radius"]["server"] | ocserv["authentication"]["radius"]["server"] + # Render radius servers + # Merge the accounting and authentication servers into a single dictionary + render(radius_servers, 'ocserv/radius_servers.j2', {'server': merged_servers}) + else: + # Render radius client configuration + render(radius_cfg, 'ocserv/radius_conf.j2', ocserv) + # Render radius servers + render(radius_servers, 'ocserv/radius_servers.j2', ocserv["authentication"]["radius"]) elif "local" in ocserv["authentication"]["mode"]: # if mode "OTP", generate OTP users file parameters if "otp" in ocserv["authentication"]["mode"]["local"]: @@ -247,7 +312,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/conf_mode/vrf.py b/src/conf_mode/vrf.py index 1b4156895..c17cca3bd 100755 --- a/src/conf_mode/vrf.py +++ b/src/conf_mode/vrf.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2020-2022 VyOS maintainers and contributors +# Copyright (C) 2020-2023 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 @@ -140,11 +140,9 @@ def verify(vrf): def generate(vrf): - render(config_file, 'vrf/vrf.conf.j2', vrf) + render(config_file, 'iproute2/vrf.conf.j2', vrf) # Render nftables zones config - render(nft_vrf_config, 'firewall/nftables-vrf-zones.j2', vrf) - return None diff --git a/src/etc/commit/post-hooks.d/00vyos-sync b/src/etc/commit/post-hooks.d/00vyos-sync new file mode 100755 index 000000000..e3bde3abb --- /dev/null +++ b/src/etc/commit/post-hooks.d/00vyos-sync @@ -0,0 +1,7 @@ +#!/bin/sh +# When power is lost right after a commit modified files, the +# system can be corrupted and e.g. login is no longer possible. +# Always sync files to the backend storage after a commit. +# https://phabricator.vyos.net/T4975 +sync + diff --git a/src/etc/dhcp/dhclient-enter-hooks.d/04-vyos-resolvconf b/src/etc/dhcp/dhclient-enter-hooks.d/04-vyos-resolvconf index b1902b585..518abeaec 100644 --- a/src/etc/dhcp/dhclient-enter-hooks.d/04-vyos-resolvconf +++ b/src/etc/dhcp/dhclient-enter-hooks.d/04-vyos-resolvconf @@ -33,8 +33,8 @@ if /usr/bin/systemctl -q is-active vyos-hostsd; then if [ -n "$new_dhcp6_name_servers" ]; then logmsg info "Deleting nameservers with tag \"dhcpv6-$interface\" via vyos-hostsd-client" $hostsd_client --delete-name-servers --tag "dhcpv6-$interface" - logmsg info "Adding nameservers \"$new_dhcpv6_name_servers\" with tag \"dhcpv6-$interface\" via vyos-hostsd-client" - $hostsd_client --add-name-servers $new_dhcpv6_name_servers --tag "dhcpv6-$interface" + logmsg info "Adding nameservers \"$new_dhcp6_name_servers\" with tag \"dhcpv6-$interface\" via vyos-hostsd-client" + $hostsd_client --add-name-servers $new_dhcp6_name_servers --tag "dhcpv6-$interface" hostsd_changes=y fi diff --git a/src/etc/dhcp/dhclient-exit-hooks.d/01-vyos-cleanup b/src/etc/dhcp/dhclient-exit-hooks.d/01-vyos-cleanup index ad6a1d5eb..da1bda137 100644 --- a/src/etc/dhcp/dhclient-exit-hooks.d/01-vyos-cleanup +++ b/src/etc/dhcp/dhclient-exit-hooks.d/01-vyos-cleanup @@ -8,7 +8,7 @@ hostsd_changes= /usr/bin/systemctl -q is-active vyos-hostsd hostsd_status=$? -if [[ $reason =~ (EXPIRE|FAIL|RELEASE|STOP) ]]; then +if [[ $reason =~ ^(EXPIRE|FAIL|RELEASE|STOP)$ ]]; then if [[ $hostsd_status -eq 0 ]]; then # delete search domains and nameservers via vyos-hostsd logmsg info "Deleting search domains with tag \"dhcp-$interface\" via vyos-hostsd-client" @@ -96,7 +96,7 @@ if [[ $reason =~ (EXPIRE|FAIL|RELEASE|STOP) ]]; then fi fi -if [[ $reason =~ (EXPIRE6|RELEASE6|STOP6) ]]; then +if [[ $reason =~ ^(EXPIRE6|RELEASE6|STOP6)$ ]]; then if [[ $hostsd_status -eq 0 ]]; then # delete search domains and nameservers via vyos-hostsd logmsg info "Deleting search domains with tag \"dhcpv6-$interface\" via vyos-hostsd-client" diff --git a/src/etc/dhcp/dhclient-exit-hooks.d/ipsec-dhclient-hook b/src/etc/dhcp/dhclient-exit-hooks.d/ipsec-dhclient-hook index 61a89e62a..1f1926e17 100755 --- a/src/etc/dhcp/dhclient-exit-hooks.d/ipsec-dhclient-hook +++ b/src/etc/dhcp/dhclient-exit-hooks.d/ipsec-dhclient-hook @@ -23,7 +23,7 @@ DHCP_HOOK_IFLIST="/tmp/ipsec_dhcp_waiting" if [ -f $DHCP_HOOK_IFLIST ] && [ "$reason" == "BOUND" ]; then if grep -qw $interface $DHCP_HOOK_IFLIST; then sudo rm $DHCP_HOOK_IFLIST - sudo python3 /usr/libexec/vyos/conf_mode/vpn_ipsec.py + sudo /usr/libexec/vyos/conf_mode/vpn_ipsec.py exit 0 fi fi diff --git a/src/etc/dhcp/dhclient-exit-hooks.d/vyatta-dhclient-hook b/src/etc/dhcp/dhclient-exit-hooks.d/vyatta-dhclient-hook index eeb8b0782..49bb18372 100644 --- a/src/etc/dhcp/dhclient-exit-hooks.d/vyatta-dhclient-hook +++ b/src/etc/dhcp/dhclient-exit-hooks.d/vyatta-dhclient-hook @@ -8,12 +8,12 @@ # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 as # published by the Free Software Foundation. -# +# # This program is distributed in the hope that it will be useful, but # WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # General Public License for more details. -# +# # This code was originally developed by Vyatta, Inc. # Portions created by Vyatta are Copyright (C) 2006, 2007, 2008 Vyatta, Inc. # All Rights Reserved. @@ -23,7 +23,7 @@ RUN="yes" proto="" -if [[ $reason =~ (REBOOT6|INIT6|EXPIRE6|RELEASE6|STOP6|INFORM6|BOUND6|REBIND6|DELEGATED6) ]]; then +if [[ $reason =~ ^(REBOOT6|INIT6|EXPIRE6|RELEASE6|STOP6|INFORM6|BOUND6|REBIND6|DELEGATED6)$ ]]; then proto="v6" fi diff --git a/src/etc/modprobe.d/ifb.conf b/src/etc/modprobe.d/ifb.conf new file mode 100644 index 000000000..2dcfb6af4 --- /dev/null +++ b/src/etc/modprobe.d/ifb.conf @@ -0,0 +1 @@ +options ifb numifbs=0 diff --git a/src/etc/ppp/ip-down.d/98-vyos-pppoe-cleanup-nameservers b/src/etc/ppp/ip-down.d/98-vyos-pppoe-cleanup-nameservers new file mode 100755 index 000000000..222c75f21 --- /dev/null +++ b/src/etc/ppp/ip-down.d/98-vyos-pppoe-cleanup-nameservers @@ -0,0 +1,15 @@ +#!/bin/bash +### Autogenerated by interfaces-pppoe.py ### + +interface=$6 +if [ -z "$interface" ]; then + exit +fi + +if ! /usr/bin/systemctl -q is-active vyos-hostsd; then + exit # vyos-hostsd is not running +fi + +hostsd_client="/usr/bin/vyos-hostsd-client" +$hostsd_client --delete-name-servers --tag "dhcp-$interface" +$hostsd_client --apply 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/etc/ppp/ip-up.d/98-vyos-pppoe-setup-nameservers b/src/etc/ppp/ip-up.d/98-vyos-pppoe-setup-nameservers new file mode 100755 index 000000000..0fcedbedc --- /dev/null +++ b/src/etc/ppp/ip-up.d/98-vyos-pppoe-setup-nameservers @@ -0,0 +1,24 @@ +#!/bin/bash +### Autogenerated by interfaces-pppoe.py ### + +interface=$6 +if [ -z "$interface" ]; then + exit +fi + +if ! /usr/bin/systemctl -q is-active vyos-hostsd; then + exit # vyos-hostsd is not running +fi + +hostsd_client="/usr/bin/vyos-hostsd-client" + +$hostsd_client --delete-name-servers --tag "dhcp-$interface" + +if [ "$USEPEERDNS" ] && [ -n "$DNS1" ]; then +$hostsd_client --add-name-servers "$DNS1" --tag "dhcp-$interface" +fi +if [ "$USEPEERDNS" ] && [ -n "$DNS2" ]; then +$hostsd_client --add-name-servers "$DNS2" --tag "dhcp-$interface" +fi + +$hostsd_client --apply diff --git a/src/etc/sysctl.d/30-vyos-router.conf b/src/etc/sysctl.d/30-vyos-router.conf index 411429510..4880605d6 100644 --- a/src/etc/sysctl.d/30-vyos-router.conf +++ b/src/etc/sysctl.d/30-vyos-router.conf @@ -98,9 +98,6 @@ net.ipv6.route.skip_notify_on_dev_down=1 # Default value of 20 seems to interfere with larger OSPF and VRRP setups net.ipv4.igmp_max_memberships = 512 -# Enable conntrack helper by default -net.netfilter.nf_conntrack_helper=1 - # Increase default garbage collection thresholds net.ipv4.neigh.default.gc_thresh1 = 1024 net.ipv4.neigh.default.gc_thresh2 = 4096 diff --git a/src/helpers/system-versions-foot.py b/src/helpers/system-versions-foot.py index 2aa687221..9614f0d28 100755 --- a/src/helpers/system-versions-foot.py +++ b/src/helpers/system-versions-foot.py @@ -1,6 +1,6 @@ #!/usr/bin/python3 -# Copyright 2019 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 @@ -16,24 +16,13 @@ # along with this library. If not, see <http://www.gnu.org/licenses/>. import sys -import vyos.formatversions as formatversions -import vyos.systemversions as systemversions import vyos.defaults -import vyos.version - -sys_versions = systemversions.get_system_component_version() - -component_string = formatversions.format_versions_string(sys_versions) - -os_version_string = vyos.version.get_version() +from vyos.component_version import write_system_footer sys.stdout.write("\n\n") if vyos.defaults.cfg_vintage == 'vyos': - formatversions.write_vyos_versions_foot(None, component_string, - os_version_string) + write_system_footer(None, vintage='vyos') elif vyos.defaults.cfg_vintage == 'vyatta': - formatversions.write_vyatta_versions_foot(None, component_string, - os_version_string) + write_system_footer(None, vintage='vyatta') else: - formatversions.write_vyatta_versions_foot(None, component_string, - os_version_string) + write_system_footer(None, vintage='vyos') diff --git a/src/helpers/vyos-domain-group-resolve.py b/src/helpers/vyos-domain-group-resolve.py deleted file mode 100755 index 6b677670b..000000000 --- a/src/helpers/vyos-domain-group-resolve.py +++ /dev/null @@ -1,60 +0,0 @@ -#!/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 time - -from vyos.configquery import ConfigTreeQuery -from vyos.firewall import get_ips_domains_dict -from vyos.firewall import nft_add_set_elements -from vyos.firewall import nft_flush_set -from vyos.firewall import nft_init_set -from vyos.firewall import nft_update_set_elements -from vyos.util import call - - -base = ['firewall', 'group', 'domain-group'] -check_required = True -# count_failed = 0 -# Timeout in sec between checks -timeout = 300 - -domain_state = {} - -if __name__ == '__main__': - - while check_required: - config = ConfigTreeQuery() - if config.exists(base): - domain_groups = config.get_config_dict(base, key_mangling=('-', '_'), get_first_key=True) - for set_name, domain_config in domain_groups.items(): - list_domains = domain_config['address'] - elements = [] - ip_dict = get_ips_domains_dict(list_domains) - - for domain in list_domains: - # Resolution succeeded, update domain state - if domain in ip_dict: - domain_state[domain] = ip_dict[domain] - elements += ip_dict[domain] - # Resolution failed, use previous domain state - elif domain in domain_state: - elements += domain_state[domain] - - # Resolve successful - if elements: - nft_update_set_elements(f'D_{set_name}', elements) - time.sleep(timeout) diff --git a/src/helpers/vyos-domain-resolver.py b/src/helpers/vyos-domain-resolver.py new file mode 100755 index 000000000..e31d9238e --- /dev/null +++ b/src/helpers/vyos-domain-resolver.py @@ -0,0 +1,183 @@ +#!/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 os +import time + +from vyos.configdict import dict_merge +from vyos.configquery import ConfigTreeQuery +from vyos.firewall import fqdn_config_parse +from vyos.firewall import fqdn_resolve +from vyos.util import cmd +from vyos.util import commit_in_progress +from vyos.util import dict_search_args +from vyos.util import run +from vyos.xml import defaults + +base = ['firewall'] +timeout = 300 +cache = False + +domain_state = {} + +ipv4_tables = { + 'ip vyos_mangle', + 'ip vyos_filter', + 'ip vyos_nat' +} + +ipv6_tables = { + 'ip6 vyos_mangle', + 'ip6 vyos_filter' +} + +def get_config(conf): + firewall = conf.get_config_dict(base, key_mangling=('-', '_'), get_first_key=True, + no_tag_node_value_mangle=True) + + default_values = defaults(base) + for tmp in ['name', 'ipv6_name']: + if tmp in default_values: + del default_values[tmp] + + if 'zone' in default_values: + del default_values['zone'] + + firewall = dict_merge(default_values, firewall) + + global timeout, cache + + if 'resolver_interval' in firewall: + timeout = int(firewall['resolver_interval']) + + if 'resolver_cache' in firewall: + cache = True + + fqdn_config_parse(firewall) + + return firewall + +def resolve(domains, ipv6=False): + global domain_state + + ip_list = set() + + for domain in domains: + resolved = fqdn_resolve(domain, ipv6=ipv6) + + if resolved and cache: + domain_state[domain] = resolved + elif not resolved: + if domain not in domain_state: + continue + resolved = domain_state[domain] + + ip_list = ip_list | resolved + return ip_list + +def nft_output(table, set_name, ip_list): + output = [f'flush set {table} {set_name}'] + if ip_list: + ip_str = ','.join(ip_list) + output.append(f'add element {table} {set_name} {{ {ip_str} }}') + return output + +def nft_valid_sets(): + try: + valid_sets = [] + sets_json = cmd('nft -j list sets') + sets_obj = json.loads(sets_json) + + for obj in sets_obj['nftables']: + if 'set' in obj: + family = obj['set']['family'] + table = obj['set']['table'] + name = obj['set']['name'] + valid_sets.append((f'{family} {table}', name)) + + return valid_sets + except: + return [] + +def update(firewall): + conf_lines = [] + count = 0 + + valid_sets = nft_valid_sets() + + domain_groups = dict_search_args(firewall, 'group', 'domain_group') + if domain_groups: + for set_name, domain_config in domain_groups.items(): + if 'address' not in domain_config: + continue + + nft_set_name = f'D_{set_name}' + domains = domain_config['address'] + + ip_list = resolve(domains, ipv6=False) + for table in ipv4_tables: + if (table, nft_set_name) in valid_sets: + conf_lines += nft_output(table, nft_set_name, ip_list) + + ip6_list = resolve(domains, ipv6=True) + for table in ipv6_tables: + if (table, nft_set_name) in valid_sets: + conf_lines += nft_output(table, nft_set_name, ip6_list) + count += 1 + + for set_name, domain in firewall['ip_fqdn'].items(): + table = 'ip vyos_filter' + nft_set_name = f'FQDN_{set_name}' + + ip_list = resolve([domain], ipv6=False) + + if (table, nft_set_name) in valid_sets: + conf_lines += nft_output(table, nft_set_name, ip_list) + count += 1 + + for set_name, domain in firewall['ip6_fqdn'].items(): + table = 'ip6 vyos_filter' + nft_set_name = f'FQDN_{set_name}' + + ip_list = resolve([domain], ipv6=True) + if (table, nft_set_name) in valid_sets: + conf_lines += nft_output(table, nft_set_name, ip_list) + count += 1 + + nft_conf_str = "\n".join(conf_lines) + "\n" + code = run(f'nft -f -', input=nft_conf_str) + + print(f'Updated {count} sets - result: {code}') + +if __name__ == '__main__': + print(f'VyOS domain resolver') + + count = 1 + while commit_in_progress(): + if ( count % 60 == 0 ): + print(f'Commit still in progress after {count}s - waiting') + count += 1 + time.sleep(1) + + conf = ConfigTreeQuery() + firewall = get_config(conf) + + print(f'interval: {timeout}s - cache: {cache}') + + while True: + update(firewall) + time.sleep(timeout) diff --git a/src/helpers/vyos-failover.py b/src/helpers/vyos-failover.py new file mode 100755 index 000000000..0de945f20 --- /dev/null +++ b/src/helpers/vyos-failover.py @@ -0,0 +1,195 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2022-2023 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 is_route_exists(route, gateway, interface, metric): + """Check if route with expected gateway, dev and metric exists""" + rc, data = rc_cmd(f'sudo ip --json route show protocol failover {route} ' + f'via {gateway} dev {interface} metric {metric}') + if rc == 0: + data = json.loads(data) + if len(data) > 0: + return True + return False + + +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_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') + + # Route not found in the current routing table + if not is_route_exists(route, next_hop, conf_iface, conf_metric): + 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 \ + is_route_exists(route, next_hop, conf_iface, conf_metric): + 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/container/0-to-1 b/src/migration-scripts/container/0-to-1 new file mode 100755 index 000000000..d0461389b --- /dev/null +++ b/src/migration-scripts/container/0-to-1 @@ -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/>. + +# T4870: change underlaying container filesystem from vfs to overlay + +import os +import shutil +import sys + +from vyos.configtree import ConfigTree +from vyos.util import call + +if (len(sys.argv) < 1): + print("Must specify file name!") + sys.exit(1) + +file_name = sys.argv[1] + +with open(file_name, 'r') as f: + config_file = f.read() + +base = ['container', 'name'] +config = ConfigTree(config_file) + +# Check if containers exist and we need to perform image manipulation +if config.exists(base): + for container in config.list_nodes(base): + # Stop any given container first + call(f'systemctl stop vyos-container-{container}.service') + # Export container image for later re-import to new filesystem. We store + # the backup on a real disk as a tmpfs (like /tmp) could probably lack + # memory if a host has too many containers stored. + image_name = config.return_value(base + [container, 'image']) + call(f'podman image save --quiet --output /root/{container}.tar --format oci-archive {image_name}') + +# No need to adjust the strage driver online (this is only used for testing and +# debugging on a live system) - it is already overlay2 when the migration script +# is run during system update. But the specified driver in the image is actually +# overwritten by the still present VFS filesystem on disk. Thus podman still +# thinks it uses VFS until we delete the libpod directory under: +# /usr/lib/live/mount/persistence/container/storage +#call('sed -i "s/vfs/overlay2/g" /etc/containers/storage.conf /usr/share/vyos/templates/container/storage.conf.j2') + +base_path = '/usr/lib/live/mount/persistence/container/storage' +for dir in ['libpod', 'vfs', 'vfs-containers', 'vfs-images', 'vfs-layers']: + if os.path.exists(f'{base_path}/{dir}'): + shutil.rmtree(f'{base_path}/{dir}') + +# Now all remaining information about VFS is gone and we operate in overlayfs2 +# filesystem mode. Time to re-import the images. +if config.exists(base): + for container in config.list_nodes(base): + # Export container image for later re-import to new filesystem + image_name = config.return_value(base + [container, 'image']) + image_path = f'/root/{container}.tar' + call(f'podman image load --quiet --input {image_path}') + + # Start any given container first + call(f'systemctl start vyos-container-{container}.service') + + # Delete temporary container image + if os.path.exists(image_path): + os.unlink(image_path) + 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/migration-scripts/https/3-to-4 b/src/migration-scripts/https/3-to-4 new file mode 100755 index 000000000..5ee528b31 --- /dev/null +++ b/src/migration-scripts/https/3-to-4 @@ -0,0 +1,53 @@ +#!/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/>. + +# T4768 rename node 'gql' to 'graphql'. + +import sys + +from vyos.configtree import ConfigTree + +if (len(sys.argv) < 2): + print("Must specify file name!") + sys.exit(1) + +file_name = sys.argv[1] + +with open(file_name, 'r') as f: + config_file = f.read() + +config = ConfigTree(config_file) + +old_base = ['service', 'https', 'api', 'gql'] +if not config.exists(old_base): + # Nothing to do + sys.exit(0) + +new_base = ['service', 'https', 'api', 'graphql'] +config.set(new_base) + +nodes = config.list_nodes(old_base) +for node in nodes: + config.copy(old_base + [node], new_base + [node]) + +config.delete(old_base) + +try: + with open(file_name, 'w') as f: + f.write(config.to_string()) +except OSError as e: + print("Failed to save the modified config: {}".format(e)) + sys.exit(1) diff --git a/src/migration-scripts/ipsec/10-to-11 b/src/migration-scripts/ipsec/10-to-11 new file mode 100755 index 000000000..ec38d0034 --- /dev/null +++ b/src/migration-scripts/ipsec/10-to-11 @@ -0,0 +1,85 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2023 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +import re + +from sys import argv +from sys import exit + +from vyos.configtree import ConfigTree + + +if (len(argv) < 1): + print("Must specify file name!") + exit(1) + +file_name = argv[1] + +with open(file_name, 'r') as f: + config_file = f.read() + +base = ['vpn', 'ipsec'] +config = ConfigTree(config_file) + +if not config.exists(base): + # Nothing to do + exit(0) + +# PEER changes +if config.exists(base + ['site-to-site', 'peer']): + for peer in config.list_nodes(base + ['site-to-site', 'peer']): + peer_base = base + ['site-to-site', 'peer', peer] + + # replace: 'ipsec site-to-site peer <tag> authentication pre-shared-secret xxx' + # => 'ipsec authentication psk <tag> secret xxx' + if config.exists(peer_base + ['authentication', 'pre-shared-secret']): + tmp = config.return_value(peer_base + ['authentication', 'pre-shared-secret']) + config.delete(peer_base + ['authentication', 'pre-shared-secret']) + config.set(base + ['authentication', 'psk', peer, 'secret'], value=tmp) + # format as tag node to avoid loading problems + config.set_tag(base + ['authentication', 'psk']) + + # Get id's from peers for "ipsec auth psk <tag> id xxx" + if config.exists(peer_base + ['authentication', 'local-id']): + local_id = config.return_value(peer_base + ['authentication', 'local-id']) + config.set(base + ['authentication', 'psk', peer, 'id'], value=local_id, replace=False) + if config.exists(peer_base + ['authentication', 'remote-id']): + remote_id = config.return_value(peer_base + ['authentication', 'remote-id']) + config.set(base + ['authentication', 'psk', peer, 'id'], value=remote_id, replace=False) + + if config.exists(peer_base + ['local-address']): + tmp = config.return_value(peer_base + ['local-address']) + config.set(base + ['authentication', 'psk', peer, 'id'], value=tmp, replace=False) + if config.exists(peer_base + ['remote-address']): + tmp = config.return_value(peer_base + ['remote-address']) + if tmp: + for remote_addr in tmp: + if remote_addr == 'any': + remote_addr = '%any' + config.set(base + ['authentication', 'psk', peer, 'id'], value=remote_addr, replace=False) + + # get DHCP peer interface as psk dhcp-interface + if config.exists(peer_base + ['dhcp-interface']): + tmp = config.return_value(peer_base + ['dhcp-interface']) + config.set(base + ['authentication', 'psk', peer, 'dhcp-interface'], value=tmp) + + +try: + with open(file_name, 'w') as f: + f.write(config.to_string()) +except OSError as e: + print(f'Failed to save the modified config: {e}') + exit(1) diff --git a/src/migration-scripts/ipsec/9-to-10 b/src/migration-scripts/ipsec/9-to-10 index 1254104cb..de366ef3b 100755 --- a/src/migration-scripts/ipsec/9-to-10 +++ b/src/migration-scripts/ipsec/9-to-10 @@ -85,10 +85,10 @@ if config.exists(base + ['site-to-site', 'peer']): config.rename(peer_base + ['authentication', 'id'], 'local-id') # For the peer '@foo' set remote-id 'foo' if remote-id is not defined - if peer.startswith('@'): - if not config.exists(peer_base + ['authentication', 'remote-id']): - tmp = peer.replace('@', '') - config.set(peer_base + ['authentication', 'remote-id'], value=tmp) + # For the peer '192.0.2.1' set remote-id '192.0.2.1' if remote-id is not defined + if not config.exists(peer_base + ['authentication', 'remote-id']): + tmp = peer.replace('@', '') if peer.startswith('@') else peer + config.set(peer_base + ['authentication', 'remote-id'], value=tmp) # replace: 'peer <tag> force-encapsulation enable' # => 'peer <tag> force-udp-encapsulation' diff --git a/src/migration-scripts/ntp/1-to-2 b/src/migration-scripts/ntp/1-to-2 new file mode 100755 index 000000000..d1e510e4c --- /dev/null +++ b/src/migration-scripts/ntp/1-to-2 @@ -0,0 +1,72 @@ +#!/usr/bin/env python3 + +# Copyright (C) 2023 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/>. + +# T3008: move from ntpd to chrony and migrate "system ntp" to "service ntp" + +import sys + +from vyos.configtree import ConfigTree + +if (len(sys.argv) < 1): + print("Must specify file name!") + sys.exit(1) + +file_name = sys.argv[1] + +with open(file_name, 'r') as f: + config_file = f.read() + +config = ConfigTree(config_file) + +base_path = ['system', 'ntp'] +new_base_path = ['service', 'ntp'] +if not config.exists(base_path): + # Nothing to do + sys.exit(0) + +# config.copy does not recursively create a path, so create ['service'] if +# it doesn't yet exist, such as for config.boot.default +if not config.exists(['service']): + config.set(['service']) + +# copy "system ntp" to "service ntp" +config.copy(base_path, new_base_path) +config.delete(base_path) + +# chrony does not support the preempt option, drop it +for server in config.list_nodes(new_base_path + ['server']): + server_base = new_base_path + ['server', server] + if config.exists(server_base + ['preempt']): + config.delete(server_base + ['preempt']) + +# Rename "allow-clients" -> "allow-client" +if config.exists(new_base_path + ['allow-clients']): + config.rename(new_base_path + ['allow-clients'], 'allow-client') + +# By default VyOS 1.3 allowed NTP queries for all networks - in chrony we +# explicitly disable this behavior and clients need to be specified using the +# allow-client CLI option. In order to be fully backwards compatible, we specify +# 0.0.0.0/0 and ::/0 as allow networks if not specified otherwise explicitly. +if not config.exists(new_base_path + ['allow-client']): + config.set(new_base_path + ['allow-client', 'address'], value='0.0.0.0/0', replace=False) + config.set(new_base_path + ['allow-client', 'address'], value='::/0', replace=False) + +try: + with open(file_name, 'w') as f: + f.write(config.to_string()) +except OSError as e: + print("Failed to save the modified config: {}".format(e)) + sys.exit(1) diff --git a/src/migration-scripts/policy/4-to-5 b/src/migration-scripts/policy/4-to-5 new file mode 100755 index 000000000..33c9e6ade --- /dev/null +++ b/src/migration-scripts/policy/4-to-5 @@ -0,0 +1,92 @@ +#!/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/>. + +# T2199: Migrate interface policy nodes to policy route <name> interface <ifname> + +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() + +base4 = ['policy', 'route'] +base6 = ['policy', 'route6'] +config = ConfigTree(config_file) + +if not config.exists(base4) and not config.exists(base6): + # Nothing to do + exit(0) + +def migrate_interface(config, iftype, ifname, vif=None, vifs=None, vifc=None): + if_path = ['interfaces', iftype, ifname] + ifname_full = ifname + + if vif: + if_path += ['vif', vif] + ifname_full = f'{ifname}.{vif}' + elif vifs: + if_path += ['vif-s', vifs] + ifname_full = f'{ifname}.{vifs}' + if vifc: + if_path += ['vif-c', vifc] + ifname_full = f'{ifname}.{vifs}.{vifc}' + + if not config.exists(if_path + ['policy']): + return + + if config.exists(if_path + ['policy', 'route']): + route_name = config.return_value(if_path + ['policy', 'route']) + config.set(base4 + [route_name, 'interface'], value=ifname_full, replace=False) + + if config.exists(if_path + ['policy', 'route6']): + route_name = config.return_value(if_path + ['policy', 'route6']) + config.set(base6 + [route_name, 'interface'], value=ifname_full, replace=False) + + config.delete(if_path + ['policy']) + +for iftype in config.list_nodes(['interfaces']): + for ifname in config.list_nodes(['interfaces', iftype]): + migrate_interface(config, iftype, ifname) + + if config.exists(['interfaces', iftype, ifname, 'vif']): + for vif in config.list_nodes(['interfaces', iftype, ifname, 'vif']): + migrate_interface(config, iftype, ifname, vif=vif) + + if config.exists(['interfaces', iftype, ifname, 'vif-s']): + for vifs in config.list_nodes(['interfaces', iftype, ifname, 'vif-s']): + migrate_interface(config, iftype, ifname, vifs=vifs) + + if config.exists(['interfaces', iftype, ifname, 'vif-s', vifs, 'vif-c']): + for vifc in config.list_nodes(['interfaces', iftype, ifname, 'vif-s', vifs, 'vif-c']): + migrate_interface(config, iftype, ifname, vifs=vifs, vifc=vifc) + +try: + with open(file_name, 'w') as f: + f.write(config.to_string()) +except OSError as e: + print("Failed to save the modified config: {}".format(e)) + exit(1) diff --git a/src/migration-scripts/qos/1-to-2 b/src/migration-scripts/qos/1-to-2 new file mode 100755 index 000000000..41026cbd6 --- /dev/null +++ b/src/migration-scripts/qos/1-to-2 @@ -0,0 +1,126 @@ +#!/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 argv,exit + +from vyos.base import Warning +from vyos.configtree import ConfigTree +from vyos.util import read_file + +def bandwidth_percent_to_val(interface, percent) -> int: + speed = read_file(f'/sys/class/net/{interface}/speed') + if not speed.isnumeric(): + Warning('Interface speed cannot be determined (assuming 10 Mbit/s)') + speed = 10 + speed = int(speed) *1000000 # convert to MBit/s + return speed * int(percent) // 100 # integer division + +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 = ['traffic-policy'] +config = ConfigTree(config_file) + +if not config.exists(base): + # Nothing to do + exit(0) + +iface_config = {} + +if config.exists(['interfaces']): + def get_qos(config, interface, interface_base): + if config.exists(interface_base): + tmp = { interface : {} } + if config.exists(interface_base + ['in']): + tmp[interface]['ingress'] = config.return_value(interface_base + ['in']) + if config.exists(interface_base + ['out']): + tmp[interface]['egress'] = config.return_value(interface_base + ['out']) + config.delete(interface_base) + return tmp + return None + + # Migrate "interface ethernet eth0 traffic-policy in|out" to "qos interface eth0 ingress|egress" + for type in config.list_nodes(['interfaces']): + for interface in config.list_nodes(['interfaces', type]): + interface_base = ['interfaces', type, interface, 'traffic-policy'] + tmp = get_qos(config, interface, interface_base) + if tmp: iface_config.update(tmp) + + vif_path = ['interfaces', type, interface, 'vif'] + if config.exists(vif_path): + for vif in config.list_nodes(vif_path): + vif_interface_base = vif_path + [vif, 'traffic-policy'] + ifname = f'{interface}.{vif}' + tmp = get_qos(config, ifname, vif_interface_base) + if tmp: iface_config.update(tmp) + + vif_s_path = ['interfaces', type, interface, 'vif-s'] + if config.exists(vif_s_path): + for vif_s in config.list_nodes(vif_s_path): + vif_s_interface_base = vif_s_path + [vif_s, 'traffic-policy'] + ifname = f'{interface}.{vif_s}' + tmp = get_qos(config, ifname, vif_s_interface_base) + if tmp: iface_config.update(tmp) + + # vif-c interfaces MUST be migrated before their parent vif-s + # interface as the migrate_*() functions delete the path! + vif_c_path = ['interfaces', type, interface, 'vif-s', vif_s, 'vif-c'] + if config.exists(vif_c_path): + for vif_c in config.list_nodes(vif_c_path): + vif_c_interface_base = vif_c_path + [vif_c, 'traffic-policy'] + ifname = f'{interface}.{vif_s}.{vif_c}' + tmp = get_qos(config, ifname, vif_s_interface_base) + if tmp: iface_config.update(tmp) + + +# Now we have the information which interface uses which QoS policy. +# Interface binding will be moved to the qos CLi tree +config.set(['qos']) +config.copy(base, ['qos', 'policy']) +config.delete(base) + +# Now map the interface policy binding to the new CLI syntax +if len(iface_config): + config.set(['qos', 'interface']) + config.set_tag(['qos', 'interface']) + +for interface, interface_config in iface_config.items(): + config.set(['qos', 'interface', interface]) + config.set_tag(['qos', 'interface', interface]) + if 'ingress' in interface_config: + config.set(['qos', 'interface', interface, 'ingress'], value=interface_config['ingress']) + if 'egress' in interface_config: + config.set(['qos', 'interface', interface, 'egress'], value=interface_config['egress']) + +# Remove "burst" CLI node from network emulator +netem_base = ['qos', 'policy', 'network-emulator'] +if config.exists(netem_base): + for policy_name in config.list_nodes(netem_base): + if config.exists(netem_base + [policy_name, 'burst']): + config.delete(netem_base + [policy_name, 'burst']) + +try: + with open(file_name, 'w') as f: + f.write(config.to_string()) +except OSError as e: + print("Failed to save the modified config: {}".format(e)) + exit(1) diff --git a/src/migration-scripts/snmp/2-to-3 b/src/migration-scripts/snmp/2-to-3 new file mode 100755 index 000000000..5f8d9c88d --- /dev/null +++ b/src/migration-scripts/snmp/2-to-3 @@ -0,0 +1,57 @@ +#!/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/>. + +# T4857: Implement FRR SNMP recomendations +# cli changes from: +# set service snmp oid-enable route-table +# To +# set service snmp oid-enable ip-forward + +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 = ['service snmp'] +config = ConfigTree(config_file) + +if not config.exists(base): + # Nothing to do + exit(0) + +if config.exists(base + ['oid-enable']): + config.delete(base + ['oid-enable']) + config.set(base + ['oid-enable'], 'ip-forward') + + +try: + with open(file_name, 'w') as f: + f.write(config.to_string()) +except OSError as e: + print("Failed to save the modified config: {}".format(e)) + exit(1) diff --git a/src/op_mode/accelppp.py b/src/op_mode/accelppp.py new file mode 100755 index 000000000..2fd045dc3 --- /dev/null +++ b/src/op_mode/accelppp.py @@ -0,0 +1,133 @@ +#!/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 sys + +import vyos.accel_ppp +import vyos.opmode + +from vyos.configquery import ConfigTreeQuery +from vyos.util import rc_cmd + + +accel_dict = { + 'ipoe': { + 'port': 2002, + 'path': 'service ipoe-server' + }, + 'pppoe': { + 'port': 2001, + 'path': 'service pppoe-server' + }, + 'pptp': { + 'port': 2003, + 'path': 'vpn pptp' + }, + 'l2tp': { + 'port': 2004, + 'path': 'vpn l2tp' + }, + 'sstp': { + 'port': 2005, + 'path': 'vpn sstp' + } +} + + +def _get_raw_statistics(accel_output, pattern): + return vyos.accel_ppp.get_server_statistics(accel_output, pattern, sep=':') + + +def _get_raw_sessions(port): + cmd_options = 'show sessions ifname,username,ip,ip6,ip6-dp,type,state,' \ + 'uptime-raw,calling-sid,called-sid,sid,comp,rx-bytes-raw,' \ + 'tx-bytes-raw,rx-pkts,tx-pkts' + output = vyos.accel_ppp.accel_cmd(port, cmd_options) + parsed_data: list[dict[str, str]] = vyos.accel_ppp.accel_out_parse( + output.splitlines()) + return parsed_data + + +def _verify(func): + """Decorator checks if accel-ppp protocol + ipoe/pppoe/pptp/l2tp/sstp is configured + + for example: + service ipoe-server + vpn sstp + """ + from functools import wraps + + @wraps(func) + def _wrapper(*args, **kwargs): + config = ConfigTreeQuery() + protocol_list = accel_dict.keys() + protocol = kwargs.get('protocol') + # unknown or incorrect protocol query + if protocol not in protocol_list: + unconf_message = f'unknown protocol "{protocol}"' + raise vyos.opmode.UnconfiguredSubsystem(unconf_message) + # Check if config does not exist + config_protocol_path = accel_dict[protocol]['path'] + if not config.exists(config_protocol_path): + unconf_message = f'"{config_protocol_path}" is not configured' + raise vyos.opmode.UnconfiguredSubsystem(unconf_message) + return func(*args, **kwargs) + + return _wrapper + + +@_verify +def show_statistics(raw: bool, protocol: str): + """show accel-cmd statistics + CPU utilization and amount of sessions + + protocol: ipoe/pppoe/ppptp/l2tp/sstp + """ + pattern = f'{protocol}:' + port = accel_dict[protocol]['port'] + rc, output = rc_cmd(f'/usr/bin/accel-cmd -p {port} show stat') + + if raw: + return _get_raw_statistics(output, pattern) + + return output + + +@_verify +def show_sessions(raw: bool, protocol: str): + """show accel-cmd sessions + + protocol: ipoe/pppoe/ppptp/l2tp/sstp + """ + port = accel_dict[protocol]['port'] + if raw: + return _get_raw_sessions(port) + + return vyos.accel_ppp.accel_cmd(port, + 'show sessions ifname,username,ip,ip6,ip6-dp,' + 'calling-sid,rate-limit,state,uptime,rx-bytes,tx-bytes') + + +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/bgp.py b/src/op_mode/bgp.py new file mode 100755 index 000000000..23001a9d7 --- /dev/null +++ b/src/op_mode/bgp.py @@ -0,0 +1,120 @@ +#!/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/>. +# +# Purpose: +# Displays bgp neighbors information. +# Used by the "show bgp (vrf <tag>) ipv4|ipv6 neighbors" commands. + +import re +import sys +import typing + +import jmespath +from jinja2 import Template +from humps import decamelize + +from vyos.configquery import ConfigTreeQuery + +import vyos.opmode + + +frr_command_template = Template(""" +{% if family %} + show bgp + {{ 'vrf ' ~ vrf if vrf else '' }} + {{ 'ipv6' if family == 'inet6' else 'ipv4'}} + {{ 'neighbor ' ~ peer if peer else 'summary' }} +{% endif %} + +{% if raw %} + json +{% endif %} +""") + + +def _verify(func): + """Decorator checks if BGP config exists + BGP configuration can be present under vrf <tag> + If we do npt get arg 'peer' then it can be 'bgp summary' + """ + from functools import wraps + + @wraps(func) + def _wrapper(*args, **kwargs): + config = ConfigTreeQuery() + afi = 'ipv6' if kwargs.get('family') == 'inet6' else 'ipv4' + global_vrfs = ['all', 'default'] + peer = kwargs.get('peer') + vrf = kwargs.get('vrf') + unconf_message = f'BGP or neighbor is not configured' + # Add option to check the specific neighbor if we have arg 'peer' + peer_opt = f'neighbor {peer} address-family {afi}-unicast' if peer else '' + vrf_opt = '' + if vrf and vrf not in global_vrfs: + vrf_opt = f'vrf name {vrf}' + # Check if config does not exist + if not config.exists(f'{vrf_opt} protocols bgp {peer_opt}'): + raise vyos.opmode.UnconfiguredSubsystem(unconf_message) + return func(*args, **kwargs) + + return _wrapper + + +@_verify +def show_neighbors(raw: bool, + family: str, + peer: typing.Optional[str], + vrf: typing.Optional[str]): + kwargs = dict(locals()) + frr_command = frr_command_template.render(kwargs) + frr_command = re.sub(r'\s+', ' ', frr_command) + + from vyos.util import cmd + output = cmd(f"vtysh -c '{frr_command}'") + + if raw: + from json import loads + data = loads(output) + # Get list of the peers + peers = jmespath.search('*.peers | [0]', data) + if peers: + # Create new dict, delete old key 'peers' + # add key 'peers' neighbors to the list + list_peers = [] + new_dict = jmespath.search('* | [0]', data) + if 'peers' in new_dict: + new_dict.pop('peers') + + for neighbor, neighbor_options in peers.items(): + neighbor_options['neighbor'] = neighbor + list_peers.append(neighbor_options) + new_dict['peers'] = list_peers + return decamelize(new_dict) + data = jmespath.search('* | [0]', data) + return decamelize(data) + + else: + return output + + +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/bridge.py b/src/op_mode/bridge.py index 5a821a287..d6098c158 100755 --- a/src/op_mode/bridge.py +++ b/src/op_mode/bridge.py @@ -32,7 +32,7 @@ def _get_json_data(): """ Get bridge data format JSON """ - return cmd(f'sudo bridge --json link show') + return cmd(f'bridge --json link show') def _get_raw_data_summary(): @@ -48,7 +48,7 @@ def _get_raw_data_vlan(): """ :returns dict """ - json_data = cmd('sudo bridge --json --compressvlans vlan show') + json_data = cmd('bridge --json --compressvlans vlan show') data_dict = json.loads(json_data) return data_dict @@ -57,7 +57,7 @@ def _get_raw_data_fdb(bridge): """Get MAC-address for the bridge brX :returns list """ - code, json_data = rc_cmd(f'sudo bridge --json fdb show br {bridge}') + code, json_data = rc_cmd(f'bridge --json fdb show br {bridge}') # From iproute2 fdb.c, fdb_show() will only exit(-1) in case of # non-existent bridge device; raise error. if code == 255: diff --git a/src/op_mode/config_mgmt.py b/src/op_mode/config_mgmt.py new file mode 100755 index 000000000..66de26d1f --- /dev/null +++ b/src/op_mode/config_mgmt.py @@ -0,0 +1,85 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2023 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +import sys +import typing + +import vyos.opmode +from vyos.config_mgmt import ConfigMgmt + +def show_commit_diff(raw: bool, rev: int, rev2: typing.Optional[int], + commands: bool): + config_mgmt = ConfigMgmt() + config_diff = config_mgmt.show_commit_diff(rev, rev2, commands) + + if raw: + rev2 = (rev+1) if rev2 is None else rev2 + if commands: + d = {f'config_command_diff_{rev2}_{rev}': config_diff} + else: + d = {f'config_file_diff_{rev2}_{rev}': config_diff} + return d + + return config_diff + +def show_commit_file(raw: bool, rev: int): + config_mgmt = ConfigMgmt() + config_file = config_mgmt.show_commit_file(rev) + + if raw: + d = {f'config_revision_{rev}': config_file} + return d + + return config_file + +def show_commit_log(raw: bool): + config_mgmt = ConfigMgmt() + + msg = '' + if config_mgmt.max_revisions == 0: + msg = ('commit-revisions is not configured;\n' + 'commit log is empty or stale:\n\n') + + data = config_mgmt.get_raw_log_data() + if raw: + return data + + out = config_mgmt.format_log_data(data) + out = msg + out + + return out + +def show_commit_log_brief(raw: bool): + # used internally for completion help for 'rollback' + # option 'raw' will return same as 'show_commit_log' + config_mgmt = ConfigMgmt() + + data = config_mgmt.get_raw_log_data() + if raw: + return data + + out = config_mgmt.format_log_data_brief(data) + + return out + +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/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/conntrack.py b/src/op_mode/conntrack.py index fff537936..df213cc5a 100755 --- a/src/op_mode/conntrack.py +++ b/src/op_mode/conntrack.py @@ -116,7 +116,7 @@ def get_formatted_output(dict_data): reply_src = f'{reply_src}:{reply_sport}' if reply_sport else reply_src reply_dst = f'{reply_dst}:{reply_dport}' if reply_dport else reply_dst state = meta['state'] if 'state' in meta else '' - mark = meta['mark'] + mark = meta['mark'] if 'mark' in meta else '' zone = meta['zone'] if 'zone' in meta else '' data_entries.append( [conn_id, orig_src, orig_dst, reply_src, reply_dst, proto, state, timeout, mark, zone]) diff --git a/src/op_mode/container.py b/src/op_mode/container.py index ce466ffc1..d48766a0c 100755 --- a/src/op_mode/container.py +++ b/src/op_mode/container.py @@ -23,7 +23,6 @@ from vyos.util import cmd import vyos.opmode - def _get_json_data(command: str) -> list: """ Get container command format JSON @@ -36,9 +35,22 @@ def _get_raw_data(command: str) -> list: data = json.loads(json_data) return data +def add_image(name: str): + from vyos.util import rc_cmd + + rc, output = rc_cmd(f'podman image pull {name}') + if rc != 0: + raise vyos.opmode.InternalError(output) + +def delete_image(name: str): + from vyos.util import rc_cmd + + rc, output = rc_cmd(f'podman image rm --force {name}') + if rc != 0: + raise vyos.opmode.InternalError(output) def show_container(raw: bool): - command = 'sudo podman ps --all' + command = 'podman ps --all' container_data = _get_raw_data(command) if raw: return container_data @@ -47,8 +59,8 @@ def show_container(raw: bool): def show_image(raw: bool): - command = 'sudo podman image ls' - container_data = _get_raw_data('sudo podman image ls') + command = 'podman image ls' + container_data = _get_raw_data('podman image ls') if raw: return container_data else: @@ -56,7 +68,7 @@ def show_image(raw: bool): def show_network(raw: bool): - command = 'sudo podman network ls' + command = 'podman network ls' container_data = _get_raw_data(command) if raw: return container_data @@ -67,7 +79,7 @@ def show_network(raw: bool): def restart(name: str): from vyos.util import rc_cmd - rc, output = rc_cmd(f'sudo podman restart {name}') + rc, output = rc_cmd(f'systemctl restart vyos-container-{name}.service') if rc != 0: print(output) return None diff --git a/src/op_mode/dhcp.py b/src/op_mode/dhcp.py new file mode 100755 index 000000000..b9e6e7bc9 --- /dev/null +++ b/src/op_mode/dhcp.py @@ -0,0 +1,291 @@ +#!/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 sys +import typing + +from datetime import datetime +from ipaddress import ip_address +from isc_dhcp_leases import IscDhcpLeases +from tabulate import tabulate + +import vyos.opmode + +from vyos.base import Warning +from vyos.configquery import ConfigTreeQuery + +from vyos.util import cmd +from vyos.util import dict_search +from vyos.util import is_systemd_service_running + +config = ConfigTreeQuery() +lease_valid_states = ['all', 'active', 'free', 'expired', 'released', 'abandoned', 'reset', 'backup'] +sort_valid_inet = ['end', 'mac', 'hostname', 'ip', 'pool', 'remaining', 'start', 'state'] +sort_valid_inet6 = ['end', 'iaid_duid', 'ip', 'last_communication', 'pool', 'remaining', 'state', 'type'] + +def _utc_to_local(utc_dt): + return datetime.fromtimestamp((datetime.fromtimestamp(utc_dt) - datetime(1970, 1, 1)).total_seconds()) + + +def _format_hex_string(in_str): + out_str = "" + # if input is divisible by 2, add : every 2 chars + if len(in_str) > 0 and len(in_str) % 2 == 0: + out_str = ':'.join(a+b for a,b in zip(in_str[::2], in_str[1::2])) + else: + out_str = in_str + + return out_str + + +def _find_list_of_dict_index(lst, key='ip', value='') -> int: + """ + Find the index entry of list of dict matching the dict value + Exampe: + % lst = [{'ip': '192.0.2.1'}, {'ip': '192.0.2.2'}] + % _find_list_of_dict_index(lst, key='ip', value='192.0.2.2') + % 1 + """ + idx = next((index for (index, d) in enumerate(lst) if d[key] == value), None) + return idx + + +def _get_raw_server_leases(family='inet', pool=None, sorted=None, state=[]) -> list: + """ + Get DHCP server leases + :return list + """ + lease_file = '/config/dhcpdv6.leases' if family == 'inet6' else '/config/dhcpd.leases' + data = [] + leases = IscDhcpLeases(lease_file).get() + + if pool is None: + pool = _get_dhcp_pools(family=family) + else: + pool = [pool] + + for lease in leases: + data_lease = {} + data_lease['ip'] = lease.ip + data_lease['state'] = lease.binding_state + data_lease['pool'] = lease.sets.get('shared-networkname', '') + data_lease['end'] = lease.end.timestamp() + + if family == 'inet': + data_lease['mac'] = lease.ethernet + data_lease['start'] = lease.start.timestamp() + data_lease['hostname'] = lease.hostname + + if family == 'inet6': + data_lease['last_communication'] = lease.last_communication.timestamp() + data_lease['iaid_duid'] = _format_hex_string(lease.host_identifier_string) + lease_types_long = {'na': 'non-temporary', 'ta': 'temporary', 'pd': 'prefix delegation'} + data_lease['type'] = lease_types_long[lease.type] + + data_lease['remaining'] = lease.end - datetime.utcnow() + + if data_lease['remaining'].days >= 0: + # substraction gives us a timedelta object which can't be formatted with strftime + # so we use str(), split gets rid of the microseconds + data_lease['remaining'] = str(data_lease["remaining"]).split('.')[0] + else: + data_lease['remaining'] = '' + + # Do not add old leases + if data_lease['remaining'] != '' and data_lease['pool'] in pool: + if not state or data_lease['state'] in state: + data.append(data_lease) + + # deduplicate + checked = [] + for entry in data: + addr = entry.get('ip') + if addr not in checked: + checked.append(addr) + else: + idx = _find_list_of_dict_index(data, key='ip', value=addr) + data.pop(idx) + + if sorted: + if sorted == 'ip': + data.sort(key = lambda x:ip_address(x['ip'])) + else: + data.sort(key = lambda x:x[sorted]) + return data + + +def _get_formatted_server_leases(raw_data, family='inet'): + data_entries = [] + if family == 'inet': + for lease in raw_data: + ipaddr = lease.get('ip') + hw_addr = lease.get('mac') + state = lease.get('state') + start = lease.get('start') + start = _utc_to_local(start).strftime('%Y/%m/%d %H:%M:%S') + end = lease.get('end') + end = _utc_to_local(end).strftime('%Y/%m/%d %H:%M:%S') + remain = lease.get('remaining') + pool = lease.get('pool') + hostname = lease.get('hostname') + data_entries.append([ipaddr, hw_addr, state, start, end, remain, pool, hostname]) + + headers = ['IP Address', 'MAC address', 'State', 'Lease start', 'Lease expiration', 'Remaining', 'Pool', + 'Hostname'] + + if family == 'inet6': + for lease in raw_data: + ipaddr = lease.get('ip') + state = lease.get('state') + start = lease.get('last_communication') + start = _utc_to_local(start).strftime('%Y/%m/%d %H:%M:%S') + end = lease.get('end') + end = _utc_to_local(end).strftime('%Y/%m/%d %H:%M:%S') + remain = lease.get('remaining') + lease_type = lease.get('type') + pool = lease.get('pool') + host_identifier = lease.get('iaid_duid') + data_entries.append([ipaddr, state, start, end, remain, lease_type, pool, host_identifier]) + + headers = ['IPv6 address', 'State', 'Last communication', 'Lease expiration', 'Remaining', 'Type', 'Pool', + 'IAID_DUID'] + + output = tabulate(data_entries, headers, numalign='left') + return output + + +def _get_dhcp_pools(family='inet') -> list: + v = 'v6' if family == 'inet6' else '' + pools = config.list_nodes(f'service dhcp{v}-server shared-network-name') + return pools + + +def _get_pool_size(pool, family='inet'): + v = 'v6' if family == 'inet6' else '' + base = f'service dhcp{v}-server shared-network-name {pool}' + size = 0 + subnets = config.list_nodes(f'{base} subnet') + for subnet in subnets: + if family == 'inet6': + ranges = config.list_nodes(f'{base} subnet {subnet} address-range start') + else: + ranges = config.list_nodes(f'{base} subnet {subnet} range') + for range in ranges: + if family == 'inet6': + start = config.list_nodes(f'{base} subnet {subnet} address-range start')[0] + stop = config.value(f'{base} subnet {subnet} address-range start {start} stop') + else: + start = config.value(f'{base} subnet {subnet} range {range} start') + stop = config.value(f'{base} subnet {subnet} range {range} stop') + # Add +1 because both range boundaries are inclusive + size += int(ip_address(stop)) - int(ip_address(start)) + 1 + return size + + +def _get_raw_pool_statistics(family='inet', pool=None): + if pool is None: + pool = _get_dhcp_pools(family=family) + else: + pool = [pool] + + v = 'v6' if family == 'inet6' else '' + stats = [] + for p in pool: + subnet = config.list_nodes(f'service dhcp{v}-server shared-network-name {p} subnet') + size = _get_pool_size(family=family, pool=p) + leases = len(_get_raw_server_leases(family=family, pool=p)) + use_percentage = round(leases / size * 100) if size != 0 else 0 + pool_stats = {'pool': p, 'size': size, 'leases': leases, + 'available': (size - leases), 'use_percentage': use_percentage, 'subnet': subnet} + stats.append(pool_stats) + return stats + + +def _get_formatted_pool_statistics(pool_data, family='inet'): + data_entries = [] + for entry in pool_data: + pool = entry.get('pool') + size = entry.get('size') + leases = entry.get('leases') + available = entry.get('available') + use_percentage = entry.get('use_percentage') + use_percentage = f'{use_percentage}%' + data_entries.append([pool, size, leases, available, use_percentage]) + + headers = ['Pool', 'Size','Leases', 'Available', 'Usage'] + output = tabulate(data_entries, headers, numalign='left') + return output + + +def _verify(func): + """Decorator checks if DHCP(v6) config exists""" + from functools import wraps + + @wraps(func) + def _wrapper(*args, **kwargs): + config = ConfigTreeQuery() + family = kwargs.get('family') + v = 'v6' if family == 'inet6' else '' + unconf_message = f'DHCP{v} server is not configured' + # Check if config does not exist + if not config.exists(f'service dhcp{v}-server'): + raise vyos.opmode.UnconfiguredSubsystem(unconf_message) + return func(*args, **kwargs) + return _wrapper + + +@_verify +def show_pool_statistics(raw: bool, family: str, pool: typing.Optional[str]): + pool_data = _get_raw_pool_statistics(family=family, pool=pool) + if raw: + return pool_data + else: + return _get_formatted_pool_statistics(pool_data, family=family) + + +@_verify +def show_server_leases(raw: bool, family: str, pool: typing.Optional[str], + sorted: typing.Optional[str], state: typing.Optional[str]): + # if dhcp server is down, inactive leases may still be shown as active, so warn the user. + if not is_systemd_service_running('isc-dhcp-server.service'): + Warning('DHCP server is configured but not started. Data may be stale.') + + v = 'v6' if family == 'inet6' else '' + if pool and pool not in _get_dhcp_pools(family=family): + raise vyos.opmode.IncorrectValue(f'DHCP{v} pool "{pool}" does not exist!') + + if state and state not in lease_valid_states: + raise vyos.opmode.IncorrectValue(f'DHCP{v} state "{state}" is invalid!') + + sort_valid = sort_valid_inet6 if family == 'inet6' else sort_valid_inet + if sorted and sorted not in sort_valid: + raise vyos.opmode.IncorrectValue(f'DHCP{v} sort "{sorted}" is invalid!') + + lease_data = _get_raw_server_leases(family=family, pool=pool, sorted=sorted, state=state) + if raw: + return lease_data + else: + return _get_formatted_server_leases(lease_data, family=family) + + +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/dns.py b/src/op_mode/dns.py index 9e5b1040c..a0e47d7ad 100755 --- a/src/op_mode/dns.py +++ b/src/op_mode/dns.py @@ -54,10 +54,10 @@ def _data_to_dict(data, sep="\t") -> dict: def _get_raw_forwarding_statistics() -> dict: - command = cmd('sudo /usr/bin/rec_control --socket-dir=/run/powerdns get-all') + command = cmd('rec_control --socket-dir=/run/powerdns get-all') data = _data_to_dict(command) data['cache-size'] = "{0:.2f}".format( int( - cmd('sudo /usr/bin/rec_control --socket-dir=/run/powerdns get cache-bytes')) / 1024 ) + cmd('rec_control --socket-dir=/run/powerdns get cache-bytes')) / 1024 ) return data diff --git a/src/op_mode/firewall.py b/src/op_mode/firewall.py index 950feb625..46bda5f7e 100755 --- a/src/op_mode/firewall.py +++ b/src/op_mode/firewall.py @@ -63,7 +63,7 @@ def get_config_firewall(conf, name=None, ipv6=False, interfaces=True): get_first_key=True, no_tag_node_value_mangle=True) if firewall and interfaces: if name: - firewall['interface'] = [] + firewall['interface'] = {} else: if 'name' in firewall: for fw_name, name_conf in firewall['name'].items(): diff --git a/src/op_mode/generate_interfaces_debug_archive.py b/src/op_mode/generate_interfaces_debug_archive.py new file mode 100755 index 000000000..f5767080a --- /dev/null +++ b/src/op_mode/generate_interfaces_debug_archive.py @@ -0,0 +1,115 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2023 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 +import os + +# define a list of commands that needs to be executed + +CMD_LIST: list[str] = [ + "journalctl -b -n 500", + "journalctl -b -k -n 500", + "ip -s l", + "cat /proc/interrupts", + "cat /proc/softirqs", + "top -b -d 1 -n 2 -1", + "netstat -l", + "cat /proc/net/dev", + "cat /proc/net/softnet_stat", + "cat /proc/net/icmp", + "cat /proc/net/udp", + "cat /proc/net/tcp", + "cat /proc/net/netstat", + "sysctl net", + "timeout 10 tcpdump -c 500 -eni any port not 22" +] + +CMD_INTERFACES_LIST: list[str] = [ + "ethtool -i ", + "ethtool -S ", + "ethtool -g ", + "ethtool -c ", + "ethtool -a ", + "ethtool -k ", + "ethtool -i ", + "ethtool --phy-statistics " +] + +# get intefaces info +interfaces_list = os.popen('ls /sys/class/net/').read().split() + +# modify CMD_INTERFACES_LIST for all interfaces +CMD_INTERFACES_LIST_MOD=[] +for command_interface in interfaces_list: + for command_interfacev2 in CMD_INTERFACES_LIST: + CMD_INTERFACES_LIST_MOD.append (f'{command_interfacev2}{command_interface}') + +# 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/drops-debug_{time_now}') +# set file paths +drops_file: Path = Path(f'{tmp_dir}/drops.txt') +interfaces_file: Path = Path(f'{tmp_dir}/interfaces.txt') +archive_file: str = f'/tmp/packet-drops-debug_{time_now}.tar.bz2' + +# create files +tmp_dir.mkdir() +drops_file.touch() +interfaces_file.touch() + +try: + # execute all commands + for command in CMD_LIST: + save_stdout(command, drops_file) + for command_interface in CMD_INTERFACES_LIST_MOD: + save_stdout(command_interface, interfaces_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.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/igmp-proxy.py b/src/op_mode/igmp-proxy.py new file mode 100755 index 000000000..0086c9aa6 --- /dev/null +++ b/src/op_mode/igmp-proxy.py @@ -0,0 +1,99 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2023 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/>. + +# File: show_igmpproxy.py +# Purpose: +# Display istatistics from IPv4 IGMP proxy. +# Used by the "run show ip multicast" command tree. + +import ipaddress +import json +import socket +import sys +import tabulate + +import vyos.config +import vyos.opmode + +from vyos.util import bytes_to_human, print_error + +def _is_configured(): + """Check if IGMP proxy is configured""" + return vyos.config.Config().exists_effective('protocols igmp-proxy') + +def _is_running(): + """Check if IGMP proxy is currently running""" + return not vyos.util.run('ps -C igmpproxy') + +def _kernel_to_ip(addr): + """ + Convert any given address from Linux kernel to a proper, IPv4 address + using the correct host byte order. + """ + # Convert from hex 'FE000A0A' to decimal '4261415434' + addr = int(addr, 16) + # Kernel ABI _always_ uses network byte order. + addr = socket.ntohl(addr) + return str(ipaddress.IPv4Address(addr)) + +def _process_mr_vif(): + """Read rows from /proc/net/ip_mr_vif into dicts.""" + result = [] + with open('/proc/net/ip_mr_vif', 'r') as f: + next(f) + for line in f: + result.append({ + 'Interface': line.split()[1], + 'PktsIn' : int(line.split()[3]), + 'PktsOut' : int(line.split()[5]), + 'BytesIn' : int(line.split()[2]), + 'BytesOut' : int(line.split()[4]), + 'Local' : _kernel_to_ip(line.split()[7]), + }) + return result + +def show_interface(raw: bool): + if data := _process_mr_vif(): + if raw: + # Make the interface name the key for each row. + table = {} + for v in data: + table[v.pop('Interface')] = v + return json.loads(json.dumps(table)) + # Make byte values human-readable for the table. + arr = [] + for x in data: + arr.append({k: bytes_to_human(v) if k.startswith('Bytes') \ + else v for k, v in x.items()}) + return tabulate.tabulate(arr, headers='keys') + + +if not _is_configured(): + print_error('IGMP proxy is not configured.') + sys.exit(0) +if not _is_running(): + print_error('IGMP proxy is not running.') + sys.exit(0) + + +if __name__ == "__main__": + try: + res = vyos.opmode.run(sys.modules[__name__]) + if res: + print(res) + except (ValueError, vyos.opmode.Error) as e: + print_error(e) + sys.exit(1) diff --git a/src/op_mode/interfaces.py b/src/op_mode/interfaces.py new file mode 100755 index 000000000..678c74980 --- /dev/null +++ b/src/op_mode/interfaces.py @@ -0,0 +1,412 @@ +#!/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 re +import sys +import glob +import json +import typing +from datetime import datetime +from tabulate import tabulate + +import vyos.opmode +from vyos.ifconfig import Section +from vyos.ifconfig import Interface +from vyos.ifconfig import VRRP +from vyos.util import cmd, rc_cmd, call + +def catch_broken_pipe(func): + def wrapped(*args, **kwargs): + try: + func(*args, **kwargs) + except (BrokenPipeError, KeyboardInterrupt): + # Flush output to /dev/null and bail out. + os.dup2(os.open(os.devnull, os.O_WRONLY), sys.stdout.fileno()) + return wrapped + +# The original implementation of filtered_interfaces has signature: +# (ifnames: list, iftypes: typing.Union[str, list], vif: bool, vrrp: bool) -> intf: Interface: +# Arg types allowed in CLI (ifnames: str, iftypes: str) were manually +# re-typed from argparse args. +# We include the function in a general form, however op-mode standard +# functions will restrict to the CLI-allowed arg types, wrapped in Optional. +def filtered_interfaces(ifnames: typing.Union[str, list], + iftypes: typing.Union[str, list], + vif: bool, vrrp: bool) -> Interface: + """ + get all interfaces from the OS and return them; ifnames can be used to + filter which interfaces should be considered + + ifnames: a list of interface names to consider, empty do not filter + + return an instance of the Interface class + """ + if isinstance(ifnames, str): + ifnames = [ifnames] if ifnames else [] + if isinstance(iftypes, list): + for iftype in iftypes: + yield from filtered_interfaces(ifnames, iftype, vif, vrrp) + + for ifname in Section.interfaces(iftypes): + # Bail out early if interface name not part of our search list + if ifnames and ifname not in ifnames: + continue + + # As we are only "reading" from the interface - we must use the + # generic base class which exposes all the data via a common API + interface = Interface(ifname, create=False, debug=False) + + # VLAN interfaces have a '.' in their name by convention + if vif and not '.' in ifname: + continue + + if vrrp: + vrrp_interfaces = VRRP.active_interfaces() + if ifname not in vrrp_interfaces: + continue + + yield interface + +def _split_text(text, used=0): + """ + take a string and attempt to split it to fit with the width of the screen + + text: the string to split + used: number of characted already used in the screen + """ + no_tty = call('tty -s') + + returned = cmd('stty size') if not no_tty else '' + returned = returned.split() + if len(returned) == 2: + _, columns = tuple(int(_) for _ in returned) + else: + _, columns = (40, 80) + + desc_len = columns - used + + line = '' + for word in text.split(): + if len(line) + len(word) < desc_len: + line = f'{line} {word}' + continue + if line: + yield line[1:] + else: + line = f'{line} {word}' + + yield line[1:] + +def _get_counter_val(prev, now): + """ + attempt to correct a counter if it wrapped, copied from perl + + prev: previous counter + now: the current counter + """ + # This function has to deal with both 32 and 64 bit counters + if prev == 0: + return now + + # device is using 64 bit values assume they never wrap + value = now - prev + if (now >> 32) != 0: + return value + + # The counter has rolled. If the counter has rolled + # multiple times since the prev value, then this math + # is meaningless. + if value < 0: + value = (4294967296 - prev) + now + + return value + +def _pppoe(ifname): + out = cmd('ps -C pppd -f') + if ifname in out: + return 'C' + if ifname in [_.split('/')[-1] for _ in glob.glob('/etc/ppp/peers/pppoe*')]: + return 'D' + return '' + +def _find_intf_by_ifname(intf_l: list, name: str): + for d in intf_l: + if d['ifname'] == name: + return d + return {} + +# lifted out of operational.py to separate formatting from data +def _format_stats(stats, indent=4): + stat_names = { + 'rx': ['bytes', 'packets', 'errors', 'dropped', 'overrun', 'mcast'], + 'tx': ['bytes', 'packets', 'errors', 'dropped', 'carrier', 'collisions'], + } + + stats_dir = { + 'rx': ['rx_bytes', 'rx_packets', 'rx_errors', 'rx_dropped', 'rx_over_errors', 'multicast'], + 'tx': ['tx_bytes', 'tx_packets', 'tx_errors', 'tx_dropped', 'tx_carrier_errors', 'collisions'], + } + tabs = [] + for rtx in list(stats_dir): + tabs.append([f'{rtx.upper()}:', ] + stat_names[rtx]) + tabs.append(['', ] + [stats[_] for _ in stats_dir[rtx]]) + + s = tabulate( + tabs, + stralign="right", + numalign="right", + tablefmt="plain" + ) + + p = ' '*indent + return f'{p}' + s.replace('\n', f'\n{p}') + +def _get_raw_data(ifname: typing.Optional[str], + iftype: typing.Optional[str], + vif: bool, vrrp: bool) -> list: + if ifname is None: + ifname = '' + if iftype is None: + iftype = '' + ret =[] + for interface in filtered_interfaces(ifname, iftype, vif, vrrp): + res_intf = {} + cache = interface.operational.load_counters() + + out = cmd(f'ip -json addr show {interface.ifname}') + res_intf_l = json.loads(out) + res_intf = res_intf_l[0] + + if res_intf['link_type'] == 'tunnel6': + # Note that 'ip -6 tun show {interface.ifname}' is not json + # aware, so find in list + out = cmd('ip -json -6 tun show') + tunnel = json.loads(out) + res_intf['tunnel6'] = _find_intf_by_ifname(tunnel, + interface.ifname) + if 'ip6_tnl_f_use_orig_tclass' in res_intf['tunnel6']: + res_intf['tunnel6']['tclass'] = 'inherit' + del res_intf['tunnel6']['ip6_tnl_f_use_orig_tclass'] + + res_intf['counters_last_clear'] = int(cache.get('timestamp', 0)) + + res_intf['description'] = interface.get_alias() + + res_intf['stats'] = interface.operational.get_stats() + + ret.append(res_intf) + + # find pppoe interfaces that are in a transitional/dead state + if ifname.startswith('pppoe') and not _find_intf_by_ifname(ret, ifname): + pppoe_intf = {} + pppoe_intf['unhandled'] = None + pppoe_intf['ifname'] = ifname + pppoe_intf['state'] = _pppoe(ifname) + ret.append(pppoe_intf) + + return ret + +def _get_summary_data(ifname: typing.Optional[str], + iftype: typing.Optional[str], + vif: bool, vrrp: bool) -> list: + if ifname is None: + ifname = '' + if iftype is None: + iftype = '' + ret = [] + for interface in filtered_interfaces(ifname, iftype, vif, vrrp): + res_intf = {} + + res_intf['ifname'] = interface.ifname + res_intf['oper_state'] = interface.operational.get_state() + res_intf['admin_state'] = interface.get_admin_state() + res_intf['addr'] = [_ for _ in interface.get_addr() if not _.startswith('fe80::')] + res_intf['description'] = interface.get_alias() + + ret.append(res_intf) + + # find pppoe interfaces that are in a transitional/dead state + if ifname.startswith('pppoe') and not _find_intf_by_ifname(ret, ifname): + pppoe_intf = {} + pppoe_intf['unhandled'] = None + pppoe_intf['ifname'] = ifname + pppoe_intf['state'] = _pppoe(ifname) + ret.append(pppoe_intf) + + return ret + +def _get_counter_data(ifname: typing.Optional[str], + iftype: typing.Optional[str], + vif: bool, vrrp: bool) -> list: + if ifname is None: + ifname = '' + if iftype is None: + iftype = '' + ret = [] + for interface in filtered_interfaces(ifname, iftype, vif, vrrp): + res_intf = {} + + oper = interface.operational.get_state() + + if oper not in ('up','unknown'): + continue + + stats = interface.operational.get_stats() + cache = interface.operational.load_counters() + res_intf['ifname'] = interface.ifname + res_intf['rx_packets'] = _get_counter_val(cache['rx_packets'], stats['rx_packets']) + res_intf['rx_bytes'] = _get_counter_val(cache['rx_bytes'], stats['rx_bytes']) + res_intf['tx_packets'] = _get_counter_val(cache['tx_packets'], stats['tx_packets']) + res_intf['tx_bytes'] = _get_counter_val(cache['tx_bytes'], stats['tx_bytes']) + + ret.append(res_intf) + + return ret + +@catch_broken_pipe +def _format_show_data(data: list): + unhandled = [] + for intf in data: + if 'unhandled' in intf: + unhandled.append(intf) + continue + # instead of reformatting data, call non-json output: + rc, out = rc_cmd(f"ip addr show {intf['ifname']}") + if rc != 0: + continue + out = re.sub('^\d+:\s+','',out) + # add additional data already collected + if 'tunnel6' in intf: + t6_d = intf['tunnel6'] + t6_str = 'encaplimit %s hoplimit %s tclass %s flowlabel %s (flowinfo %s)' % ( + t6_d.get('encap_limit', ''), t6_d.get('hoplimit', ''), + t6_d.get('tclass', ''), t6_d.get('flowlabel', ''), + t6_d.get('flowinfo', '')) + out = re.sub('(\n\s+)(link/tunnel6)', f'\g<1>{t6_str}\g<1>\g<2>', out) + print(out) + ts = intf.get('counters_last_clear', 0) + if ts: + when = datetime.fromtimestamp(ts).strftime("%a %b %d %R:%S %Z %Y") + print(f' Last clear: {when}') + description = intf.get('description', '') + if description: + print(f' Description: {description}') + + stats = intf.get('stats', {}) + if stats: + print() + print(_format_stats(stats)) + + for intf in unhandled: + string = { + 'C': 'Coming up', + 'D': 'Link down' + }[intf['state']] + print(f"{intf['ifname']}: {string}") + + return 0 + +@catch_broken_pipe +def _format_show_summary(data): + format1 = '%-16s %-33s %-4s %s' + format2 = '%-16s %s' + + print('Codes: S - State, L - Link, u - Up, D - Down, A - Admin Down') + print(format1 % ("Interface", "IP Address", "S/L", "Description")) + print(format1 % ("---------", "----------", "---", "-----------")) + + unhandled = [] + for intf in data: + if 'unhandled' in intf: + unhandled.append(intf) + continue + ifname = [intf['ifname'],] + oper = ['u',] if intf['oper_state'] in ('up', 'unknown') else ['D',] + admin = ['u',] if intf['admin_state'] in ('up', 'unknown') else ['A',] + addrs = intf['addr'] or ['-',] + descs = list(_split_text(intf['description'], 0)) + + while ifname or oper or admin or addrs or descs: + i = ifname.pop(0) if ifname else '' + a = addrs.pop(0) if addrs else '' + d = descs.pop(0) if descs else '' + s = [admin.pop(0)] if admin else [] + l = [oper.pop(0)] if oper else [] + if len(a) < 33: + print(format1 % (i, a, '/'.join(s+l), d)) + else: + print(format2 % (i, a)) + print(format1 % ('', '', '/'.join(s+l), d)) + + for intf in unhandled: + string = { + 'C': 'u/D', + 'D': 'A/D' + }[intf['state']] + print(format1 % (ifname, '', string, '')) + + return 0 + +@catch_broken_pipe +def _format_show_counters(data: list): + formatting = '%-12s %10s %10s %10s %10s' + print(formatting % ('Interface', 'Rx Packets', 'Rx Bytes', 'Tx Packets', 'Tx Bytes')) + + for intf in data: + print(formatting % ( + intf['ifname'], + intf['rx_packets'], + intf['rx_bytes'], + intf['tx_packets'], + intf['tx_bytes'] + )) + + return 0 + +def show(raw: bool, intf_name: typing.Optional[str], + intf_type: typing.Optional[str], + vif: bool, vrrp: bool): + data = _get_raw_data(intf_name, intf_type, vif, vrrp) + if raw: + return data + return _format_show_data(data) + +def show_summary(raw: bool, intf_name: typing.Optional[str], + intf_type: typing.Optional[str], + vif: bool, vrrp: bool): + data = _get_summary_data(intf_name, intf_type, vif, vrrp) + if raw: + return data + return _format_show_summary(data) + +def show_counters(raw: bool, intf_name: typing.Optional[str], + intf_type: typing.Optional[str], + vif: bool, vrrp: bool): + data = _get_counter_data(intf_name, intf_type, vif, vrrp) + if raw: + return data + return _format_show_counters(data) + +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/ipsec.py b/src/op_mode/ipsec.py index 7ec35d7bd..f6417764a 100755 --- a/src/op_mode/ipsec.py +++ b/src/op_mode/ipsec.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2022 VyOS maintainers and contributors +# Copyright (C) 2022-2023 VyOS maintainers and contributors # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 or later as @@ -14,13 +14,16 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see <http://www.gnu.org/licenses/>. +import os import re 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 @@ -43,7 +46,10 @@ def _alphanum_key(key): def _get_vici_sas(): from vici import Session as vici_session - session = vici_session() + try: + session = vici_session() + except Exception: + raise vyos.opmode.UnconfiguredSubsystem("IPsec not initialized") sas = list(session.list_sas()) return sas @@ -132,41 +138,307 @@ 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(): + continue + 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 + """ + ike_state = 'down' + if not data: + return ike_state + for sa in data: + # check if parent SA exist + for connection, connection_conf in sa.items(): + if connection_name != connection: + continue + if connection_conf['state'].lower() == 'established': + ike_state = 'up' + return ike_state + + +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` + """ + child_sa = 'down' + if not data: + return child_sa + for sa in data: + # check if parent SA exist + if connection_name not in sa.keys(): + continue + 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 child_sa + + +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(): + continue + 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): - if not peer: - print('Invalid peer, aborting') - return - - 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: - print('Tunnel(s) not found, aborting') - return + raise vyos.opmode.IncorrectValue('Peer or tunnel(s) not found, aborting') - 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) except TimeoutExpired as e: - print(f'Timed out while resetting {conn}') - result = False - + raise vyos.opmode.InternalError(f'Timed out while resetting {conn}') - print('Peer reset result: ' + ('success' if result else 'failed')) + print('Peer reset result: success') def show_sa(raw: bool): @@ -176,6 +448,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/lldp.py b/src/op_mode/lldp.py new file mode 100755 index 000000000..1a1b94783 --- /dev/null +++ b/src/op_mode/lldp.py @@ -0,0 +1,149 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2023 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +import jmespath +import json +import sys +import typing + +from tabulate import tabulate + +from vyos.configquery import ConfigTreeQuery +from vyos.util import cmd +from vyos.util import dict_search + +import vyos.opmode +unconf_message = 'LLDP is not configured' +capability_codes = """Capability Codes: R - Router, B - Bridge, W - Wlan r - Repeater, S - Station + D - Docsis, T - Telephone, O - Other + +""" + +def _verify(func): + """Decorator checks if LLDP config exists""" + from functools import wraps + + @wraps(func) + def _wrapper(*args, **kwargs): + config = ConfigTreeQuery() + if not config.exists(['service', 'lldp']): + raise vyos.opmode.UnconfiguredSubsystem(unconf_message) + return func(*args, **kwargs) + return _wrapper + +def _get_raw_data(interface=None, detail=False): + """ + If interface name is not set - get all interfaces + """ + tmp = 'lldpcli -f json show neighbors' + if detail: + tmp += f' details' + if interface: + tmp += f' ports {interface}' + output = cmd(tmp) + data = json.loads(output) + if not data: + return [] + return data + +def _get_formatted_output(raw_data): + data_entries = [] + tmp = dict_search('lldp.interface', raw_data) + if not tmp: + return None + # One can not always ensure that "interface" is of type list, add safeguard. + # E.G. Juniper Networks, Inc. ex2300-c-12t only has a dict, not a list of dicts + if isinstance(tmp, dict): + tmp = [tmp] + for neighbor in tmp: + for local_if, values in neighbor.items(): + tmp = [] + + # Device field + if 'chassis' in values: + tmp.append(next(iter(values['chassis']))) + else: + tmp.append('') + + # Local Port field + tmp.append(local_if) + + # Protocol field + tmp.append(values['via']) + + # Capabilities + cap = '' + capabilities = jmespath.search('chassis.[*][0][0].capability', values) + # One can not always ensure that "capability" is of type list, add + # safeguard. E.G. Unify US-24-250W only has a dict, not a list of dicts + if isinstance(capabilities, dict): + capabilities = [capabilities] + if capabilities: + for capability in capabilities: + if capability['enabled']: + if capability['type'] == 'Router': + cap += 'R' + if capability['type'] == 'Bridge': + cap += 'B' + if capability['type'] == 'Wlan': + cap += 'W' + if capability['type'] == 'Station': + cap += 'S' + if capability['type'] == 'Repeater': + cap += 'r' + if capability['type'] == 'Telephone': + cap += 'T' + if capability['type'] == 'Docsis': + cap += 'D' + if capability['type'] == 'Other': + cap += 'O' + tmp.append(cap) + + # Remote software platform + platform = jmespath.search('chassis.[*][0][0].descr', values) + tmp.append(platform[:37]) + + # Remote interface + interface = jmespath.search('port.descr', values) + if not interface: + interface = jmespath.search('port.id.value', values) + if not interface: + interface = 'Unknown' + tmp.append(interface) + + # Add individual neighbor to output list + data_entries.append(tmp) + + headers = ["Device", "Local Port", "Protocol", "Capability", "Platform", "Remote Port"] + output = tabulate(data_entries, headers, numalign="left") + return capability_codes + output + +@_verify +def show_neighbors(raw: bool, interface: typing.Optional[str], detail: typing.Optional[bool]): + lldp_data = _get_raw_data(interface=interface, detail=detail) + if raw: + return lldp_data + else: + return _get_formatted_output(lldp_data) + +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/lldp_op.py b/src/op_mode/lldp_op.py deleted file mode 100755 index 17f6bf552..000000000 --- a/src/op_mode/lldp_op.py +++ /dev/null @@ -1,127 +0,0 @@ -#!/usr/bin/env python3 -# -# Copyright (C) 2019-2020 VyOS maintainers and contributors -# -# This program is free software; you can redistribute it and/or modify -# it under the terms of the GNU General Public License version 2 or later as -# published by the Free Software Foundation. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see <http://www.gnu.org/licenses/>. - -import argparse -import jinja2 -import json - -from sys import exit -from tabulate import tabulate - -from vyos.util import cmd -from vyos.config import Config - -parser = argparse.ArgumentParser() -parser.add_argument("-a", "--all", action="store_true", help="Show LLDP neighbors on all interfaces") -parser.add_argument("-d", "--detail", action="store_true", help="Show detailes LLDP neighbor information on all interfaces") -parser.add_argument("-i", "--interface", action="store", help="Show LLDP neighbors on specific interface") - -# Please be careful if you edit the template. -lldp_out = """Capability Codes: R - Router, B - Bridge, W - Wlan r - Repeater, S - Station - D - Docsis, T - Telephone, O - Other - -Device ID Local Proto Cap Platform Port ID ---------- ----- ----- --- -------- ------- -{% for neighbor in neighbors %} -{% for local_if, info in neighbor.items() %} -{{ "%-25s" | format(info.chassis) }} {{ "%-9s" | format(local_if) }} {{ "%-6s" | format(info.proto) }} {{ "%-5s" | format(info.capabilities) }} {{ "%-20s" | format(info.platform[:18]) }} {{ info.remote_if }} -{% endfor %} -{% endfor %} -""" - -def get_neighbors(): - return cmd('/usr/sbin/lldpcli -f json show neighbors') - -def parse_data(data, interface): - output = [] - if not isinstance(data, list): - data = [data] - - for neighbor in data: - for local_if, values in neighbor.items(): - if interface is not None and local_if != interface: - continue - cap = '' - for chassis, c_value in values.get('chassis', {}).items(): - # bail out early if no capabilities found - if 'capability' not in c_value: - continue - capabilities = c_value['capability'] - if isinstance(capabilities, dict): - capabilities = [capabilities] - - for capability in capabilities: - if capability['enabled']: - if capability['type'] == 'Router': - cap += 'R' - if capability['type'] == 'Bridge': - cap += 'B' - if capability['type'] == 'Wlan': - cap += 'W' - if capability['type'] == 'Station': - cap += 'S' - if capability['type'] == 'Repeater': - cap += 'r' - if capability['type'] == 'Telephone': - cap += 'T' - if capability['type'] == 'Docsis': - cap += 'D' - if capability['type'] == 'Other': - cap += 'O' - - remote_if = 'Unknown' - if 'descr' in values.get('port', {}): - remote_if = values.get('port', {}).get('descr') - elif 'id' in values.get('port', {}): - remote_if = values.get('port', {}).get('id').get('value', 'Unknown') - - output.append({local_if: {'chassis': chassis, - 'remote_if': remote_if, - 'proto': values.get('via','Unknown'), - 'platform': c_value.get('descr', 'Unknown'), - 'capabilities': cap}}) - - output = {'neighbors': output} - return output - -if __name__ == '__main__': - args = parser.parse_args() - tmp = { 'neighbors' : [] } - - c = Config() - if not c.exists_effective(['service', 'lldp']): - print('Service LLDP is not configured') - exit(0) - - if args.detail: - print(cmd('/usr/sbin/lldpctl -f plain')) - exit(0) - elif args.all or args.interface: - tmp = json.loads(get_neighbors()) - neighbors = dict() - - if 'interface' in tmp.get('lldp'): - neighbors = tmp['lldp']['interface'] - - else: - parser.print_help() - exit(1) - - tmpl = jinja2.Template(lldp_out, trim_blocks=True) - config_text = tmpl.render(parse_data(neighbors, interface=args.interface)) - print(config_text) - - exit(0) diff --git a/src/op_mode/log.py b/src/op_mode/log.py new file mode 100755 index 000000000..b0abd6191 --- /dev/null +++ b/src/op_mode/log.py @@ -0,0 +1,94 @@ +#!/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 re +import sys +import typing + +from jinja2 import Template + +from vyos.util import rc_cmd + +import vyos.opmode + +journalctl_command_template = Template(""" +--no-hostname +--quiet + +{% if boot %} + --boot +{% endif %} + +{% if count %} + --lines={{ count }} +{% endif %} + +{% if reverse %} + --reverse +{% endif %} + +{% if since %} + --since={{ since }} +{% endif %} + +{% if unit %} + --unit={{ unit }} +{% endif %} + +{% if utc %} + --utc +{% endif %} + +{% if raw %} +{# By default show 100 only lines for raw option if count does not set #} +{# Protection from parsing the full log by default #} +{% if not boot %} + --lines={{ '' ~ count if count else '100' }} +{% endif %} + --no-pager + --output=json +{% endif %} +""") + + +def show(raw: bool, + boot: typing.Optional[bool], + count: typing.Optional[int], + facility: typing.Optional[str], + reverse: typing.Optional[bool], + utc: typing.Optional[bool], + unit: typing.Optional[str]): + kwargs = dict(locals()) + + journalctl_options = journalctl_command_template.render(kwargs) + journalctl_options = re.sub(r'\s+', ' ', journalctl_options) + rc, output = rc_cmd(f'journalctl {journalctl_options}') + if raw: + # Each 'journalctl --output json' line is a separate JSON object + # So we should return list of dict + return [json.loads(line) for line in output.split('\n')] + return output + + +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/memory.py b/src/op_mode/memory.py index 178544be4..7666de646 100755 --- a/src/op_mode/memory.py +++ b/src/op_mode/memory.py @@ -20,7 +20,7 @@ import sys import vyos.opmode -def _get_system_memory(): +def _get_raw_data(): from re import search as re_search def find_value(keyword, mem_data): @@ -38,7 +38,7 @@ def _get_system_memory(): used = total - available - res = { + mem_data = { "total": total, "free": available, "used": used, @@ -46,24 +46,21 @@ def _get_system_memory(): "cached": cached } - return res - -def _get_system_memory_human(): - from vyos.util import bytes_to_human - - mem = _get_system_memory() - - for key in mem: + for key in mem_data: # The Linux kernel exposes memory values in kilobytes, # so we need to normalize them - mem[key] = bytes_to_human(mem[key], initial_exponent=10) + mem_data[key] = mem_data[key] * 1024 - return mem - -def _get_raw_data(): - return _get_system_memory_human() + return mem_data def _get_formatted_output(mem): + from vyos.util import bytes_to_human + + # For human-readable outputs, we convert bytes to more convenient units + # (100M, 1.3G...) + for key in mem: + mem[key] = bytes_to_human(mem[key]) + out = "Total: {}\n".format(mem["total"]) out += "Free: {}\n".format(mem["free"]) out += "Used: {}".format(mem["used"]) diff --git a/src/op_mode/nat.py b/src/op_mode/nat.py index 845dbbb2c..cf06de0e9 100755 --- a/src/op_mode/nat.py +++ b/src/op_mode/nat.py @@ -18,17 +18,21 @@ import jmespath import json import sys import xmltodict +import typing -from sys import exit from tabulate import tabulate +import vyos.opmode + +from vyos.configquery import ConfigTreeQuery from vyos.util import cmd from vyos.util import dict_search -import vyos.opmode +base = 'nat' +unconf_message = 'NAT is not configured' -def _get_xml_translation(direction, family): +def _get_xml_translation(direction, family, address=None): """ Get conntrack XML output --src-nat|--dst-nat """ @@ -36,7 +40,10 @@ def _get_xml_translation(direction, family): opt = '--src-nat' if direction == 'destination': opt = '--dst-nat' - return cmd(f'sudo conntrack --dump --family {family} {opt} --output xml') + tmp = f'conntrack --dump --family {family} {opt} --output xml' + if address: + tmp += f' --src {address}' + return cmd(tmp) def _xml_to_dict(xml): @@ -60,7 +67,7 @@ def _get_json_data(direction, family): if direction == 'destination': chain = 'PREROUTING' family = 'ip6' if family == 'inet6' else 'ip' - return cmd(f'sudo nft --json list chain {family} vyos_nat {chain}') + return cmd(f'nft --json list chain {family} vyos_nat {chain}') def _get_raw_data_rules(direction, family): @@ -76,11 +83,11 @@ def _get_raw_data_rules(direction, family): return rules -def _get_raw_translation(direction, family): +def _get_raw_translation(direction, family, address=None): """ Return: dictionary """ - xml = _get_xml_translation(direction, family) + xml = _get_xml_translation(direction, family, address) if len(xml) == 0: output = {'conntrack': { @@ -225,7 +232,7 @@ def _get_formatted_output_statistics(data, direction): return output -def _get_formatted_translation(dict_data, nat_direction, family): +def _get_formatted_translation(dict_data, nat_direction, family, verbose): data_entries = [] if 'error' in dict_data['conntrack']: return 'Entries not found' @@ -263,20 +270,34 @@ def _get_formatted_translation(dict_data, nat_direction, family): reply_src = f'{reply_src}:{reply_sport}' if reply_sport else reply_src reply_dst = f'{reply_dst}:{reply_dport}' if reply_dport else reply_dst state = meta['state'] if 'state' in meta else '' - mark = meta['mark'] + mark = meta.get('mark', '') zone = meta['zone'] if 'zone' in meta else '' if nat_direction == 'source': - data_entries.append( - [orig_src, reply_dst, proto, timeout, mark, zone]) + tmp = [orig_src, reply_dst, proto, timeout, mark, zone] + data_entries.append(tmp) elif nat_direction == 'destination': - data_entries.append( - [orig_dst, reply_src, proto, timeout, mark, zone]) + tmp = [orig_dst, reply_src, proto, timeout, mark, zone] + data_entries.append(tmp) headers = ["Pre-NAT", "Post-NAT", "Proto", "Timeout", "Mark", "Zone"] output = tabulate(data_entries, headers, numalign="left") return output +def _verify(func): + """Decorator checks if NAT config exists""" + from functools import wraps + + @wraps(func) + def _wrapper(*args, **kwargs): + config = ConfigTreeQuery() + if not config.exists(base): + raise vyos.opmode.UnconfiguredSubsystem(unconf_message) + return func(*args, **kwargs) + return _wrapper + + +@_verify def show_rules(raw: bool, direction: str, family: str): nat_rules = _get_raw_data_rules(direction, family) if raw: @@ -285,6 +306,7 @@ def show_rules(raw: bool, direction: str, family: str): return _get_formatted_output_rules(nat_rules, direction, family) +@_verify def show_statistics(raw: bool, direction: str, family: str): nat_statistics = _get_raw_data_rules(direction, family) if raw: @@ -293,13 +315,21 @@ def show_statistics(raw: bool, direction: str, family: str): return _get_formatted_output_statistics(nat_statistics, direction) -def show_translations(raw: bool, direction: str, family: str): +@_verify +def show_translations(raw: bool, direction: + str, family: str, + address: typing.Optional[str], + verbose: typing.Optional[bool]): family = 'ipv6' if family == 'inet6' else 'ipv4' - nat_translation = _get_raw_translation(direction, family) + nat_translation = _get_raw_translation(direction, + family=family, + address=address) + if raw: return nat_translation else: - return _get_formatted_translation(nat_translation, direction, family) + return _get_formatted_translation(nat_translation, direction, family, + verbose) if __name__ == '__main__': 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/ping.py b/src/op_mode/ping.py index 60bbc0c78..610e63cb3 100755 --- a/src/op_mode/ping.py +++ b/src/op_mode/ping.py @@ -18,6 +18,25 @@ import os import sys import socket import ipaddress +from vyos.util import get_all_vrfs +from vyos.ifconfig import Section + + +def interface_list() -> list: + """ + Get list of interfaces in system + :rtype: list + """ + return Section.interfaces() + + +def vrf_list() -> list: + """ + Get list of VRFs in system + :rtype: list + """ + return list(get_all_vrfs().keys()) + options = { 'audible': { @@ -63,6 +82,7 @@ options = { 'interface': { 'ping': '{command} -I {value}', 'type': '<interface>', + 'helpfunction': interface_list, 'help': 'Source interface' }, 'interval': { @@ -128,6 +148,7 @@ options = { 'ping': 'sudo ip vrf exec {value} {command}', 'type': '<vrf>', 'help': 'Use specified VRF table', + 'helpfunction': vrf_list, 'dflt': 'default', }, 'verbose': { @@ -142,20 +163,33 @@ ping = { } -class List (list): - def first (self): +class List(list): + def first(self): return self.pop(0) if self else '' def last(self): return self.pop() if self else '' - def prepend(self,value): - self.insert(0,value) + def prepend(self, value): + self.insert(0, value) + + +def completion_failure(option: str) -> None: + """ + Shows failure message after TAB when option is wrong + :param option: failure option + :type str: + """ + sys.stderr.write('\n\n Invalid option: {}\n\n'.format(option)) + sys.stdout.write('<nocomps>') + sys.exit(1) def expension_failure(option, completions): reason = 'Ambiguous' if completions else 'Invalid' - sys.stderr.write('\n\n {} command: {} [{}]\n\n'.format(reason,' '.join(sys.argv), option)) + sys.stderr.write( + '\n\n {} command: {} [{}]\n\n'.format(reason, ' '.join(sys.argv), + option)) if completions: sys.stderr.write(' Possible completions:\n ') sys.stderr.write('\n '.join(completions)) @@ -196,28 +230,44 @@ if __name__ == '__main__': if host == '--get-options': args.first() # pop ping args.first() # pop IP + usedoptionslist = [] while args: - option = args.first() - - matched = complete(option) + option = args.first() # pop option + matched = complete(option) # get option parameters + usedoptionslist.append(option) # list of used options + # Select options if not args: + # remove from Possible completions used options + for o in usedoptionslist: + if o in matched: + matched.remove(o) sys.stdout.write(' '.join(matched)) sys.exit(0) - if len(matched) > 1 : + if len(matched) > 1: sys.stdout.write(' '.join(matched)) sys.exit(0) + # If option doesn't have value + if matched: + if options[matched[0]]['type'] == 'noarg': + continue + else: + # Unexpected option + completion_failure(option) - if options[matched[0]]['type'] == 'noarg': - continue - - value = args.first() + value = args.first() # pop option's value if not args: matched = complete(option) - sys.stdout.write(options[matched[0]]['type']) + helplines = options[matched[0]]['type'] + # Run helpfunction to get list of possible values + if 'helpfunction' in options[matched[0]]: + result = options[matched[0]]['helpfunction']() + if result: + helplines = '\n' + ' '.join(result) + sys.stdout.write(helplines) sys.exit(0) - for name,option in options.items(): + for name, option in options.items(): if 'dflt' in option and name not in args: args.append(name) args.append(option['dflt']) @@ -234,8 +284,7 @@ if __name__ == '__main__': except ValueError: sys.exit(f'ping: Unknown host: {host}') - command = convert(ping[version],args) + command = convert(ping[version], args) # print(f'{command} {host}') os.system(f'{command} {host}') - diff --git a/src/op_mode/policy_route.py b/src/op_mode/policy_route.py index 5be40082f..5953786f3 100755 --- a/src/op_mode/policy_route.py +++ b/src/op_mode/policy_route.py @@ -22,53 +22,13 @@ from vyos.config import Config from vyos.util import cmd from vyos.util import dict_search_args -def get_policy_interfaces(conf, policy, name=None, ipv6=False): - interfaces = conf.get_config_dict(['interfaces'], key_mangling=('-', '_'), - get_first_key=True, no_tag_node_value_mangle=True) - - routes = ['route', 'route6'] - - def parse_if(ifname, if_conf): - if 'policy' in if_conf: - for route in routes: - if route in if_conf['policy']: - route_name = if_conf['policy'][route] - name_str = f'({ifname},{route})' - - if not name: - policy[route][route_name]['interface'].append(name_str) - elif not ipv6 and name == route_name: - policy['interface'].append(name_str) - - for iftype in ['vif', 'vif_s', 'vif_c']: - if iftype in if_conf: - for vifname, vif_conf in if_conf[iftype].items(): - parse_if(f'{ifname}.{vifname}', vif_conf) - - for iftype, iftype_conf in interfaces.items(): - for ifname, if_conf in iftype_conf.items(): - parse_if(ifname, if_conf) - -def get_config_policy(conf, name=None, ipv6=False, interfaces=True): +def get_config_policy(conf, name=None, ipv6=False): config_path = ['policy'] if name: config_path += ['route6' if ipv6 else 'route', name] policy = conf.get_config_dict(config_path, key_mangling=('-', '_'), get_first_key=True, no_tag_node_value_mangle=True) - if policy and interfaces: - if name: - policy['interface'] = [] - else: - if 'route' in policy: - for route_name, route_conf in policy['route'].items(): - route_conf['interface'] = [] - - if 'route6' in policy: - for route_name, route_conf in policy['route6'].items(): - route_conf['interface'] = [] - - get_policy_interfaces(conf, policy, name, ipv6) return policy diff --git a/src/op_mode/route.py b/src/op_mode/route.py index e1eee5bbf..7f0f9cbac 100755 --- a/src/op_mode/route.py +++ b/src/op_mode/route.py @@ -54,6 +54,45 @@ frr_command_template = Template(""" {% endif %} """) +def show_summary(raw: bool, family: str, table: typing.Optional[int], vrf: typing.Optional[str]): + from vyos.util import cmd + + if family == 'inet': + family_cmd = 'ip' + elif family == 'inet6': + family_cmd = 'ipv6' + else: + raise ValueError(f"Unsupported address family {family}") + + if (table is not None) and (vrf is not None): + raise ValueError("table and vrf options are mutually exclusive") + + # Replace with Jinja if it ever starts growing + if table: + table_cmd = f"table {table}" + else: + table_cmd = "" + + if vrf: + vrf_cmd = f"vrf {vrf}" + else: + vrf_cmd = "" + + if raw: + from json import loads + + output = cmd(f"vtysh -c 'show {family_cmd} route {vrf_cmd} summary {table_cmd} json'").strip() + + # If there are no routes in a table, its "JSON" output is an empty string, + # as of FRR 8.4.1 + if output: + return loads(output) + else: + return {} + else: + output = cmd(f"vtysh -c 'show {family_cmd} route {vrf_cmd} summary {table_cmd}'") + return output + def show(raw: bool, family: str, net: typing.Optional[str], @@ -83,7 +122,12 @@ def show(raw: bool, if raw: from json import loads - return loads(output) + d = loads(output) + collect = [] + for k,_ in d.items(): + for l in d[k]: + collect.append(l) + return collect else: return output 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_dhcp.py b/src/op_mode/show_dhcp.py deleted file mode 100755 index 4b1758eea..000000000 --- a/src/op_mode/show_dhcp.py +++ /dev/null @@ -1,260 +0,0 @@ -#!/usr/bin/env python3 -# -# Copyright (C) 2018-2021 VyOS maintainers and contributors -# -# This program is free software; you can redistribute it and/or modify -# it under the terms of the GNU General Public License version 2 or later as -# published by the Free Software Foundation. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see <http://www.gnu.org/licenses/>. -# -# TODO: merge with show_dhcpv6.py - -from json import dumps -from argparse import ArgumentParser -from ipaddress import ip_address -from tabulate import tabulate -from sys import exit -from collections import OrderedDict -from datetime import datetime - -from isc_dhcp_leases import Lease, IscDhcpLeases - -from vyos.base import Warning -from vyos.config import Config -from vyos.util import is_systemd_service_running - -lease_file = "/config/dhcpd.leases" -pool_key = "shared-networkname" - -lease_display_fields = OrderedDict() -lease_display_fields['ip'] = 'IP address' -lease_display_fields['hardware_address'] = 'Hardware address' -lease_display_fields['state'] = 'State' -lease_display_fields['start'] = 'Lease start' -lease_display_fields['end'] = 'Lease expiration' -lease_display_fields['remaining'] = 'Remaining' -lease_display_fields['pool'] = 'Pool' -lease_display_fields['hostname'] = 'Hostname' - -lease_valid_states = ['all', 'active', 'free', 'expired', 'released', 'abandoned', 'reset', 'backup'] - -def in_pool(lease, pool): - if pool_key in lease.sets: - if lease.sets[pool_key] == pool: - return True - - return False - -def utc_to_local(utc_dt): - return datetime.fromtimestamp((utc_dt - datetime(1970,1,1)).total_seconds()) - -def get_lease_data(lease): - data = {} - - # isc-dhcp lease times are in UTC so we need to convert them to local time to display - try: - data["start"] = utc_to_local(lease.start).strftime("%Y/%m/%d %H:%M:%S") - except: - data["start"] = "" - - try: - data["end"] = utc_to_local(lease.end).strftime("%Y/%m/%d %H:%M:%S") - except: - data["end"] = "" - - try: - data["remaining"] = lease.end - datetime.utcnow() - # negative timedelta prints wrong so bypass it - if (data["remaining"].days >= 0): - # substraction gives us a timedelta object which can't be formatted with strftime - # so we use str(), split gets rid of the microseconds - data["remaining"] = str(data["remaining"]).split('.')[0] - else: - data["remaining"] = "" - except: - data["remaining"] = "" - - # currently not used but might come in handy - # todo: parse into datetime string - for prop in ['tstp', 'tsfp', 'atsfp', 'cltt']: - if prop in lease.data: - data[prop] = lease.data[prop] - else: - data[prop] = '' - - data["hardware_address"] = lease.ethernet - data["hostname"] = lease.hostname - - data["state"] = lease.binding_state - data["ip"] = lease.ip - - try: - data["pool"] = lease.sets[pool_key] - except: - data["pool"] = "" - - return data - -def get_leases(config, leases, state, pool=None, sort='ip'): - # get leases from file - leases = IscDhcpLeases(lease_file).get() - - # filter leases by state - if 'all' not in state: - leases = list(filter(lambda x: x.binding_state in state, leases)) - - # filter leases by pool name - if pool is not None: - if config.exists_effective("service dhcp-server shared-network-name {0}".format(pool)): - leases = list(filter(lambda x: in_pool(x, pool), leases)) - else: - print("Pool {0} does not exist.".format(pool)) - exit(0) - - # should maybe filter all state=active by lease.valid here? - - # sort by start time to dedupe (newest lease overrides older) - leases = sorted(leases, key = lambda lease: lease.start) - - # dedupe by converting to dict - leases_dict = {} - for lease in leases: - # dedupe by IP - leases_dict[lease.ip] = lease - - # convert the lease data - leases = list(map(get_lease_data, leases_dict.values())) - - # apply output/display sort - if sort == 'ip': - leases = sorted(leases, key = lambda lease: int(ip_address(lease['ip']))) - else: - leases = sorted(leases, key = lambda lease: lease[sort]) - - return leases - -def show_leases(leases): - lease_list = [] - for l in leases: - lease_list_params = [] - for k in lease_display_fields.keys(): - lease_list_params.append(l[k]) - lease_list.append(lease_list_params) - - output = tabulate(lease_list, lease_display_fields.values()) - - print(output) - -def get_pool_size(config, pool): - size = 0 - subnets = config.list_effective_nodes("service dhcp-server shared-network-name {0} subnet".format(pool)) - for s in subnets: - ranges = config.list_effective_nodes("service dhcp-server shared-network-name {0} subnet {1} range".format(pool, s)) - for r in ranges: - start = config.return_effective_value("service dhcp-server shared-network-name {0} subnet {1} range {2} start".format(pool, s, r)) - stop = config.return_effective_value("service dhcp-server shared-network-name {0} subnet {1} range {2} stop".format(pool, s, r)) - - # Add +1 because both range boundaries are inclusive - size += int(ip_address(stop)) - int(ip_address(start)) + 1 - - return size - -def show_pool_stats(stats): - headers = ["Pool", "Size", "Leases", "Available", "Usage"] - output = tabulate(stats, headers) - - print(output) - -if __name__ == '__main__': - parser = ArgumentParser() - - group = parser.add_mutually_exclusive_group() - group.add_argument("-l", "--leases", action="store_true", help="Show DHCP leases") - group.add_argument("-s", "--statistics", action="store_true", help="Show DHCP statistics") - group.add_argument("--allowed", type=str, choices=["sort", "state"], help="Show allowed values for argument") - - parser.add_argument("-p", "--pool", type=str, help="Show lease for specific pool") - parser.add_argument("-S", "--sort", type=str, default='ip', help="Sort by") - parser.add_argument("-t", "--state", type=str, nargs="+", default=["active"], help="Lease state to show (can specify multiple with spaces)") - parser.add_argument("-j", "--json", action="store_true", default=False, help="Produce JSON output") - - args = parser.parse_args() - - conf = Config() - - if args.allowed == 'sort': - print(' '.join(lease_display_fields.keys())) - exit(0) - elif args.allowed == 'state': - print(' '.join(lease_valid_states)) - exit(0) - elif args.allowed: - parser.print_help() - exit(1) - - if args.sort not in lease_display_fields.keys(): - print(f'Invalid sort key, choose from: {list(lease_display_fields.keys())}') - exit(0) - - if not set(args.state) < set(lease_valid_states): - print(f'Invalid lease state, choose from: {lease_valid_states}') - exit(0) - - # Do nothing if service is not configured - if not conf.exists_effective('service dhcp-server'): - print("DHCP service is not configured.") - exit(0) - - # if dhcp server is down, inactive leases may still be shown as active, so warn the user. - if not is_systemd_service_running('isc-dhcp-server.service'): - Warning('DHCP server is configured but not started. Data may be stale.') - - if args.leases: - leases = get_leases(conf, lease_file, args.state, args.pool, args.sort) - - if args.json: - print(dumps(leases, indent=4)) - else: - show_leases(leases) - - elif args.statistics: - pools = [] - - # Get relevant pools - if args.pool: - pools = [args.pool] - else: - pools = conf.list_effective_nodes("service dhcp-server shared-network-name") - - # Get pool usage stats - stats = [] - for p in pools: - size = get_pool_size(conf, p) - leases = len(get_leases(conf, lease_file, state='active', pool=p)) - - use_percentage = round(leases / size * 100) if size != 0 else 0 - - if args.json: - pool_stats = {"pool": p, "size": size, "leases": leases, - "available": (size - leases), "percentage": use_percentage} - else: - # For tabulate - pool_stats = [p, size, leases, size - leases, "{0}%".format(use_percentage)] - stats.append(pool_stats) - - # Print stats - if args.json: - print(dumps(stats, indent=4)) - else: - show_pool_stats(stats) - - else: - parser.print_help() - exit(1) diff --git a/src/op_mode/show_dhcpv6.py b/src/op_mode/show_dhcpv6.py deleted file mode 100755 index b34b730e6..000000000 --- a/src/op_mode/show_dhcpv6.py +++ /dev/null @@ -1,220 +0,0 @@ -#!/usr/bin/env python3 -# -# Copyright (C) 2018-2021 VyOS maintainers and contributors -# -# This program is free software; you can redistribute it and/or modify -# it under the terms of the GNU General Public License version 2 or later as -# published by the Free Software Foundation. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see <http://www.gnu.org/licenses/>. -# -# TODO: merge with show_dhcp.py - -from json import dumps -from argparse import ArgumentParser -from ipaddress import ip_address -from tabulate import tabulate -from sys import exit -from collections import OrderedDict -from datetime import datetime - -from isc_dhcp_leases import Lease, IscDhcpLeases - -from vyos.base import Warning -from vyos.config import Config -from vyos.util import is_systemd_service_running - -lease_file = "/config/dhcpdv6.leases" -pool_key = "shared-networkname" - -lease_display_fields = OrderedDict() -lease_display_fields['ip'] = 'IPv6 address' -lease_display_fields['state'] = 'State' -lease_display_fields['last_comm'] = 'Last communication' -lease_display_fields['expires'] = 'Lease expiration' -lease_display_fields['remaining'] = 'Remaining' -lease_display_fields['type'] = 'Type' -lease_display_fields['pool'] = 'Pool' -lease_display_fields['iaid_duid'] = 'IAID_DUID' - -lease_valid_states = ['all', 'active', 'free', 'expired', 'released', 'abandoned', 'reset', 'backup'] - -def in_pool(lease, pool): - if pool_key in lease.sets: - if lease.sets[pool_key] == pool: - return True - - return False - -def format_hex_string(in_str): - out_str = "" - - # if input is divisible by 2, add : every 2 chars - if len(in_str) > 0 and len(in_str) % 2 == 0: - out_str = ':'.join(a+b for a,b in zip(in_str[::2], in_str[1::2])) - else: - out_str = in_str - - return out_str - -def utc_to_local(utc_dt): - return datetime.fromtimestamp((utc_dt - datetime(1970,1,1)).total_seconds()) - -def get_lease_data(lease): - data = {} - - # isc-dhcp lease times are in UTC so we need to convert them to local time to display - try: - data["expires"] = utc_to_local(lease.end).strftime("%Y/%m/%d %H:%M:%S") - except: - data["expires"] = "" - - try: - data["last_comm"] = utc_to_local(lease.last_communication).strftime("%Y/%m/%d %H:%M:%S") - except: - data["last_comm"] = "" - - try: - data["remaining"] = lease.end - datetime.utcnow() - # negative timedelta prints wrong so bypass it - if (data["remaining"].days >= 0): - # substraction gives us a timedelta object which can't be formatted with strftime - # so we use str(), split gets rid of the microseconds - data["remaining"] = str(data["remaining"]).split('.')[0] - else: - data["remaining"] = "" - except: - data["remaining"] = "" - - # isc-dhcp records lease declarations as ia_{na|ta|pd} IAID_DUID {...} - # where IAID_DUID is the combined IAID and DUID - data["iaid_duid"] = format_hex_string(lease.host_identifier_string) - - lease_types_long = {"na": "non-temporary", "ta": "temporary", "pd": "prefix delegation"} - data["type"] = lease_types_long[lease.type] - - data["state"] = lease.binding_state - data["ip"] = lease.ip - - try: - data["pool"] = lease.sets[pool_key] - except: - data["pool"] = "" - - return data - -def get_leases(config, leases, state, pool=None, sort='ip'): - leases = IscDhcpLeases(lease_file).get() - - # filter leases by state - if 'all' not in state: - leases = list(filter(lambda x: x.binding_state in state, leases)) - - # filter leases by pool name - if pool is not None: - if config.exists_effective("service dhcp-server shared-network-name {0}".format(pool)): - leases = list(filter(lambda x: in_pool(x, pool), leases)) - else: - print("Pool {0} does not exist.".format(pool)) - exit(0) - - # should maybe filter all state=active by lease.valid here? - - # sort by last_comm time to dedupe (newest lease overrides older) - leases = sorted(leases, key = lambda lease: lease.last_communication) - - # dedupe by converting to dict - leases_dict = {} - for lease in leases: - # dedupe by IP - leases_dict[lease.ip] = lease - - # convert the lease data - leases = list(map(get_lease_data, leases_dict.values())) - - # apply output/display sort - if sort == 'ip': - leases = sorted(leases, key = lambda k: int(ip_address(k['ip'].split('/')[0]))) - else: - leases = sorted(leases, key = lambda k: k[sort]) - - return leases - -def show_leases(leases): - lease_list = [] - for l in leases: - lease_list_params = [] - for k in lease_display_fields.keys(): - lease_list_params.append(l[k]) - lease_list.append(lease_list_params) - - output = tabulate(lease_list, lease_display_fields.values()) - - print(output) - -if __name__ == '__main__': - parser = ArgumentParser() - - group = parser.add_mutually_exclusive_group() - group.add_argument("-l", "--leases", action="store_true", help="Show DHCPv6 leases") - group.add_argument("-s", "--statistics", action="store_true", help="Show DHCPv6 statistics") - group.add_argument("--allowed", type=str, choices=["pool", "sort", "state"], help="Show allowed values for argument") - - parser.add_argument("-p", "--pool", type=str, help="Show lease for specific pool") - parser.add_argument("-S", "--sort", type=str, default='ip', help="Sort by") - parser.add_argument("-t", "--state", type=str, nargs="+", default=["active"], help="Lease state to show (can specify multiple with spaces)") - parser.add_argument("-j", "--json", action="store_true", default=False, help="Produce JSON output") - - args = parser.parse_args() - - conf = Config() - - if args.allowed == 'pool': - if conf.exists_effective('service dhcpv6-server'): - print(' '.join(conf.list_effective_nodes("service dhcpv6-server shared-network-name"))) - exit(0) - elif args.allowed == 'sort': - print(' '.join(lease_display_fields.keys())) - exit(0) - elif args.allowed == 'state': - print(' '.join(lease_valid_states)) - exit(0) - elif args.allowed: - parser.print_help() - exit(1) - - if args.sort not in lease_display_fields.keys(): - print(f'Invalid sort key, choose from: {list(lease_display_fields.keys())}') - exit(0) - - if not set(args.state) < set(lease_valid_states): - print(f'Invalid lease state, choose from: {lease_valid_states}') - exit(0) - - # Do nothing if service is not configured - if not conf.exists_effective('service dhcpv6-server'): - print("DHCPv6 service is not configured") - exit(0) - - # if dhcp server is down, inactive leases may still be shown as active, so warn the user. - if not is_systemd_service_running('isc-dhcp-server6.service'): - Warning('DHCPv6 server is configured but not started. Data may be stale.') - - if args.leases: - leases = get_leases(conf, lease_file, args.state, args.pool, args.sort) - - if args.json: - print(dumps(leases, indent=4)) - else: - show_leases(leases) - elif args.statistics: - print("DHCPv6 statistics option is not available") - else: - parser.print_help() - exit(1) diff --git a/src/op_mode/show_igmpproxy.py b/src/op_mode/show_igmpproxy.py deleted file mode 100755 index 4714e494b..000000000 --- a/src/op_mode/show_igmpproxy.py +++ /dev/null @@ -1,241 +0,0 @@ -#!/usr/bin/env python3 -# -# Copyright (C) 2018 VyOS maintainers and contributors -# -# This program is free software; you can redistribute it and/or modify -# it under the terms of the GNU General Public License version 2 or later as -# published by the Free Software Foundation. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see <http://www.gnu.org/licenses/>. - -# File: show_igmpproxy.py -# Purpose: -# Display istatistics from IPv4 IGMP proxy. -# Used by the "run show ip multicast" command tree. - -import sys -import jinja2 -import argparse -import ipaddress -import socket - -import vyos.config - -# Output Template for "show ip multicast interface" command -# -# Example: -# Interface BytesIn PktsIn BytesOut PktsOut Local -# eth0 0.0b 0 0.0b 0 xxx.xxx.xxx.65 -# eth1 0.0b 0 0.0b 0 xxx.xxx.xx.201 -# eth0.3 0.0b 0 0.0b 0 xxx.xxx.x.7 -# tun1 0.0b 0 0.0b 0 xxx.xxx.xxx.2 -vif_out_tmpl = """ -{% for r in data %} -{{ "%-10s"|format(r.interface) }} {{ "%-12s"|format(r.bytes_in) }} {{ "%-12s"|format(r.pkts_in) }} {{ "%-12s"|format(r.bytes_out) }} {{ "%-12s"|format(r.pkts_out) }} {{ "%-15s"|format(r.loc) }} -{% endfor %} -""" - -# Output Template for "show ip multicast mfc" command -# -# Example: -# Group Origin In Out Pkts Bytes Wrong -# xxx.xxx.xxx.250 xxx.xx.xxx.75 -- -# xxx.xxx.xx.124 xx.xxx.xxx.26 -- -mfc_out_tmpl = """ -{% for r in data %} -{{ "%-15s"|format(r.group) }} {{ "%-15s"|format(r.origin) }} {{ "%-12s"|format(r.pkts) }} {{ "%-12s"|format(r.bytes) }} {{ "%-12s"|format(r.wrong) }} {{ "%-10s"|format(r.iif) }} {{ "%-20s"|format(r.oifs|join(', ')) }} -{% endfor %} -""" - -parser = argparse.ArgumentParser() -parser.add_argument("--interface", action="store_true", help="Interface Statistics") -parser.add_argument("--mfc", action="store_true", help="Multicast Forwarding Cache") - -def byte_string(size): - # convert size to integer - size = int(size) - - # One Terrabyte - s_TB = 1024 * 1024 * 1024 * 1024 - # One Gigabyte - s_GB = 1024 * 1024 * 1024 - # One Megabyte - s_MB = 1024 * 1024 - # One Kilobyte - s_KB = 1024 - # One Byte - s_B = 1 - - if size > s_TB: - return str(round((size/s_TB), 2)) + 'TB' - elif size > s_GB: - return str(round((size/s_GB), 2)) + 'GB' - elif size > s_MB: - return str(round((size/s_MB), 2)) + 'MB' - elif size > s_KB: - return str(round((size/s_KB), 2)) + 'KB' - else: - return str(round((size/s_B), 2)) + 'b' - - return None - -def kernel2ip(addr): - """ - Convert any given addr from Linux Kernel to a proper, IPv4 address - using the correct host byte order. - """ - - # Convert from hex 'FE000A0A' to decimal '4261415434' - addr = int(addr, 16) - # Kernel ABI _always_ uses network byteorder - addr = socket.ntohl(addr) - - return ipaddress.IPv4Address( addr ) - -def do_mr_vif(): - """ - Read contents of file /proc/net/ip_mr_vif and print a more human - friendly version to the command line. IPv4 addresses present as - 32bit integers in hex format are converted to IPv4 notation, too. - """ - - with open('/proc/net/ip_mr_vif', 'r') as f: - lines = len(f.readlines()) - if lines < 2: - return None - - result = { - 'data': [] - } - - # Build up table format string - table_format = { - 'interface': 'Interface', - 'pkts_in' : 'PktsIn', - 'pkts_out' : 'PktsOut', - 'bytes_in' : 'BytesIn', - 'bytes_out': 'BytesOut', - 'loc' : 'Local' - } - result['data'].append(table_format) - - # read and parse information from /proc filesystema - with open('/proc/net/ip_mr_vif', 'r') as f: - header_line = next(f) - for line in f: - data = { - 'interface': line.split()[1], - 'pkts_in' : line.split()[3], - 'pkts_out' : line.split()[5], - - # convert raw byte number to something more human readable - # Note: could be replaced by Python3 hurry.filesize module - 'bytes_in' : byte_string( line.split()[2] ), - 'bytes_out': byte_string( line.split()[4] ), - - # convert IP address from hex 'FE000A0A' to decimal '4261415434' - 'loc' : kernel2ip( line.split()[7] ), - } - result['data'].append(data) - - return result - -def do_mr_mfc(): - """ - Read contents of file /proc/net/ip_mr_cache and print a more human - friendly version to the command line. IPv4 addresses present as - 32bit integers in hex format are converted to IPv4 notation, too. - """ - - with open('/proc/net/ip_mr_cache', 'r') as f: - lines = len(f.readlines()) - if lines < 2: - return None - - # We need this to convert from interface index to a real interface name - # Thus we also skip the format identifier on list index 0 - vif = do_mr_vif()['data'][1:] - - result = { - 'data': [] - } - - # Build up table format string - table_format = { - 'group' : 'Group', - 'origin': 'Origin', - 'iif' : 'In', - 'oifs' : ['Out'], - 'pkts' : 'Pkts', - 'bytes' : 'Bytes', - 'wrong' : 'Wrong' - } - result['data'].append(table_format) - - # read and parse information from /proc filesystem - with open('/proc/net/ip_mr_cache', 'r') as f: - header_line = next(f) - for line in f: - data = { - # convert IP address from hex 'FE000A0A' to decimal '4261415434' - 'group' : kernel2ip( line.split()[0] ), - 'origin': kernel2ip( line.split()[1] ), - - 'iif' : '--', - 'pkts' : '', - 'bytes' : '', - 'wrong' : '', - 'oifs' : [] - } - - iif = int( line.split()[2] ) - if not ((iif == -1) or (iif == 65535)): - data['pkts'] = line.split()[3] - data['bytes'] = byte_string( line.split()[4] ) - data['wrong'] = line.split()[5] - - # convert index to real interface name - data['iif'] = vif[iif]['interface'] - - # convert each output interface index to a real interface name - for oif in line.split()[6:]: - idx = int( oif.split(':')[0] ) - data['oifs'].append( vif[idx]['interface'] ) - - result['data'].append(data) - - return result - -if __name__ == '__main__': - args = parser.parse_args() - - # Do nothing if service is not configured - c = vyos.config.Config() - if not c.exists_effective('protocols igmp-proxy'): - print("IGMP proxy is not configured") - sys.exit(0) - - if args.interface: - data = do_mr_vif() - if data: - tmpl = jinja2.Template(vif_out_tmpl) - print(tmpl.render(data)) - - sys.exit(0) - elif args.mfc: - data = do_mr_mfc() - if data: - tmpl = jinja2.Template(mfc_out_tmpl) - print(tmpl.render(data)) - - sys.exit(0) - else: - parser.print_help() - sys.exit(1) - diff --git a/src/op_mode/show_ipsec_sa.py b/src/op_mode/show_ipsec_sa.py deleted file mode 100755 index 5b8f00dba..000000000 --- a/src/op_mode/show_ipsec_sa.py +++ /dev/null @@ -1,130 +0,0 @@ -#!/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 re import split as re_split -from sys import exit - -from hurry import filesize -from tabulate import tabulate -from vici import Session as vici_session - -from vyos.util import seconds_to_human - - -def convert(text): - return int(text) if text.isdigit() else text.lower() - - -def alphanum_key(key): - return [convert(c) for c in re_split('([0-9]+)', str(key))] - - -def format_output(sas): - sa_data = [] - - for sa in sas: - for parent_sa in sa.values(): - # create an item for each child-sa - for child_sa in parent_sa.get('child-sas', {}).values(): - # prepare a list for output data - sa_out_name = sa_out_state = sa_out_uptime = sa_out_bytes = sa_out_packets = sa_out_remote_addr = sa_out_remote_id = sa_out_proposal = 'N/A' - - # collect raw data - sa_name = child_sa.get('name') - sa_state = child_sa.get('state') - sa_uptime = child_sa.get('install-time') - sa_bytes_in = child_sa.get('bytes-in') - sa_bytes_out = child_sa.get('bytes-out') - sa_packets_in = child_sa.get('packets-in') - sa_packets_out = child_sa.get('packets-out') - sa_remote_addr = parent_sa.get('remote-host') - sa_remote_id = parent_sa.get('remote-id') - sa_proposal_encr_alg = child_sa.get('encr-alg') - sa_proposal_integ_alg = child_sa.get('integ-alg') - sa_proposal_encr_keysize = child_sa.get('encr-keysize') - sa_proposal_dh_group = child_sa.get('dh-group') - - # format data to display - if sa_name: - sa_out_name = sa_name.decode() - if sa_state: - if sa_state == b'INSTALLED': - sa_out_state = 'up' - else: - sa_out_state = 'down' - if sa_uptime: - sa_out_uptime = seconds_to_human(sa_uptime.decode()) - if sa_bytes_in and sa_bytes_out: - bytes_in = filesize.size(int(sa_bytes_in.decode())) - bytes_out = filesize.size(int(sa_bytes_out.decode())) - sa_out_bytes = f'{bytes_in}/{bytes_out}' - if sa_packets_in and sa_packets_out: - packets_in = filesize.size(int(sa_packets_in.decode()), - system=filesize.si) - packets_out = filesize.size(int(sa_packets_out.decode()), - system=filesize.si) - sa_out_packets = f'{packets_in}/{packets_out}' - if sa_remote_addr: - sa_out_remote_addr = sa_remote_addr.decode() - if sa_remote_id: - sa_out_remote_id = sa_remote_id.decode() - # format proposal - if sa_proposal_encr_alg: - sa_out_proposal = sa_proposal_encr_alg.decode() - if sa_proposal_encr_keysize: - sa_proposal_encr_keysize_str = sa_proposal_encr_keysize.decode() - sa_out_proposal = f'{sa_out_proposal}_{sa_proposal_encr_keysize_str}' - if sa_proposal_integ_alg: - sa_proposal_integ_alg_str = sa_proposal_integ_alg.decode() - sa_out_proposal = f'{sa_out_proposal}/{sa_proposal_integ_alg_str}' - if sa_proposal_dh_group: - sa_proposal_dh_group_str = sa_proposal_dh_group.decode() - sa_out_proposal = f'{sa_out_proposal}/{sa_proposal_dh_group_str}' - - # add a new item to output data - sa_data.append([ - sa_out_name, sa_out_state, sa_out_uptime, sa_out_bytes, - sa_out_packets, sa_out_remote_addr, sa_out_remote_id, - sa_out_proposal - ]) - - # return output data - return sa_data - - -if __name__ == '__main__': - try: - session = vici_session() - sas = list(session.list_sas()) - - sa_data = format_output(sas) - sa_data = sorted(sa_data, key=alphanum_key) - - headers = [ - "Connection", "State", "Uptime", "Bytes In/Out", "Packets In/Out", - "Remote address", "Remote ID", "Proposal" - ] - output = tabulate(sa_data, headers) - print(output) - except PermissionError: - print("You do not have a permission to connect to the IPsec daemon") - exit(1) - except ConnectionRefusedError: - print("IPsec is not runing") - exit(1) - except Exception as e: - print("An error occured: {0}".format(e)) - exit(1) diff --git a/src/op_mode/show_nat66_statistics.py b/src/op_mode/show_nat66_statistics.py deleted file mode 100755 index cb10aed9f..000000000 --- a/src/op_mode/show_nat66_statistics.py +++ /dev/null @@ -1,63 +0,0 @@ -#!/usr/bin/env python3 -# -# Copyright (C) 2018 VyOS maintainers and contributors -# -# This program is free software; you can redistribute it and/or modify -# it under the terms of the GNU General Public License version 2 or later as -# published by the Free Software Foundation. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see <http://www.gnu.org/licenses/>. - -import jmespath -import json - -from argparse import ArgumentParser -from jinja2 import Template -from sys import exit -from vyos.util import cmd - -OUT_TMPL_SRC=""" -rule pkts bytes interface ----- ---- ----- --------- -{% for r in output %} -{% if r.comment %} -{% set packets = r.counter.packets %} -{% set bytes = r.counter.bytes %} -{% set interface = r.interface %} -{# remove rule comment prefix #} -{% set comment = r.comment | replace('SRC-NAT66-', '') | replace('DST-NAT66-', '') %} -{{ "%-4s" | format(comment) }} {{ "%9s" | format(packets) }} {{ "%12s" | format(bytes) }} {{ interface }} -{% endif %} -{% endfor %} -""" - -parser = ArgumentParser() -group = parser.add_mutually_exclusive_group() -group.add_argument("--source", help="Show statistics for configured source NAT rules", action="store_true") -group.add_argument("--destination", help="Show statistics for configured destination NAT rules", action="store_true") -args = parser.parse_args() - -if args.source or args.destination: - tmp = cmd('sudo nft -j list table ip6 vyos_nat') - tmp = json.loads(tmp) - - source = r"nftables[?rule.chain=='POSTROUTING'].rule.{chain: chain, handle: handle, comment: comment, counter: expr[].counter | [0], interface: expr[].match.right | [0] }" - destination = r"nftables[?rule.chain=='PREROUTING'].rule.{chain: chain, handle: handle, comment: comment, counter: expr[].counter | [0], interface: expr[].match.right | [0] }" - data = { - 'output' : jmespath.search(source if args.source else destination, tmp), - 'direction' : 'source' if args.source else 'destination' - } - - tmpl = Template(OUT_TMPL_SRC, lstrip_blocks=True) - print(tmpl.render(data)) - exit(0) -else: - parser.print_help() - exit(1) - diff --git a/src/op_mode/show_nat66_translations.py b/src/op_mode/show_nat66_translations.py deleted file mode 100755 index 045d64065..000000000 --- a/src/op_mode/show_nat66_translations.py +++ /dev/null @@ -1,204 +0,0 @@ -#!/usr/bin/env python3 -# -# Copyright (C) 2020 VyOS maintainers and contributors -# -# This program is free software; you can redistribute it and/or modify -# it under the terms of the GNU General Public License version 2 or later as -# published by the Free Software Foundation. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see <http://www.gnu.org/licenses/>. - -''' -show nat translations -''' - -import os -import sys -import ipaddress -import argparse -import xmltodict - -from vyos.util import popen -from vyos.util import DEVNULL - -conntrack = '/usr/sbin/conntrack' - -verbose_format = "%-20s %-18s %-20s %-18s" -normal_format = "%-20s %-20s %-4s %-8s %s" - - -def headers(verbose, pipe): - if verbose: - return verbose_format % ('Pre-NAT src', 'Pre-NAT dst', 'Post-NAT src', 'Post-NAT dst') - return normal_format % ('Pre-NAT', 'Post-NAT', 'Prot', 'Timeout', 'Type' if pipe else '') - - -def command(srcdest, proto, ipaddr): - command = f'{conntrack} -o xml -L -f ipv6' - - if proto: - command += f' -p {proto}' - - if srcdest == 'source': - command += ' -n' - if ipaddr: - command += f' --orig-src {ipaddr}' - if srcdest == 'destination': - command += ' -g' - if ipaddr: - command += f' --orig-dst {ipaddr}' - - return command - - -def run(command): - xml, code = popen(command,stderr=DEVNULL) - if code: - sys.exit('conntrack failed') - return xml - - -def content(xmlfile): - xml = '' - with open(xmlfile,'r') as r: - xml += r.read() - return xml - - -def pipe(): - xml = '' - while True: - line = sys.stdin.readline() - xml += line - if '</conntrack>' in line: - break - - sys.stdin = open('/dev/tty') - return xml - - -def process(data, stats, protocol, pipe, verbose, flowtype=''): - if not data: - return - - parsed = xmltodict.parse(data) - - print(headers(verbose, pipe)) - - # to help the linter to detect typos - ORIGINAL = 'original' - REPLY = 'reply' - INDEPENDANT = 'independent' - SPORT = 'sport' - DPORT = 'dport' - SRC = 'src' - DST = 'dst' - - for rule in parsed['conntrack']['flow']: - src, dst, sport, dport, proto = {}, {}, {}, {}, {} - packet_count, byte_count = {}, {} - timeout, use = 0, 0 - - rule_type = rule.get('type', '') - - for meta in rule['meta']: - # print(meta) - direction = meta['@direction'] - - if direction in (ORIGINAL, REPLY): - if 'layer3' in meta: - l3 = meta['layer3'] - src[direction] = l3[SRC] - dst[direction] = l3[DST] - - if 'layer4' in meta: - l4 = meta['layer4'] - sp = l4.get(SPORT, '') - dp = l4.get(DPORT, '') - if sp: - sport[direction] = sp - if dp: - dport[direction] = dp - proto[direction] = l4.get('@protoname','') - - if stats and 'counters' in meta: - packet_count[direction] = meta['packets'] - byte_count[direction] = meta['bytes'] - continue - - if direction == INDEPENDANT: - timeout = meta['timeout'] - use = meta['use'] - continue - - in_src = '%s:%s' % (src[ORIGINAL], sport[ORIGINAL]) if ORIGINAL in sport else src[ORIGINAL] - in_dst = '%s:%s' % (dst[ORIGINAL], dport[ORIGINAL]) if ORIGINAL in dport else dst[ORIGINAL] - - # inverted the the perl code !!? - out_dst = '%s:%s' % (dst[REPLY], dport[REPLY]) if REPLY in dport else dst[REPLY] - out_src = '%s:%s' % (src[REPLY], sport[REPLY]) if REPLY in sport else src[REPLY] - - if flowtype == 'source': - v = ORIGINAL in sport and REPLY in dport - f = '%s:%s' % (src[ORIGINAL], sport[ORIGINAL]) if v else src[ORIGINAL] - t = '%s:%s' % (dst[REPLY], dport[REPLY]) if v else dst[REPLY] - else: - v = ORIGINAL in dport and REPLY in sport - f = '%s:%s' % (dst[ORIGINAL], dport[ORIGINAL]) if v else dst[ORIGINAL] - t = '%s:%s' % (src[REPLY], sport[REPLY]) if v else src[REPLY] - - # Thomas: I do not believe proto should be an option - p = proto.get('original', '') - if protocol and p != protocol: - continue - - if verbose: - msg = verbose_format % (in_src, in_dst, out_dst, out_src) - p = f'{p}: ' if p else '' - msg += f'\n {p}{f} ==> {t}' - msg += f' timeout: {timeout}' if timeout else '' - msg += f' use: {use} ' if use else '' - msg += f' type: {rule_type}' if rule_type else '' - print(msg) - else: - print(normal_format % (f, t, p, timeout, rule_type if rule_type else '')) - - if stats: - for direction in ('original', 'reply'): - if direction in packet_count: - print(' %-8s: packets %s, bytes %s' % direction, packet_count[direction], byte_count[direction]) - - -def main(): - parser = argparse.ArgumentParser(description=sys.modules[__name__].__doc__) - parser.add_argument('--verbose', help='provide more details about the flows', action='store_true') - parser.add_argument('--proto', help='filter by protocol', default='', type=str) - parser.add_argument('--file', help='read the conntrack xml from a file', type=str) - parser.add_argument('--stats', help='add usage statistics', action='store_true') - parser.add_argument('--type', help='NAT type (source, destination)', required=True, type=str) - parser.add_argument('--ipaddr', help='source ip address to filter on', type=ipaddress.ip_address) - parser.add_argument('--pipe', help='read conntrack xml data from stdin', action='store_true') - - arg = parser.parse_args() - - if arg.type not in ('source', 'destination'): - sys.exit('Unknown NAT type!') - - if arg.pipe: - process(pipe(), arg.stats, arg.proto, arg.pipe, arg.verbose, arg.type) - elif arg.file: - process(content(arg.file), arg.stats, arg.proto, arg.pipe, arg.verbose, arg.type) - else: - try: - process(run(command(arg.type, arg.proto, arg.ipaddr)), arg.stats, arg.proto, arg.pipe, arg.verbose, arg.type) - except: - pass - -if __name__ == '__main__': - main() diff --git a/src/op_mode/show_nat_statistics.py b/src/op_mode/show_nat_statistics.py deleted file mode 100755 index be41e083b..000000000 --- a/src/op_mode/show_nat_statistics.py +++ /dev/null @@ -1,63 +0,0 @@ -#!/usr/bin/env python3 -# -# Copyright (C) 2018 VyOS maintainers and contributors -# -# This program is free software; you can redistribute it and/or modify -# it under the terms of the GNU General Public License version 2 or later as -# published by the Free Software Foundation. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see <http://www.gnu.org/licenses/>. - -import jmespath -import json - -from argparse import ArgumentParser -from jinja2 import Template -from sys import exit -from vyos.util import cmd - -OUT_TMPL_SRC=""" -rule pkts bytes interface ----- ---- ----- --------- -{% for r in output %} -{% if r.comment %} -{% set packets = r.counter.packets %} -{% set bytes = r.counter.bytes %} -{% set interface = r.interface %} -{# remove rule comment prefix #} -{% set comment = r.comment | replace('SRC-NAT-', '') | replace('DST-NAT-', '') | replace(' tcp_udp', '') %} -{{ "%-4s" | format(comment) }} {{ "%9s" | format(packets) }} {{ "%12s" | format(bytes) }} {{ interface }} -{% endif %} -{% endfor %} -""" - -parser = ArgumentParser() -group = parser.add_mutually_exclusive_group() -group.add_argument("--source", help="Show statistics for configured source NAT rules", action="store_true") -group.add_argument("--destination", help="Show statistics for configured destination NAT rules", action="store_true") -args = parser.parse_args() - -if args.source or args.destination: - tmp = cmd('sudo nft -j list table ip vyos_nat') - tmp = json.loads(tmp) - - source = r"nftables[?rule.chain=='POSTROUTING'].rule.{chain: chain, handle: handle, comment: comment, counter: expr[].counter | [0], interface: expr[].match.right | [0] }" - destination = r"nftables[?rule.chain=='PREROUTING'].rule.{chain: chain, handle: handle, comment: comment, counter: expr[].counter | [0], interface: expr[].match.right | [0] }" - data = { - 'output' : jmespath.search(source if args.source else destination, tmp), - 'direction' : 'source' if args.source else 'destination' - } - - tmpl = Template(OUT_TMPL_SRC, lstrip_blocks=True) - print(tmpl.render(data)) - exit(0) -else: - parser.print_help() - exit(1) - diff --git a/src/op_mode/show_nat_translations.py b/src/op_mode/show_nat_translations.py deleted file mode 100755 index 508845e23..000000000 --- a/src/op_mode/show_nat_translations.py +++ /dev/null @@ -1,216 +0,0 @@ -#!/usr/bin/env python3 -# -# Copyright (C) 2020-2022 VyOS maintainers and contributors -# -# This program is free software; you can redistribute it and/or modify -# it under the terms of the GNU General Public License version 2 or later as -# 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/>. - -''' -show nat translations -''' - -import os -import sys -import ipaddress -import argparse -import xmltodict - -from vyos.util import popen -from vyos.util import DEVNULL - -conntrack = '/usr/sbin/conntrack' - -verbose_format = "%-20s %-18s %-20s %-18s" -normal_format = "%-20s %-20s %-4s %-8s %s" - - -def headers(verbose, pipe): - if verbose: - return verbose_format % ('Pre-NAT src', 'Pre-NAT dst', 'Post-NAT src', 'Post-NAT dst') - return normal_format % ('Pre-NAT', 'Post-NAT', 'Prot', 'Timeout', 'Type' if pipe else '') - - -def command(srcdest, proto, ipaddr): - command = f'{conntrack} -o xml -L' - - if proto: - command += f' -p {proto}' - - if srcdest == 'source': - command += ' -n' - if ipaddr: - command += f' --orig-src {ipaddr}' - if srcdest == 'destination': - command += ' -g' - if ipaddr: - command += f' --orig-dst {ipaddr}' - - return command - - -def run(command): - xml, code = popen(command,stderr=DEVNULL) - if code: - sys.exit('conntrack failed') - return xml - - -def content(xmlfile): - xml = '' - with open(xmlfile,'r') as r: - xml += r.read() - return xml - - -def pipe(): - xml = '' - while True: - line = sys.stdin.readline() - xml += line - if '</conntrack>' in line: - break - - sys.stdin = open('/dev/tty') - return xml - - -def xml_to_dict(xml): - """ - Convert XML to dictionary - Return: dictionary - """ - parse = xmltodict.parse(xml) - # If only one NAT entry we must change dict T4499 - if 'meta' in parse['conntrack']['flow']: - return dict(conntrack={'flow': [parse['conntrack']['flow']]}) - return parse - - -def process(data, stats, protocol, pipe, verbose, flowtype=''): - if not data: - return - - parsed = xml_to_dict(data) - - print(headers(verbose, pipe)) - - # to help the linter to detect typos - ORIGINAL = 'original' - REPLY = 'reply' - INDEPENDANT = 'independent' - SPORT = 'sport' - DPORT = 'dport' - SRC = 'src' - DST = 'dst' - - for rule in parsed['conntrack']['flow']: - src, dst, sport, dport, proto = {}, {}, {}, {}, {} - packet_count, byte_count = {}, {} - timeout, use = 0, 0 - - rule_type = rule.get('type', '') - - for meta in rule['meta']: - # print(meta) - direction = meta['@direction'] - - if direction in (ORIGINAL, REPLY): - if 'layer3' in meta: - l3 = meta['layer3'] - src[direction] = l3[SRC] - dst[direction] = l3[DST] - - if 'layer4' in meta: - l4 = meta['layer4'] - sp = l4.get(SPORT, '') - dp = l4.get(DPORT, '') - if sp: - sport[direction] = sp - if dp: - dport[direction] = dp - proto[direction] = l4.get('@protoname','') - - if stats and 'counters' in meta: - packet_count[direction] = meta['packets'] - byte_count[direction] = meta['bytes'] - continue - - if direction == INDEPENDANT: - timeout = meta['timeout'] - use = meta['use'] - continue - - in_src = '%s:%s' % (src[ORIGINAL], sport[ORIGINAL]) if ORIGINAL in sport else src[ORIGINAL] - in_dst = '%s:%s' % (dst[ORIGINAL], dport[ORIGINAL]) if ORIGINAL in dport else dst[ORIGINAL] - - # inverted the the perl code !!? - out_dst = '%s:%s' % (dst[REPLY], dport[REPLY]) if REPLY in dport else dst[REPLY] - out_src = '%s:%s' % (src[REPLY], sport[REPLY]) if REPLY in sport else src[REPLY] - - if flowtype == 'source': - v = ORIGINAL in sport and REPLY in dport - f = '%s:%s' % (src[ORIGINAL], sport[ORIGINAL]) if v else src[ORIGINAL] - t = '%s:%s' % (dst[REPLY], dport[REPLY]) if v else dst[REPLY] - else: - v = ORIGINAL in dport and REPLY in sport - f = '%s:%s' % (dst[ORIGINAL], dport[ORIGINAL]) if v else dst[ORIGINAL] - t = '%s:%s' % (src[REPLY], sport[REPLY]) if v else src[REPLY] - - # Thomas: I do not believe proto should be an option - p = proto.get('original', '') - if protocol and p != protocol: - continue - - if verbose: - msg = verbose_format % (in_src, in_dst, out_dst, out_src) - p = f'{p}: ' if p else '' - msg += f'\n {p}{f} ==> {t}' - msg += f' timeout: {timeout}' if timeout else '' - msg += f' use: {use} ' if use else '' - msg += f' type: {rule_type}' if rule_type else '' - print(msg) - else: - print(normal_format % (f, t, p, timeout, rule_type if rule_type else '')) - - if stats: - for direction in ('original', 'reply'): - if direction in packet_count: - print(' %-8s: packets %s, bytes %s' % direction, packet_count[direction], byte_count[direction]) - - -def main(): - parser = argparse.ArgumentParser(description=sys.modules[__name__].__doc__) - parser.add_argument('--verbose', help='provide more details about the flows', action='store_true') - parser.add_argument('--proto', help='filter by protocol', default='', type=str) - parser.add_argument('--file', help='read the conntrack xml from a file', type=str) - parser.add_argument('--stats', help='add usage statistics', action='store_true') - parser.add_argument('--type', help='NAT type (source, destination)', required=True, type=str) - parser.add_argument('--ipaddr', help='source ip address to filter on', type=ipaddress.ip_address) - parser.add_argument('--pipe', help='read conntrack xml data from stdin', action='store_true') - - arg = parser.parse_args() - - if arg.type not in ('source', 'destination'): - sys.exit('Unknown NAT type!') - - if arg.pipe: - process(pipe(), arg.stats, arg.proto, arg.pipe, arg.verbose, arg.type) - elif arg.file: - process(content(arg.file), arg.stats, arg.proto, arg.pipe, arg.verbose, arg.type) - else: - try: - process(run(command(arg.type, arg.proto, arg.ipaddr)), arg.stats, arg.proto, arg.pipe, arg.verbose, arg.type) - except: - pass - -if __name__ == '__main__': - main() diff --git a/src/op_mode/show_ntp.sh b/src/op_mode/show_ntp.sh index e9dd6c5c9..85f8eda15 100755 --- a/src/op_mode/show_ntp.sh +++ b/src/op_mode/show_ntp.sh @@ -1,39 +1,34 @@ #!/bin/sh -basic=0 -info=0 +sourcestats=0 +tracking=0 while [[ "$#" -gt 0 ]]; do case $1 in - --info) info=1 ;; - --basic) basic=1 ;; - --server) server=$2; shift ;; + --sourcestats) sourcestats=1 ;; + --tracking) tracking=1 ;; *) echo "Unknown parameter passed: $1" ;; esac shift done -if ! ps -C ntpd &>/dev/null; then +if ! ps -C chronyd &>/dev/null; then echo NTP daemon disabled exit 1 fi -PID=$(pgrep ntpd) -VRF_NAME=$(ip vrf identify ${PID}) +PID=$(pgrep chronyd | head -n1) +VRF_NAME=$(ip vrf identify ) if [ ! -z ${VRF_NAME} ]; then VRF_CMD="sudo ip vrf exec ${VRF_NAME}" fi -if [ $basic -eq 1 ]; then - $VRF_CMD ntpq -n -c peers -elif [ $info -eq 1 ]; then - echo "=== sysingo ===" - $VRF_CMD ntpq -n -c sysinfo - echo - echo "=== kerninfo ===" - $VRF_CMD ntpq -n -c kerninfo -elif [ ! -z $server ]; then - $VRF_CMD /usr/sbin/ntpdate -q $server +if [ $sourcestats -eq 1 ]; then + $VRF_CMD chronyc sourcestats -v +elif [ $tracking -eq 1 ]; then + $VRF_CMD chronyc tracking -v +else + echo "Unknown option" fi 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/storage.py b/src/op_mode/storage.py index 75964c493..d16e271bd 100755 --- a/src/op_mode/storage.py +++ b/src/op_mode/storage.py @@ -20,6 +20,16 @@ import sys import vyos.opmode from vyos.util import cmd +# FIY: As of coreutils from Debian Buster and Bullseye, +# the outpt looks like this: +# +# $ df -h -t ext4 --output=source,size,used,avail,pcent +# Filesystem Size Used Avail Use% +# /dev/sda1 16G 7.6G 7.3G 51% +# +# Those field names are automatically normalized by vyos.opmode.run, +# so we don't touch them here, +# and only normalize values. def _get_system_storage(only_persistent=False): if not only_persistent: @@ -32,11 +42,19 @@ def _get_system_storage(only_persistent=False): return res def _get_raw_data(): + from re import sub as re_sub + from vyos.util import human_to_bytes + out = _get_system_storage(only_persistent=True) lines = out.splitlines() lists = [l.split() for l in lines] res = {lists[0][i]: lists[1][i] for i in range(len(lists[0]))} + res["Size"] = human_to_bytes(res["Size"]) + res["Used"] = human_to_bytes(res["Used"]) + res["Avail"] = human_to_bytes(res["Avail"]) + res["Use%"] = re_sub(r'%', '', res["Use%"]) + return res def _get_formatted_output(): diff --git a/src/op_mode/traceroute.py b/src/op_mode/traceroute.py index 4299d6e5f..6c7030ea0 100755 --- a/src/op_mode/traceroute.py +++ b/src/op_mode/traceroute.py @@ -18,6 +18,25 @@ import os import sys import socket import ipaddress +from vyos.util import get_all_vrfs +from vyos.ifconfig import Section + + +def interface_list() -> list: + """ + Get list of interfaces in system + :rtype: list + """ + return Section.interfaces() + + +def vrf_list() -> list: + """ + Get list of VRFs in system + :rtype: list + """ + return list(get_all_vrfs().keys()) + options = { 'backward-hops': { @@ -48,6 +67,7 @@ options = { 'interface': { 'traceroute': '{command} -i {value}', 'type': '<interface>', + 'helpfunction': interface_list, 'help': 'Source interface' }, 'lookup-as': { @@ -99,6 +119,7 @@ options = { 'traceroute': 'sudo ip vrf exec {value} {command}', 'type': '<vrf>', 'help': 'Use specified VRF table', + 'helpfunction': vrf_list, 'dflt': 'default'} } @@ -108,20 +129,33 @@ traceroute = { } -class List (list): - def first (self): +class List(list): + def first(self): return self.pop(0) if self else '' def last(self): return self.pop() if self else '' - def prepend(self,value): - self.insert(0,value) + def prepend(self, value): + self.insert(0, value) + + +def completion_failure(option: str) -> None: + """ + Shows failure message after TAB when option is wrong + :param option: failure option + :type str: + """ + sys.stderr.write('\n\n Invalid option: {}\n\n'.format(option)) + sys.stdout.write('<nocomps>') + sys.exit(1) def expension_failure(option, completions): reason = 'Ambiguous' if completions else 'Invalid' - sys.stderr.write('\n\n {} command: {} [{}]\n\n'.format(reason,' '.join(sys.argv), option)) + sys.stderr.write( + '\n\n {} command: {} [{}]\n\n'.format(reason, ' '.join(sys.argv), + option)) if completions: sys.stderr.write(' Possible completions:\n ') sys.stderr.write('\n '.join(completions)) @@ -160,30 +194,46 @@ if __name__ == '__main__': sys.exit("traceroute: Missing host") if host == '--get-options': - args.first() # pop traceroute + args.first() # pop ping args.first() # pop IP + usedoptionslist = [] while args: - option = args.first() - - matched = complete(option) + option = args.first() # pop option + matched = complete(option) # get option parameters + usedoptionslist.append(option) # list of used options + # Select options if not args: + # remove from Possible completions used options + for o in usedoptionslist: + if o in matched: + matched.remove(o) sys.stdout.write(' '.join(matched)) sys.exit(0) - if len(matched) > 1 : + if len(matched) > 1: sys.stdout.write(' '.join(matched)) sys.exit(0) + # If option doesn't have value + if matched: + if options[matched[0]]['type'] == 'noarg': + continue + else: + # Unexpected option + completion_failure(option) - if options[matched[0]]['type'] == 'noarg': - continue - - value = args.first() + value = args.first() # pop option's value if not args: matched = complete(option) - sys.stdout.write(options[matched[0]]['type']) + helplines = options[matched[0]]['type'] + # Run helpfunction to get list of possible values + if 'helpfunction' in options[matched[0]]: + result = options[matched[0]]['helpfunction']() + if result: + helplines = '\n' + ' '.join(result) + sys.stdout.write(helplines) sys.exit(0) - for name,option in options.items(): + for name, option in options.items(): if 'dflt' in option and name not in args: args.append(name) args.append(option['dflt']) @@ -200,8 +250,7 @@ if __name__ == '__main__': except ValueError: sys.exit(f'traceroute: Unknown host: {host}') - command = convert(traceroute[version],args) + command = convert(traceroute[version], args) # print(f'{command} {host}') os.system(f'{command} {host}') - 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/vrf.py b/src/op_mode/vrf.py index aeb50fe6e..a9a416761 100755 --- a/src/op_mode/vrf.py +++ b/src/op_mode/vrf.py @@ -31,14 +31,14 @@ def _get_raw_data(name=None): If vrf name is set - get only this name data If vrf name set and not found - return [] """ - output = cmd('sudo ip --json --brief link show type vrf') + output = cmd('ip --json --brief link show type vrf') data = json.loads(output) if not data: return [] if name: is_vrf_exists = True if [vrf for vrf in data if vrf.get('ifname') == name] else False if is_vrf_exists: - output = cmd(f'sudo ip --json --brief link show dev {name}') + output = cmd(f'ip --json --brief link show dev {name}') data = json.loads(output) return data return [] @@ -51,7 +51,7 @@ def _get_vrf_members(vrf: str) -> list: :param vrf: str :return: list """ - output = cmd(f'sudo ip --json --brief link show master {vrf}') + output = cmd(f'ip --json --brief link show master {vrf}') answer = json.loads(output) interfaces = [] for data in answer: 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/op_mode/zone.py b/src/op_mode/zone.py new file mode 100755 index 000000000..f326215b1 --- /dev/null +++ b/src/op_mode/zone.py @@ -0,0 +1,215 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2023 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 typing +import sys +import vyos.opmode + +import tabulate +from vyos.configquery import ConfigTreeQuery +from vyos.util import dict_search_args +from vyos.util import dict_search + + +def get_config_zone(conf, name=None): + config_path = ['firewall', 'zone'] + if name: + config_path += [name] + + zone_policy = conf.get_config_dict(config_path, key_mangling=('-', '_'), + get_first_key=True, + no_tag_node_value_mangle=True) + return zone_policy + + +def _convert_one_zone_data(zone: str, zone_config: dict) -> dict: + """ + Convert config dictionary of one zone to API dictionary + :param zone: Zone name + :type zone: str + :param zone_config: config dictionary + :type zone_config: dict + :return: AP dictionary + :rtype: dict + """ + list_of_rules = [] + intrazone_dict = {} + if dict_search('from', zone_config): + for from_zone, from_zone_config in zone_config['from'].items(): + from_zone_dict = {'name': from_zone} + if dict_search('firewall.name', from_zone_config): + from_zone_dict['firewall'] = dict_search('firewall.name', + from_zone_config) + if dict_search('firewall.ipv6_name', from_zone_config): + from_zone_dict['firewall_v6'] = dict_search( + 'firewall.ipv6_name', from_zone_config) + list_of_rules.append(from_zone_dict) + + zone_dict = { + 'name': zone, + 'interface': dict_search('interface', zone_config), + 'type': 'LOCAL' if dict_search('local_zone', + zone_config) is not None else None, + } + if list_of_rules: + zone_dict['from'] = list_of_rules + if dict_search('intra_zone_filtering.firewall.name', zone_config): + intrazone_dict['firewall'] = dict_search( + 'intra_zone_filtering.firewall.name', zone_config) + if dict_search('intra_zone_filtering.firewall.ipv6_name', zone_config): + intrazone_dict['firewall_v6'] = dict_search( + 'intra_zone_filtering.firewall.ipv6_name', zone_config) + if intrazone_dict: + zone_dict['intrazone'] = intrazone_dict + return zone_dict + + +def _convert_zones_data(zone_policies: dict) -> list: + """ + Convert all config dictionary to API list of zone dictionaries + :param zone_policies: config dictionary + :type zone_policies: dict + :return: API list + :rtype: list + """ + zone_list = [] + for zone, zone_config in zone_policies.items(): + zone_list.append(_convert_one_zone_data(zone, zone_config)) + return zone_list + + +def _convert_config(zones_config: dict, zone: str = None) -> list: + """ + convert config to API list + :param zones_config: zones config + :type zones_config: + :param zone: zone name + :type zone: str + :return: API list + :rtype: list + """ + if zone: + if zones_config: + output = [_convert_one_zone_data(zone, zones_config)] + else: + raise vyos.opmode.DataUnavailable(f'Zone {zone} not found') + else: + if zones_config: + output = _convert_zones_data(zones_config) + else: + raise vyos.opmode.UnconfiguredSubsystem( + 'Zone entries are not configured') + return output + + +def output_zone_list(zone_conf: dict) -> list: + """ + Format one zone row + :param zone_conf: zone config + :type zone_conf: dict + :return: formatted list of zones + :rtype: list + """ + zone_info = [zone_conf['name']] + if zone_conf['type'] == 'LOCAL': + zone_info.append('LOCAL') + else: + zone_info.append("\n".join(zone_conf['interface'])) + + from_zone = [] + firewall = [] + firewall_v6 = [] + if 'intrazone' in zone_conf: + from_zone.append(zone_conf['name']) + + v4_name = dict_search_args(zone_conf['intrazone'], 'firewall') + v6_name = dict_search_args(zone_conf['intrazone'], 'firewall_v6') + if v4_name: + firewall.append(v4_name) + else: + firewall.append('') + if v6_name: + firewall_v6.append(v6_name) + else: + firewall_v6.append('') + + if 'from' in zone_conf: + for from_conf in zone_conf['from']: + from_zone.append(from_conf['name']) + + v4_name = dict_search_args(from_conf, 'firewall') + v6_name = dict_search_args(from_conf, 'firewall_v6') + if v4_name: + firewall.append(v4_name) + else: + firewall.append('') + if v6_name: + firewall_v6.append(v6_name) + else: + firewall_v6.append('') + + zone_info.append("\n".join(from_zone)) + zone_info.append("\n".join(firewall)) + zone_info.append("\n".join(firewall_v6)) + return zone_info + + +def get_formatted_output(zone_policy: list) -> str: + """ + Formatted output of all zones + :param zone_policy: list of zones + :type zone_policy: list + :return: formatted table with zones + :rtype: str + """ + headers = ["Zone", + "Interfaces", + "From Zone", + "Firewall IPv4", + "Firewall IPv6" + ] + formatted_list = [] + for zone_conf in zone_policy: + formatted_list.append(output_zone_list(zone_conf)) + tabulate.PRESERVE_WHITESPACE = True + output = tabulate.tabulate(formatted_list, headers, numalign="left") + return output + + +def show(raw: bool, zone: typing.Optional[str]): + """ + Show zone-policy command + :param raw: if API + :type raw: bool + :param zone: zone name + :type zone: str + """ + conf: ConfigTreeQuery = ConfigTreeQuery() + zones_config: dict = get_config_zone(conf, zone) + zone_policy_api: list = _convert_config(zones_config, zone) + if raw: + return zone_policy_api + else: + return get_formatted_output(zone_policy_api) + + +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/zone_policy.py b/src/op_mode/zone_policy.py deleted file mode 100755 index 7b43018c2..000000000 --- a/src/op_mode/zone_policy.py +++ /dev/null @@ -1,81 +0,0 @@ -#!/usr/bin/env python3 -# -# Copyright (C) 2021 VyOS maintainers and contributors -# -# This program is free software; you can redistribute it and/or modify -# it under the terms of the GNU General Public License version 2 or later as -# published by the Free Software Foundation. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see <http://www.gnu.org/licenses/>. - -import argparse -import tabulate - -from vyos.config import Config -from vyos.util import dict_search_args - -def get_config_zone(conf, name=None): - config_path = ['zone-policy'] - if name: - config_path += ['zone', name] - - zone_policy = conf.get_config_dict(config_path, key_mangling=('-', '_'), - get_first_key=True, no_tag_node_value_mangle=True) - return zone_policy - -def output_zone_name(zone, zone_conf): - print(f'\n---------------------------------\nZone: "{zone}"\n') - - interfaces = ', '.join(zone_conf['interface']) if 'interface' in zone_conf else '' - if 'local_zone' in zone_conf: - interfaces = 'LOCAL' - - print(f'Interfaces: {interfaces}\n') - - header = ['From Zone', 'Firewall'] - rows = [] - - if 'from' in zone_conf: - for from_name, from_conf in zone_conf['from'].items(): - row = [from_name] - v4_name = dict_search_args(from_conf, 'firewall', 'name') - v6_name = dict_search_args(from_conf, 'firewall', 'ipv6_name') - - if v4_name: - rows.append(row + [v4_name]) - - if v6_name: - rows.append(row + [f'{v6_name} [IPv6]']) - - if rows: - print('From Zones:\n') - print(tabulate.tabulate(rows, header)) - -def show_zone_policy(zone): - conf = Config() - zone_policy = get_config_zone(conf, zone) - - if not zone_policy: - return - - if 'zone' in zone_policy: - for zone, zone_conf in zone_policy['zone'].items(): - output_zone_name(zone, zone_conf) - elif zone: - output_zone_name(zone, zone_policy) - -if __name__ == '__main__': - parser = argparse.ArgumentParser() - parser.add_argument('--action', help='Action', required=False) - parser.add_argument('--name', help='Zone name', required=False, action='store', nargs='?', default='') - - args = parser.parse_args() - - if args.action == 'show': - show_zone_policy(args.name) diff --git a/src/services/api/graphql/__init__.py b/src/services/api/graphql/__init__.py new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/src/services/api/graphql/__init__.py diff --git a/src/services/api/graphql/bindings.py b/src/services/api/graphql/bindings.py index 0b1260912..aa1ba0eb0 100644 --- a/src/services/api/graphql/bindings.py +++ b/src/services/api/graphql/bindings.py @@ -18,16 +18,26 @@ from . graphql.queries import query from . graphql.mutations import mutation from . graphql.directives import directives_dict from . graphql.errors import op_mode_error -from . utils.schema_from_op_mode import generate_op_mode_definitions +from . graphql.auth_token_mutation import auth_token_mutation +from . generate.schema_from_op_mode import generate_op_mode_definitions +from . generate.schema_from_config_session import generate_config_session_definitions +from . generate.schema_from_composite import generate_composite_definitions +from . libs.token_auth import init_secret +from . import state from ariadne import make_executable_schema, load_schema_from_path, snake_case_fallback_resolvers def generate_schema(): api_schema_dir = vyos.defaults.directories['api_schema'] generate_op_mode_definitions() + generate_config_session_definitions() + generate_composite_definitions() + + if state.settings['app'].state.vyos_auth_type == 'token': + init_secret() type_defs = load_schema_from_path(api_schema_dir) - schema = make_executable_schema(type_defs, query, op_mode_error, mutation, snake_case_fallback_resolvers, directives=directives_dict) + schema = make_executable_schema(type_defs, query, op_mode_error, mutation, auth_token_mutation, snake_case_fallback_resolvers, directives=directives_dict) return schema diff --git a/src/services/api/graphql/generate/composite_function.py b/src/services/api/graphql/generate/composite_function.py new file mode 100644 index 000000000..bc9d80fbb --- /dev/null +++ b/src/services/api/graphql/generate/composite_function.py @@ -0,0 +1,11 @@ +# typing information for composite functions: those that invoke several +# elementary requests, and return the result as a single dict +import typing + +def system_status(): + pass + +queries = {'system_status': system_status} + +mutations = {} + diff --git a/src/services/api/graphql/utils/config_session_function.py b/src/services/api/graphql/generate/config_session_function.py index fc0dd7a87..fc0dd7a87 100644 --- a/src/services/api/graphql/utils/config_session_function.py +++ b/src/services/api/graphql/generate/config_session_function.py diff --git a/src/services/api/graphql/generate/schema_from_composite.py b/src/services/api/graphql/generate/schema_from_composite.py new file mode 100755 index 000000000..61a08cb2f --- /dev/null +++ b/src/services/api/graphql/generate/schema_from_composite.py @@ -0,0 +1,165 @@ +#!/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/>. +# +# +# A utility to generate GraphQL schema defintions from typing information of +# composite functions comprising several requests. + +import os +import sys +import json +from inspect import signature, getmembers, isfunction, isclass, getmro +from jinja2 import Template + +from vyos.defaults import directories +if __package__ is None or __package__ == '': + sys.path.append("/usr/libexec/vyos/services/api") + from graphql.libs.op_mode import snake_to_pascal_case, map_type_name + from composite_function import queries, mutations + from vyos.config import Config + from vyos.configdict import dict_merge + from vyos.xml import defaults +else: + from .. libs.op_mode import snake_to_pascal_case, map_type_name + from . composite_function import queries, mutations + from .. import state + +SCHEMA_PATH = directories['api_schema'] + +if __package__ is None or __package__ == '': + # allow running stand-alone + conf = Config() + base = ['service', 'https', 'api'] + graphql_dict = conf.get_config_dict(base, key_mangling=('-', '_'), + no_tag_node_value_mangle=True, + get_first_key=True) + if 'graphql' not in graphql_dict: + exit("graphql is not configured") + + graphql_dict = dict_merge(defaults(base), graphql_dict) + auth_type = graphql_dict['graphql']['authentication']['type'] +else: + auth_type = state.settings['app'].state.vyos_auth_type + +schema_data: dict = {'auth_type': auth_type, + 'schema_name': '', + 'schema_fields': []} + +query_template = """ +{%- if auth_type == 'key' %} +input {{ schema_name }}Input { + key: String! + {%- for field_entry in schema_fields %} + {{ field_entry }} + {%- endfor %} +} +{%- elif schema_fields %} +input {{ schema_name }}Input { + {%- for field_entry in schema_fields %} + {{ field_entry }} + {%- endfor %} +} +{%- endif %} + +type {{ schema_name }} { + result: Generic +} + +type {{ schema_name }}Result { + data: {{ schema_name }} + success: Boolean! + errors: [String] +} + +extend type Query { +{%- if auth_type == 'key' or schema_fields %} + {{ schema_name }}(data: {{ schema_name }}Input) : {{ schema_name }}Result @compositequery +{%- else %} + {{ schema_name }} : {{ schema_name }}Result @compositequery +{%- endif %} +} +""" + +mutation_template = """ +{%- if auth_type == 'key' %} +input {{ schema_name }}Input { + key: String! + {%- for field_entry in schema_fields %} + {{ field_entry }} + {%- endfor %} +} +{%- elif schema_fields %} +input {{ schema_name }}Input { + {%- for field_entry in schema_fields %} + {{ field_entry }} + {%- endfor %} +} +{%- endif %} + +type {{ schema_name }} { + result: Generic +} + +type {{ schema_name }}Result { + data: {{ schema_name }} + success: Boolean! + errors: [String] +} + +extend type Mutation { +{%- if auth_type == 'key' or schema_fields %} + {{ schema_name }}(data: {{ schema_name }}Input) : {{ schema_name }}Result @compositemutation +{%- else %} + {{ schema_name }} : {{ schema_name }}Result @compositemutation +{%- endif %} +} +""" + +def create_schema(func_name: str, func: callable, template: str) -> str: + sig = signature(func) + + field_dict = {} + for k in sig.parameters: + field_dict[sig.parameters[k].name] = map_type_name(sig.parameters[k].annotation) + + schema_fields = [] + for k,v in field_dict.items(): + schema_fields.append(k+': '+v) + + schema_data['schema_name'] = snake_to_pascal_case(func_name) + schema_data['schema_fields'] = schema_fields + + j2_template = Template(template) + res = j2_template.render(schema_data) + + return res + +def generate_composite_definitions(): + results = [] + for name,func in queries.items(): + res = create_schema(name, func, query_template) + results.append(res) + + for name,func in mutations.items(): + res = create_schema(name, func, mutation_template) + results.append(res) + + out = '\n'.join(results) + with open(f'{SCHEMA_PATH}/composite.graphql', 'w') as f: + f.write(out) + +if __name__ == '__main__': + generate_composite_definitions() diff --git a/src/services/api/graphql/utils/schema_from_config_session.py b/src/services/api/graphql/generate/schema_from_config_session.py index ea78aaf88..49bf2440e 100755 --- a/src/services/api/graphql/utils/schema_from_config_session.py +++ b/src/services/api/graphql/generate/schema_from_config_session.py @@ -19,28 +19,60 @@ # (wrappers of) native configsession functions. import os +import sys import json from inspect import signature, getmembers, isfunction, isclass, getmro from jinja2 import Template +from vyos.defaults import directories if __package__ is None or __package__ == '': - from util import snake_to_pascal_case, map_type_name + sys.path.append("/usr/libexec/vyos/services/api") + from graphql.libs.op_mode import snake_to_pascal_case, map_type_name + from config_session_function import queries, mutations + from vyos.config import Config + from vyos.configdict import dict_merge + from vyos.xml import defaults else: - from . util import snake_to_pascal_case, map_type_name + from .. libs.op_mode import snake_to_pascal_case, map_type_name + from . config_session_function import queries, mutations + from .. import state + +SCHEMA_PATH = directories['api_schema'] -# this will be run locally before the build -SCHEMA_PATH = '../graphql/schema' +if __package__ is None or __package__ == '': + # allow running stand-alone + conf = Config() + base = ['service', 'https', 'api'] + graphql_dict = conf.get_config_dict(base, key_mangling=('-', '_'), + no_tag_node_value_mangle=True, + get_first_key=True) + if 'graphql' not in graphql_dict: + exit("graphql is not configured") + + graphql_dict = dict_merge(defaults(base), graphql_dict) + auth_type = graphql_dict['graphql']['authentication']['type'] +else: + auth_type = state.settings['app'].state.vyos_auth_type -schema_data: dict = {'schema_name': '', +schema_data: dict = {'auth_type': auth_type, + 'schema_name': '', 'schema_fields': []} query_template = """ +{%- if auth_type == 'key' %} input {{ schema_name }}Input { key: String! {%- for field_entry in schema_fields %} {{ field_entry }} {%- endfor %} } +{%- elif schema_fields %} +input {{ schema_name }}Input { + {%- for field_entry in schema_fields %} + {{ field_entry }} + {%- endfor %} +} +{%- endif %} type {{ schema_name }} { result: Generic @@ -53,17 +85,29 @@ type {{ schema_name }}Result { } extend type Query { +{%- if auth_type == 'key' or schema_fields %} {{ schema_name }}(data: {{ schema_name }}Input) : {{ schema_name }}Result @configsessionquery +{%- else %} + {{ schema_name }} : {{ schema_name }}Result @configsessionquery +{%- endif %} } """ mutation_template = """ +{%- if auth_type == 'key' %} input {{ schema_name }}Input { key: String! {%- for field_entry in schema_fields %} {{ field_entry }} {%- endfor %} } +{%- elif schema_fields %} +input {{ schema_name }}Input { + {%- for field_entry in schema_fields %} + {{ field_entry }} + {%- endfor %} +} +{%- endif %} type {{ schema_name }} { result: Generic @@ -76,7 +120,11 @@ type {{ schema_name }}Result { } extend type Mutation { +{%- if auth_type == 'key' or schema_fields %} {{ schema_name }}(data: {{ schema_name }}Input) : {{ schema_name }}Result @configsessionmutation +{%- else %} + {{ schema_name }} : {{ schema_name }}Result @configsessionmutation +{%- endif %} } """ @@ -100,8 +148,6 @@ def create_schema(func_name: str, func: callable, template: str) -> str: return res def generate_config_session_definitions(): - from config_session_function import queries, mutations - results = [] for name,func in queries.items(): res = create_schema(name, func, query_template) diff --git a/src/services/api/graphql/utils/schema_from_op_mode.py b/src/services/api/graphql/generate/schema_from_op_mode.py index 57d63628b..b320a529e 100755 --- a/src/services/api/graphql/utils/schema_from_op_mode.py +++ b/src/services/api/graphql/generate/schema_from_op_mode.py @@ -19,17 +19,25 @@ # scripts. import os +import sys import json from inspect import signature, getmembers, isfunction, isclass, getmro from jinja2 import Template from vyos.defaults import directories +from vyos.opmode import _is_op_mode_function_name as is_op_mode_function_name +from vyos.util import load_as_module if __package__ is None or __package__ == '': - from util import load_as_module, is_op_mode_function_name, is_show_function_name - from util import snake_to_pascal_case, map_type_name + sys.path.append("/usr/libexec/vyos/services/api") + from graphql.libs.op_mode import 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 . util import load_as_module, is_op_mode_function_name, is_show_function_name - from . util import snake_to_pascal_case, map_type_name + from .. libs.op_mode import is_show_function_name + from .. libs.op_mode import snake_to_pascal_case, map_type_name + from .. import state OP_MODE_PATH = directories['op_mode'] SCHEMA_PATH = directories['api_schema'] @@ -38,16 +46,40 @@ DATA_DIR = directories['data'] op_mode_include_file = os.path.join(DATA_DIR, 'op-mode-standardized.json') op_mode_error_schema = 'op_mode_error.graphql' -schema_data: dict = {'schema_name': '', +if __package__ is None or __package__ == '': + # allow running stand-alone + conf = Config() + base = ['service', 'https', 'api'] + graphql_dict = conf.get_config_dict(base, key_mangling=('-', '_'), + no_tag_node_value_mangle=True, + get_first_key=True) + if 'graphql' not in graphql_dict: + exit("graphql is not configured") + + graphql_dict = dict_merge(defaults(base), graphql_dict) + auth_type = graphql_dict['graphql']['authentication']['type'] +else: + auth_type = state.settings['app'].state.vyos_auth_type + +schema_data: dict = {'auth_type': auth_type, + 'schema_name': '', 'schema_fields': []} query_template = """ +{%- if auth_type == 'key' %} input {{ schema_name }}Input { key: String! {%- for field_entry in schema_fields %} {{ field_entry }} {%- endfor %} } +{%- elif schema_fields %} +input {{ schema_name }}Input { + {%- for field_entry in schema_fields %} + {{ field_entry }} + {%- endfor %} +} +{%- endif %} type {{ schema_name }} { result: Generic @@ -61,17 +93,29 @@ type {{ schema_name }}Result { } extend type Query { +{%- if auth_type == 'key' or schema_fields %} {{ schema_name }}(data: {{ schema_name }}Input) : {{ schema_name }}Result @genopquery +{%- else %} + {{ schema_name }} : {{ schema_name }}Result @genopquery +{%- endif %} } """ mutation_template = """ +{%- if auth_type == 'key' %} input {{ schema_name }}Input { key: String! {%- for field_entry in schema_fields %} {{ field_entry }} {%- endfor %} } +{%- elif schema_fields %} +input {{ schema_name }}Input { + {%- for field_entry in schema_fields %} + {{ field_entry }} + {%- endfor %} +} +{%- endif %} type {{ schema_name }} { result: Generic @@ -85,7 +129,11 @@ type {{ schema_name }}Result { } extend type Mutation { +{%- if auth_type == 'key' or schema_fields %} {{ schema_name }}(data: {{ schema_name }}Input) : {{ schema_name }}Result @genopmutation +{%- else %} + {{ schema_name }} : {{ schema_name }}Result @genopquery +{%- endif %} } """ diff --git a/src/services/api/graphql/graphql/auth_token_mutation.py b/src/services/api/graphql/graphql/auth_token_mutation.py new file mode 100644 index 000000000..21ac40094 --- /dev/null +++ b/src/services/api/graphql/graphql/auth_token_mutation.py @@ -0,0 +1,49 @@ +# 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/>. + +import jwt +import datetime +from typing import Any, Dict +from ariadne import ObjectType, UnionType +from graphql import GraphQLResolveInfo + +from .. libs.token_auth import generate_token +from .. import state + +auth_token_mutation = ObjectType("Mutation") + +@auth_token_mutation.field('AuthToken') +def auth_token_resolver(obj: Any, info: GraphQLResolveInfo, data: Dict): + # non-nullable fields + user = data['username'] + passwd = data['password'] + + secret = state.settings['secret'] + exp_interval = int(state.settings['app'].state.vyos_token_exp) + expiration = (datetime.datetime.now(tz=datetime.timezone.utc) + + datetime.timedelta(seconds=exp_interval)) + + res = generate_token(user, passwd, secret, expiration) + if res: + data['result'] = res + return { + "success": True, + "data": data + } + + return { + "success": False, + "errors": ['token generation failed'] + } diff --git a/src/services/api/graphql/graphql/directives.py b/src/services/api/graphql/graphql/directives.py index d75d72582..a7919854a 100644 --- a/src/services/api/graphql/graphql/directives.py +++ b/src/services/api/graphql/graphql/directives.py @@ -63,16 +63,25 @@ class GenOpMutationDirective(VyosDirective): super().visit_field_definition(field, object_type, make_resolver=make_gen_op_mutation_resolver) -class SystemStatusDirective(VyosDirective): +class CompositeQueryDirective(VyosDirective): """ Class providing implementation of 'system_status' directive in schema. """ def visit_field_definition(self, field, object_type): super().visit_field_definition(field, object_type, - make_resolver=make_system_status_resolver) + make_resolver=make_composite_query_resolver) + +class CompositeMutationDirective(VyosDirective): + """ + Class providing implementation of 'system_status' directive in schema. + """ + def visit_field_definition(self, field, object_type): + super().visit_field_definition(field, object_type, + make_resolver=make_composite_mutation_resolver) directives_dict = {"configsessionquery": ConfigSessionQueryDirective, "configsessionmutation": ConfigSessionMutationDirective, "genopquery": GenOpQueryDirective, "genopmutation": GenOpMutationDirective, - "systemstatus": SystemStatusDirective} + "compositequery": CompositeQueryDirective, + "compositemutation": CompositeMutationDirective} diff --git a/src/services/api/graphql/graphql/mutations.py b/src/services/api/graphql/graphql/mutations.py index f7d285a77..8254e22b1 100644 --- a/src/services/api/graphql/graphql/mutations.py +++ b/src/services/api/graphql/graphql/mutations.py @@ -14,13 +14,13 @@ # along with this library. If not, see <http://www.gnu.org/licenses/>. from importlib import import_module -from typing import Any, Dict -from ariadne import ObjectType, convert_kwargs_to_snake_case, convert_camel_case_to_snake +from typing import Any, Dict, Optional +from ariadne import ObjectType, convert_camel_case_to_snake from graphql import GraphQLResolveInfo from makefun import with_signature from .. import state -from .. import key_auth +from .. libs import key_auth from api.graphql.session.session import Session from api.graphql.session.errors.op_mode_errors import op_mode_err_msg, op_mode_err_code from vyos.opmode import Error as OpModeError @@ -42,32 +42,51 @@ 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 @with_signature(func_sig, func_name=resolver_name) async def func_impl(*args, **kwargs): try: - if 'data' not in kwargs: - return { - "success": False, - "errors": ['missing data'] - } - - data = kwargs['data'] - key = data['key'] - - auth = key_auth.auth_required(key) - if auth is None: - return { - "success": False, - "errors": ['invalid API key'] - } - - # We are finished with the 'key' entry, and may remove so as to - # pass the rest of data (if any) to function. - del data['key'] + auth_type = state.settings['app'].state.vyos_auth_type + + if auth_type == 'key': + data = kwargs['data'] + key = data['key'] + + auth = key_auth.auth_required(key) + if auth is None: + return { + "success": False, + "errors": ['invalid API key'] + } + + # We are finished with the 'key' entry, and may remove so as to + # pass the rest of data (if any) to function. + del data['key'] + + elif auth_type == 'token': + data = kwargs['data'] + 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'] + } + else: + # AtrributeError will have already been raised if no + # vyos_auth_type; validation and defaultValue ensure it is + # one of the previous cases, so this is never reached. + pass session = state.settings['app'].state.vyos_session @@ -112,3 +131,7 @@ def make_config_session_mutation_resolver(mutation_name): def make_gen_op_mutation_resolver(mutation_name): return make_mutation_resolver(mutation_name, mutation_name, 'gen_op_mutation') + +def make_composite_mutation_resolver(mutation_name): + return make_mutation_resolver(mutation_name, mutation_name, + convert_camel_case_to_snake(mutation_name)) diff --git a/src/services/api/graphql/graphql/queries.py b/src/services/api/graphql/graphql/queries.py index 5f3a7d005..daccc19b2 100644 --- a/src/services/api/graphql/graphql/queries.py +++ b/src/services/api/graphql/graphql/queries.py @@ -14,13 +14,13 @@ # along with this library. If not, see <http://www.gnu.org/licenses/>. from importlib import import_module -from typing import Any, Dict -from ariadne import ObjectType, convert_kwargs_to_snake_case, convert_camel_case_to_snake +from typing import Any, Dict, Optional +from ariadne import ObjectType, convert_camel_case_to_snake from graphql import GraphQLResolveInfo from makefun import with_signature from .. import state -from .. import key_auth +from .. libs import key_auth from api.graphql.session.session import Session from api.graphql.session.errors.op_mode_errors import op_mode_err_msg, op_mode_err_code from vyos.opmode import Error as OpModeError @@ -42,32 +42,51 @@ 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 @with_signature(func_sig, func_name=resolver_name) async def func_impl(*args, **kwargs): try: - if 'data' not in kwargs: - return { - "success": False, - "errors": ['missing data'] - } - - data = kwargs['data'] - key = data['key'] - - auth = key_auth.auth_required(key) - if auth is None: - return { - "success": False, - "errors": ['invalid API key'] - } - - # We are finished with the 'key' entry, and may remove so as to - # pass the rest of data (if any) to function. - del data['key'] + auth_type = state.settings['app'].state.vyos_auth_type + + if auth_type == 'key': + data = kwargs['data'] + key = data['key'] + + auth = key_auth.auth_required(key) + if auth is None: + return { + "success": False, + "errors": ['invalid API key'] + } + + # We are finished with the 'key' entry, and may remove so as to + # pass the rest of data (if any) to function. + del data['key'] + + elif auth_type == 'token': + data = kwargs['data'] + 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'] + } + else: + # AtrributeError will have already been raised if no + # vyos_auth_type; validation and defaultValue ensure it is + # one of the previous cases, so this is never reached. + pass session = state.settings['app'].state.vyos_session @@ -113,5 +132,6 @@ def make_config_session_query_resolver(query_name): def make_gen_op_query_resolver(query_name): return make_query_resolver(query_name, query_name, 'gen_op_query') -def make_system_status_resolver(query_name): - return make_query_resolver(query_name, query_name, 'system_status') +def make_composite_query_resolver(query_name): + return make_query_resolver(query_name, query_name, + convert_camel_case_to_snake(query_name)) diff --git a/src/services/api/graphql/graphql/schema/auth_token.graphql b/src/services/api/graphql/graphql/schema/auth_token.graphql new file mode 100644 index 000000000..af53a293a --- /dev/null +++ b/src/services/api/graphql/graphql/schema/auth_token.graphql @@ -0,0 +1,19 @@ + +input AuthTokenInput { + username: String! + password: String! +} + +type AuthToken { + result: Generic +} + +type AuthTokenResult { + data: AuthToken + success: Boolean! + errors: [String] +} + +extend type Mutation { + AuthToken(data: AuthTokenInput) : AuthTokenResult +} diff --git a/src/services/api/graphql/graphql/schema/configsession.graphql b/src/services/api/graphql/graphql/schema/configsession.graphql deleted file mode 100644 index b1deac4b3..000000000 --- a/src/services/api/graphql/graphql/schema/configsession.graphql +++ /dev/null @@ -1,115 +0,0 @@ - -input ShowConfigInput { - key: String! - path: [String!]! - configFormat: String = null -} - -type ShowConfig { - result: Generic -} - -type ShowConfigResult { - data: ShowConfig - success: Boolean! - errors: [String] -} - -extend type Query { - ShowConfig(data: ShowConfigInput) : ShowConfigResult @configsessionquery -} - -input ShowInput { - key: String! - path: [String!]! -} - -type Show { - result: Generic -} - -type ShowResult { - data: Show - success: Boolean! - errors: [String] -} - -extend type Query { - Show(data: ShowInput) : ShowResult @configsessionquery -} - -input SaveConfigFileInput { - key: String! - fileName: String = null -} - -type SaveConfigFile { - result: Generic -} - -type SaveConfigFileResult { - data: SaveConfigFile - success: Boolean! - errors: [String] -} - -extend type Mutation { - SaveConfigFile(data: SaveConfigFileInput) : SaveConfigFileResult @configsessionmutation -} - -input LoadConfigFileInput { - key: String! - fileName: String! -} - -type LoadConfigFile { - result: Generic -} - -type LoadConfigFileResult { - data: LoadConfigFile - success: Boolean! - errors: [String] -} - -extend type Mutation { - LoadConfigFile(data: LoadConfigFileInput) : LoadConfigFileResult @configsessionmutation -} - -input AddSystemImageInput { - key: String! - location: String! -} - -type AddSystemImage { - result: Generic -} - -type AddSystemImageResult { - data: AddSystemImage - success: Boolean! - errors: [String] -} - -extend type Mutation { - AddSystemImage(data: AddSystemImageInput) : AddSystemImageResult @configsessionmutation -} - -input DeleteSystemImageInput { - key: String! - name: String! -} - -type DeleteSystemImage { - result: Generic -} - -type DeleteSystemImageResult { - data: DeleteSystemImage - success: Boolean! - errors: [String] -} - -extend type Mutation { - DeleteSystemImage(data: DeleteSystemImageInput) : DeleteSystemImageResult @configsessionmutation -}
\ No newline at end of file diff --git a/src/services/api/graphql/graphql/schema/schema.graphql b/src/services/api/graphql/graphql/schema/schema.graphql index 2acecade4..62b0d30bb 100644 --- a/src/services/api/graphql/graphql/schema/schema.graphql +++ b/src/services/api/graphql/graphql/schema/schema.graphql @@ -3,7 +3,8 @@ schema { mutation: Mutation } -directive @systemstatus on FIELD_DEFINITION +directive @compositequery on FIELD_DEFINITION +directive @compositemutation on FIELD_DEFINITION directive @configsessionquery on FIELD_DEFINITION directive @configsessionmutation on FIELD_DEFINITION directive @genopquery on FIELD_DEFINITION @@ -11,8 +12,5 @@ directive @genopmutation on FIELD_DEFINITION scalar Generic -type Query { - SystemStatus(data: SystemStatusInput) : SystemStatusResult @systemstatus -} - +type Query type Mutation diff --git a/src/services/api/graphql/graphql/schema/system_status.graphql b/src/services/api/graphql/graphql/schema/system_status.graphql deleted file mode 100644 index be8d87535..000000000 --- a/src/services/api/graphql/graphql/schema/system_status.graphql +++ /dev/null @@ -1,18 +0,0 @@ -""" -Use 'scalar Generic' for system status output, to avoid attempts to -JSON-serialize in case of JSON output. -""" - -input SystemStatusInput { - key: String! -} - -type SystemStatus { - result: Generic -} - -type SystemStatusResult { - data: SystemStatus - success: Boolean! - errors: [String] -} diff --git a/src/services/api/graphql/key_auth.py b/src/services/api/graphql/libs/key_auth.py index f756ed6d8..2db0f7d48 100644 --- a/src/services/api/graphql/key_auth.py +++ b/src/services/api/graphql/libs/key_auth.py @@ -1,5 +1,5 @@ -from . import state +from .. import state def check_auth(key_list, key): if not key_list: diff --git a/src/services/api/graphql/utils/util.py b/src/services/api/graphql/libs/op_mode.py index da2bcdb5b..c553bbd67 100644 --- a/src/services/api/graphql/utils/util.py +++ b/src/services/api/graphql/libs/op_mode.py @@ -17,25 +17,18 @@ import os import re import typing import importlib.util +from typing import Union +from humps import decamelize from vyos.defaults import directories - -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 +from vyos.util import load_as_module +from vyos.opmode import _normalize_field_names def load_op_mode_as_module(name: str): path = os.path.join(directories['op_mode'], name) name = os.path.splitext(name)[0].replace('-', '_') return load_as_module(name, path) -def is_op_mode_function_name(name): - if re.match(r"^(show|clear|reset|restart)", name): - return True - return False - def is_show_function_name(name): if re.match(r"^show", name): return True @@ -86,7 +79,7 @@ def map_type_name(type_name: type, optional: bool = False) -> str: if type_name == int: return 'Int!' if not optional else 'Int = null' if type_name == bool: - return 'Boolean!' if not optional else 'Boolean = false' + return 'Boolean = false' if typing.get_origin(type_name) == list: if not optional: return f'[{map_type_name(typing.get_args(type_name)[0])}]!' @@ -98,3 +91,6 @@ def map_type_name(type_name: type, optional: bool = False) -> str: # scalar 'Generic' is defined in schema.graphql return 'Generic' + +def normalize_output(result: Union[dict, list]) -> Union[dict, list]: + return _normalize_field_names(decamelize(result)) diff --git a/src/services/api/graphql/libs/token_auth.py b/src/services/api/graphql/libs/token_auth.py new file mode 100644 index 000000000..2100eba7f --- /dev/null +++ b/src/services/api/graphql/libs/token_auth.py @@ -0,0 +1,71 @@ +import jwt +import uuid +import pam +from secrets import token_hex + +from .. import state + +def _check_passwd_pam(username: str, passwd: str) -> bool: + if pam.authenticate(username, passwd): + return True + return False + +def init_secret(): + length = int(state.settings['app'].state.vyos_secret_len) + secret = token_hex(length) + state.settings['secret'] = secret + +def generate_token(user: str, passwd: str, secret: str, exp: int) -> dict: + if user is None or passwd is None: + return {} + if _check_passwd_pam(user, passwd): + app = state.settings['app'] + try: + users = app.state.vyos_token_users + except AttributeError: + app.state.vyos_token_users = {} + users = app.state.vyos_token_users + user_id = uuid.uuid1().hex + payload_data = {'iss': user, 'sub': user_id, 'exp': exp} + secret = state.settings.get('secret') + if secret is None: + return { + "success": False, + "errors": ['failed secret generation'] + } + token = jwt.encode(payload=payload_data, key=secret, algorithm="HS256") + + users |= {user_id: user} + return {'token': token} + +def get_user_context(request): + context = {} + context['request'] = request + context['user'] = None + if 'Authorization' in request.headers: + auth = request.headers['Authorization'] + scheme, token = auth.split() + if scheme.lower() != 'bearer': + return context + + try: + secret = state.settings.get('secret') + payload = jwt.decode(token, secret, algorithms=["HS256"]) + 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: + users = state.settings['app'].state.vyos_token_users + except AttributeError: + return context + + user = users.get(user_id) + if user is not None: + context['user'] = user + + return context diff --git a/src/services/api/graphql/session/composite/system_status.py b/src/services/api/graphql/session/composite/system_status.py index 3c1a3d45b..d809f32e3 100755 --- a/src/services/api/graphql/session/composite/system_status.py +++ b/src/services/api/graphql/session/composite/system_status.py @@ -23,7 +23,7 @@ import importlib.util from vyos.defaults import directories -from api.graphql.utils.util import load_op_mode_as_module +from api.graphql.libs.op_mode import load_op_mode_as_module def get_system_version() -> dict: show_version = load_op_mode_as_module('version.py') diff --git a/src/services/api/graphql/session/errors/op_mode_errors.py b/src/services/api/graphql/session/errors/op_mode_errors.py index 7ba75455d..18d555f2d 100644 --- a/src/services/api/graphql/session/errors/op_mode_errors.py +++ b/src/services/api/graphql/session/errors/op_mode_errors.py @@ -1,13 +1,17 @@ - - op_mode_err_msg = { "UnconfiguredSubsystem": "subsystem is not configured or not running", "DataUnavailable": "data currently unavailable", - "PermissionDenied": "client does not have permission" + "PermissionDenied": "client does not have permission", + "InsufficientResources": "insufficient system resources", + "IncorrectValue": "argument value is incorrect", + "UnsupportedOperation": "operation is not supported (yet)", } op_mode_err_code = { "UnconfiguredSubsystem": 2000, "DataUnavailable": 2001, - "PermissionDenied": 1003 + "InsufficientResources": 2002, + "PermissionDenied": 1003, + "IncorrectValue": 1002, + "UnsupportedOperation": 1004, } diff --git a/src/services/api/graphql/session/session.py b/src/services/api/graphql/session/session.py index f990e63d0..0b77b1433 100644 --- a/src/services/api/graphql/session/session.py +++ b/src/services/api/graphql/session/session.py @@ -24,7 +24,8 @@ from vyos.defaults import directories from vyos.template import render from vyos.opmode import Error as OpModeError -from api.graphql.utils.util import load_op_mode_as_module, split_compound_op_mode_name +from api.graphql.libs.op_mode import load_op_mode_as_module, split_compound_op_mode_name +from api.graphql.libs.op_mode import normalize_output op_mode_include_file = os.path.join(directories['data'], 'op-mode-standardized.json') @@ -149,6 +150,8 @@ class Session: except OpModeError as e: raise e + res = normalize_output(res) + return res def gen_op_mutation(self): diff --git a/src/services/vyos-hostsd b/src/services/vyos-hostsd index 9ae7b1ea9..a380f2e66 100755 --- a/src/services/vyos-hostsd +++ b/src/services/vyos-hostsd @@ -406,8 +406,7 @@ def validate_schema(data): def pdns_rec_control(command): - # pdns-r process name is NOT equal to the name shown in ps - if not process_named_running('pdns-r/worker'): + if not process_named_running('pdns_recursor'): logger.info(f'pdns_recursor not running, not sending "{command}"') return diff --git a/src/services/vyos-http-api-server b/src/services/vyos-http-api-server index 4ace981ca..f59e089ae 100755 --- a/src/services/vyos-http-api-server +++ b/src/services/vyos-http-api-server @@ -175,6 +175,19 @@ class ImageModel(ApiModel): } } +class ContainerImageModel(ApiModel): + op: StrictStr + name: StrictStr = None + + class Config: + schema_extra = { + "example": { + "key": "id_key", + "op": "add | delete | show", + "name": "imagename", + } + } + class GenerateModel(ApiModel): op: StrictStr path: List[StrictStr] @@ -389,7 +402,7 @@ class MultipartRoute(APIRoute): if endpoint in ('/retrieve','/generate','/show','/reset'): if request.ERR_NO_OP or request.ERR_NO_PATH: return error(400, "Missing required field. \"op\" and \"path\" fields are required") - if endpoint in ('/config-file', '/image'): + if endpoint in ('/config-file', '/image', '/container-image'): if request.ERR_NO_OP: return error(400, "Missing required field \"op\"") @@ -581,6 +594,37 @@ def image_op(data: ImageModel): return success(res) +@app.post('/container-image') +def image_op(data: ContainerImageModel): + session = app.state.vyos_session + + op = data.op + + try: + if op == 'add': + if data.name: + name = data.name + else: + return error(400, "Missing required field \"name\"") + res = session.add_container_image(name) + elif op == 'delete': + if data.name: + name = data.name + else: + return error(400, "Missing required field \"name\"") + res = session.delete_container_image(name) + elif op == 'show': + res = session.show_container_image() + else: + return error(400, "\"{0}\" is not a valid operation".format(op)) + except ConfigSessionError as e: + return error(400, str(e)) + except Exception as e: + logger.critical(traceback.format_exc()) + return error(500, "An internal error occured. Check the logs for details.") + + return success(res) + @app.post('/generate') def generate_op(data: GenerateModel): session = app.state.vyos_session @@ -647,21 +691,30 @@ def reset_op(data: ResetModel): ### def graphql_init(fast_api_app): - from api.graphql.bindings import generate_schema - + from api.graphql.libs.token_auth import get_user_context api.graphql.state.init() api.graphql.state.settings['app'] = app + # import after initializaion of state + from api.graphql.bindings import generate_schema schema = generate_schema() in_spec = app.state.vyos_introspection if app.state.vyos_origins: origins = app.state.vyos_origins - app.add_route('/graphql', CORSMiddleware(GraphQL(schema, debug=True, introspection=in_spec), allow_origins=origins, allow_methods=("GET", "POST", "OPTIONS"))) + app.add_route('/graphql', CORSMiddleware(GraphQL(schema, + context_value=get_user_context, + debug=True, + introspection=in_spec), + allow_origins=origins, + allow_methods=("GET", "POST", "OPTIONS"), + allow_headers=("Authorization",))) else: - app.add_route('/graphql', GraphQL(schema, debug=True, introspection=in_spec)) - + app.add_route('/graphql', GraphQL(schema, + context_value=get_user_context, + debug=True, + introspection=in_spec)) ### if __name__ == '__main__': @@ -688,16 +741,21 @@ if __name__ == '__main__': app.state.vyos_debug = server_config['debug'] app.state.vyos_strict = server_config['strict'] app.state.vyos_origins = server_config.get('cors', {}).get('allow_origin', []) - if 'gql' in server_config: - app.state.vyos_gql = True - if isinstance(server_config['gql'], dict) and 'introspection' in server_config['gql']: - app.state.vyos_introspection = True - else: - app.state.vyos_introspection = False + if 'graphql' in server_config: + app.state.vyos_graphql = True + if isinstance(server_config['graphql'], dict): + if 'introspection' in server_config['graphql']: + app.state.vyos_introspection = True + else: + app.state.vyos_introspection = False + # default value is merged in conf_mode http-api.py, if not set + app.state.vyos_auth_type = server_config['graphql']['authentication']['type'] + app.state.vyos_token_exp = server_config['graphql']['authentication']['expiration'] + app.state.vyos_secret_len = server_config['graphql']['authentication']['secret_length'] else: - app.state.vyos_gql = False + app.state.vyos_graphql = False - if app.state.vyos_gql: + if app.state.vyos_graphql: graphql_init(app) try: diff --git a/src/system/keepalived-fifo.py b/src/system/keepalived-fifo.py index a0fccd1d0..864ee8419 100755 --- a/src/system/keepalived-fifo.py +++ b/src/system/keepalived-fifo.py @@ -67,13 +67,13 @@ class KeepalivedFifo: # For VRRP configuration to be read, the commit must be finished count = 1 while commit_in_progress(): - if ( count <= 40 ): - logger.debug(f'commit in progress try: {count}') + if ( count <= 20 ): + logger.debug(f'Attempt to load keepalived configuration aborted due to a commit in progress (attempt {count}/20)') else: - logger.error(f'commit still in progress after {count} continuing anyway') + logger.error(f'Forced keepalived configuration loading despite a commit in progress ({count} wait time expired, not waiting further)') break count += 1 - time.sleep(0.5) + time.sleep(1) try: base = ['high-availability', 'vrrp'] diff --git a/src/systemd/vyos-domain-group-resolve.service b/src/systemd/vyos-domain-group-resolve.service deleted file mode 100644 index 29628fddb..000000000 --- a/src/systemd/vyos-domain-group-resolve.service +++ /dev/null @@ -1,11 +0,0 @@ -[Unit] -Description=VyOS firewall domain-group resolver -After=vyos-router.service - -[Service] -Type=simple -Restart=always -ExecStart=/usr/bin/python3 /usr/libexec/vyos/vyos-domain-group-resolve.py - -[Install] -WantedBy=multi-user.target diff --git a/src/systemd/vyos-domain-resolver.service b/src/systemd/vyos-domain-resolver.service new file mode 100644 index 000000000..c56b51f0c --- /dev/null +++ b/src/systemd/vyos-domain-resolver.service @@ -0,0 +1,13 @@ +[Unit] +Description=VyOS firewall domain resolver +After=vyos-router.service + +[Service] +Type=simple +Restart=always +ExecStart=/usr/bin/python3 -u /usr/libexec/vyos/vyos-domain-resolver.py +StandardError=journal +StandardOutput=journal + +[Install] +WantedBy=multi-user.target diff --git a/src/tests/test_op_mode.py b/src/tests/test_op_mode.py new file mode 100644 index 000000000..90963b3c5 --- /dev/null +++ b/src/tests/test_op_mode.py @@ -0,0 +1,65 @@ +#!/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 unittest import TestCase + +import vyos.opmode + +class TestVyOSOpMode(TestCase): + def test_field_name_normalization(self): + from vyos.opmode import _normalize_field_name + + self.assertEqual(_normalize_field_name(" foo bar "), "foo_bar") + self.assertEqual(_normalize_field_name("foo-bar"), "foo_bar") + self.assertEqual(_normalize_field_name("foo (bar) baz"), "foo_bar_baz") + self.assertEqual(_normalize_field_name("load%"), "load_percentage") + + def test_dict_fields_normalization_non_unique(self): + from vyos.opmode import _normalize_field_names + + # Space and dot are both replaced by an underscore, + # so dicts like this cannor be normalized uniquely + data = {"foo bar": True, "foo.bar": False} + + with self.assertRaises(vyos.opmode.InternalError): + _normalize_field_names(data) + + def test_dict_fields_normalization_simple_dict(self): + from vyos.opmode import _normalize_field_names + + data = {"foo bar": True, "Bar-Baz": False} + self.assertEqual(_normalize_field_names(data), {"foo_bar": True, "bar_baz": False}) + + def test_dict_fields_normalization_nested_dict(self): + from vyos.opmode import _normalize_field_names + + data = {"foo bar": True, "bar-baz": {"baz-quux": {"quux-xyzzy": False}}} + self.assertEqual(_normalize_field_names(data), + {"foo_bar": True, "bar_baz": {"baz_quux": {"quux_xyzzy": False}}}) + + def test_dict_fields_normalization_mixed(self): + from vyos.opmode import _normalize_field_names + + data = [{"foo bar": True, "bar-baz": [{"baz-quux": {"quux-xyzzy": [False]}}]}] + self.assertEqual(_normalize_field_names(data), + [{"foo_bar": True, "bar_baz": [{"baz_quux": {"quux_xyzzy": [False]}}]}]) + + def test_dict_fields_normalization_primitive(self): + from vyos.opmode import _normalize_field_names + + data = [1, False, "foo"] + self.assertEqual(_normalize_field_names(data), [1, False, "foo"]) + diff --git a/src/tests/test_util.py b/src/tests/test_util.py index 8ac9a500a..d8b2b7940 100644 --- a/src/tests/test_util.py +++ b/src/tests/test_util.py @@ -26,3 +26,17 @@ class TestVyOSUtil(TestCase): def test_sysctl_read(self): self.assertEqual(sysctl_read('net.ipv4.conf.lo.forwarding'), '1') + + def test_camel_to_snake_case(self): + self.assertEqual(camel_to_snake_case('ConnectionTimeout'), + 'connection_timeout') + self.assertEqual(camel_to_snake_case('connectionTimeout'), + 'connection_timeout') + self.assertEqual(camel_to_snake_case('TCPConnectionTimeout'), + 'tcp_connection_timeout') + self.assertEqual(camel_to_snake_case('TCPPort'), + 'tcp_port') + self.assertEqual(camel_to_snake_case('UseHTTPProxy'), + 'use_http_proxy') + self.assertEqual(camel_to_snake_case('CustomerID'), + 'customer_id') diff --git a/src/validators/allowed-vlan b/src/validators/allowed-vlan deleted file mode 100755 index 11389390b..000000000 --- a/src/validators/allowed-vlan +++ /dev/null @@ -1,19 +0,0 @@ -#! /usr/bin/python3 - -import sys -import re - -if __name__ == '__main__': - if len(sys.argv)>1: - allowed_vlan = sys.argv[1] - if re.search('[0-9]{1,4}-[0-9]{1,4}', allowed_vlan): - for tmp in allowed_vlan.split('-'): - if int(tmp) not in range(1, 4095): - sys.exit(1) - else: - if int(allowed_vlan) not in range(1, 4095): - sys.exit(1) - else: - sys.exit(2) - - sys.exit(0) diff --git a/src/validators/dotted-decimal b/src/validators/dotted-decimal deleted file mode 100755 index 652110346..000000000 --- a/src/validators/dotted-decimal +++ /dev/null @@ -1,33 +0,0 @@ -#!/usr/bin/env python3 -# -# Copyright (C) 2020 VyOS maintainers and contributors -# -# This program is free software; you can redistribute it and/or modify -# it under the terms of the GNU General Public License version 2 or later as -# published by the Free Software Foundation. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see <http://www.gnu.org/licenses/>. - -import re -import sys - -area = sys.argv[1] - -res = re.match(r'^(\d+)\.(\d+)\.(\d+)\.(\d+)$', area) -if not res: - print("\'{0}\' is not a valid dotted decimal value".format(area)) - sys.exit(1) -else: - components = res.groups() - for n in range(0, 4): - if (int(components[n]) > 255): - print("Invalid component of a dotted decimal value: {0} exceeds 255".format(components[n])) - sys.exit(1) - -sys.exit(0) 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() diff --git a/src/validators/fqdn b/src/validators/fqdn index a4027e4ca..a65d2d5d4 100755 --- a/src/validators/fqdn +++ b/src/validators/fqdn @@ -1,27 +1,2 @@ -#!/usr/bin/env python3 -# -# Copyright (C) 2020-2021 VyOS maintainers and contributors -# -# This program is free software; you can redistribute it and/or modify -# it under the terms of the GNU General Public License version 2 or later as -# published by the Free Software Foundation. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see <http://www.gnu.org/licenses/>. - -import re -import sys - -pattern = '[A-Za-z0-9][-.A-Za-z0-9]*' - -if __name__ == '__main__': - if len(sys.argv) != 2: - sys.exit(1) - if not re.match(pattern, sys.argv[1]): - sys.exit(1) - sys.exit(0) +#!/usr/bin/env sh +${vyos_libexec_dir}/validate-value --regex "[A-Za-z0-9][-.A-Za-z0-9]*" --value "$1" diff --git a/src/validators/mac-address b/src/validators/mac-address index 7d020f387..bb859a603 100755 --- a/src/validators/mac-address +++ b/src/validators/mac-address @@ -1,27 +1,2 @@ -#!/usr/bin/env python3 -# -# Copyright (C) 2018-2020 VyOS maintainers and contributors -# -# This program is free software; you can redistribute it and/or modify -# it under the terms of the GNU General Public License version 2 or later as -# published by the Free Software Foundation. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see <http://www.gnu.org/licenses/>. - -import re -import sys - -pattern = "^([0-9A-Fa-f]{2}:){5}([0-9A-Fa-f]{2})$" - -if __name__ == '__main__': - if len(sys.argv) != 2: - sys.exit(1) - if not re.match(pattern, sys.argv[1]): - sys.exit(1) - sys.exit(0) +#!/usr/bin/env sh +${vyos_libexec_dir}/validate-value --regex "([0-9A-Fa-f]{2}:){5}([0-9A-Fa-f]{2})" --value "$1" diff --git a/src/validators/mac-address-exclude b/src/validators/mac-address-exclude new file mode 100755 index 000000000..c44913023 --- /dev/null +++ b/src/validators/mac-address-exclude @@ -0,0 +1,2 @@ +#!/usr/bin/env sh +${vyos_libexec_dir}/validate-value --regex "!([0-9A-Fa-f]{2}:){5}([0-9A-Fa-f]{2})" --value "$1" diff --git a/src/validators/mac-address-firewall b/src/validators/mac-address-firewall deleted file mode 100755 index 70551f86d..000000000 --- a/src/validators/mac-address-firewall +++ /dev/null @@ -1,27 +0,0 @@ -#!/usr/bin/env python3 -# -# Copyright (C) 2018-2022 VyOS maintainers and contributors -# -# This program is free software; you can redistribute it and/or modify -# it under the terms of the GNU General Public License version 2 or later as -# published by the Free Software Foundation. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see <http://www.gnu.org/licenses/>. - -import re -import sys - -pattern = "^!?([0-9A-Fa-f]{2}:){5}([0-9A-Fa-f]{2})$" - -if __name__ == '__main__': - if len(sys.argv) != 2: - sys.exit(1) - if not re.match(pattern, sys.argv[1]): - sys.exit(1) - sys.exit(0) diff --git a/src/validators/tcp-flag b/src/validators/tcp-flag deleted file mode 100755 index 1496b904a..000000000 --- a/src/validators/tcp-flag +++ /dev/null @@ -1,17 +0,0 @@ -#!/usr/bin/python3 - -import sys -import re - -if __name__ == '__main__': - if len(sys.argv)>1: - flag = sys.argv[1] - if flag and flag[0] == '!': - flag = flag[1:] - if flag not in ['syn', 'ack', 'rst', 'fin', 'urg', 'psh', 'ecn', 'cwr']: - print(f'Error: {flag} is not a valid TCP flag') - sys.exit(1) - else: - sys.exit(2) - - sys.exit(0) diff --git a/src/xdp/common/common.mk b/src/xdp/common/common.mk index ebe23a9ed..ffb86a65c 100644 --- a/src/xdp/common/common.mk +++ b/src/xdp/common/common.mk @@ -39,7 +39,7 @@ KERN_USER_H ?= $(wildcard common_kern_user.h) CFLAGS ?= -g -I../include/ BPF_CFLAGS ?= -I../include/ -LIBS = -l:libbpf.a -lelf $(USER_LIBS) +LIBS = -lbpf -lelf $(USER_LIBS) all: llvm-check $(USER_TARGETS) $(XDP_OBJ) $(COPY_LOADER) $(COPY_STATS) diff --git a/src/xdp/common/common_user_bpf_xdp.c b/src/xdp/common/common_user_bpf_xdp.c index e7ef77174..faf7f4f91 100644 --- a/src/xdp/common/common_user_bpf_xdp.c +++ b/src/xdp/common/common_user_bpf_xdp.c @@ -274,7 +274,7 @@ struct bpf_object *load_bpf_and_xdp_attach(struct config *cfg) exit(EXIT_FAIL_BPF); } - strncpy(cfg->progsec, bpf_program__title(bpf_prog, false), sizeof(cfg->progsec)); + strncpy(cfg->progsec, bpf_program__section_name(bpf_prog), sizeof(cfg->progsec)); prog_fd = bpf_program__fd(bpf_prog); if (prog_fd <= 0) { |