summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.github/workflows/add-pr-labels.yml4
-rw-r--r--.github/workflows/add-rebase-label.yml14
-rw-r--r--.github/workflows/check-pr-message.yml (renamed from .github/workflows/chceck-pr-message.yml)4
-rw-r--r--.github/workflows/check-unused-imports.yml10
-rw-r--r--.github/workflows/codeql.yml10
-rw-r--r--.github/workflows/package-smoketest.yml112
-rw-r--r--.github/workflows/sonarcloud.yml8
-rw-r--r--.github/workflows/trigger-pr.yml19
-rw-r--r--Makefile6
-rw-r--r--data/config-mode-dependencies/vyos-1x.json4
-rw-r--r--data/templates/accel-ppp/ipoe.config.j23
-rw-r--r--data/templates/accel-ppp/l2tp.config.j23
-rw-r--r--data/templates/accel-ppp/pptp.config.j23
-rw-r--r--data/templates/accel-ppp/sstp.config.j23
-rw-r--r--data/templates/conntrack/sysctl.conf.j23
-rw-r--r--data/templates/ipsec/swanctl/peer.j210
-rw-r--r--data/templates/ipsec/swanctl/remote_access.j218
-rw-r--r--data/templates/load-balancing/haproxy.cfg.j217
-rw-r--r--data/templates/openvpn/server.conf.j24
-rw-r--r--data/templates/rsyslog/logrotate.j26
-rw-r--r--data/templates/ssh/sshd_config.j25
-rw-r--r--data/templates/telegraf/telegraf.j22
-rw-r--r--data/templates/wifi/wpa_supplicant.conf.j214
-rw-r--r--debian/control49
-rwxr-xr-xdebian/rules4
-rw-r--r--debian/vyos-1x.install2
-rw-r--r--debian/vyos-1x.postinst58
-rw-r--r--interface-definitions/include/conntrack/log-common.xml.i20
-rw-r--r--interface-definitions/include/conntrack/log-protocols.xml.i26
-rw-r--r--interface-definitions/include/firewall/common-rule-inet.xml.i1
-rw-r--r--interface-definitions/include/firewall/common-rule-ipv4-raw.xml.i1
-rw-r--r--interface-definitions/include/firewall/common-rule-ipv6-raw.xml.i1
-rw-r--r--interface-definitions/include/firewall/ipv4-hook-input.xml.i2
-rw-r--r--interface-definitions/include/firewall/ipv4-hook-output.xml.i2
-rw-r--r--interface-definitions/include/firewall/ipv4-hook-prerouting.xml.i1
-rw-r--r--interface-definitions/include/firewall/ipv6-hook-input.xml.i2
-rw-r--r--interface-definitions/include/firewall/ipv6-hook-output.xml.i2
-rw-r--r--interface-definitions/include/firewall/ipv6-hook-prerouting.xml.i1
-rw-r--r--interface-definitions/include/firewall/match-ipsec-in.xml.i21
-rw-r--r--interface-definitions/include/firewall/match-ipsec-out.xml.i21
-rw-r--r--interface-definitions/include/firewall/match-ipsec.xml.i22
-rw-r--r--interface-definitions/include/haproxy/logging.xml.i10
-rw-r--r--interface-definitions/include/policy/route-common.xml.i18
-rw-r--r--interface-definitions/include/version/firewall-version.xml.i2
-rw-r--r--interface-definitions/include/version/openvpn-version.xml.i2
-rw-r--r--interface-definitions/interfaces_geneve.xml.in1
-rw-r--r--interface-definitions/interfaces_openvpn.xml.in14
-rw-r--r--interface-definitions/interfaces_vxlan.xml.in28
-rw-r--r--interface-definitions/interfaces_wireless.xml.in20
-rw-r--r--interface-definitions/load-balancing_reverse-proxy.xml.in3
-rw-r--r--interface-definitions/service_ssh.xml.in13
-rw-r--r--interface-definitions/system_conntrack.xml.in81
-rw-r--r--interface-definitions/vpn_ipsec.xml.in6
-rw-r--r--op-mode-definitions/clear-interfaces.xml.in (renamed from op-mode-definitions/counters.xml.in)18
-rw-r--r--op-mode-definitions/configure.xml.in6
-rw-r--r--op-mode-definitions/connect.xml.in2
-rw-r--r--op-mode-definitions/dhcp.xml.in4
-rw-r--r--op-mode-definitions/disconnect.xml.in2
-rw-r--r--op-mode-definitions/dns-dynamic.xml.in2
-rw-r--r--op-mode-definitions/dns-forwarding.xml.in2
-rw-r--r--op-mode-definitions/firewall.xml.in12
-rw-r--r--op-mode-definitions/generate_tech-support_archive.xml.in6
-rw-r--r--op-mode-definitions/igmp-proxy.xml.in2
-rw-r--r--op-mode-definitions/mdns-reflector.xml.in2
-rw-r--r--op-mode-definitions/monitor-bandwidth.xml.in3
-rw-r--r--op-mode-definitions/restart-ntp.xml.in13
-rw-r--r--op-mode-definitions/restart-router-advert.xml.in2
-rw-r--r--op-mode-definitions/restart-serial.xml.in31
-rw-r--r--op-mode-definitions/restart-snmp.xml.in2
-rw-r--r--op-mode-definitions/restart-ssh.xml.in2
-rw-r--r--op-mode-definitions/reverse-proxy.xml.in2
-rw-r--r--op-mode-definitions/show-techsupport_report.xml.in8
-rw-r--r--op-mode-definitions/suricata.xml.in2
-rw-r--r--op-mode-definitions/vpn-ipsec.xml.in2
-rw-r--r--op-mode-definitions/vrrp.xml.in2
-rw-r--r--op-mode-definitions/webproxy.xml.in2
-rw-r--r--python/vyos/component_version.py19
-rw-r--r--python/vyos/config.py1
-rw-r--r--python/vyos/configdep.py49
-rw-r--r--python/vyos/configtree.py9
-rw-r--r--python/vyos/defaults.py10
-rw-r--r--python/vyos/firewall.py22
-rw-r--r--python/vyos/ifconfig/interface.py59
-rw-r--r--python/vyos/ifconfig/l2tpv3.py12
-rw-r--r--python/vyos/ifconfig/vxlan.py20
-rw-r--r--python/vyos/ipsec.py136
-rw-r--r--python/vyos/opmode.py12
-rw-r--r--python/vyos/template.py4
-rw-r--r--python/vyos/utils/__init__.py1
-rw-r--r--python/vyos/utils/locking.py115
-rw-r--r--python/vyos/utils/network.py51
-rw-r--r--python/vyos/utils/process.py2
-rw-r--r--python/vyos/utils/serial.py118
-rw-r--r--python/vyos/utils/strip_config.py210
-rw-r--r--ruff.toml18
-rwxr-xr-xscripts/build-command-op-templates6
-rw-r--r--smoketest/config-tests/dialup-router-medium-vpn6
-rw-r--r--smoketest/scripts/cli/base_interfaces_test.py74
-rw-r--r--smoketest/scripts/cli/base_vyostest_shim.py17
-rwxr-xr-xsmoketest/scripts/cli/test_config_dependency.py49
-rwxr-xr-xsmoketest/scripts/cli/test_firewall.py76
-rwxr-xr-xsmoketest/scripts/cli/test_interfaces_l2tpv3.py8
-rwxr-xr-xsmoketest/scripts/cli/test_interfaces_openvpn.py10
-rwxr-xr-xsmoketest/scripts/cli/test_interfaces_vxlan.py47
-rwxr-xr-xsmoketest/scripts/cli/test_interfaces_wireless.py52
-rwxr-xr-xsmoketest/scripts/cli/test_load-balancing_reverse-proxy.py39
-rwxr-xr-xsmoketest/scripts/cli/test_op-mode_show.py39
-rwxr-xr-xsmoketest/scripts/cli/test_policy_route.py49
-rwxr-xr-xsmoketest/scripts/cli/test_protocols_ospf.py14
-rwxr-xr-xsmoketest/scripts/cli/test_protocols_static.py5
-rwxr-xr-xsmoketest/scripts/cli/test_service_snmp.py14
-rwxr-xr-xsmoketest/scripts/cli/test_service_ssh.py16
-rwxr-xr-xsmoketest/scripts/cli/test_system_conntrack.py35
-rwxr-xr-xsmoketest/scripts/cli/test_system_syslog.py6
-rwxr-xr-xsmoketest/scripts/cli/test_vpn_ipsec.py130
-rwxr-xr-xsmoketest/scripts/cli/test_vrf.py29
-rwxr-xr-xsmoketest/scripts/system/test_kernel_options.py6
-rw-r--r--src/completion/list_login_ttys.py25
-rwxr-xr-xsrc/conf_mode/firewall.py70
-rwxr-xr-xsrc/conf_mode/interfaces_geneve.py2
-rwxr-xr-xsrc/conf_mode/interfaces_l2tpv3.py2
-rwxr-xr-xsrc/conf_mode/interfaces_openvpn.py30
-rwxr-xr-xsrc/conf_mode/interfaces_vti.py2
-rwxr-xr-xsrc/conf_mode/interfaces_vxlan.py31
-rwxr-xr-xsrc/conf_mode/interfaces_wireless.py84
-rwxr-xr-xsrc/conf_mode/policy_route.py29
-rwxr-xr-xsrc/conf_mode/service_monitoring_telegraf.py3
-rwxr-xr-xsrc/conf_mode/service_snmp.py13
-rwxr-xr-xsrc/conf_mode/system_conntrack.py21
-rwxr-xr-xsrc/conf_mode/system_console.py15
-rwxr-xr-xsrc/conf_mode/system_option.py9
-rwxr-xr-xsrc/conf_mode/vpn_ipsec.py16
-rwxr-xr-xsrc/conf_mode/vrf.py23
-rw-r--r--src/etc/bash_completion.d/vyatta-op685
-rw-r--r--src/etc/default/vyatta217
-rwxr-xr-xsrc/helpers/vyos_net_name143
-rwxr-xr-xsrc/migration-scripts/firewall/16-to-1760
-rw-r--r--src/migration-scripts/firewall/7-to-88
-rw-r--r--src/migration-scripts/nat/6-to-72
-rw-r--r--src/migration-scripts/openvpn/1-to-28
-rw-r--r--src/migration-scripts/openvpn/2-to-339
-rw-r--r--src/migration-scripts/openvpn/3-to-426
-rwxr-xr-xsrc/op_mode/bridge.py2
-rwxr-xr-xsrc/op_mode/connect_disconnect.py22
-rwxr-xr-xsrc/op_mode/dhcp.py2
-rwxr-xr-xsrc/op_mode/interfaces.py20
-rwxr-xr-xsrc/op_mode/ipsec.py490
-rwxr-xr-xsrc/op_mode/nat.py56
-rwxr-xr-xsrc/op_mode/openconnect.py6
-rwxr-xr-xsrc/op_mode/pki.py3
-rwxr-xr-xsrc/op_mode/powerctrl.py7
-rwxr-xr-xsrc/op_mode/restart.py127
-rw-r--r--src/op_mode/serial.py38
-rwxr-xr-xsrc/op_mode/ssh.py2
-rw-r--r--src/op_mode/tech_support.py394
-rw-r--r--src/op_mode/zone.py4
-rwxr-xr-xsrc/opt/vyatta/bin/restricted-shell11
-rwxr-xr-xsrc/opt/vyatta/bin/vyatta-op-cmd-wrapper6
-rw-r--r--src/opt/vyatta/etc/LICENSE340
-rw-r--r--src/opt/vyatta/etc/shell/level/users/allowed-op20
-rw-r--r--src/opt/vyatta/etc/shell/level/users/allowed-op.in16
-rwxr-xr-xsrc/opt/vyatta/sbin/if-mib-alias130
-rwxr-xr-xsrc/opt/vyatta/sbin/vyos-persistpath19
-rw-r--r--src/opt/vyatta/share/vyatta-op/functions/interpreter/vyatta-common82
-rw-r--r--src/opt/vyatta/share/vyatta-op/functions/interpreter/vyatta-op-run240
-rw-r--r--src/opt/vyatta/share/vyatta-op/functions/interpreter/vyatta-unpriv97
-rw-r--r--src/services/api/graphql/session/errors/op_mode_errors.py2
-rwxr-xr-xsrc/services/vyos-configd16
-rwxr-xr-xsrc/services/vyos-conntrack-logger458
-rw-r--r--src/systemd/vyos-conntrack-logger.service21
170 files changed, 5941 insertions, 693 deletions
diff --git a/.github/workflows/add-pr-labels.yml b/.github/workflows/add-pr-labels.yml
index a7ee8446f..a0c076064 100644
--- a/.github/workflows/add-pr-labels.yml
+++ b/.github/workflows/add-pr-labels.yml
@@ -5,10 +5,6 @@ on:
pull_request_target:
branches:
- current
- - crux
- - equuleus
- - sagitta
- - circinus
permissions:
pull-requests: write
diff --git a/.github/workflows/add-rebase-label.yml b/.github/workflows/add-rebase-label.yml
new file mode 100644
index 000000000..9041303af
--- /dev/null
+++ b/.github/workflows/add-rebase-label.yml
@@ -0,0 +1,14 @@
+name: Add rebase label
+
+on:
+ pull_request_target:
+ types: [synchronize, opened, reopened, labeled, unlabeled]
+
+permissions:
+ pull-requests: write
+ contents: read
+
+jobs:
+ add-rebase-label:
+ uses: vyos/.github/.github/workflows/add-rebase-label.yml@current
+ secrets: inherit
diff --git a/.github/workflows/chceck-pr-message.yml b/.github/workflows/check-pr-message.yml
index c567a5934..625ba2d75 100644
--- a/.github/workflows/chceck-pr-message.yml
+++ b/.github/workflows/check-pr-message.yml
@@ -5,10 +5,6 @@ on:
pull_request_target:
branches:
- current
- - crux
- - equuleus
- - sagitta
- - circinus
types: [opened, synchronize, edited]
permissions:
diff --git a/.github/workflows/check-unused-imports.yml b/.github/workflows/check-unused-imports.yml
index 322d4f3a8..76bba94be 100644
--- a/.github/workflows/check-unused-imports.yml
+++ b/.github/workflows/check-unused-imports.yml
@@ -1,14 +1,16 @@
name: Check for unused imports using Pylint
on:
- pull_request:
+ pull_request_target:
branches:
- current
- - equuleus
- - sagitta
- - circinus
+ paths:
+ - '**'
+ - '!.github/**'
+ - '!**/*.md'
workflow_dispatch:
permissions:
+ pull-requests: write
contents: read
jobs:
diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml
index 12654e42e..143029c14 100644
--- a/.github/workflows/codeql.yml
+++ b/.github/workflows/codeql.yml
@@ -2,10 +2,18 @@ name: "Perform CodeQL Analysis"
on:
push:
- branches: [ "current", "sagitta", "equuleus", "circinus" ]
+ branches: [ "current" ]
+ paths:
+ - '**'
+ - '!.github/**'
+ - '!**/*.md'
pull_request:
# The branches below must be a subset of the branches above
branches: [ "current" ]
+ paths:
+ - '**'
+ - '!.github/**'
+ - '!**/*.md'
schedule:
- cron: '22 10 * * 0'
diff --git a/.github/workflows/package-smoketest.yml b/.github/workflows/package-smoketest.yml
index 27272a6e2..824cd64b1 100644
--- a/.github/workflows/package-smoketest.yml
+++ b/.github/workflows/package-smoketest.yml
@@ -4,7 +4,10 @@ on:
pull_request_target:
branches:
- current
- - circinus
+ paths:
+ - '**'
+ - '!.github/**'
+ - '!**/*.md'
permissions:
pull-requests: write
@@ -14,7 +17,7 @@ env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # Needed for PR comments
jobs:
- build:
+ build_iso:
runs-on: ubuntu-24.04
timeout-minutes: 45
container:
@@ -42,6 +45,7 @@ jobs:
run: |
echo "build_version=1.5-integration-$(date -u +%Y%m%d%H%M)" >> $GITHUB_OUTPUT
- name: Build custom ISO image
+ shell: bash
run: |
sudo --preserve-env ./build-vyos-image \
--architecture amd64 \
@@ -55,13 +59,15 @@ jobs:
name: vyos-${{ steps.version.outputs.build_version }}
path: build/live-image-amd64.hybrid.iso
- cli-smoketests:
- needs: build
+ test_smoketest_cli:
+ needs: build_iso
runs-on: ubuntu-24.04
timeout-minutes: 180
container:
image: vyos/vyos-build:current
options: --sysctl net.ipv6.conf.lo.disable_ipv6=0 --privileged
+ outputs:
+ exit_code: ${{ steps.test.outputs.exit_code }}
steps:
# We need the test script from vyos-build repo
- name: Clone vyos-build source code
@@ -70,27 +76,29 @@ jobs:
repository: vyos/vyos-build
- uses: actions/download-artifact@v4
with:
- name: vyos-${{ needs.build.outputs.build_version }}
+ name: vyos-${{ needs.build_iso.outputs.build_version }}
path: build
- name: VyOS CLI smoketests
- run: sudo make test
- - name: Add PR comment
- if: always()
- uses: mshick/add-pr-comment@v2
- with:
- message-success: '👍 VyOS CLI smoketests finished successfully!'
- message-failure: '❌ VyOS CLI smoketests failed!'
- message-cancelled: '❌ VyOS CLI smoketests cancelled!'
- allow-repeats: false
- refresh-message-position: true
+ id: test
+ shell: bash
+ run: |
+ set -e
+ sudo make test
+ if [[ $? == 0 ]]; then
+ echo "exit_code=success" >> $GITHUB_OUTPUT
+ else
+ echo "exit_code=fail" >> $GITHUB_OUTPUT
+ fi
- config-load-tests:
- needs: build
+ test_config_load:
+ needs: build_iso
runs-on: ubuntu-24.04
timeout-minutes: 90
container:
image: vyos/vyos-build:current
options: --sysctl net.ipv6.conf.lo.disable_ipv6=0 --privileged
+ outputs:
+ exit_code: ${{ steps.test.outputs.exit_code }}
steps:
# We need the test script from vyos-build repo
- name: Clone vyos-build source code
@@ -99,27 +107,29 @@ jobs:
repository: vyos/vyos-build
- uses: actions/download-artifact@v4
with:
- name: vyos-${{ needs.build.outputs.build_version }}
+ name: vyos-${{ needs.build_iso.outputs.build_version }}
path: build
- - name: VyOS config tests
- run: sudo make testc
- - name: Add PR comment
- if: always()
- uses: mshick/add-pr-comment@v2
- with:
- message-success: '👍 VyOS config tests finished successfully!'
- message-failure: '❌ VyOS config tests failed!'
- message-cancelled: '❌ VyOS config tests cancelled!'
- allow-repeats: false
- refresh-message-position: true
+ - name: VyOS config load tests
+ id: test
+ shell: bash
+ run: |
+ set -e
+ sudo make testc
+ if [[ $? == 0 ]]; then
+ echo "exit_code=success" >> $GITHUB_OUTPUT
+ else
+ echo "exit_code=fail" >> $GITHUB_OUTPUT
+ fi
- raid1-install-test:
- needs: build
+ test_raid1_install:
+ needs: build_iso
runs-on: ubuntu-24.04
timeout-minutes: 20
container:
image: vyos/vyos-build:current
options: --sysctl net.ipv6.conf.lo.disable_ipv6=0 --privileged
+ outputs:
+ exit_code: ${{ steps.test.outputs.exit_code }}
steps:
# We need the test script from vyos-build repo
- name: Clone vyos-build source code
@@ -128,16 +138,44 @@ jobs:
repository: vyos/vyos-build
- uses: actions/download-artifact@v4
with:
- name: vyos-${{ needs.build.outputs.build_version }}
+ name: vyos-${{ needs.build_iso.outputs.build_version }}
path: build
- - name: VyOS RAID1 install test
- run: sudo make testraid
+ - name: VyOS RAID1 installation tests
+ id: test
+ shell: bash
+ run: |
+ set -e
+ sudo make testraid
+ if [[ $? == 0 ]]; then
+ echo "exit_code=success" >> $GITHUB_OUTPUT
+ else
+ echo "exit_code=fail" >> $GITHUB_OUTPUT
+ fi
+
+ result:
+ needs:
+ - test_smoketest_cli
+ - test_config_load
+ - test_raid1_install
+ runs-on: ubuntu-24.04
+ timeout-minutes: 5
+ if: always()
+ steps:
- name: Add PR comment
if: always()
uses: mshick/add-pr-comment@v2
with:
- message-success: '👍 RAID1 Smoketests finished successfully!'
- message-failure: '❌ RAID1 Smoketests failed!'
- message-cancelled: '❌ RAID1 action cancelled!'
+ message: |
+ CI integration ${{ needs.test_smoketest_cli.outputs.exit_code == 'success' && needs.test_config_load.outputs.exit_code == 'success' && needs.test_raid1_install.outputs.exit_code == 'success' && '👍 passed!' || '❌ failed!' }}
+
+ ### Details
+
+ [CI logs](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }})
+
+ * CLI Smoketests ${{ needs.test_smoketest_cli.outputs.exit_code == 'success' && '👍 passed' || '❌ failed' }}
+ * Config tests ${{ needs.test_config_load.outputs.exit_code == 'success' && '👍 passed' || '❌ failed' }}
+ * RAID1 tests ${{ needs.test_raid1_install.outputs.exit_code == 'success' && '👍 passed' || '❌ failed' }}
+
+ message-id: "SMOKETEST_RESULTS"
allow-repeats: false
refresh-message-position: true
diff --git a/.github/workflows/sonarcloud.yml b/.github/workflows/sonarcloud.yml
index 5fa005631..a8eaca777 100644
--- a/.github/workflows/sonarcloud.yml
+++ b/.github/workflows/sonarcloud.yml
@@ -3,8 +3,16 @@ on:
push:
branches:
- current
+ paths:
+ - '**'
+ - '!.github/**'
+ - '!**/*.md'
pull_request_target:
types: [opened, synchronize, reopened]
+ paths:
+ - '**'
+ - '!.github/**'
+ - '!**/*.md'
jobs:
sonar-cloud:
name: SonarCloud
diff --git a/.github/workflows/trigger-pr.yml b/.github/workflows/trigger-pr.yml
new file mode 100644
index 000000000..f88458a81
--- /dev/null
+++ b/.github/workflows/trigger-pr.yml
@@ -0,0 +1,19 @@
+name: Trigger PR
+
+on:
+ pull_request_target:
+ types:
+ - closed
+ branches:
+ - circinus
+
+jobs:
+ trigger-PR:
+ uses: vyos/.github/.github/workflows/trigger-pr.yml@current
+ with:
+ source_branch: 'circinus'
+ target_branch: 'circinus'
+ secrets:
+ REMOTE_REPO: ${{ secrets.REMOTE_REPO }}
+ REMOTE_OWNER: ${{ secrets.REMOTE_OWNER }}
+ PAT: ${{ secrets.PAT }}
diff --git a/Makefile b/Makefile
index 06b4aaa6f..685c8f150 100644
--- a/Makefile
+++ b/Makefile
@@ -55,12 +55,6 @@ op_mode_definitions: $(op_xml_obj)
find $(BUILD_DIR)/op-mode-definitions/ -type f -name "*.xml" | xargs -I {} $(CURDIR)/scripts/build-command-op-templates {} $(CURDIR)/schema/op-mode-definition.rng $(OP_TMPL_DIR) || exit 1
- # XXX: delete top level op mode node.def's that now live in other packages
- rm -f $(OP_TMPL_DIR)/add/node.def
- rm -f $(OP_TMPL_DIR)/clear/interfaces/node.def
- rm -f $(OP_TMPL_DIR)/clear/node.def
- rm -f $(OP_TMPL_DIR)/delete/node.def
-
# XXX: tcpdump, ping, traceroute and mtr must be able to recursivly call themselves as the
# options are provided from the scripts themselves
ln -s ../node.tag $(OP_TMPL_DIR)/ping/node.tag/node.tag/
diff --git a/data/config-mode-dependencies/vyos-1x.json b/data/config-mode-dependencies/vyos-1x.json
index 9361f4e7c..239842550 100644
--- a/data/config-mode-dependencies/vyos-1x.json
+++ b/data/config-mode-dependencies/vyos-1x.json
@@ -63,5 +63,9 @@
},
"system_wireless": {
"wireless": ["interfaces_wireless"]
+ },
+ "system_option": {
+ "ip": ["system_ip"],
+ "ipv6": ["system_ipv6"]
}
}
diff --git a/data/templates/accel-ppp/ipoe.config.j2 b/data/templates/accel-ppp/ipoe.config.j2
index d87b90473..9729b295e 100644
--- a/data/templates/accel-ppp/ipoe.config.j2
+++ b/data/templates/accel-ppp/ipoe.config.j2
@@ -16,6 +16,9 @@ net-snmp
{% if limits is vyos_defined %}
connlimit
{% endif %}
+{% if extended_scripts is vyos_defined %}
+pppd_compat
+{% endif %}
[core]
thread-count={{ thread_count }}
diff --git a/data/templates/accel-ppp/l2tp.config.j2 b/data/templates/accel-ppp/l2tp.config.j2
index db4db66a7..099bc59da 100644
--- a/data/templates/accel-ppp/l2tp.config.j2
+++ b/data/templates/accel-ppp/l2tp.config.j2
@@ -16,6 +16,9 @@ net-snmp
{% if limits is vyos_defined %}
connlimit
{% endif %}
+{% if extended_scripts is vyos_defined %}
+pppd_compat
+{% endif %}
[core]
thread-count={{ thread_count }}
diff --git a/data/templates/accel-ppp/pptp.config.j2 b/data/templates/accel-ppp/pptp.config.j2
index 44f35998b..52ef3cb0e 100644
--- a/data/templates/accel-ppp/pptp.config.j2
+++ b/data/templates/accel-ppp/pptp.config.j2
@@ -16,6 +16,9 @@ net-snmp
{% if limits is vyos_defined %}
connlimit
{% endif %}
+{% if extended_scripts is vyos_defined %}
+pppd_compat
+{% endif %}
[core]
thread-count={{ thread_count }}
diff --git a/data/templates/accel-ppp/sstp.config.j2 b/data/templates/accel-ppp/sstp.config.j2
index 38da829f3..45d0658af 100644
--- a/data/templates/accel-ppp/sstp.config.j2
+++ b/data/templates/accel-ppp/sstp.config.j2
@@ -16,6 +16,9 @@ net-snmp
{% if limits is vyos_defined %}
connlimit
{% endif %}
+{% if extended_scripts is vyos_defined %}
+pppd_compat
+{% endif %}
[core]
thread-count={{ thread_count }}
diff --git a/data/templates/conntrack/sysctl.conf.j2 b/data/templates/conntrack/sysctl.conf.j2
index 554512f4d..cd6c34ede 100644
--- a/data/templates/conntrack/sysctl.conf.j2
+++ b/data/templates/conntrack/sysctl.conf.j2
@@ -6,4 +6,5 @@ net.netfilter.nf_conntrack_max = {{ table_size }}
net.ipv4.tcp_max_syn_backlog = {{ tcp.half_open_connections }}
net.netfilter.nf_conntrack_tcp_loose = {{ '1' if tcp.loose is vyos_defined('enable') else '0' }}
net.netfilter.nf_conntrack_tcp_max_retrans = {{ tcp.max_retrans }}
-net.netfilter.nf_conntrack_acct = {{ '1' if flow_accounting is vyos_defined else '0' }} \ No newline at end of file
+net.netfilter.nf_conntrack_acct = {{ '1' if flow_accounting is vyos_defined else '0' }}
+net.netfilter.nf_conntrack_timestamp = {{ '1' if log.timestamp is vyos_defined else '0' }} \ No newline at end of file
diff --git a/data/templates/ipsec/swanctl/peer.j2 b/data/templates/ipsec/swanctl/peer.j2
index 58f0199fa..3a9af2c94 100644
--- a/data/templates/ipsec/swanctl/peer.j2
+++ b/data/templates/ipsec/swanctl/peer.j2
@@ -63,6 +63,11 @@
life_packets = {{ vti_esp.life_packets }}
{% endif %}
life_time = {{ vti_esp.lifetime }}s
+{% if vti_esp.disable_rekey is vyos_defined %}
+ rekey_bytes = 0
+ rekey_packets = 0
+ rekey_time = 0s
+{% endif %}
local_ts = 0.0.0.0/0,::/0
remote_ts = 0.0.0.0/0,::/0
updown = "/etc/ipsec.d/vti-up-down {{ peer_conf.vti.bind }}"
@@ -108,6 +113,11 @@
life_packets = {{ tunnel_esp.life_packets }}
{% endif %}
life_time = {{ tunnel_esp.lifetime }}s
+{% if tunnel_esp.disable_rekey is vyos_defined %}
+ rekey_bytes = 0
+ rekey_packets = 0
+ rekey_time = 0s
+{% endif %}
{% if tunnel_esp.mode is not defined or tunnel_esp.mode == 'tunnel' %}
{% if tunnel_conf.local.prefix is vyos_defined %}
{% set local_prefix = tunnel_conf.local.prefix if 'any' not in tunnel_conf.local.prefix else ['0.0.0.0/0', '::/0'] %}
diff --git a/data/templates/ipsec/swanctl/remote_access.j2 b/data/templates/ipsec/swanctl/remote_access.j2
index 6bced88c7..e384ae972 100644
--- a/data/templates/ipsec/swanctl/remote_access.j2
+++ b/data/templates/ipsec/swanctl/remote_access.j2
@@ -8,6 +8,10 @@
proposals = {{ ike_group[rw_conf.ike_group] | get_esp_ike_cipher | join(',') }}
version = {{ ike.key_exchange[4:] if ike.key_exchange is vyos_defined else "0" }}
send_certreq = no
+{% if ike.dead_peer_detection is vyos_defined %}
+ dpd_timeout = {{ ike.dead_peer_detection.timeout }}
+ dpd_delay = {{ ike.dead_peer_detection.interval }}
+{% endif %}
rekey_time = {{ ike.lifetime }}s
keyingtries = 0
{% if rw_conf.unique is vyos_defined %}
@@ -44,8 +48,18 @@
children {
ikev2-vpn {
esp_proposals = {{ esp | get_esp_ike_cipher(ike) | join(',') }}
- rekey_time = {{ esp.lifetime }}s
- rand_time = 540s
+{% if esp.life_bytes is vyos_defined %}
+ life_bytes = {{ esp.life_bytes }}
+{% endif %}
+{% if esp.life_packets is vyos_defined %}
+ life_packets = {{ esp.life_packets }}
+{% endif %}
+ life_time = {{ esp.lifetime }}s
+{% if esp.disable_rekey is vyos_defined %}
+ rekey_bytes = 0
+ rekey_packets = 0
+ rekey_time = 0s
+{% endif %}
dpd_action = clear
inactivity = {{ rw_conf.timeout }}
{% if rw_conf.replay_window is vyos_defined %}
diff --git a/data/templates/load-balancing/haproxy.cfg.j2 b/data/templates/load-balancing/haproxy.cfg.j2
index c18a998b8..5137966c1 100644
--- a/data/templates/load-balancing/haproxy.cfg.j2
+++ b/data/templates/load-balancing/haproxy.cfg.j2
@@ -1,8 +1,6 @@
### Autogenerated by load-balancing_reverse-proxy.py ###
global
- log /dev/log local0
- log /dev/log local1 notice
chroot /var/lib/haproxy
stats socket /run/haproxy/admin.sock mode 660 level admin
stats timeout 30s
@@ -11,6 +9,11 @@ global
daemon
{% if global_parameters is vyos_defined %}
+{% if global_parameters.logging is vyos_defined %}
+{% for facility, facility_config in global_parameters.logging.facility.items() %}
+ log /dev/log {{ facility }} {{ facility_config.level }}
+{% endfor %}
+{% endif %}
{% if global_parameters.max_connections is vyos_defined %}
maxconn {{ global_parameters.max_connections }}
{% endif %}
@@ -67,6 +70,11 @@ frontend {{ front }}
{% if front_config.redirect_http_to_https is vyos_defined %}
http-request redirect scheme https unless { ssl_fc }
{% endif %}
+{% if front_config.logging is vyos_defined %}
+{% for facility, facility_config in front_config.logging.facility.items() %}
+ log /dev/log {{ facility }} {{ facility_config.level }}
+{% endfor %}
+{% endif %}
mode {{ front_config.mode }}
{% if front_config.tcp_request.inspect_delay is vyos_defined %}
tcp-request inspect-delay {{ front_config.tcp_request.inspect_delay }}
@@ -166,6 +174,11 @@ backend {{ back }}
http-request set-header X-Forwarded-Port %[dst_port]
http-request add-header X-Forwarded-Proto https if { ssl_fc }
{% endif %}
+{% if back_config.logging is vyos_defined %}
+{% for facility, facility_config in back_config.logging.facility.items() %}
+ log /dev/log {{ facility }} {{ facility_config.level }}
+{% endfor %}
+{% endif %}
mode {{ back_config.mode }}
{% if back_config.http_response_headers is vyos_defined %}
{% for header, header_config in back_config.http_response_headers.items() %}
diff --git a/data/templates/openvpn/server.conf.j2 b/data/templates/openvpn/server.conf.j2
index 6ac525443..f69519697 100644
--- a/data/templates/openvpn/server.conf.j2
+++ b/data/templates/openvpn/server.conf.j2
@@ -206,8 +206,8 @@ tls-server
{% if encryption.cipher is vyos_defined %}
cipher {{ encryption.cipher | openvpn_cipher }}
{% endif %}
-{% if encryption.ncp_ciphers is vyos_defined %}
-data-ciphers {{ encryption.ncp_ciphers | openvpn_ncp_ciphers }}
+{% if encryption.data_ciphers is vyos_defined %}
+data-ciphers {{ encryption.data_ciphers | openvpn_data_ciphers }}
{% endif %}
{% endif %}
providers default
diff --git a/data/templates/rsyslog/logrotate.j2 b/data/templates/rsyslog/logrotate.j2
index ea33fea4f..b9689a1cf 100644
--- a/data/templates/rsyslog/logrotate.j2
+++ b/data/templates/rsyslog/logrotate.j2
@@ -5,9 +5,6 @@
create
rotate 5
size=256k
- postrotate
- invoke-rc.d rsyslog rotate > /dev/null
- endscript
}
{% if file is vyos_defined %}
@@ -18,9 +15,6 @@
create
rotate {{ file_options.archive.file }}
size={{ file_options.archive.size | int // 1024 }}k
- postrotate
- invoke-rc.d rsyslog rotate > /dev/null
- endscript
}
{% endfor %}
diff --git a/data/templates/ssh/sshd_config.j2 b/data/templates/ssh/sshd_config.j2
index 650fd25e6..2cf0494c4 100644
--- a/data/templates/ssh/sshd_config.j2
+++ b/data/templates/ssh/sshd_config.j2
@@ -67,6 +67,11 @@ Ciphers {{ ciphers | join(',') }}
HostKeyAlgorithms {{ hostkey_algorithm | join(',') }}
{% endif %}
+{% if pubkey_accepted_algorithm is vyos_defined %}
+# Specifies the available PubKey signature algorithms
+PubkeyAcceptedAlgorithms {{ pubkey_accepted_algorithm | join(',') }}
+{% endif %}
+
{% if mac is vyos_defined %}
# Specifies the available MAC (message authentication code) algorithms
MACs {{ mac | join(',') }}
diff --git a/data/templates/telegraf/telegraf.j2 b/data/templates/telegraf/telegraf.j2
index f382dbf2e..535e3a347 100644
--- a/data/templates/telegraf/telegraf.j2
+++ b/data/templates/telegraf/telegraf.j2
@@ -130,7 +130,9 @@ metric_name_label = "{{ loki.metric_name_label }}"
{% if influxdb is vyos_defined %}
[[inputs.exec]]
commands = [
+{% if nft_chains is vyos_defined %}
"{{ custom_scripts_dir }}/show_firewall_input_filter.py",
+{% endif %}
"{{ custom_scripts_dir }}/show_interfaces_input_filter.py",
"{{ custom_scripts_dir }}/vyos_services_input_filter.py"
]
diff --git a/data/templates/wifi/wpa_supplicant.conf.j2 b/data/templates/wifi/wpa_supplicant.conf.j2
index ac857a04a..04088e1ad 100644
--- a/data/templates/wifi/wpa_supplicant.conf.j2
+++ b/data/templates/wifi/wpa_supplicant.conf.j2
@@ -61,6 +61,8 @@ network={
# If not set, this defaults to: WPA-PSK WPA-EAP
{% if security.wpa.mode is vyos_defined('wpa3') %}
key_mgmt=SAE
+{% elif security.wpa.username is vyos_defined %}
+ key_mgmt=WPA-EAP WPA-EAP-SHA256
{% else %}
key_mgmt=WPA-PSK WPA-PSK-SHA256
{% endif %}
@@ -76,8 +78,18 @@ network={
# from ASCII passphrase. This process uses lot of CPU and wpa_supplicant
# startup and reconfiguration time can be optimized by generating the PSK only
# only when the passphrase or SSID has actually changed.
+{% if security.wpa.username is vyos_defined %}
+ identity="{{ security.wpa.username }}"
+ password="{{ security.wpa.passphrase }}"
+ phase2="auth=MSCHAPV2"
+ eap=PEAP
+{% elif security.wpa.username is not vyos_defined %}
psk="{{ security.wpa.passphrase }}"
-{% else %}
+{% else %}
key_mgmt=NONE
+{% endif %}
+{% endif %}
+{% if bssid is vyos_defined %}
+ bssid={{ bssid }}
{% endif %}
}
diff --git a/debian/control b/debian/control
index 883e08649..d3f5fb464 100644
--- a/debian/control
+++ b/debian/control
@@ -10,7 +10,6 @@ Build-Depends:
iproute2,
libvyosconfig0 (>= 0.0.7),
libzmq3-dev,
- procps,
python3 (>= 3.10),
# For QA
pylint,
@@ -38,14 +37,24 @@ Standards-Version: 3.9.6
Package: vyos-1x
Architecture: amd64 arm64
Pre-Depends:
+ libpam-runtime [amd64],
libnss-tacplus [amd64],
libpam-tacplus [amd64],
libpam-radius-auth [amd64]
Depends:
## Fundamentals
${python3:Depends} (>= 3.10),
+ dialog,
libvyosconfig0,
+ libpam-cap,
+ bash-completion,
+ ipvsadm,
+ udev,
+ less,
+ at,
+ rsync,
vyatta-bash,
+ vyatta-biosdevname,
vyatta-cfg,
vyos-http-api-tools,
vyos-utils,
@@ -61,6 +70,7 @@ Depends:
python3-netifaces,
python3-paramiko,
python3-passlib,
+ python3-pyroute2,
python3-psutil,
python3-pyhumps,
python3-pystache,
@@ -72,6 +82,7 @@ Depends:
python3-zmq,
## End of Python libraries
## Basic System services and utilities
+ coreutils,
sudo,
systemd,
bsdmainutils,
@@ -84,7 +95,6 @@ Depends:
# ipaddrcheck is widely used in IP value validators
ipaddrcheck,
ethtool,
- fdisk,
lm-sensors,
procps,
netplug,
@@ -97,6 +107,14 @@ Depends:
grc,
## End of System services and utilities
## For the installer
+ fdisk,
+ gdisk,
+ mdadm,
+ efibootmgr,
+ libefivar1,
+ dosfstools,
+ grub-efi-amd64-bin [amd64],
+ grub-efi-arm64-bin [arm64],
# Image signature verification tool
minisign,
# Live filesystem tools
@@ -105,6 +123,7 @@ Depends:
## End installer
auditd,
iputils-arping,
+ iputils-ping,
isc-dhcp-client,
# For "vpn pptp", "vpn l2tp", "vpn sstp", "service ipoe-server"
accel-ppp,
@@ -143,7 +162,7 @@ Depends:
sstp-client,
# End "interfaces sstpc"
# For "protocols *"
- frr (>= 7.5),
+ frr (>= 9.1),
frr-pythontools,
frr-rpki-rtrlib,
frr-snmp,
@@ -179,9 +198,12 @@ Depends:
# For "service router-advert"
radvd,
# End "service route-advert"
-# For "high-availability reverse-proxy"
+# For "load-balancing reverse-proxy"
haproxy,
-# End "high-availability reverse-proxy"
+# End "load-balancing reverse-proxy"
+# For "load-balancing wan"
+ vyatta-wanloadbalance,
+# End "load-balancing wan"
# For "service dhcp-relay"
isc-dhcp-relay,
# For "service dhcp-server"
@@ -235,6 +257,9 @@ Depends:
# For "high-availability vrrp"
keepalived (>=2.0.5),
# End "high-availability-vrrp"
+# For "system console"
+ util-linux,
+# End "system console"
# For "system task-scheduler"
cron,
# End "system task-scheduler"
@@ -267,7 +292,7 @@ Depends:
# For "system conntrack modules rtsp"
nat-rtsp,
# End "system conntrack modules rtsp"
-# For "system ntp"
+# For "service ntp"
chrony,
# End "system ntp"
# For "vpn openconnect"
@@ -276,8 +301,14 @@ Depends:
# For "system flow-accounting"
pmacct (>= 1.6.0),
# End "system flow-accounting"
-# For container
- podman,
+# For "system syslog"
+ rsyslog,
+# End "system syslog"
+# For "system option keyboard-layout"
+ kbd,
+# End "system option keyboard-layout"
+# For "container"
+ podman (>=4.9.5),
netavark,
aardvark-dns,
# iptables is only used for containers now, not the the firewall CLI
@@ -314,6 +345,8 @@ Depends:
ndisc6,
# For "run monitor bandwidth"
bmon,
+# For "run format disk"
+ parted,
# End Operational mode
## TPM tools
cryptsetup,
diff --git a/debian/rules b/debian/rules
index 9da40465f..df1d9e7f3 100755
--- a/debian/rules
+++ b/debian/rules
@@ -103,6 +103,10 @@ override_dh_auto_install:
mkdir -p $(DIR)/etc
cp -r src/etc/* $(DIR)/etc
+ # Install legacy Vyatta files
+ mkdir -p $(DIR)/opt
+ cp -r src/opt/* $(DIR)/opt
+
# Install PAM configuration snippets
mkdir -p $(DIR)/usr/share/pam-configs
cp -r src/pam-configs/* $(DIR)/usr/share/pam-configs
diff --git a/debian/vyos-1x.install b/debian/vyos-1x.install
index b3978d38a..7171911dc 100644
--- a/debian/vyos-1x.install
+++ b/debian/vyos-1x.install
@@ -1,4 +1,6 @@
+etc/bash_completion.d
etc/commit
+etc/default
etc/dhcp
etc/ipsec.d
etc/logrotate.d
diff --git a/debian/vyos-1x.postinst b/debian/vyos-1x.postinst
index 78e895d6e..141a9e8f9 100644
--- a/debian/vyos-1x.postinst
+++ b/debian/vyos-1x.postinst
@@ -120,6 +120,61 @@ fi
# ensure the proxy user has a proper shell
chsh -s /bin/sh proxy
+# Set file capabilities
+setcap cap_net_admin=pe /sbin/ethtool
+setcap cap_net_admin=pe /sbin/tc
+setcap cap_net_admin=pe /bin/ip
+setcap cap_net_admin=pe /sbin/xtables-legacy-multi
+setcap cap_net_admin=pe /sbin/xtables-nft-multi
+setcap cap_net_admin=pe /usr/sbin/conntrack
+setcap cap_net_admin=pe /usr/sbin/arp
+setcap cap_net_raw=pe /usr/bin/tcpdump
+setcap cap_net_admin,cap_sys_admin=pe /sbin/sysctl
+setcap cap_sys_module=pe /bin/kmod
+setcap cap_sys_time=pe /bin/date
+
+# create needed directories
+mkdir -p /var/log/user
+mkdir -p /var/core
+mkdir -p /opt/vyatta/etc/config/auth
+mkdir -p /opt/vyatta/etc/config/scripts
+mkdir -p /opt/vyatta/etc/config/user-data
+mkdir -p /opt/vyatta/etc/config/support
+chown -R root:vyattacfg /opt/vyatta/etc/config
+chmod -R 775 /opt/vyatta/etc/config
+mkdir -p /opt/vyatta/etc/logrotate
+mkdir -p /opt/vyatta/etc/netdevice.d
+
+touch /etc/environment
+
+if [ ! -f /etc/bash_completion ]; then
+ echo "source /etc/bash_completion.d/10vyatta-op" > /etc/bash_completion
+ echo "source /etc/bash_completion.d/20vyatta-cfg" >> /etc/bash_completion
+fi
+
+sed -i 's/^set /builtin set /' /etc/bash_completion
+
+# Fix up PAM configuration for login so that invalid users are prompted
+# for password
+sed -i 's/requisite[ \t][ \t]*pam_securetty.so/required pam_securetty.so/' $rootfsdir/etc/pam.d/login
+
+# Change default shell for new accounts
+sed -i -e ':^DSHELL:s:/bin/bash:/bin/vbash:' /etc/adduser.conf
+
+# Do not allow users to change full name field (controlled by vyos-1x)
+sed -i -e 's/^CHFN_RESTRICT/#&/' /etc/login.defs
+
+# Only allow root to use passwd command
+if ! grep -q 'pam_succeed_if.so' /etc/pam.d/passwd ; then
+ sed -i -e '/^@include/i \
+password requisite pam_succeed_if.so user = root
+' /etc/pam.d/passwd
+fi
+
+# remove unnecessary ddclient script in /etc/ppp/ip-up.d/
+# this logs unnecessary messages trying to start ddclient
+rm -f /etc/ppp/ip-up.d/ddclient
+
# create /opt/vyatta/etc/config/scripts/vyos-preconfig-bootup.script
PRECONFIG_SCRIPT=/opt/vyatta/etc/config/scripts/vyos-preconfig-bootup.script
if [ ! -x $PRECONFIG_SCRIPT ]; then
@@ -201,3 +256,6 @@ python3 /usr/lib/python3/dist-packages/vyos/xml_ref/update_cache.py
if [ ! -f /lib/systemd/system/ssh@.service ]; then
ln /lib/systemd/system/ssh.service /lib/systemd/system/ssh@.service
fi
+
+# T4287 - as we have a non-signed kernel use the upstream wireless reulatory database
+update-alternatives --set regulatory.db /lib/firmware/regulatory.db-upstream
diff --git a/interface-definitions/include/conntrack/log-common.xml.i b/interface-definitions/include/conntrack/log-common.xml.i
deleted file mode 100644
index 38799f8f4..000000000
--- a/interface-definitions/include/conntrack/log-common.xml.i
+++ /dev/null
@@ -1,20 +0,0 @@
-<!-- include start from conntrack/log-common.xml.i -->
-<leafNode name="destroy">
- <properties>
- <help>Log connection deletion</help>
- <valueless/>
- </properties>
-</leafNode>
-<leafNode name="new">
- <properties>
- <help>Log connection creation</help>
- <valueless/>
- </properties>
-</leafNode>
-<leafNode name="update">
- <properties>
- <help>Log connection updates</help>
- <valueless/>
- </properties>
-</leafNode>
-<!-- include end -->
diff --git a/interface-definitions/include/conntrack/log-protocols.xml.i b/interface-definitions/include/conntrack/log-protocols.xml.i
new file mode 100644
index 000000000..019250760
--- /dev/null
+++ b/interface-definitions/include/conntrack/log-protocols.xml.i
@@ -0,0 +1,26 @@
+<!-- include start from conntrack/log-protocols.xml.i -->
+<leafNode name="icmp">
+ <properties>
+ <help>Log connection tracking events for ICMP</help>
+ <valueless/>
+ </properties>
+</leafNode>
+<leafNode name="other">
+ <properties>
+ <help>Log connection tracking events for all protocols other than TCP, UDP and ICMP</help>
+ <valueless/>
+ </properties>
+</leafNode>
+<leafNode name="tcp">
+ <properties>
+ <help>Log connection tracking events for TCP</help>
+ <valueless/>
+ </properties>
+</leafNode>
+<leafNode name="udp">
+ <properties>
+ <help>Log connection tracking events for UDP</help>
+ <valueless/>
+ </properties>
+</leafNode>
+<!-- include end -->
diff --git a/interface-definitions/include/firewall/common-rule-inet.xml.i b/interface-definitions/include/firewall/common-rule-inet.xml.i
index 55ffa3a8b..0acb08ec9 100644
--- a/interface-definitions/include/firewall/common-rule-inet.xml.i
+++ b/interface-definitions/include/firewall/common-rule-inet.xml.i
@@ -7,7 +7,6 @@
#include <include/generic-disable-node.xml.i>
#include <include/firewall/dscp.xml.i>
#include <include/firewall/fragment.xml.i>
-#include <include/firewall/match-ipsec.xml.i>
#include <include/firewall/limit.xml.i>
#include <include/firewall/log.xml.i>
#include <include/firewall/log-options.xml.i>
diff --git a/interface-definitions/include/firewall/common-rule-ipv4-raw.xml.i b/interface-definitions/include/firewall/common-rule-ipv4-raw.xml.i
index 960c960db..e8da1a0e1 100644
--- a/interface-definitions/include/firewall/common-rule-ipv4-raw.xml.i
+++ b/interface-definitions/include/firewall/common-rule-ipv4-raw.xml.i
@@ -9,7 +9,6 @@
#include <include/firewall/limit.xml.i>
#include <include/firewall/log.xml.i>
#include <include/firewall/log-options.xml.i>
-#include <include/firewall/match-ipsec.xml.i>
#include <include/firewall/protocol.xml.i>
#include <include/firewall/nft-queue.xml.i>
#include <include/firewall/recent.xml.i>
diff --git a/interface-definitions/include/firewall/common-rule-ipv6-raw.xml.i b/interface-definitions/include/firewall/common-rule-ipv6-raw.xml.i
index 958167b89..3f7c5a0a3 100644
--- a/interface-definitions/include/firewall/common-rule-ipv6-raw.xml.i
+++ b/interface-definitions/include/firewall/common-rule-ipv6-raw.xml.i
@@ -9,7 +9,6 @@
#include <include/firewall/limit.xml.i>
#include <include/firewall/log.xml.i>
#include <include/firewall/log-options.xml.i>
-#include <include/firewall/match-ipsec.xml.i>
#include <include/firewall/protocol.xml.i>
#include <include/firewall/nft-queue.xml.i>
#include <include/firewall/recent.xml.i>
diff --git a/interface-definitions/include/firewall/ipv4-hook-input.xml.i b/interface-definitions/include/firewall/ipv4-hook-input.xml.i
index cefb1ffa7..491d1a9f3 100644
--- a/interface-definitions/include/firewall/ipv4-hook-input.xml.i
+++ b/interface-definitions/include/firewall/ipv4-hook-input.xml.i
@@ -27,7 +27,7 @@
<children>
#include <include/firewall/common-rule-ipv4.xml.i>
#include <include/firewall/inbound-interface.xml.i>
- #include <include/firewall/match-ipsec.xml.i>
+ #include <include/firewall/match-ipsec-in.xml.i>
</children>
</tagNode>
</children>
diff --git a/interface-definitions/include/firewall/ipv4-hook-output.xml.i b/interface-definitions/include/firewall/ipv4-hook-output.xml.i
index ca47ae09b..ee9157592 100644
--- a/interface-definitions/include/firewall/ipv4-hook-output.xml.i
+++ b/interface-definitions/include/firewall/ipv4-hook-output.xml.i
@@ -26,6 +26,7 @@
</properties>
<children>
#include <include/firewall/common-rule-ipv4.xml.i>
+ #include <include/firewall/match-ipsec-out.xml.i>
#include <include/firewall/outbound-interface.xml.i>
</children>
</tagNode>
@@ -53,6 +54,7 @@
</properties>
<children>
#include <include/firewall/common-rule-ipv4-raw.xml.i>
+ #include <include/firewall/match-ipsec-out.xml.i>
#include <include/firewall/outbound-interface.xml.i>
</children>
</tagNode>
diff --git a/interface-definitions/include/firewall/ipv4-hook-prerouting.xml.i b/interface-definitions/include/firewall/ipv4-hook-prerouting.xml.i
index 17ecfe824..b431303ae 100644
--- a/interface-definitions/include/firewall/ipv4-hook-prerouting.xml.i
+++ b/interface-definitions/include/firewall/ipv4-hook-prerouting.xml.i
@@ -33,6 +33,7 @@
</properties>
<children>
#include <include/firewall/common-rule-ipv4-raw.xml.i>
+ #include <include/firewall/match-ipsec-in.xml.i>
#include <include/firewall/inbound-interface.xml.i>
<leafNode name="jump-target">
<properties>
diff --git a/interface-definitions/include/firewall/ipv6-hook-input.xml.i b/interface-definitions/include/firewall/ipv6-hook-input.xml.i
index e1f41e64c..154b10259 100644
--- a/interface-definitions/include/firewall/ipv6-hook-input.xml.i
+++ b/interface-definitions/include/firewall/ipv6-hook-input.xml.i
@@ -27,7 +27,7 @@
<children>
#include <include/firewall/common-rule-ipv6.xml.i>
#include <include/firewall/inbound-interface.xml.i>
- #include <include/firewall/match-ipsec.xml.i>
+ #include <include/firewall/match-ipsec-in.xml.i>
</children>
</tagNode>
</children>
diff --git a/interface-definitions/include/firewall/ipv6-hook-output.xml.i b/interface-definitions/include/firewall/ipv6-hook-output.xml.i
index f877cfaaf..d3c4c1ead 100644
--- a/interface-definitions/include/firewall/ipv6-hook-output.xml.i
+++ b/interface-definitions/include/firewall/ipv6-hook-output.xml.i
@@ -26,6 +26,7 @@
</properties>
<children>
#include <include/firewall/common-rule-ipv6.xml.i>
+ #include <include/firewall/match-ipsec-out.xml.i>
#include <include/firewall/outbound-interface.xml.i>
</children>
</tagNode>
@@ -53,6 +54,7 @@
</properties>
<children>
#include <include/firewall/common-rule-ipv6-raw.xml.i>
+ #include <include/firewall/match-ipsec-out.xml.i>
#include <include/firewall/outbound-interface.xml.i>
</children>
</tagNode>
diff --git a/interface-definitions/include/firewall/ipv6-hook-prerouting.xml.i b/interface-definitions/include/firewall/ipv6-hook-prerouting.xml.i
index 3f384828d..21f8de6f9 100644
--- a/interface-definitions/include/firewall/ipv6-hook-prerouting.xml.i
+++ b/interface-definitions/include/firewall/ipv6-hook-prerouting.xml.i
@@ -33,6 +33,7 @@
</properties>
<children>
#include <include/firewall/common-rule-ipv6-raw.xml.i>
+ #include <include/firewall/match-ipsec-in.xml.i>
#include <include/firewall/inbound-interface.xml.i>
<leafNode name="jump-target">
<properties>
diff --git a/interface-definitions/include/firewall/match-ipsec-in.xml.i b/interface-definitions/include/firewall/match-ipsec-in.xml.i
new file mode 100644
index 000000000..62ed6466b
--- /dev/null
+++ b/interface-definitions/include/firewall/match-ipsec-in.xml.i
@@ -0,0 +1,21 @@
+<!-- include start from firewall/match-ipsec-in.xml.i -->
+<node name="ipsec">
+ <properties>
+ <help>Inbound IPsec packets</help>
+ </properties>
+ <children>
+ <leafNode name="match-ipsec-in">
+ <properties>
+ <help>Inbound traffic that was IPsec encapsulated</help>
+ <valueless/>
+ </properties>
+ </leafNode>
+ <leafNode name="match-none-in">
+ <properties>
+ <help>Inbound traffic that was not IPsec encapsulated</help>
+ <valueless/>
+ </properties>
+ </leafNode>
+ </children>
+</node>
+<!-- include end --> \ No newline at end of file
diff --git a/interface-definitions/include/firewall/match-ipsec-out.xml.i b/interface-definitions/include/firewall/match-ipsec-out.xml.i
new file mode 100644
index 000000000..880fdd4d8
--- /dev/null
+++ b/interface-definitions/include/firewall/match-ipsec-out.xml.i
@@ -0,0 +1,21 @@
+<!-- include start from firewall/match-ipsec-out.xml.i -->
+<node name="ipsec">
+ <properties>
+ <help>Outbound IPsec packets</help>
+ </properties>
+ <children>
+ <leafNode name="match-ipsec-out">
+ <properties>
+ <help>Outbound traffic to be IPsec encapsulated</help>
+ <valueless/>
+ </properties>
+ </leafNode>
+ <leafNode name="match-none-out">
+ <properties>
+ <help>Outbound traffic that will not be IPsec encapsulated</help>
+ <valueless/>
+ </properties>
+ </leafNode>
+ </children>
+</node>
+<!-- include end --> \ No newline at end of file
diff --git a/interface-definitions/include/firewall/match-ipsec.xml.i b/interface-definitions/include/firewall/match-ipsec.xml.i
index 82c2b324d..d8d31ef1a 100644
--- a/interface-definitions/include/firewall/match-ipsec.xml.i
+++ b/interface-definitions/include/firewall/match-ipsec.xml.i
@@ -1,21 +1,33 @@
<!-- include start from firewall/match-ipsec.xml.i -->
<node name="ipsec">
<properties>
- <help>Inbound IPsec packets</help>
+ <help>IPsec encapsulated packets</help>
</properties>
<children>
- <leafNode name="match-ipsec">
+ <leafNode name="match-ipsec-in">
<properties>
- <help>Inbound IPsec packets</help>
+ <help>Inbound traffic that was IPsec encapsulated</help>
<valueless/>
</properties>
</leafNode>
- <leafNode name="match-none">
+ <leafNode name="match-none-in">
<properties>
- <help>Inbound non-IPsec packets</help>
+ <help>Inbound traffic that was not IPsec encapsulated</help>
<valueless/>
</properties>
</leafNode>
+ <leafNode name="match-ipsec-out">
+ <properties>
+ <help>Outbound traffic to be IPsec encapsulated</help>
+ <valueless/>
+ </properties>
+ </leafNode>
+ <leafNode name="match-none-out">
+ <properties>
+ <help>Outbound traffic that will not be IPsec encapsulated</help>
+ <valueless/>
+ </properties>
+ </leafNode>
</children>
</node>
<!-- include end --> \ No newline at end of file
diff --git a/interface-definitions/include/haproxy/logging.xml.i b/interface-definitions/include/haproxy/logging.xml.i
new file mode 100644
index 000000000..e0af54fa4
--- /dev/null
+++ b/interface-definitions/include/haproxy/logging.xml.i
@@ -0,0 +1,10 @@
+<!-- include start from haproxy/logging.xml.i -->
+<node name="logging">
+ <properties>
+ <help>Logging parameters</help>
+ </properties>
+ <children>
+ #include <include/syslog-facility.xml.i>
+ </children>
+</node>
+<!-- include end -->
diff --git a/interface-definitions/include/policy/route-common.xml.i b/interface-definitions/include/policy/route-common.xml.i
index 97795601e..203be73e7 100644
--- a/interface-definitions/include/policy/route-common.xml.i
+++ b/interface-definitions/include/policy/route-common.xml.i
@@ -128,6 +128,24 @@
</completionHelp>
</properties>
</leafNode>
+ <leafNode name="vrf">
+ <properties>
+ <help>VRF to forward packet with</help>
+ <valueHelp>
+ <format>txt</format>
+ <description>VRF instance name</description>
+ </valueHelp>
+ <valueHelp>
+ <format>default</format>
+ <description>Forward into default global VRF</description>
+ </valueHelp>
+ <completionHelp>
+ <list>default</list>
+ <path>vrf name</path>
+ </completionHelp>
+ #include <include/constraint/vrf.xml.i>
+ </properties>
+ </leafNode>
<leafNode name="tcp-mss">
<properties>
<help>TCP Maximum Segment Size</help>
diff --git a/interface-definitions/include/version/firewall-version.xml.i b/interface-definitions/include/version/firewall-version.xml.i
index 560ed9e5f..a15cf0eec 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='16'></syntaxVersion>
+<syntaxVersion component='firewall' version='17'></syntaxVersion>
<!-- include end -->
diff --git a/interface-definitions/include/version/openvpn-version.xml.i b/interface-definitions/include/version/openvpn-version.xml.i
index e4eb13b7c..67ef21983 100644
--- a/interface-definitions/include/version/openvpn-version.xml.i
+++ b/interface-definitions/include/version/openvpn-version.xml.i
@@ -1,3 +1,3 @@
<!-- include start from include/version/openvpn-version.xml.i -->
-<syntaxVersion component='openvpn' version='2'></syntaxVersion>
+<syntaxVersion component='openvpn' version='4'></syntaxVersion>
<!-- include end -->
diff --git a/interface-definitions/interfaces_geneve.xml.in b/interface-definitions/interfaces_geneve.xml.in
index c94113271..990c5bd91 100644
--- a/interface-definitions/interfaces_geneve.xml.in
+++ b/interface-definitions/interfaces_geneve.xml.in
@@ -52,6 +52,7 @@
#include <include/interface/mirror.xml.i>
#include <include/interface/redirect.xml.i>
#include <include/interface/tunnel-remote.xml.i>
+ #include <include/interface/vrf.xml.i>
#include <include/vni.xml.i>
</children>
</tagNode>
diff --git a/interface-definitions/interfaces_openvpn.xml.in b/interface-definitions/interfaces_openvpn.xml.in
index 23cc83e9a..13ef3ae5b 100644
--- a/interface-definitions/interfaces_openvpn.xml.in
+++ b/interface-definitions/interfaces_openvpn.xml.in
@@ -87,7 +87,7 @@
</constraint>
</properties>
</leafNode>
- <leafNode name="ncp-ciphers">
+ <leafNode name="data-ciphers">
<properties>
<help>Cipher negotiation list for use in server or client mode</help>
<completionHelp>
@@ -589,25 +589,25 @@
<properties>
<help>Topology for clients</help>
<completionHelp>
- <list>net30 point-to-point subnet</list>
+ <list>subnet point-to-point net30</list>
</completionHelp>
<valueHelp>
- <format>net30</format>
- <description>net30 topology</description>
+ <format>subnet</format>
+ <description>Subnet topology (recommended)</description>
</valueHelp>
<valueHelp>
<format>point-to-point</format>
<description>Point-to-point topology</description>
</valueHelp>
<valueHelp>
- <format>subnet</format>
- <description>Subnet topology</description>
+ <format>net30</format>
+ <description>net30 topology (deprecated)</description>
</valueHelp>
<constraint>
<regex>(subnet|point-to-point|net30)</regex>
</constraint>
</properties>
- <defaultValue>net30</defaultValue>
+ <defaultValue>subnet</defaultValue>
</leafNode>
<node name="mfa">
<properties>
diff --git a/interface-definitions/interfaces_vxlan.xml.in b/interface-definitions/interfaces_vxlan.xml.in
index 504c08e7e..937acb123 100644
--- a/interface-definitions/interfaces_vxlan.xml.in
+++ b/interface-definitions/interfaces_vxlan.xml.in
@@ -117,15 +117,35 @@
<format>u32:0-4094</format>
<description>Virtual Local Area Network (VLAN) ID</description>
</valueHelp>
+ <valueHelp>
+ <format>&lt;start-end&gt;</format>
+ <description>VLAN IDs range (use '-' as delimiter)</description>
+ </valueHelp>
<constraint>
- <validator name="numeric" argument="--range 0-4094"/>
+ <validator name="numeric" argument="--allow-range --range 0-4094"/>
</constraint>
- <constraintErrorMessage>VLAN ID must be between 0 and 4094</constraintErrorMessage>
+ <constraintErrorMessage>Not a valid VLAN ID or range, VLAN ID must be between 0 and 4094</constraintErrorMessage>
</properties>
<children>
- #include <include/vni.xml.i>
+ <leafNode name="vni">
+ <properties>
+ <help>Virtual Network Identifier</help>
+ <valueHelp>
+ <format>u32:0-16777214</format>
+ <description>VXLAN virtual network identifier</description>
+ </valueHelp>
+ <valueHelp>
+ <format>&lt;start-end&gt;</format>
+ <description>VXLAN virtual network IDs range (use '-' as delimiter)</description>
+ </valueHelp>
+ <constraint>
+ <validator name="numeric" argument="--allow-range --range 0-16777214"/>
+ </constraint>
+ <constraintErrorMessage>Not a valid VXLAN virtual network ID or range</constraintErrorMessage>
+ </properties>
+ </leafNode>
</children>
- </tagNode>
+ </tagNode>
</children>
</tagNode>
</children>
diff --git a/interface-definitions/interfaces_wireless.xml.in b/interface-definitions/interfaces_wireless.xml.in
index 0a62b3255..fdcb79b19 100644
--- a/interface-definitions/interfaces_wireless.xml.in
+++ b/interface-definitions/interfaces_wireless.xml.in
@@ -899,15 +899,16 @@
</properties>
<defaultValue>wpa+wpa2</defaultValue>
</leafNode>
+ #include <include/generic-username.xml.i>
<leafNode name="passphrase">
<properties>
- <help>WPA personal shared pass phrase. If you are using special characters in the WPA passphrase then single quotes are required.</help>
+ <help>WPA passphrase. If you are using special characters in the WPA passphrase then single quotes are required.</help>
<valueHelp>
<format>txt</format>
- <description>Passphrase of at least 8 but not more than 63 printable characters</description>
+ <description>Passphrase of at least 8 but not more than 63 printable characters for WPA-Personal and any passphrase for WPA-Enterprise</description>
</valueHelp>
<constraint>
- <regex>.{8,63}</regex>
+ <regex>[[:ascii:]]{1,256}</regex>
</constraint>
<constraintErrorMessage>Invalid WPA pass phrase, must be 8 to 63 printable characters!</constraintErrorMessage>
</properties>
@@ -940,6 +941,19 @@
<constraintErrorMessage>Invalid SSID</constraintErrorMessage>
</properties>
</leafNode>
+ <leafNode name="bssid">
+ <properties>
+ <help>Basic Service Set Identifier (BSSID) - currently station mode only</help>
+ <valueHelp>
+ <format>macaddr</format>
+ <description>BSSID (MAC) address</description>
+ </valueHelp>
+ <constraint>
+ <validator name="mac-address"/>
+ </constraint>
+ <constraintErrorMessage>Invalid BSSID</constraintErrorMessage>
+ </properties>
+ </leafNode>
<leafNode name="type">
<properties>
<help>Wireless device type for this interface</help>
diff --git a/interface-definitions/load-balancing_reverse-proxy.xml.in b/interface-definitions/load-balancing_reverse-proxy.xml.in
index 1a432be6d..18274622c 100644
--- a/interface-definitions/load-balancing_reverse-proxy.xml.in
+++ b/interface-definitions/load-balancing_reverse-proxy.xml.in
@@ -36,6 +36,7 @@
</leafNode>
#include <include/generic-description.xml.i>
#include <include/listen-address.xml.i>
+ #include <include/haproxy/logging.xml.i>
#include <include/haproxy/mode.xml.i>
#include <include/port-number.xml.i>
#include <include/haproxy/rule-frontend.xml.i>
@@ -91,6 +92,7 @@
<defaultValue>round-robin</defaultValue>
</leafNode>
#include <include/generic-description.xml.i>
+ #include <include/haproxy/logging.xml.i>
#include <include/haproxy/mode.xml.i>
#include <include/haproxy/http-response-headers.xml.i>
<node name="http-check">
@@ -254,6 +256,7 @@
<help>Global perfomance parameters and limits</help>
</properties>
<children>
+ #include <include/haproxy/logging.xml.i>
<leafNode name="max-connections">
<properties>
<help>Maximum allowed connections</help>
diff --git a/interface-definitions/service_ssh.xml.in b/interface-definitions/service_ssh.xml.in
index d9eee1ab8..221e451d1 100644
--- a/interface-definitions/service_ssh.xml.in
+++ b/interface-definitions/service_ssh.xml.in
@@ -146,6 +146,19 @@
</constraint>
</properties>
</leafNode>
+ <leafNode name="pubkey-accepted-algorithm">
+ <properties>
+ <help>Allowed pubkey signature algorithms</help>
+ <completionHelp>
+ <!-- generated by ssh -Q PubkeyAcceptedAlgorithms | 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 ecdsa-sha2-nistp256 ecdsa-sha2-nistp256-cert-v01@openssh.com ecdsa-sha2-nistp384 ecdsa-sha2-nistp384-cert-v01@openssh.com ecdsa-sha2-nistp521 ecdsa-sha2-nistp521-cert-v01@openssh.com sk-ecdsa-sha2-nistp256@openssh.com sk-ecdsa-sha2-nistp256-cert-v01@openssh.com webauthn-sk-ecdsa-sha2-nistp256@openssh.com ssh-dss ssh-dss-cert-v01@openssh.com ssh-rsa ssh-rsa-cert-v01@openssh.com rsa-sha2-256 rsa-sha2-256-cert-v01@openssh.com rsa-sha2-512 rsa-sha2-512-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|ecdsa-sha2-nistp256|ecdsa-sha2-nistp256-cert-v01@openssh.com|ecdsa-sha2-nistp384|ecdsa-sha2-nistp384-cert-v01@openssh.com|ecdsa-sha2-nistp521|ecdsa-sha2-nistp521-cert-v01@openssh.com|sk-ecdsa-sha2-nistp256@openssh.com|sk-ecdsa-sha2-nistp256-cert-v01@openssh.com|webauthn-sk-ecdsa-sha2-nistp256@openssh.com|ssh-dss|ssh-dss-cert-v01@openssh.com|ssh-rsa|ssh-rsa-cert-v01@openssh.com|rsa-sha2-256|rsa-sha2-256-cert-v01@openssh.com|rsa-sha2-512|rsa-sha2-512-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_conntrack.xml.in b/interface-definitions/system_conntrack.xml.in
index 0dfa2ea81..cd59d1308 100644
--- a/interface-definitions/system_conntrack.xml.in
+++ b/interface-definitions/system_conntrack.xml.in
@@ -223,41 +223,78 @@
</node>
<node name="log">
<properties>
- <help>Log connection tracking events per protocol</help>
+ <help>Log connection tracking</help>
</properties>
<children>
- <node name="icmp">
+ <node name="event">
<properties>
- <help>Log connection tracking events for ICMP</help>
+ <help>Event type and protocol</help>
</properties>
<children>
- #include <include/conntrack/log-common.xml.i>
+ <node name="destroy">
+ <properties>
+ <help>Log connection deletion</help>
+ </properties>
+ <children>
+ #include <include/conntrack/log-protocols.xml.i>
+ </children>
+ </node>
+ <node name="new">
+ <properties>
+ <help>Log connection creation</help>
+ </properties>
+ <children>
+ #include <include/conntrack/log-protocols.xml.i>
+ </children>
+ </node>
+ <node name="update">
+ <properties>
+ <help>Log connection updates</help>
+ </properties>
+ <children>
+ #include <include/conntrack/log-protocols.xml.i>
+ </children>
+ </node>
</children>
</node>
- <node name="other">
+ <leafNode name="timestamp">
<properties>
- <help>Log connection tracking events for all protocols other than TCP, UDP and ICMP</help>
+ <help>Log connection tracking events include flow-based timestamp</help>
+ <valueless/>
</properties>
- <children>
- #include <include/conntrack/log-common.xml.i>
- </children>
- </node>
- <node name="tcp">
+ </leafNode>
+ <leafNode name="queue-size">
<properties>
- <help>Log connection tracking events for TCP</help>
+ <help>Internal message queue size</help>
+ <valueHelp>
+ <format>u32:100-999999</format>
+ <description>Queue size</description>
+ </valueHelp>
+ <constraint>
+ <validator name="numeric" argument="--range 1-999999"/>
+ </constraint>
+ <constraintErrorMessage>Queue size must be between 100 and 999999</constraintErrorMessage>
</properties>
- <children>
- #include <include/conntrack/log-common.xml.i>
- </children>
- </node>
- <node name="udp">
+ </leafNode>
+ <leafNode name="log-level">
<properties>
- <help>Log connection tracking events for UDP</help>
+ <help>Set log-level. Log must be enable.</help>
+ <completionHelp>
+ <list>info debug</list>
+ </completionHelp>
+ <valueHelp>
+ <format>info</format>
+ <description>Info log level</description>
+ </valueHelp>
+ <valueHelp>
+ <format>debug</format>
+ <description>Debug log level</description>
+ </valueHelp>
+ <constraint>
+ <regex>(info|debug)</regex>
+ </constraint>
</properties>
- <children>
- #include <include/conntrack/log-common.xml.i>
- </children>
- </node>
+ </leafNode>
</children>
</node>
<node name="modules">
diff --git a/interface-definitions/vpn_ipsec.xml.in b/interface-definitions/vpn_ipsec.xml.in
index 7f425d982..4a7fde75b 100644
--- a/interface-definitions/vpn_ipsec.xml.in
+++ b/interface-definitions/vpn_ipsec.xml.in
@@ -99,6 +99,12 @@
</constraint>
</properties>
</leafNode>
+ <leafNode name="disable-rekey">
+ <properties>
+ <help>Do not locally initiate a re-key of the SA, remote peer must re-key before expiration</help>
+ <valueless/>
+ </properties>
+ </leafNode>
<leafNode name="mode">
<properties>
<help>ESP mode</help>
diff --git a/op-mode-definitions/counters.xml.in b/op-mode-definitions/clear-interfaces.xml.in
index f563cb9a0..de2c3443e 100644
--- a/op-mode-definitions/counters.xml.in
+++ b/op-mode-definitions/clear-interfaces.xml.in
@@ -1,8 +1,14 @@
<?xml version="1.0"?>
<interfaceDefinition>
<node name="clear">
+ <properties>
+ <help>Clear system information</help>
+ </properties>
<children>
<node name="interfaces">
+ <properties>
+ <help>Clear interface information</help>
+ </properties>
<children>
<node name="counters">
<properties>
@@ -10,6 +16,17 @@
</properties>
<command>sudo ${vyos_op_scripts_dir}/interfaces.py clear_counters</command>
</node>
+ <tagNode name="connection">
+ <properties>
+ <help>Bring connection-oriented network interface down and up</help>
+ <completionHelp>
+ <path>interfaces pppoe</path>
+ <path>interfaces sstpc</path>
+ <path>interfaces wwan</path>
+ </completionHelp>
+ </properties>
+ <command>sudo ${vyos_op_scripts_dir}/connect_disconnect.py --connect --disconnect --interface "$3"</command>
+ </tagNode>
<node name="bonding">
<properties>
<help>Clear Bonding interface information</help>
@@ -595,4 +612,3 @@
</children>
</node>
</interfaceDefinition>
-
diff --git a/op-mode-definitions/configure.xml.in b/op-mode-definitions/configure.xml.in
index a711fa4a9..d7657289b 100644
--- a/op-mode-definitions/configure.xml.in
+++ b/op-mode-definitions/configure.xml.in
@@ -11,11 +11,11 @@
echo "Please do it as an administrator level VyOS user instead."
else
if grep -q -e '^overlay.*/filesystem.squashfs' /proc/mounts; then
- echo "WARNING: You are currently configuring a live-ISO environment, changes will not persist until installed"
+ echo "WARNING: You are currently configuring a live-ISO environment, changes will not persist until installed"
else
if grep -q -s '1' /tmp/vyos-config-status; then
- echo "WARNING: There was a config error on boot: saving the configuration now could overwrite data."
- echo "You may want to check and reload the boot config"
+ echo "WARNING: There was a config error on boot: saving the configuration now could overwrite data."
+ echo "You may want to check and reload the boot config"
fi
fi
history -w
diff --git a/op-mode-definitions/connect.xml.in b/op-mode-definitions/connect.xml.in
index 116cd6231..9027056a6 100644
--- a/op-mode-definitions/connect.xml.in
+++ b/op-mode-definitions/connect.xml.in
@@ -24,7 +24,7 @@
<path>interfaces wwan</path>
</completionHelp>
</properties>
- <command>sudo ${vyos_op_scripts_dir}/connect_disconnect.py --connect "$3"</command>
+ <command>sudo ${vyos_op_scripts_dir}/connect_disconnect.py --connect --interface "$3"</command>
</tagNode>
</children>
</node>
diff --git a/op-mode-definitions/dhcp.xml.in b/op-mode-definitions/dhcp.xml.in
index eee6937d6..b3438ab80 100644
--- a/op-mode-definitions/dhcp.xml.in
+++ b/op-mode-definitions/dhcp.xml.in
@@ -245,7 +245,7 @@
<properties>
<help>Restart DHCP server</help>
</properties>
- <command>if cli-shell-api existsActive service dhcp-server; then sudo systemctl restart kea-dhcp4-server.service; else echo "DHCP server not configured"; fi</command>
+ <command>sudo ${vyos_op_scripts_dir}/restart.py restart_service --name dhcp</command>
</node>
<node name="relay-agent">
<properties>
@@ -264,7 +264,7 @@
<properties>
<help>Restart DHCPv6 server</help>
</properties>
- <command>if cli-shell-api existsActive service dhcpv6-server; then sudo systemctl restart kea-dhcp6-server.service; else echo "DHCPv6 server not configured"; fi</command>
+ <command>sudo ${vyos_op_scripts_dir}/restart.py restart_service --name dhcpv6</command>
</node>
<node name="relay-agent">
<properties>
diff --git a/op-mode-definitions/disconnect.xml.in b/op-mode-definitions/disconnect.xml.in
index 843998c4f..f0523d9b9 100644
--- a/op-mode-definitions/disconnect.xml.in
+++ b/op-mode-definitions/disconnect.xml.in
@@ -14,7 +14,7 @@
<path>interfaces wwan</path>
</completionHelp>
</properties>
- <command>sudo ${vyos_op_scripts_dir}/connect_disconnect.py --disconnect "$3"</command>
+ <command>sudo ${vyos_op_scripts_dir}/connect_disconnect.py --disconnect --interface "$3"</command>
</tagNode>
</children>
</node>
diff --git a/op-mode-definitions/dns-dynamic.xml.in b/op-mode-definitions/dns-dynamic.xml.in
index 45d58e2e8..ef0f03988 100644
--- a/op-mode-definitions/dns-dynamic.xml.in
+++ b/op-mode-definitions/dns-dynamic.xml.in
@@ -97,7 +97,7 @@
<properties>
<help>Restart Dynamic DNS service</help>
</properties>
- <command>if cli-shell-api existsActive service dns dynamic; then sudo systemctl restart ddclient.service; else echo "Dynamic DNS not configured"; fi</command>
+ <command>sudo ${vyos_op_scripts_dir}/restart.py restart_service --name dns_dynamic</command>
</node>
</children>
</node>
diff --git a/op-mode-definitions/dns-forwarding.xml.in b/op-mode-definitions/dns-forwarding.xml.in
index 29bfc61cf..fac3fc345 100644
--- a/op-mode-definitions/dns-forwarding.xml.in
+++ b/op-mode-definitions/dns-forwarding.xml.in
@@ -73,7 +73,7 @@
<properties>
<help>Restart DNS Forwarding service</help>
</properties>
- <command>if cli-shell-api existsActive service dns forwarding; then sudo systemctl restart pdns-recursor.service; else echo "DNS forwarding not configured"; fi</command>
+ <command>sudo ${vyos_op_scripts_dir}/restart.py restart_service --name dns_forwarding</command>
</leafNode>
</children>
</node>
diff --git a/op-mode-definitions/firewall.xml.in b/op-mode-definitions/firewall.xml.in
index 6a254ee11..b6ce5bae2 100644
--- a/op-mode-definitions/firewall.xml.in
+++ b/op-mode-definitions/firewall.xml.in
@@ -119,7 +119,7 @@
<properties>
<help>Show summary of bridge custom firewall ruleset</help>
<completionHelp>
- <path>firewall bridge name ${COMP_WORDS[5]} rule</path>
+ <path>firewall bridge name ${COMP_WORDS[4]} rule</path>
</completionHelp>
</properties>
<children>
@@ -127,7 +127,7 @@
<properties>
<help>Show list view of bridge custom firewall rules</help>
<completionHelp>
- <path>firewall bridge name ${COMP_WORDS[5]} rule detail</path>
+ <path>firewall bridge name ${COMP_WORDS[4]} rule detail</path>
</completionHelp>
</properties>
<command>sudo ${vyos_op_scripts_dir}/firewall.py --action show --family $3 --hook $4 --priority $5 --rule $7 --detail $8</command>
@@ -299,7 +299,7 @@
<properties>
<help>Show summary of IPv6 custom firewall ruleset</help>
<completionHelp>
- <path>firewall ipv6 name ${COMP_WORDS[5]} rule</path>
+ <path>firewall ipv6 name ${COMP_WORDS[4]} rule</path>
</completionHelp>
</properties>
<children>
@@ -307,7 +307,7 @@
<properties>
<help>Show list view of IPv6 custom firewall rules</help>
<completionHelp>
- <path>firewall ipv6 name ${COMP_WORDS[5]} rule detail</path>
+ <path>firewall ipv6 name ${COMP_WORDS[4]} rule detail</path>
</completionHelp>
</properties>
<command>sudo ${vyos_op_scripts_dir}/firewall.py --action show --family $3 --hook $4 --priority $5 --rule $7 --detail $8</command>
@@ -479,7 +479,7 @@
<properties>
<help>Show summary of IPv4 custom firewall ruleset</help>
<completionHelp>
- <path>firewall ipv4 name ${COMP_WORDS[5]} rule</path>
+ <path>firewall ipv4 name ${COMP_WORDS[4]} rule</path>
</completionHelp>
</properties>
<children>
@@ -487,7 +487,7 @@
<properties>
<help>Show list view of IPv4 custom firewall ruleset</help>
<completionHelp>
- <path>firewall ipv4 name ${COMP_WORDS[5]} rule detail</path>
+ <path>firewall ipv4 name ${COMP_WORDS[4]} rule detail</path>
</completionHelp>
</properties>
<command>sudo ${vyos_op_scripts_dir}/firewall.py --action show --family $3 --hook $4 --priority $5 --rule $7 --detail $8</command>
diff --git a/op-mode-definitions/generate_tech-support_archive.xml.in b/op-mode-definitions/generate_tech-support_archive.xml.in
index e95be3e28..fc664eb90 100644
--- a/op-mode-definitions/generate_tech-support_archive.xml.in
+++ b/op-mode-definitions/generate_tech-support_archive.xml.in
@@ -11,16 +11,16 @@
<properties>
<help>Generate tech support archive</help>
</properties>
- <command>sudo ${vyos_op_scripts_dir}/generate_tech-support_archive.py</command>
+ <command>sudo ${vyos_op_scripts_dir}/tech_support.py show --raw | gzip> $4.json.gz</command>
</node>
<tagNode name="archive">
<properties>
<help>Generate tech support archive to defined location</help>
<completionHelp>
- <list> &lt;file&gt; &lt;scp://user:passwd@host&gt; &lt;ftp://user:passwd@host&gt;</list>
+ <list> &lt;file&gt; </list>
</completionHelp>
</properties>
- <command>sudo ${vyos_op_scripts_dir}/generate_tech-support_archive.py $4</command>
+ <command>sudo ${vyos_op_scripts_dir}/tech_support.py show --raw | gzip > $4.json.gz</command>
</tagNode>
</children>
</node>
diff --git a/op-mode-definitions/igmp-proxy.xml.in b/op-mode-definitions/igmp-proxy.xml.in
index 8533138d7..d6ad7ed7e 100644
--- a/op-mode-definitions/igmp-proxy.xml.in
+++ b/op-mode-definitions/igmp-proxy.xml.in
@@ -6,7 +6,7 @@
<properties>
<help>Restart the IGMP proxy process</help>
</properties>
- <command>sudo systemctl restart igmpproxy.service</command>
+ <command>sudo ${vyos_op_scripts_dir}/restart.py restart_service --name igmp_proxy</command>
</node>
</children>
</node>
diff --git a/op-mode-definitions/mdns-reflector.xml.in b/op-mode-definitions/mdns-reflector.xml.in
index a90d4d385..115b2858c 100644
--- a/op-mode-definitions/mdns-reflector.xml.in
+++ b/op-mode-definitions/mdns-reflector.xml.in
@@ -53,7 +53,7 @@
<properties>
<help>Restart mDNS repeater service</help>
</properties>
- <command>sudo systemctl restart avahi-daemon.service</command>
+ <command>sudo ${vyos_op_scripts_dir}/restart.py restart_service --name mdns_repeater</command>
</node>
</children>
</node>
diff --git a/op-mode-definitions/monitor-bandwidth.xml.in b/op-mode-definitions/monitor-bandwidth.xml.in
index 2da9d34f4..fc1d751d6 100644
--- a/op-mode-definitions/monitor-bandwidth.xml.in
+++ b/op-mode-definitions/monitor-bandwidth.xml.in
@@ -6,9 +6,10 @@
<properties>
<help>Monitor interface bandwidth in real time</help>
</properties>
+ <command>bmon --use-bit</command>
<children>
<tagNode name="interface">
- <command>bmon -b -p $4</command>
+ <command>bmon --use-bit --policy $4</command>
<properties>
<help>Monitor bandwidth usage on specified interface</help>
<completionHelp>
diff --git a/op-mode-definitions/restart-ntp.xml.in b/op-mode-definitions/restart-ntp.xml.in
new file mode 100644
index 000000000..961fae252
--- /dev/null
+++ b/op-mode-definitions/restart-ntp.xml.in
@@ -0,0 +1,13 @@
+<?xml version="1.0"?>
+<interfaceDefinition>
+ <node name="restart">
+ <children>
+ <node name="ntp">
+ <properties>
+ <help>Restart NTP service</help>
+ </properties>
+ <command>if cli-shell-api existsActive service ntp; then sudo systemctl restart chrony.service; else echo "Service NTP not configured"; fi</command>
+ </node>
+ </children>
+ </node>
+</interfaceDefinition>
diff --git a/op-mode-definitions/restart-router-advert.xml.in b/op-mode-definitions/restart-router-advert.xml.in
index 304b4dfd3..9eea3dfc4 100644
--- a/op-mode-definitions/restart-router-advert.xml.in
+++ b/op-mode-definitions/restart-router-advert.xml.in
@@ -6,7 +6,7 @@
<properties>
<help>Restart IPv6 Router Advertisement service</help>
</properties>
- <command>if cli-shell-api existsActive service router-advert; then sudo systemctl restart radvd.service; else echo "IPv6 Router Advertisement service not configured"; fi</command>
+ <command>sudo ${vyos_op_scripts_dir}/restart.py restart_service --name router_advert</command>
</node>
</children>
</node>
diff --git a/op-mode-definitions/restart-serial.xml.in b/op-mode-definitions/restart-serial.xml.in
new file mode 100644
index 000000000..4d8a03633
--- /dev/null
+++ b/op-mode-definitions/restart-serial.xml.in
@@ -0,0 +1,31 @@
+<?xml version="1.0"?>
+<interfaceDefinition>
+ <node name="restart">
+ <children>
+ <node name="serial">
+ <properties>
+ <help>Restart services on serial ports</help>
+ </properties>
+ <children>
+ <node name="console">
+ <properties>
+ <help>Restart serial console service for login TTYs</help>
+ </properties>
+ <command>sudo ${vyos_op_scripts_dir}/serial.py restart_console</command>
+ <children>
+ <tagNode name="device">
+ <properties>
+ <help>Restart specific TTY device</help>
+ <completionHelp>
+ <script>${vyos_completion_dir}/list_login_ttys.py</script>
+ </completionHelp>
+ </properties>
+ <command>sudo ${vyos_op_scripts_dir}/serial.py restart_console --device-name "$5"</command>
+ </tagNode>
+ </children>
+ </node>
+ </children>
+ </node>
+ </children>
+ </node>
+</interfaceDefinition>
diff --git a/op-mode-definitions/restart-snmp.xml.in b/op-mode-definitions/restart-snmp.xml.in
index 7de27df64..e9c43de01 100644
--- a/op-mode-definitions/restart-snmp.xml.in
+++ b/op-mode-definitions/restart-snmp.xml.in
@@ -6,7 +6,7 @@
<properties>
<help>Restart SNMP service</help>
</properties>
- <command>if cli-shell-api existsActive service snmp; then sudo systemctl restart snmpd.service; else echo "Service SNMP not configured"; fi</command>
+ <command>sudo ${vyos_op_scripts_dir}/restart.py restart_service --name snmp</command>
</node>
</children>
</node>
diff --git a/op-mode-definitions/restart-ssh.xml.in b/op-mode-definitions/restart-ssh.xml.in
index 543cafc24..914586df8 100644
--- a/op-mode-definitions/restart-ssh.xml.in
+++ b/op-mode-definitions/restart-ssh.xml.in
@@ -6,7 +6,7 @@
<properties>
<help>Restart SSH service</help>
</properties>
- <command>if cli-shell-api existsActive service ssh; then sudo systemctl restart "ssh@*.service"; else echo "Service SSH not configured"; fi</command>
+ <command>sudo ${vyos_op_scripts_dir}/restart.py restart_service --name ssh --vrf "*"</command>
</node>
</children>
</node>
diff --git a/op-mode-definitions/reverse-proxy.xml.in b/op-mode-definitions/reverse-proxy.xml.in
index 4af24880b..b45ce107f 100644
--- a/op-mode-definitions/reverse-proxy.xml.in
+++ b/op-mode-definitions/reverse-proxy.xml.in
@@ -6,7 +6,7 @@
<properties>
<help>Restart reverse-proxy service</help>
</properties>
- <command>if cli-shell-api existsActive load-balancing reverse-proxy; then sudo systemctl restart haproxy.service; else echo "Reverse-Proxy not configured"; fi</command>
+ <command>sudo ${vyos_op_scripts_dir}/restart.py restart_service --name reverse_proxy</command>
</node>
</children>
</node>
diff --git a/op-mode-definitions/show-techsupport_report.xml.in b/op-mode-definitions/show-techsupport_report.xml.in
index ef051e940..4fd6e5d1e 100644
--- a/op-mode-definitions/show-techsupport_report.xml.in
+++ b/op-mode-definitions/show-techsupport_report.xml.in
@@ -12,6 +12,14 @@
<help>Show consolidated tech-support report (contains private information)</help>
</properties>
<command>${vyos_op_scripts_dir}/show_techsupport_report.py</command>
+ <children>
+ <node name="machine-readable">
+ <properties>
+ <help>Show consolidated tech-support report in JSON</help>
+ </properties>
+ <command>sudo ${vyos_op_scripts_dir}/tech_support.py show --raw</command>
+ </node>
+ </children>
</node>
</children>
</node>
diff --git a/op-mode-definitions/suricata.xml.in b/op-mode-definitions/suricata.xml.in
index a5025afba..ff1f84706 100644
--- a/op-mode-definitions/suricata.xml.in
+++ b/op-mode-definitions/suricata.xml.in
@@ -16,7 +16,7 @@
<properties>
<help>Restart Suricata service</help>
</properties>
- <command>if systemctl is-active --quiet suricata; then sudo systemctl restart suricata.service; else echo "Service Suricata not configured"; fi</command>
+ <command>sudo ${vyos_op_scripts_dir}/restart.py restart_service --name suricata</command>
</node>
</children>
</node>
diff --git a/op-mode-definitions/vpn-ipsec.xml.in b/op-mode-definitions/vpn-ipsec.xml.in
index b551af2be..0a8671aeb 100644
--- a/op-mode-definitions/vpn-ipsec.xml.in
+++ b/op-mode-definitions/vpn-ipsec.xml.in
@@ -112,7 +112,7 @@
<properties>
<help>Restart the IPsec VPN process</help>
</properties>
- <command>if systemctl is-active --quiet strongswan; then sudo systemctl restart strongswan ; echo "IPsec process restarted"; else echo "IPsec process not running" ; fi</command>
+ <command>sudo ${vyos_op_scripts_dir}/restart.py restart_service --name ipsec</command>
</node>
</children>
</node>
diff --git a/op-mode-definitions/vrrp.xml.in b/op-mode-definitions/vrrp.xml.in
index 34484c706..158e7093e 100644
--- a/op-mode-definitions/vrrp.xml.in
+++ b/op-mode-definitions/vrrp.xml.in
@@ -30,7 +30,7 @@
<properties>
<help>Restart VRRP (Virtual Router Redundancy Protocol) process</help>
</properties>
- <command>sudo systemctl restart keepalived.service</command>
+ <command>sudo ${vyos_op_scripts_dir}/restart.py restart_service --name vrrp</command>
</node>
</children>
</node>
diff --git a/op-mode-definitions/webproxy.xml.in b/op-mode-definitions/webproxy.xml.in
index 57df44ff8..ba13907b8 100644
--- a/op-mode-definitions/webproxy.xml.in
+++ b/op-mode-definitions/webproxy.xml.in
@@ -34,7 +34,7 @@
<properties>
<help>Restart WebProxy service</help>
</properties>
- <command>if cli-shell-api existsActive service webproxy; then sudo systemctl restart squid.service; else echo "Service WebProxy not configured"; fi</command>
+ <command>sudo ${vyos_op_scripts_dir}/restart.py restart_service --name webproxy</command>
</node>
</children>
</node>
diff --git a/python/vyos/component_version.py b/python/vyos/component_version.py
index 0c305e5e0..94215531d 100644
--- a/python/vyos/component_version.py
+++ b/python/vyos/component_version.py
@@ -129,6 +129,7 @@ def component_from_string(string: str) -> dict:
return {k: int(v) for k, v in re.findall(r'([\w,-]+)@(\d+)', string)}
def version_info_from_file(config_file) -> VersionInfo:
+ """Return config file component and release version info."""
version_info = VersionInfo()
try:
with open(config_file) as f:
@@ -166,9 +167,7 @@ def version_info_from_file(config_file) -> VersionInfo:
return version_info
def version_info_from_system() -> VersionInfo:
- """
- Return system component versions.
- """
+ """Return system component and release version info."""
d = component_version()
sort_d = dict(sorted(d.items(), key=lambda x: x[0]))
version_info = VersionInfo(
@@ -180,20 +179,18 @@ def version_info_from_system() -> VersionInfo:
return version_info
def version_info_copy(v: VersionInfo) -> VersionInfo:
- """
- Make a copy of dataclass.
- """
+ """Make a copy of dataclass."""
return replace(v)
def version_info_prune_component(x: VersionInfo, y: VersionInfo) -> VersionInfo:
- """
- In place pruning of component keys of x not in y.
- """
+ """In place pruning of component keys of x not in y."""
+ if x.component is None or y.component is None:
+ return
x.component = { k: v for k,v in x.component.items() if k in y.component }
def add_system_version(config_str: str = None, out_file: str = None):
- """
- Wrap config string with system version and write to out_file.
+ """Wrap config string with system version and write to out_file.
+
For convenience, calling with no argument will write system version
string to stdout, for use in bash scripts.
"""
diff --git a/python/vyos/config.py b/python/vyos/config.py
index cca65f0eb..b7ee606a9 100644
--- a/python/vyos/config.py
+++ b/python/vyos/config.py
@@ -140,6 +140,7 @@ class Config(object):
self._level = []
self._dict_cache = {}
+ self.dependency_list = []
(self._running_config,
self._session_config) = self._config_source.get_configtree_tuple()
diff --git a/python/vyos/configdep.py b/python/vyos/configdep.py
index 73bd9ea96..e0fe1ddac 100644
--- a/python/vyos/configdep.py
+++ b/python/vyos/configdep.py
@@ -33,10 +33,9 @@ if typing.TYPE_CHECKING:
dependency_dir = os.path.join(directories['data'],
'config-mode-dependencies')
-local_dependent_func: dict[str, list[typing.Callable]] = {}
+dependency_list: list[typing.Callable] = []
DEBUG = False
-FORCE_LOCAL = False
def debug_print(s: str):
if DEBUG:
@@ -50,7 +49,8 @@ def canon_name_of_path(path: str) -> str:
return canon_name(script)
def caller_name() -> str:
- return stack()[2].filename
+ filename = stack()[2].filename
+ return canon_name_of_path(filename)
def name_of(f: typing.Callable) -> str:
return f.__name__
@@ -107,46 +107,47 @@ def run_config_mode_script(script: str, config: 'Config'):
mod.generate(c)
mod.apply(c)
except (VyOSError, ConfigError) as e:
- raise ConfigError(repr(e))
+ raise ConfigError(str(e)) from e
def def_closure(target: str, config: 'Config',
tagnode: typing.Optional[str] = None) -> typing.Callable:
script = target + '.py'
def func_impl():
- if tagnode:
+ if tagnode is not None:
os.environ['VYOS_TAGNODE_VALUE'] = tagnode
run_config_mode_script(script, config)
+ tag_ext = f'_{tagnode}' if tagnode is not None else ''
+ func_impl.__name__ = f'{target}{tag_ext}'
return func_impl
def set_dependents(case: str, config: 'Config',
tagnode: typing.Optional[str] = None):
+ global dependency_list
+
+ dependency_list = config.dependency_list
+
d = get_dependency_dict(config)
- k = canon_name_of_path(caller_name())
- tag_ext = f'_{tagnode}' if tagnode is not None else ''
- if hasattr(config, 'dependent_func') and not FORCE_LOCAL:
- dependent_func = getattr(config, 'dependent_func')
- l = dependent_func.setdefault('vyos_configd', [])
- else:
- dependent_func = local_dependent_func
- l = dependent_func.setdefault(k, [])
+ k = caller_name()
+ l = dependency_list
+
for target in d[k][case]:
func = def_closure(target, config, tagnode)
- func.__name__ = f'{target}{tag_ext}'
append_uniq(l, func)
- debug_print(f'set_dependents: caller {k}, dependents {names_of(l)}')
-def call_dependents(dependent_func: dict = None):
- k = canon_name_of_path(caller_name())
- if dependent_func is None or FORCE_LOCAL:
- dependent_func = local_dependent_func
- l = dependent_func.get(k, [])
- else:
- l = dependent_func.get('vyos_configd', [])
- debug_print(f'call_dependents: caller {k}, dependents {names_of(l)}')
+ debug_print(f'set_dependents: caller {k}, current dependents {names_of(l)}')
+
+def call_dependents():
+ k = caller_name()
+ l = dependency_list
+ debug_print(f'call_dependents: caller {k}, remaining dependents {names_of(l)}')
while l:
f = l.pop(0)
debug_print(f'calling: {f.__name__}')
- f()
+ try:
+ f()
+ except ConfigError as e:
+ s = f'dependent {f.__name__}: {str(e)}'
+ raise ConfigError(s) from e
def called_as_dependent() -> bool:
st = stack()[1:]
diff --git a/python/vyos/configtree.py b/python/vyos/configtree.py
index 5775070e2..bd77ab899 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-2022 VyOS maintainers and contributors
+# Copyright (C) 2018-2024 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;
@@ -290,7 +290,7 @@ class ConfigTree(object):
else:
return True
- def list_nodes(self, path):
+ def list_nodes(self, path, path_must_exist=True):
check_path(path)
path_str = " ".join(map(str, path)).encode()
@@ -298,7 +298,10 @@ class ConfigTree(object):
res = json.loads(res_json)
if res is None:
- raise ConfigTreeError("Path [{}] doesn't exist".format(path_str))
+ if path_must_exist:
+ raise ConfigTreeError("Path [{}] doesn't exist".format(path_str))
+ else:
+ return []
else:
return res
diff --git a/python/vyos/defaults.py b/python/vyos/defaults.py
index 9ccd925ce..25ee45391 100644
--- a/python/vyos/defaults.py
+++ b/python/vyos/defaults.py
@@ -50,3 +50,13 @@ commit_lock = os.path.join(directories['vyos_configdir'], '.lock')
component_version_json = os.path.join(directories['data'], 'component-versions.json')
config_default = os.path.join(directories['data'], 'config.boot.default')
+
+rt_symbolic_names = {
+ # Standard routing tables for Linux & reserved IDs for VyOS
+ 'default': 253, # Confusingly, a final fallthru, not the default.
+ 'main': 254, # The actual global table used by iproute2 unless told otherwise.
+ 'local': 255, # Special kernel loopback table.
+}
+
+rt_global_vrf = rt_symbolic_names['main']
+rt_global_table = rt_symbolic_names['main']
diff --git a/python/vyos/firewall.py b/python/vyos/firewall.py
index 664df28cc..facd498ca 100644
--- a/python/vyos/firewall.py
+++ b/python/vyos/firewall.py
@@ -30,6 +30,9 @@ from vyos.utils.dict import dict_search_args
from vyos.utils.dict import dict_search_recursive
from vyos.utils.process import cmd
from vyos.utils.process import run
+from vyos.utils.network import get_vrf_tableid
+from vyos.defaults import rt_global_table
+from vyos.defaults import rt_global_vrf
# Conntrack
def conntrack_required(conf):
@@ -366,10 +369,14 @@ def parse_rule(rule_conf, hook, fw_name, rule_id, ip_name):
output.append(f'ip{def_suffix} dscp != {{{negated_dscp_str}}}')
if 'ipsec' in rule_conf:
- if 'match_ipsec' in rule_conf['ipsec']:
+ if 'match_ipsec_in' in rule_conf['ipsec']:
output.append('meta ipsec == 1')
- if 'match_none' in rule_conf['ipsec']:
+ if 'match_none_in' in rule_conf['ipsec']:
output.append('meta ipsec == 0')
+ if 'match_ipsec_out' in rule_conf['ipsec']:
+ output.append('rt ipsec exists')
+ if 'match_none_out' in rule_conf['ipsec']:
+ output.append('rt ipsec missing')
if 'fragment' in rule_conf:
# Checking for fragmentation after priority -400 is not possible,
@@ -469,11 +476,20 @@ def parse_rule(rule_conf, hook, fw_name, rule_id, ip_name):
if 'mark' in rule_conf['set']:
mark = rule_conf['set']['mark']
output.append(f'meta mark set {mark}')
+ if 'vrf' in rule_conf['set']:
+ set_table = True
+ vrf_name = rule_conf['set']['vrf']
+ if vrf_name == 'default':
+ table = rt_global_vrf
+ else:
+ # NOTE: VRF->table ID lookup depends on the VRF iface already existing.
+ table = get_vrf_tableid(vrf_name)
if 'table' in rule_conf['set']:
set_table = True
table = rule_conf['set']['table']
if table == 'main':
- table = '254'
+ table = rt_global_table
+ if set_table:
mark = 0x7FFFFFFF - int(table)
output.append(f'meta mark set {mark}')
if 'tcp_mss' in rule_conf['set']:
diff --git a/python/vyos/ifconfig/interface.py b/python/vyos/ifconfig/interface.py
index 117479ade..72d3d3afe 100644
--- a/python/vyos/ifconfig/interface.py
+++ b/python/vyos/ifconfig/interface.py
@@ -37,6 +37,7 @@ from vyos.utils.network import mac2eui64
from vyos.utils.dict import dict_search
from vyos.utils.network import get_interface_config
from vyos.utils.network import get_interface_namespace
+from vyos.utils.network import get_vrf_tableid
from vyos.utils.network import is_netns_interface
from vyos.utils.process import is_systemd_service_active
from vyos.utils.process import run
@@ -382,6 +383,9 @@ class Interface(Control):
# can not delete ALL interfaces, see below
self.flush_addrs()
+ # remove interface from conntrack VRF interface map
+ self._del_interface_from_ct_iface_map()
+
# ---------------------------------------------------------------------
# Any class can define an eternal regex in its definition
# interface matching the regex will not be deleted
@@ -402,29 +406,20 @@ class Interface(Control):
if netns: cmd = f'ip netns exec {netns} {cmd}'
return self._cmd(cmd)
- def _set_vrf_ct_zone(self, vrf):
- """
- Add/Remove rules in nftables to associate traffic in VRF to an
- individual conntack zone
- """
- # Don't allow for netns yet
- if 'netns' in self.config:
- return None
+ def _nft_check_and_run(self, nft_command):
+ # Check if deleting is possible first to avoid raising errors
+ _, err = self._popen(f'nft --check {nft_command}')
+ if not err:
+ # Remove map element
+ self._cmd(f'nft {nft_command}')
- if vrf:
- # Get routing table ID for VRF
- vrf_table_id = get_interface_config(vrf).get('linkinfo', {}).get(
- 'info_data', {}).get('table')
- # Add map element with interface and zone ID
- if vrf_table_id:
- self._cmd(f'nft add element inet vrf_zones ct_iface_map {{ "{self.ifname}" : {vrf_table_id} }}')
- else:
- nft_del_element = f'delete element inet vrf_zones ct_iface_map {{ "{self.ifname}" }}'
- # Check if deleting is possible first to avoid raising errors
- _, err = self._popen(f'nft --check {nft_del_element}')
- if not err:
- # Remove map element
- self._cmd(f'nft {nft_del_element}')
+ def _del_interface_from_ct_iface_map(self):
+ nft_command = f'delete element inet vrf_zones ct_iface_map {{ "{self.ifname}" }}'
+ self._nft_check_and_run(nft_command)
+
+ def _add_interface_to_ct_iface_map(self, vrf_table_id: int):
+ nft_command = f'add element inet vrf_zones ct_iface_map {{ "{self.ifname}" : {vrf_table_id} }}'
+ self._nft_check_and_run(nft_command)
def get_min_mtu(self):
"""
@@ -597,12 +592,30 @@ class Interface(Control):
>>> Interface('eth0').set_vrf()
"""
+ # Don't allow for netns yet
+ if 'netns' in self.config:
+ return False
+
tmp = self.get_interface('vrf')
if tmp == vrf:
return False
+ # Get current VRF table ID
+ old_vrf_tableid = get_vrf_tableid(self.ifname)
self.set_interface('vrf', vrf)
- self._set_vrf_ct_zone(vrf)
+
+ if vrf:
+ # Get routing table ID number for VRF
+ vrf_table_id = get_vrf_tableid(vrf)
+ # Add map element with interface and zone ID
+ if vrf_table_id:
+ # delete old table ID from nftables if it has changed, e.g. interface moved to a different VRF
+ if old_vrf_tableid and old_vrf_tableid != int(vrf_table_id):
+ self._del_interface_from_ct_iface_map()
+ self._add_interface_to_ct_iface_map(vrf_table_id)
+ else:
+ self._del_interface_from_ct_iface_map()
+
return True
def set_arp_cache_tmo(self, tmo):
diff --git a/python/vyos/ifconfig/l2tpv3.py b/python/vyos/ifconfig/l2tpv3.py
index 85a89ef8b..c1f2803ee 100644
--- a/python/vyos/ifconfig/l2tpv3.py
+++ b/python/vyos/ifconfig/l2tpv3.py
@@ -90,9 +90,17 @@ class L2TPv3If(Interface):
"""
if self.exists(self.ifname):
- # interface is always A/D down. It needs to be enabled explicitly
self.set_admin_state('down')
+ # remove all assigned IP addresses from interface - this is a bit redundant
+ # as the kernel will remove all addresses on interface deletion
+ self.flush_addrs()
+
+ # remove interface from conntrack VRF interface map, here explicitly and do not
+ # rely on the base class implementation as the interface will
+ # vanish as soon as the l2tp session is deleted
+ self._del_interface_from_ct_iface_map()
+
if {'tunnel_id', 'session_id'} <= set(self.config):
cmd = 'ip l2tp del session tunnel_id {tunnel_id}'
cmd += ' session_id {session_id}'
@@ -101,3 +109,5 @@ class L2TPv3If(Interface):
if 'tunnel_id' in self.config:
cmd = 'ip l2tp del tunnel tunnel_id {tunnel_id}'
self._cmd(cmd.format(**self.config))
+
+ # No need to call the baseclass as the interface is now already gone
diff --git a/python/vyos/ifconfig/vxlan.py b/python/vyos/ifconfig/vxlan.py
index 918aea202..1023c58d1 100644
--- a/python/vyos/ifconfig/vxlan.py
+++ b/python/vyos/ifconfig/vxlan.py
@@ -134,6 +134,19 @@ class VXLANIf(Interface):
Controls whether vlan to tunnel mapping is enabled on the port.
By default this flag is off.
"""
+ def range_to_dict(vlan_to_vni):
+ """ Converts dict of ranges to dict """
+ result_dict = {}
+ for vlan, vlan_conf in vlan_to_vni.items():
+ vni = vlan_conf['vni']
+ vlan_range, vni_range = vlan.split('-'), vni.split('-')
+ if len(vlan_range) > 1:
+ vlan_range = range(int(vlan_range[0]), int(vlan_range[1]) + 1)
+ vni_range = range(int(vni_range[0]), int(vni_range[1]) + 1)
+ dict_to_add = {str(k): {'vni': str(v)} for k, v in zip(vlan_range, vni_range)}
+ result_dict.update(dict_to_add)
+ return result_dict
+
if not isinstance(state, bool):
raise ValueError('Value out of range')
@@ -142,7 +155,7 @@ class VXLANIf(Interface):
if dict_search('parameters.vni_filter', self.config) != None:
cur_vni_filter = get_vxlan_vni_filter(self.ifname)
- for vlan, vlan_config in self.config['vlan_to_vni_removed'].items():
+ for vlan, vlan_config in range_to_dict(self.config['vlan_to_vni_removed']).items():
# If VNI filtering is enabled, remove matching VNI filter
if cur_vni_filter != None:
vni = vlan_config['vni']
@@ -159,10 +172,11 @@ class VXLANIf(Interface):
if 'vlan_to_vni' in self.config:
# Determine current OS Kernel configured VLANs
+ vlan_vni_mapping = range_to_dict(self.config['vlan_to_vni'])
os_configured_vlan_ids = get_vxlan_vlan_tunnels(self.ifname)
- add_vlan = list_diff(list(self.config['vlan_to_vni'].keys()), os_configured_vlan_ids)
+ add_vlan = list_diff(list(vlan_vni_mapping.keys()), os_configured_vlan_ids)
- for vlan, vlan_config in self.config['vlan_to_vni'].items():
+ for vlan, vlan_config in vlan_vni_mapping.items():
# VLAN mapping already exists - skip
if vlan not in add_vlan:
continue
diff --git a/python/vyos/ipsec.py b/python/vyos/ipsec.py
index 4603aab22..28f77565a 100644
--- a/python/vyos/ipsec.py
+++ b/python/vyos/ipsec.py
@@ -1,4 +1,4 @@
-# Copyright 2020-2023 VyOS maintainers and contributors <maintainers@vyos.io>
+# Copyright 2020-2024 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
@@ -13,31 +13,38 @@
# 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/>.
-#Package to communicate with Strongswan VICI
+# Package to communicate with Strongswan VICI
+
class ViciInitiateError(Exception):
"""
- VICI can't initiate a session.
+ VICI can't initiate a session.
"""
+
pass
+
+
class ViciCommandError(Exception):
"""
- VICI can't execute a command by any reason.
+ VICI can't execute a command by any reason.
"""
+
pass
+
def get_vici_sas():
from vici import Session as vici_session
try:
session = vici_session()
except Exception:
- raise ViciInitiateError("IPsec not initialized")
+ raise ViciInitiateError('IPsec not initialized')
try:
sas = list(session.list_sas())
return sas
except Exception:
- raise ViciCommandError(f'Failed to get SAs')
+ raise ViciCommandError('Failed to get SAs')
+
def get_vici_connections():
from vici import Session as vici_session
@@ -45,18 +52,19 @@ def get_vici_connections():
try:
session = vici_session()
except Exception:
- raise ViciInitiateError("IPsec not initialized")
+ raise ViciInitiateError('IPsec not initialized')
try:
connections = list(session.list_conns())
return connections
except Exception:
- raise ViciCommandError(f'Failed to get connections')
+ raise ViciCommandError('Failed to get connections')
+
def get_vici_sas_by_name(ike_name: str, tunnel: str) -> list:
"""
- Find sas by IKE_SA name and/or CHILD_SA name
- and return list of OrdinaryDicts with SASs info
- If tunnel is not None return value is list of OrdenaryDicts contained only
+ Find installed SAs by IKE_SA name and/or CHILD_SA name
+ and return list with SASs info.
+ If tunnel is not None return a list contained only
CHILD_SAs wich names equal tunnel value.
:param ike_name: IKE SA name
:type ike_name: str
@@ -70,7 +78,7 @@ def get_vici_sas_by_name(ike_name: str, tunnel: str) -> list:
try:
session = vici_session()
except Exception:
- raise ViciInitiateError("IPsec not initialized")
+ raise ViciInitiateError('IPsec not initialized')
vici_dict = {}
if ike_name:
vici_dict['ike'] = ike_name
@@ -80,7 +88,31 @@ def get_vici_sas_by_name(ike_name: str, tunnel: str) -> list:
sas = list(session.list_sas(vici_dict))
return sas
except Exception:
- raise ViciCommandError(f'Failed to get SAs')
+ raise ViciCommandError('Failed to get SAs')
+
+
+def get_vici_connection_by_name(ike_name: str) -> list:
+ """
+ Find loaded SAs by IKE_SA name and return list with SASs info
+ :param ike_name: IKE SA name
+ :type ike_name: str
+ :return: list of Ordinary Dicts with SASs
+ :rtype: list
+ """
+ from vici import Session as vici_session
+
+ try:
+ session = vici_session()
+ except Exception:
+ raise ViciInitiateError('IPsec is not initialized')
+ vici_dict = {}
+ if ike_name:
+ vici_dict['ike'] = ike_name
+ try:
+ sas = list(session.list_conns(vici_dict))
+ return sas
+ except Exception:
+ raise ViciCommandError('Failed to get SAs')
def terminate_vici_ikeid_list(ike_id_list: list) -> None:
@@ -94,19 +126,17 @@ def terminate_vici_ikeid_list(ike_id_list: list) -> None:
try:
session = vici_session()
except Exception:
- raise ViciInitiateError("IPsec not initialized")
+ raise ViciInitiateError('IPsec is not initialized')
try:
for ikeid in ike_id_list:
- session_generator = session.terminate(
- {'ike-id': ikeid, 'timeout': '-1'})
+ session_generator = session.terminate({'ike-id': ikeid, 'timeout': '-1'})
# a dummy `for` loop is required because of requirements
# from vici. Without a full iteration on the output, the
# command to vici may not be executed completely
for _ in session_generator:
pass
except Exception:
- raise ViciCommandError(
- f'Failed to terminate SA for IKE ids {ike_id_list}')
+ raise ViciCommandError(f'Failed to terminate SA for IKE ids {ike_id_list}')
def terminate_vici_by_name(ike_name: str, child_name: str) -> None:
@@ -123,9 +153,9 @@ def terminate_vici_by_name(ike_name: str, child_name: str) -> None:
try:
session = vici_session()
except Exception:
- raise ViciInitiateError("IPsec not initialized")
+ raise ViciInitiateError('IPsec is not initialized')
try:
- vici_dict: dict= {}
+ vici_dict: dict = {}
if ike_name:
vici_dict['ike'] = ike_name
if child_name:
@@ -138,16 +168,48 @@ def terminate_vici_by_name(ike_name: str, child_name: str) -> None:
pass
except Exception:
if child_name:
- raise ViciCommandError(
- f'Failed to terminate SA for IPSEC {child_name}')
+ raise ViciCommandError(f'Failed to terminate SA for IPSEC {child_name}')
else:
- raise ViciCommandError(
- f'Failed to terminate SA for IKE {ike_name}')
+ raise ViciCommandError(f'Failed to terminate SA for IKE {ike_name}')
+
+
+def vici_initiate_all_child_sa_by_ike(ike_sa_name: str, child_sa_list: list) -> bool:
+ """
+ Initiate IKE SA with scpecified CHILD_SAs in list
+
+ Args:
+ ike_sa_name (str): an IKE SA connection name
+ child_sa_list (list): a list of child SA names
+
+ Returns:
+ bool: a result of initiation command
+ """
+ from vici import Session as vici_session
+
+ try:
+ session = vici_session()
+ except Exception:
+ raise ViciInitiateError('IPsec is not initialized')
+
+ try:
+ for child_sa_name in child_sa_list:
+ session_generator = session.initiate(
+ {'ike': ike_sa_name, 'child': child_sa_name, 'timeout': '-1'}
+ )
+ # a dummy `for` loop is required because of requirements
+ # from vici. Without a full iteration on the output, the
+ # command to vici may not be executed completely
+ for _ in session_generator:
+ pass
+ return True
+ except Exception:
+ raise ViciCommandError(f'Failed to initiate SA for IKE {ike_sa_name}')
-def vici_initiate(ike_sa_name: str, child_sa_name: str, src_addr: str,
- dst_addr: str) -> bool:
- """Initiate IKE SA connection with specific peer
+def vici_initiate(
+ ike_sa_name: str, child_sa_name: str, src_addr: str, dst_addr: str
+) -> bool:
+ """Initiate IKE SA with one child_sa connection with specific peer
Args:
ike_sa_name (str): an IKE SA connection name
@@ -163,16 +225,18 @@ def vici_initiate(ike_sa_name: str, child_sa_name: str, src_addr: str,
try:
session = vici_session()
except Exception:
- raise ViciInitiateError("IPsec not initialized")
+ raise ViciInitiateError('IPsec is not initialized')
try:
- session_generator = session.initiate({
- 'ike': ike_sa_name,
- 'child': child_sa_name,
- 'timeout': '-1',
- 'my-host': src_addr,
- 'other-host': dst_addr
- })
+ session_generator = session.initiate(
+ {
+ 'ike': ike_sa_name,
+ 'child': child_sa_name,
+ 'timeout': '-1',
+ 'my-host': src_addr,
+ 'other-host': dst_addr,
+ }
+ )
# a dummy `for` loop is required because of requirements
# from vici. Without a full iteration on the output, the
# command to vici may not be executed completely
@@ -180,4 +244,4 @@ def vici_initiate(ike_sa_name: str, child_sa_name: str, src_addr: str,
pass
return True
except Exception:
- raise ViciCommandError(f'Failed to initiate SA for IKE {ike_sa_name}') \ No newline at end of file
+ raise ViciCommandError(f'Failed to initiate SA for IKE {ike_sa_name}')
diff --git a/python/vyos/opmode.py b/python/vyos/opmode.py
index 8dab9a4ca..a6c64adfb 100644
--- a/python/vyos/opmode.py
+++ b/python/vyos/opmode.py
@@ -31,7 +31,15 @@ class Error(Exception):
class UnconfiguredSubsystem(Error):
""" Requested operation is valid, but cannot be completed
- because corresponding subsystem is not configured and running.
+ because corresponding subsystem is not configured
+ and thus is not running.
+ """
+ pass
+
+class UnconfiguredObject(UnconfiguredSubsystem):
+ """ Requested operation is valid but cannot be completed
+ because its parameter refers to an object that does not exist
+ in the system configuration.
"""
pass
@@ -81,7 +89,7 @@ class InternalError(Error):
def _is_op_mode_function_name(name):
- if re.match(r"^(show|clear|reset|restart|add|update|delete|generate|set|renew)", name):
+ if re.match(r"^(show|clear|reset|restart|add|update|delete|generate|set|renew|release)", name):
return True
else:
return False
diff --git a/python/vyos/template.py b/python/vyos/template.py
index e8d7ba669..3507e0940 100644
--- a/python/vyos/template.py
+++ b/python/vyos/template.py
@@ -556,8 +556,8 @@ def get_openvpn_cipher(cipher):
return openvpn_translate[cipher].upper()
return cipher.upper()
-@register_filter('openvpn_ncp_ciphers')
-def get_openvpn_ncp_ciphers(ciphers):
+@register_filter('openvpn_data_ciphers')
+def get_openvpn_data_ciphers(ciphers):
out = []
for cipher in ciphers:
if cipher in openvpn_translate:
diff --git a/python/vyos/utils/__init__.py b/python/vyos/utils/__init__.py
index 90620071b..3759b2125 100644
--- a/python/vyos/utils/__init__.py
+++ b/python/vyos/utils/__init__.py
@@ -25,6 +25,7 @@ from vyos.utils import file
from vyos.utils import io
from vyos.utils import kernel
from vyos.utils import list
+from vyos.utils import locking
from vyos.utils import misc
from vyos.utils import network
from vyos.utils import permission
diff --git a/python/vyos/utils/locking.py b/python/vyos/utils/locking.py
new file mode 100644
index 000000000..63cb1a816
--- /dev/null
+++ b/python/vyos/utils/locking.py
@@ -0,0 +1,115 @@
+# Copyright 2024 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 fcntl
+import re
+import time
+from pathlib import Path
+
+
+class LockTimeoutError(Exception):
+ """Custom exception raised when lock acquisition times out."""
+
+ pass
+
+
+class InvalidLockNameError(Exception):
+ """Custom exception raised when the lock name is invalid."""
+
+ pass
+
+
+class Lock:
+ """Lock class to acquire and release a lock file"""
+
+ def __init__(self, lock_name: str) -> None:
+ """Lock class constructor
+
+ Args:
+ lock_name (str): Name of the lock file
+
+ Raises:
+ InvalidLockNameError: If the lock name is invalid
+ """
+ # Validate lock name
+ if not re.match(r'^[a-zA-Z0-9_\-]+$', lock_name):
+ raise InvalidLockNameError(f'Invalid lock name: {lock_name}')
+
+ self.__lock_dir = Path('/run/vyos/lock')
+ self.__lock_dir.mkdir(parents=True, exist_ok=True)
+
+ self.__lock_file_path: Path = self.__lock_dir / f'{lock_name}.lock'
+ self.__lock_file = None
+
+ self._is_locked = False
+
+ def __del__(self) -> None:
+ """Ensure the lock file is removed when the object is deleted"""
+ self.release()
+
+ @property
+ def is_locked(self) -> bool:
+ """Check if the lock is acquired
+
+ Returns:
+ bool: True if the lock is acquired, False otherwise
+ """
+ return self._is_locked
+
+ def __unlink_lockfile(self) -> None:
+ """Remove the lock file if it is not currently locked."""
+ try:
+ with self.__lock_file_path.open('w') as f:
+ fcntl.flock(f, fcntl.LOCK_EX | fcntl.LOCK_NB)
+ self.__lock_file_path.unlink(missing_ok=True)
+ except IOError:
+ # If we cannot acquire the lock, it means another process has it, so we do nothing.
+ pass
+
+ def acquire(self, timeout: int = 0) -> None:
+ """Acquire a lock file
+
+ Args:
+ timeout (int, optional): A time to wait for lock. Defaults to 0.
+
+ Raises:
+ LockTimeoutError: If lock could not be acquired within timeout
+ """
+ start_time: float = time.time()
+ while True:
+ try:
+ self.__lock_file = self.__lock_file_path.open('w')
+ fcntl.flock(self.__lock_file, fcntl.LOCK_EX | fcntl.LOCK_NB)
+ self._is_locked = True
+ return
+ except IOError:
+ if timeout > 0 and (time.time() - start_time) >= timeout:
+ if self.__lock_file:
+ self.__lock_file.close()
+ raise LockTimeoutError(
+ f'Could not acquire lock within {timeout} seconds'
+ )
+ time.sleep(0.1)
+
+ def release(self) -> None:
+ """Release a lock file"""
+ if self.__lock_file and self._is_locked:
+ try:
+ fcntl.flock(self.__lock_file, fcntl.LOCK_UN)
+ self._is_locked = False
+ finally:
+ self.__lock_file.close()
+ self.__lock_file = None
+ self.__unlink_lockfile()
diff --git a/python/vyos/utils/network.py b/python/vyos/utils/network.py
index 829124b57..8fce08de0 100644
--- a/python/vyos/utils/network.py
+++ b/python/vyos/utils/network.py
@@ -83,6 +83,19 @@ def get_interface_vrf(interface):
return tmp['master']
return 'default'
+def get_vrf_tableid(interface: str):
+ """ Return VRF table ID for given interface name or None """
+ from vyos.utils.dict import dict_search
+ table = None
+ tmp = get_interface_config(interface)
+ # Check if we are "the" VRF interface
+ if dict_search('linkinfo.info_kind', tmp) == 'vrf':
+ table = tmp['linkinfo']['info_data']['table']
+ # or an interface bound to a VRF
+ elif dict_search('linkinfo.info_slave_kind', tmp) == 'vrf':
+ table = tmp['linkinfo']['info_slave_data']['table']
+ return table
+
def get_interface_config(interface):
""" Returns the used encapsulation protocol for given interface.
If interface does not exist, None is returned.
@@ -537,22 +550,50 @@ def ipv6_prefix_length(low, high):
return None
xor = bytearray(a ^ b for a, b in zip(lo, hi))
-
+
plen = 0
while plen < 128 and xor[plen // 8] == 0:
plen += 8
-
+
if plen == 128:
return plen
-
+
for i in range((plen // 8) + 1, 16):
if xor[i] != 0:
return None
-
+
for i in range(8):
msk = ~xor[plen // 8] & 0xff
-
+
if msk == bytemasks[i]:
return plen + i + 1
return None
+
+def get_nft_vrf_zone_mapping() -> dict:
+ """
+ Retrieve current nftables conntrack mapping list from Kernel
+
+ returns: [{'interface': 'red', 'vrf_tableid': 1000},
+ {'interface': 'eth2', 'vrf_tableid': 1000},
+ {'interface': 'blue', 'vrf_tableid': 2000}]
+ """
+ from json import loads
+ from jmespath import search
+ from vyos.utils.process import cmd
+ output = []
+ tmp = loads(cmd('sudo nft -j list table inet vrf_zones'))
+ # {'nftables': [{'metainfo': {'json_schema_version': 1,
+ # 'release_name': 'Old Doc Yak #3',
+ # 'version': '1.0.9'}},
+ # {'table': {'family': 'inet', 'handle': 6, 'name': 'vrf_zones'}},
+ # {'map': {'elem': [['eth0', 666],
+ # ['dum0', 666],
+ # ['wg500', 666],
+ # ['bond10.666', 666]],
+ vrf_list = search('nftables[].map.elem | [0]', tmp)
+ if not vrf_list:
+ return output
+ for (vrf_name, vrf_id) in vrf_list:
+ output.append({'interface' : vrf_name, 'vrf_tableid' : vrf_id})
+ return output
diff --git a/python/vyos/utils/process.py b/python/vyos/utils/process.py
index 60ef87a51..ce880f4a4 100644
--- a/python/vyos/utils/process.py
+++ b/python/vyos/utils/process.py
@@ -225,7 +225,7 @@ def process_named_running(name: str, cmdline: str=None, timeout: int=0):
if not tmp:
if time.time() > time_expire:
break
- time.sleep(0.100) # wait 250ms
+ time.sleep(0.100) # wait 100ms
continue
return tmp
else:
diff --git a/python/vyos/utils/serial.py b/python/vyos/utils/serial.py
new file mode 100644
index 000000000..b646f881e
--- /dev/null
+++ b/python/vyos/utils/serial.py
@@ -0,0 +1,118 @@
+# Copyright 2024 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, re, json
+from typing import List
+
+from vyos.base import Warning
+from vyos.utils.io import ask_yes_no
+from vyos.utils.process import cmd
+
+GLOB_GETTY_UNITS = 'serial-getty@*.service'
+RE_GETTY_DEVICES = re.compile(r'.+@(.+).service$')
+
+SD_UNIT_PATH = '/run/systemd/system'
+UTMP_PATH = '/run/utmp'
+
+def get_serial_units(include_devices=[]):
+ # Since we cannot depend on the current config for decommissioned ports,
+ # we just grab everything that systemd knows about.
+ tmp = cmd(f'systemctl list-units {GLOB_GETTY_UNITS} --all --output json --no-pager')
+ getty_units = json.loads(tmp)
+ for sdunit in getty_units:
+ m = RE_GETTY_DEVICES.search(sdunit['unit'])
+ if m is None:
+ Warning(f'Serial console unit name "{sdunit["unit"]}" is malformed and cannot be checked for activity!')
+ continue
+
+ getty_device = m.group(1)
+ if include_devices and getty_device not in include_devices:
+ continue
+
+ sdunit['device'] = getty_device
+
+ return getty_units
+
+def get_authenticated_ports(units):
+ connected = []
+ ports = [ x['device'] for x in units if 'device' in x ]
+ #
+ # utmpdump just gives us an easily parseable dump of currently logged-in sessions, for eg:
+ # $ utmpdump /run/utmp
+ # Utmp dump of /run/utmp
+ # [2] [00000] [~~ ] [reboot ] [~ ] [6.6.31-amd64-vyos ] [0.0.0.0 ] [2024-06-18T13:56:53,958484+00:00]
+ # [1] [00051] [~~ ] [runlevel] [~ ] [6.6.31-amd64-vyos ] [0.0.0.0 ] [2024-06-18T13:57:01,790808+00:00]
+ # [6] [03178] [tty1] [LOGIN ] [tty1 ] [ ] [0.0.0.0 ] [2024-06-18T13:57:31,015392+00:00]
+ # [7] [37151] [ts/0] [vyos ] [pts/0 ] [10.9.8.7 ] [10.9.8.7 ] [2024-07-04T13:42:08,760892+00:00]
+ # [8] [24812] [ts/1] [ ] [pts/1 ] [10.9.8.7 ] [10.9.8.7 ] [2024-06-20T18:10:07,309365+00:00]
+ #
+ # We can safely skip blank or LOGIN sessions with valid device names.
+ #
+ for line in cmd(f'utmpdump {UTMP_PATH}').splitlines():
+ row = line.split('] [')
+ user_name = row[3].strip()
+ user_term = row[4].strip()
+ if user_name and user_name != 'LOGIN' and user_term in ports:
+ connected.append(user_term)
+
+ return connected
+
+def restart_login_consoles(prompt_user=False, quiet=True, devices: List[str]=[]):
+ # restart_login_consoles() is called from both conf- and op-mode scripts, including
+ # the warning messages and user prompts common to both.
+ #
+ # The default case, called with no arguments, is a simple serial-getty restart &
+ # cleanup wrapper with no output or prompts that can be used from anywhere.
+ #
+ # quiet and prompt_user args have been split from an original "no_prompt", in
+ # order to support the completely silent default use case. "no_prompt" would
+ # only suppress the user interactive prompt.
+ #
+ # quiet intentionally does not suppress a vyos.base.Warning() for malformed
+ # device names in _get_serial_units().
+ #
+ cmd('systemctl daemon-reload')
+
+ units = get_serial_units(devices)
+ connected = get_authenticated_ports(units)
+
+ if connected:
+ if not quiet:
+ Warning('There are user sessions connected via serial console that '\
+ 'will be terminated when serial console settings are changed!')
+ if not prompt_user:
+ # This flag is used by conf_mode/system_console.py to reset things, if there's
+ # a problem, the user should issue a manual restart for serial-getty.
+ Warning('Please ensure all settings are committed and saved before issuing a ' \
+ '"restart serial console" command to apply new configuration!')
+ if not prompt_user:
+ return False
+ if not ask_yes_no('Any uncommitted changes from these sessions will be lost\n' \
+ 'and in-progress actions may be left in an inconsistent state.\n'\
+ '\nContinue?'):
+ return False
+
+ for unit in units:
+ if 'device' not in unit:
+ continue # malformed or filtered.
+ unit_name = unit['unit']
+ unit_device = unit['device']
+ if os.path.exists(os.path.join(SD_UNIT_PATH, unit_name)):
+ cmd(f'systemctl restart {unit_name}')
+ else:
+ # Deleted stubs don't need to be restarted, just shut them down.
+ cmd(f'systemctl stop {unit_name}')
+
+ return True
diff --git a/python/vyos/utils/strip_config.py b/python/vyos/utils/strip_config.py
new file mode 100644
index 000000000..7a9c78c9f
--- /dev/null
+++ b/python/vyos/utils/strip_config.py
@@ -0,0 +1,210 @@
+#!/usr/bin/python3
+#
+# Copyright 2024 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/>.
+
+# XXX: these functions assume that the config is at the top level,
+# and aren't capable of anonymizing config subtress.
+# They shouldn't be used as a basis for a strip-private filter
+# until we figure out if we can pass the config path information to the filter.
+
+import copy
+
+import vyos.configtree
+
+
+def __anonymize_password(v):
+ return "<PASSWORD REDACTED>"
+
+def __anonymize_key(v):
+ return "<KEY DATA REDACTED>"
+
+def __anonymize_data(v):
+ return "<DATA REDACTED>"
+
+__secret_paths = [
+ # System user password hashes
+ {"base_path": ['system', 'login', 'user'], "secret_path": ["authentication", "encrypted-password"], "func": __anonymize_password},
+
+ # PKI data
+ {"base_path": ["pki", "ca"], "secret_path": ["private", "key"], "func": __anonymize_key},
+ {"base_path": ["pki", "ca"], "secret_path": ["certificate"], "func": __anonymize_key},
+ {"base_path": ["pki", "ca"], "secret_path": ["crl"], "func": __anonymize_key},
+ {"base_path": ["pki", "certificate"], "secret_path": ["private", "key"], "func": __anonymize_key},
+ {"base_path": ["pki", "certificate"], "secret_path": ["certificate"], "func": __anonymize_key},
+ {"base_path": ["pki", "certificate"], "secret_path": ["acme", "email"], "func": __anonymize_data},
+ {"base_path": ["pki", "key-pair"], "secret_path": ["private", "key"], "func": __anonymize_key},
+ {"base_path": ["pki", "key-pair"], "secret_path": ["public", "key"], "func": __anonymize_key},
+ {"base_path": ["pki", "openssh"], "secret_path": ["private", "key"], "func": __anonymize_key},
+ {"base_path": ["pki", "openssh"], "secret_path": ["public", "key"], "func": __anonymize_key},
+ {"base_path": ["pki", "openvpn", "shared-secret"], "secret_path": ["key"], "func": __anonymize_key},
+ {"base_path": ["pki", "dh"], "secret_path": ["parameters"], "func": __anonymize_key},
+
+ # IPsec pre-shared secrets
+ {"base_path": ['vpn', 'ipsec', 'authentication', 'psk'], "secret_path": ["secret"], "func": __anonymize_password},
+
+ # IPsec x509 passphrases
+ {"base_path": ['vpn', 'ipsec', 'site-to-site', 'peer'], "secret_path": ['authentication', 'x509'], "func": __anonymize_password},
+
+ # IPsec remote-access secrets and passwords
+ {"base_path": ["vpn", "ipsec", "remote-access", "connection"], "secret_path": ["authentication", "pre-shared-secret"], "func": __anonymize_password},
+ # Passwords in remote-access IPsec local users have their own fixup
+ # due to deeper nesting.
+
+ # PPTP passwords
+ {"base_path": ['vpn', 'pptp', 'remote-access', 'authentication', 'local-users', 'username'], "secret_path": ['password'], "func": __anonymize_password},
+
+ # L2TP passwords
+ {"base_path": ['vpn', 'l2tp', 'remote-access', 'authentication', 'local-users', 'username'], "secret_path": ['password'], "func": __anonymize_password},
+ {"path": ['vpn', 'l2tp', 'remote-access', 'ipsec-settings', 'authentication', 'pre-shared-secret'], "func": __anonymize_password},
+
+ # SSTP passwords
+ {"base_path": ['vpn', 'sstp', 'remote-access', 'authentication', 'local-users', 'username'], "secret_path": ['password'], "func": __anonymize_password},
+
+ # OpenConnect passwords
+ {"base_path": ['vpn', 'openconnect', 'authentication', 'local-users', 'username'], "secret_path": ['password'], "func": __anonymize_password},
+
+ # PPPoE server passwords
+ {"base_path": ['service', 'pppoe-server', 'authentication', 'local-users', 'username'], "secret_path": ['password'], "func": __anonymize_password},
+
+ # RADIUS PSKs for VPN services
+ {"base_path": ["vpn", "sstp", "authentication", "radius", "server"], "secret_path": ["key"], "func": __anonymize_password},
+ {"base_path": ["vpn", "l2tp", "authentication", "radius", "server"], "secret_path": ["key"], "func": __anonymize_password},
+ {"base_path": ["vpn", "pptp", "authentication", "radius", "server"], "secret_path": ["key"], "func": __anonymize_password},
+ {"base_path": ["vpn", "openconnect", "authentication", "radius", "server"], "secret_path": ["key"], "func": __anonymize_password},
+ {"base_path": ["service", "ipoe-server", "authentication", "radius", "server"], "secret_path": ["key"], "func": __anonymize_password},
+ {"base_path": ["service", "pppoe-server", "authentication", "radius", "server"], "secret_path": ["key"], "func": __anonymize_password},
+
+ # VRRP passwords
+ {"base_path": ['high-availability', 'vrrp', 'group'], "secret_path": ['authentication', 'password'], "func": __anonymize_password},
+
+ # BGP neighbor and peer group passwords
+ {"base_path": ['protocols', 'bgp', 'neighbor'], "secret_path": ["password"], "func": __anonymize_password},
+ {"base_path": ['protocols', 'bgp', 'peer-group'], "secret_path": ["password"], "func": __anonymize_password},
+
+ # WireGuard private keys
+ {"base_path": ["interfaces", "wireguard"], "secret_path": ["private-key"], "func": __anonymize_password},
+
+ # NHRP passwords
+ {"base_path": ["protocols", "nhrp", "tunnel"], "secret_path": ["cisco-authentication"], "func": __anonymize_password},
+
+ # RIP passwords
+ {"base_path": ["protocols", "rip", "interface"], "secret_path": ["authentication", "plaintext-password"], "func": __anonymize_password},
+
+ # IS-IS passwords
+ {"path": ["protocols", "isis", "area-password", "plaintext-password"], "func": __anonymize_password},
+ {"base_path": ["protocols", "isis", "interface"], "secret_path": ["password", "plaintext-password"], "func": __anonymize_password},
+
+ # HTTP API servers
+ {"base_path": ["service", "https", "api", "keys", "id"], "secret_path": ["key"], "func": __anonymize_password},
+
+ # Telegraf
+ {"path": ["service", "monitoring", "telegraf", "prometheus-client", "authentication", "password"], "func": __anonymize_password},
+ {"path": ["service", "monitoring", "telegraf", "influxdb", "authentication", "token"], "func": __anonymize_password},
+ {"path": ["service", "monitoring", "telegraf", "azure-data-explorer", "authentication", "client-secret"], "func": __anonymize_password},
+ {"path": ["service", "monitoring", "telegraf", "splunk", "authentication", "token"], "func": __anonymize_password},
+
+ # SNMPv3 passwords
+ {"base_path": ["service", "snmp", "v3", "user"], "secret_path": ["privacy", "encrypted-password"], "func": __anonymize_password},
+ {"base_path": ["service", "snmp", "v3", "user"], "secret_path": ["privacy", "plaintext-password"], "func": __anonymize_password},
+ {"base_path": ["service", "snmp", "v3", "user"], "secret_path": ["auth", "encrypted-password"], "func": __anonymize_password},
+ {"base_path": ["service", "snmp", "v3", "user"], "secret_path": ["auth", "encrypted-password"], "func": __anonymize_password},
+]
+
+def __prepare_secret_paths(config_tree, secret_paths):
+ """ Generate a list of secret paths for the current system,
+ adjusted for variable parts such as VRFs and remote access IPsec instances
+ """
+
+ # Fixup for remote-access IPsec local users that are nested under two tag nodes
+ # We generate the list of their paths dynamically
+ ipsec_ra_base = {"base_path": ["vpn", "ipsec", "remote-access", "connection"], "func": __anonymize_password}
+ if config_tree.exists(ipsec_ra_base["base_path"]):
+ for conn in config_tree.list_nodes(ipsec_ra_base["base_path"]):
+ if config_tree.exists(ipsec_ra_base["base_path"] + [conn] + ["authentication", "local-users", "username"]):
+ for u in config_tree.list_nodes(ipsec_ra_base["base_path"] + [conn] + ["authentication", "local-users", "username"]):
+ p = copy.copy(ipsec_ra_base)
+ p["base_path"] = p["base_path"] + [conn] + ["authentication", "local-users", "username"]
+ p["secret_path"] = ["password"]
+ secret_paths.append(p)
+
+ # Fixup for VRFs that may contain routing protocols and other nodes nested under them
+ vrf_paths = []
+ vrf_base_path = ["vrf", "name"]
+ if config_tree.exists(vrf_base_path):
+ for v in config_tree.list_nodes(vrf_base_path):
+ vrf_secret_paths = copy.deepcopy(secret_paths)
+ for sp in vrf_secret_paths:
+ if "base_path" in sp:
+ sp["base_path"] = vrf_base_path + [v] + sp["base_path"]
+ elif "path" in sp:
+ sp["path"] = vrf_base_path + [v] + sp["path"]
+ vrf_paths.append(sp)
+
+ secret_paths = secret_paths + vrf_paths
+
+ # Fixup for user SSH keys, that are nested under a tag node
+ #ssh_key_base_path = {"base_path": ['system', 'login', 'user'], "secret_path": ["authentication", "encrypted-password"], "func": __anonymize_password},
+ user_base_path = ['system', 'login', 'user']
+ ssh_key_paths = []
+ if config_tree.exists(user_base_path):
+ for u in config_tree.list_nodes(user_base_path):
+ kp = {"base_path": user_base_path + [u, "authentication", "public-keys"], "secret_path": ["key"], "func": __anonymize_key}
+ ssh_key_paths.append(kp)
+
+ secret_paths = secret_paths + ssh_key_paths
+
+ # Fixup for OSPF passwords and keys that are nested under OSPF interfaces
+ ospf_base_path = ["protocols", "ospf", "interface"]
+ ospf_paths = []
+ if config_tree.exists(ospf_base_path):
+ for i in config_tree.list_nodes(ospf_base_path):
+ # Plaintext password, there can be only one
+ opp = {"path": ospf_base_path + [i, "authentication", "plaintext-password"], "func": __anonymize_password}
+ md5kp = {"base_path": ospf_base_path + [i, "authentication", "md5", "key-id"], "secret_path": ["md5-key"], "func": __anonymize_password}
+ ospf_paths.append(opp)
+ ospf_paths.append(md5kp)
+
+ secret_paths = secret_paths + ospf_paths
+
+ return secret_paths
+
+def __strip_private(ct, secret_paths):
+ for sp in secret_paths:
+ if "base_path" in sp:
+ if ct.exists(sp["base_path"]):
+ for n in ct.list_nodes(sp["base_path"]):
+ if ct.exists(sp["base_path"] + [n] + sp["secret_path"]):
+ secret = ct.return_value(sp["base_path"] + [n] + sp["secret_path"])
+ ct.set(sp["base_path"] + [n] + sp["secret_path"], value=sp["func"](secret))
+ elif "path" in sp:
+ if ct.exists(sp["path"]):
+ secret = ct.return_value(sp["path"])
+ ct.set(sp["path"], value=sp["func"](secret))
+ else:
+ raise ValueError("Malformed secret path dict, has neither base_path nor path in it ")
+
+ return ct.to_string()
+
+def strip_config_source(config_source):
+ config_tree = vyos.configtree.ConfigTree(config_source)
+ secret_paths = __prepare_secret_paths(config_tree, __secret_paths)
+ stripped_config = __strip_private(config_tree, secret_paths)
+
+ return stripped_config
+
+def strip_config_tree(config_tree):
+ secret_paths = __prepare_secret_paths(config_tree, __secret_paths)
+ return __strip_private(config_tree, secret_paths)
diff --git a/ruff.toml b/ruff.toml
new file mode 100644
index 000000000..189f2838f
--- /dev/null
+++ b/ruff.toml
@@ -0,0 +1,18 @@
+# Same as Black.
+line-length = 88
+indent-width = 4
+
+# Assume Python 3.11
+target-version = "py311"
+
+[format]
+quote-style = "single"
+
+# Like Black, indent with spaces, rather than tabs.
+indent-style = "space"
+
+# Like Black, respect magic trailing commas.
+skip-magic-trailing-comma = false
+
+# Like Black, automatically detect the appropriate line ending.
+line-ending = "auto"
diff --git a/scripts/build-command-op-templates b/scripts/build-command-op-templates
index 46ad634b9..d203fdcef 100755
--- a/scripts/build-command-op-templates
+++ b/scripts/build-command-op-templates
@@ -3,7 +3,7 @@
# build-command-template: converts new style command definitions in XML
# to the old style (bunch of dirs and node.def's) command templates
#
-# Copyright (C) 2017 VyOS maintainers <maintainers@vyos.net>
+# Copyright (C) 2017-2024 VyOS maintainers <maintainers@vyos.net>
#
# This library is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public
@@ -20,6 +20,7 @@
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301
# USA
+import re
import sys
import os
import argparse
@@ -108,7 +109,8 @@ def get_properties(p):
for i in lists:
comp_exprs.append("echo \"{0}\"".format(i.text))
for i in paths:
- comp_exprs.append("/bin/cli-shell-api listActiveNodes {0} | sed -e \"s/'//g\" && echo".format(i.text))
+ path = re.sub(r'\s+', '/', i.text)
+ comp_exprs.append("ls /opt/vyatta/config/active/{0} 2>/dev/null".format(path))
for i in scripts:
comp_exprs.append("{0}".format(i.text))
if comptype is not None:
diff --git a/smoketest/config-tests/dialup-router-medium-vpn b/smoketest/config-tests/dialup-router-medium-vpn
index 67af456f4..d6b00c678 100644
--- a/smoketest/config-tests/dialup-router-medium-vpn
+++ b/smoketest/config-tests/dialup-router-medium-vpn
@@ -33,7 +33,7 @@ set interfaces ethernet eth1 mtu '9000'
set interfaces ethernet eth1 offload gro
set interfaces ethernet eth1 speed 'auto'
set interfaces loopback lo
-set interfaces openvpn vtun0 encryption ncp-ciphers 'aes256'
+set interfaces openvpn vtun0 encryption data-ciphers 'aes256'
set interfaces openvpn vtun0 hash 'sha512'
set interfaces openvpn vtun0 ip adjust-mss '1380'
set interfaces openvpn vtun0 ip source-validation 'strict'
@@ -52,7 +52,7 @@ set interfaces openvpn vtun0 tls ca-certificate 'openvpn_vtun0_2'
set interfaces openvpn vtun0 tls certificate 'openvpn_vtun0'
set interfaces openvpn vtun1 authentication password 'vyos1'
set interfaces openvpn vtun1 authentication username 'vyos1'
-set interfaces openvpn vtun1 encryption ncp-ciphers 'aes256'
+set interfaces openvpn vtun1 encryption data-ciphers 'aes256'
set interfaces openvpn vtun1 hash 'sha1'
set interfaces openvpn vtun1 ip adjust-mss '1380'
set interfaces openvpn vtun1 keep-alive failure-count '3'
@@ -77,7 +77,7 @@ set interfaces openvpn vtun1 tls ca-certificate 'openvpn_vtun1_2'
set interfaces openvpn vtun2 authentication password 'vyos2'
set interfaces openvpn vtun2 authentication username 'vyos2'
set interfaces openvpn vtun2 disable
-set interfaces openvpn vtun2 encryption ncp-ciphers 'aes256'
+set interfaces openvpn vtun2 encryption data-ciphers 'aes256'
set interfaces openvpn vtun2 hash 'sha512'
set interfaces openvpn vtun2 ip adjust-mss '1380'
set interfaces openvpn vtun2 keep-alive failure-count '3'
diff --git a/smoketest/scripts/cli/base_interfaces_test.py b/smoketest/scripts/cli/base_interfaces_test.py
index 9be2c2f1a..e7e29387f 100644
--- a/smoketest/scripts/cli/base_interfaces_test.py
+++ b/smoketest/scripts/cli/base_interfaces_test.py
@@ -15,7 +15,6 @@
from netifaces import AF_INET
from netifaces import AF_INET6
from netifaces import ifaddresses
-from netifaces import interfaces
from base_vyostest_shim import VyOSUnitTestSHIM
@@ -25,12 +24,15 @@ from vyos.ifconfig import Interface
from vyos.ifconfig import Section
from vyos.utils.file import read_file
from vyos.utils.dict import dict_search
+from vyos.utils.process import cmd
from vyos.utils.process import process_named_running
from vyos.utils.network import get_interface_config
from vyos.utils.network import get_interface_vrf
-from vyos.utils.process import cmd
+from vyos.utils.network import get_vrf_tableid
+from vyos.utils.network import interface_exists
from vyos.utils.network import is_intf_addr_assigned
from vyos.utils.network import is_ipv6_link_local
+from vyos.utils.network import get_nft_vrf_zone_mapping
from vyos.xml_ref import cli_defined
dhclient_base_dir = directories['isc_dhclient_dir']
@@ -116,8 +118,11 @@ class BasicInterfaceTest:
self.cli_commit()
# Verify that no previously interface remained on the system
+ ct_map = get_nft_vrf_zone_mapping()
for intf in self._interfaces:
- self.assertNotIn(intf, interfaces())
+ self.assertFalse(interface_exists(intf))
+ for map_entry in ct_map:
+ self.assertNotEqual(intf, map_entry['interface'])
# No daemon started during tests should remain running
for daemon in ['dhcp6c', 'dhclient']:
@@ -257,6 +262,69 @@ class BasicInterfaceTest:
self.cli_delete(['vrf', 'name', vrf_name])
+ def test_move_interface_between_vrf_instances(self):
+ if not self._test_vrf:
+ self.skipTest('not supported')
+
+ vrf1_name = 'smoketest_mgmt1'
+ vrf1_table = '5424'
+ vrf2_name = 'smoketest_mgmt2'
+ vrf2_table = '7412'
+
+ self.cli_set(['vrf', 'name', vrf1_name, 'table', vrf1_table])
+ self.cli_set(['vrf', 'name', vrf2_name, 'table', vrf2_table])
+
+ # move interface into first VRF
+ for interface in self._interfaces:
+ for option in self._options.get(interface, []):
+ self.cli_set(self._base_path + [interface] + option.split())
+ self.cli_set(self._base_path + [interface, 'vrf', vrf1_name])
+
+ self.cli_commit()
+
+ # check that interface belongs to proper VRF
+ for interface in self._interfaces:
+ tmp = get_interface_vrf(interface)
+ self.assertEqual(tmp, vrf1_name)
+
+ tmp = get_interface_config(vrf1_name)
+ self.assertEqual(int(vrf1_table), get_vrf_tableid(interface))
+
+ # move interface into second VRF
+ for interface in self._interfaces:
+ self.cli_set(self._base_path + [interface, 'vrf', vrf2_name])
+
+ self.cli_commit()
+
+ # check that interface belongs to proper VRF
+ for interface in self._interfaces:
+ tmp = get_interface_vrf(interface)
+ self.assertEqual(tmp, vrf2_name)
+
+ tmp = get_interface_config(vrf2_name)
+ self.assertEqual(int(vrf2_table), get_vrf_tableid(interface))
+
+ self.cli_delete(['vrf', 'name', vrf1_name])
+ self.cli_delete(['vrf', 'name', vrf2_name])
+
+ def test_add_to_invalid_vrf(self):
+ if not self._test_vrf:
+ self.skipTest('not supported')
+
+ # move interface into first VRF
+ for interface in self._interfaces:
+ for option in self._options.get(interface, []):
+ self.cli_set(self._base_path + [interface] + option.split())
+ self.cli_set(self._base_path + [interface, 'vrf', 'invalid'])
+
+ # check validate() - can not use a non-existing VRF
+ with self.assertRaises(ConfigSessionError):
+ self.cli_commit()
+
+ for interface in self._interfaces:
+ self.cli_delete(self._base_path + [interface, 'vrf', 'invalid'])
+ self.cli_set(self._base_path + [interface, 'description', 'test_add_to_invalid_vrf'])
+
def test_span_mirror(self):
if not self._mirror_interfaces:
self.skipTest('not supported')
diff --git a/smoketest/scripts/cli/base_vyostest_shim.py b/smoketest/scripts/cli/base_vyostest_shim.py
index 4bcc50453..940306ac3 100644
--- a/smoketest/scripts/cli/base_vyostest_shim.py
+++ b/smoketest/scripts/cli/base_vyostest_shim.py
@@ -15,6 +15,7 @@
import os
import unittest
import paramiko
+import pprint
from time import sleep
from typing import Type
@@ -80,18 +81,32 @@ class VyOSUnitTestSHIM:
self._session.discard()
def cli_commit(self):
+ if self.debug:
+ print('commit')
self._session.commit()
# during a commit there is a process opening commit_lock, and run() returns 0
while run(f'sudo lsof -nP {commit_lock}') == 0:
sleep(0.250)
+ def op_mode(self, path : list) -> None:
+ """
+ Execute OP-mode command and return stdout
+ """
+ if self.debug:
+ print('commit')
+ path = ' '.join(path)
+ out = cmd(f'/opt/vyatta/bin/vyatta-op-cmd-wrapper {path}')
+ if self.debug:
+ print(f'\n\ncommand "{path}" returned:\n')
+ pprint.pprint(out)
+ return out
+
def getFRRconfig(self, string=None, end='$', endsection='^!', daemon=''):
""" Retrieve current "running configuration" from FRR """
command = f'vtysh -c "show run {daemon} no-header"'
if string: command += f' | sed -n "/^{string}{end}/,/{endsection}/p"'
out = cmd(command)
if self.debug:
- import pprint
print(f'\n\ncommand "{command}" returned:\n')
pprint.pprint(out)
return out
diff --git a/smoketest/scripts/cli/test_config_dependency.py b/smoketest/scripts/cli/test_config_dependency.py
new file mode 100755
index 000000000..14e88321a
--- /dev/null
+++ b/smoketest/scripts/cli/test_config_dependency.py
@@ -0,0 +1,49 @@
+#!/usr/bin/env python3
+# Copyright 2024 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 unittest
+
+from base_vyostest_shim import VyOSUnitTestSHIM
+
+from vyos.configsession import ConfigSessionError
+
+
+class TestConfigDep(VyOSUnitTestSHIM.TestCase):
+ def test_configdep_error(self):
+ address_group = 'AG'
+ address = '192.168.137.5'
+ nat_base = ['nat', 'source', 'rule', '10']
+ interface = 'eth1'
+
+ self.cli_set(['firewall', 'group', 'address-group', address_group,
+ 'address', address])
+ self.cli_set(nat_base + ['outbound-interface', 'name', interface])
+ self.cli_set(nat_base + ['source', 'group', 'address-group', address_group])
+ self.cli_set(nat_base + ['translation', 'address', 'masquerade'])
+ self.cli_commit()
+
+ self.cli_delete(['firewall'])
+ # check error in call to dependent script (nat)
+ with self.assertRaises(ConfigSessionError):
+ self.cli_commit()
+
+ # clean up remaining
+ self.cli_delete(['nat'])
+ self.cli_commit()
+
+if __name__ == '__main__':
+ unittest.main(verbosity=2)
diff --git a/smoketest/scripts/cli/test_firewall.py b/smoketest/scripts/cli/test_firewall.py
index 0943d8e24..e6317050c 100755
--- a/smoketest/scripts/cli/test_firewall.py
+++ b/smoketest/scripts/cli/test_firewall.py
@@ -995,5 +995,81 @@ class TestFirewall(VyOSUnitTestSHIM.TestCase):
self.verify_nftables_chain([['accept']], 'ip vyos_conntrack', 'FW_CONNTRACK')
self.verify_nftables_chain([['accept']], 'ip6 vyos_conntrack', 'FW_CONNTRACK')
+ def test_ipsec_metadata_match(self):
+ self.cli_set(['firewall', 'ipv4', 'name', 'smoketest-ipsec-in4', 'rule', '1', 'action', 'accept'])
+ self.cli_set(['firewall', 'ipv4', 'name', 'smoketest-ipsec-in4', 'rule', '1', 'ipsec', 'match-ipsec-in'])
+ self.cli_set(['firewall', 'ipv4', 'name', 'smoketest-ipsec-in4', 'rule', '2', 'action', 'drop'])
+ self.cli_set(['firewall', 'ipv4', 'name', 'smoketest-ipsec-in4', 'rule', '2', 'ipsec', 'match-none-in'])
+ self.cli_set(['firewall', 'ipv4', 'name', 'smoketest-ipsec-out4', 'rule', '1', 'action', 'continue'])
+ self.cli_set(['firewall', 'ipv4', 'name', 'smoketest-ipsec-out4', 'rule', '1', 'ipsec', 'match-ipsec-out'])
+ self.cli_set(['firewall', 'ipv4', 'name', 'smoketest-ipsec-out4', 'rule', '2', 'action', 'reject'])
+ self.cli_set(['firewall', 'ipv4', 'name', 'smoketest-ipsec-out4', 'rule', '2', 'ipsec', 'match-none-out'])
+ self.cli_set(['firewall', 'ipv6', 'name', 'smoketest-ipsec-in6', 'rule', '1', 'action', 'accept'])
+ self.cli_set(['firewall', 'ipv6', 'name', 'smoketest-ipsec-in6', 'rule', '1', 'ipsec', 'match-ipsec-in'])
+ self.cli_set(['firewall', 'ipv6', 'name', 'smoketest-ipsec-in6', 'rule', '2', 'action', 'drop'])
+ self.cli_set(['firewall', 'ipv6', 'name', 'smoketest-ipsec-in6', 'rule', '2', 'ipsec', 'match-none-in'])
+ self.cli_set(['firewall', 'ipv6', 'name', 'smoketest-ipsec-out6', 'rule', '1', 'action', 'continue'])
+ self.cli_set(['firewall', 'ipv6', 'name', 'smoketest-ipsec-out6', 'rule', '1', 'ipsec', 'match-ipsec-out'])
+ self.cli_set(['firewall', 'ipv6', 'name', 'smoketest-ipsec-out6', 'rule', '2', 'action', 'reject'])
+ self.cli_set(['firewall', 'ipv6', 'name', 'smoketest-ipsec-out6', 'rule', '2', 'ipsec', 'match-none-out'])
+
+ self.cli_commit()
+
+ nftables_search = [
+ ['meta ipsec exists', 'accept comment'],
+ ['meta ipsec missing', 'drop comment'],
+ ['rt ipsec exists', 'continue comment'],
+ ['rt ipsec missing', 'reject comment'],
+ ]
+
+ self.verify_nftables(nftables_search, 'ip vyos_filter')
+ self.verify_nftables(nftables_search, 'ip6 vyos_filter')
+
+ self.cli_set(['firewall', 'ipv4', 'input', 'filter', 'rule', '1', 'action', 'jump'])
+ self.cli_set(['firewall', 'ipv4', 'input', 'filter', 'rule', '1', 'jump-target', 'smoketest-ipsec-in4'])
+ self.cli_set(['firewall', 'ipv4', 'forward', 'filter', 'rule', '1', 'action', 'jump'])
+ self.cli_set(['firewall', 'ipv4', 'forward', 'filter', 'rule', '1', 'jump-target', 'smoketest-ipsec-in4'])
+ self.cli_set(['firewall', 'ipv4', 'prerouting', 'raw', 'rule', '1', 'action', 'jump'])
+ self.cli_set(['firewall', 'ipv4', 'prerouting', 'raw', 'rule', '1', 'jump-target', 'smoketest-ipsec-in4'])
+
+ self.cli_set(['firewall', 'ipv4', 'output', 'filter', 'rule', '1', 'action', 'jump'])
+ self.cli_set(['firewall', 'ipv4', 'output', 'filter', 'rule', '1', 'jump-target', 'smoketest-ipsec-out4'])
+ self.cli_set(['firewall', 'ipv4', 'forward', 'filter', 'rule', '1', 'action', 'jump'])
+ self.cli_set(['firewall', 'ipv4', 'forward', 'filter', 'rule', '1', 'jump-target', 'smoketest-ipsec-out4'])
+
+ # All valid directional usage of ipsec matches
+ self.cli_commit()
+
+ self.cli_set(['firewall', 'ipv4', 'name', 'smoketest-ipsec-in-indirect', 'rule', '1', 'action', 'jump'])
+ self.cli_set(['firewall', 'ipv4', 'name', 'smoketest-ipsec-in-indirect', 'rule', '1', 'jump-target', 'smoketest-ipsec-in4'])
+
+ self.cli_set(['firewall', 'ipv4', 'output', 'filter', 'rule', '1', 'action', 'jump'])
+ self.cli_set(['firewall', 'ipv4', 'output', 'filter', 'rule', '1', 'jump-target', 'smoketest-ipsec-in-indirect'])
+
+ # nft does not support ANY usage of 'meta ipsec' under an output hook, it will fail to load cfg
+ with self.assertRaises(ConfigSessionError):
+ self.cli_commit()
+
+ def test_cyclic_jump_validation(self):
+ self.cli_set(['firewall', 'ipv4', 'name', 'smoketest-cycle-1', 'rule', '1', 'action', 'jump'])
+ self.cli_set(['firewall', 'ipv4', 'name', 'smoketest-cycle-1', 'rule', '1', 'jump-target', 'smoketest-cycle-2'])
+ self.cli_set(['firewall', 'ipv4', 'name', 'smoketest-cycle-2', 'rule', '1', 'action', 'jump'])
+ self.cli_set(['firewall', 'ipv4', 'name', 'smoketest-cycle-2', 'rule', '1', 'jump-target', 'smoketest-cycle-3'])
+ self.cli_set(['firewall', 'ipv4', 'name', 'smoketest-cycle-3', 'rule', '1', 'action', 'accept'])
+ self.cli_set(['firewall', 'ipv4', 'name', 'smoketest-cycle-3', 'rule', '1', 'log'])
+ self.cli_set(['firewall', 'ipv4', 'input', 'filter', 'rule', '1', 'action', 'jump'])
+ self.cli_set(['firewall', 'ipv4', 'input', 'filter', 'rule', '1', 'jump-target', 'smoketest-cycle-1'])
+
+ # Multi-level jumps are unwise but allowed
+ self.cli_commit()
+
+ self.cli_set(['firewall', 'ipv4', 'name', 'smoketest-cycle-3', 'rule', '1', 'action', 'jump'])
+ self.cli_set(['firewall', 'ipv4', 'name', 'smoketest-cycle-3', 'rule', '1', 'jump-target', 'smoketest-cycle-1'])
+
+ # nft will fail to load cyclic jumps in any form, whether the rule is reachable or not.
+ # It should be caught by conf validation.
+ with self.assertRaises(ConfigSessionError):
+ self.cli_commit()
+
if __name__ == '__main__':
unittest.main(verbosity=2)
diff --git a/smoketest/scripts/cli/test_interfaces_l2tpv3.py b/smoketest/scripts/cli/test_interfaces_l2tpv3.py
index af3d49f75..28165736b 100755
--- a/smoketest/scripts/cli/test_interfaces_l2tpv3.py
+++ b/smoketest/scripts/cli/test_interfaces_l2tpv3.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2020-2021 VyOS maintainers and contributors
+# Copyright (C) 2020-2024 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,12 @@
# 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 json
import unittest
from base_interfaces_test import BasicInterfaceTest
from vyos.utils.process import cmd
-
+from vyos.utils.kernel import unload_kmod
class L2TPv3InterfaceTest(BasicInterfaceTest.TestCase):
@classmethod
def setUpClass(cls):
@@ -62,7 +61,6 @@ if __name__ == '__main__':
# reloaded on demand - not needed but test more and more features
for module in ['l2tp_ip6', 'l2tp_ip', 'l2tp_eth', 'l2tp_eth',
'l2tp_netlink', 'l2tp_core']:
- if os.path.exists(f'/sys/module/{module}'):
- cmd(f'sudo rmmod {module}')
+ unload_kmod(module)
unittest.main(verbosity=2)
diff --git a/smoketest/scripts/cli/test_interfaces_openvpn.py b/smoketest/scripts/cli/test_interfaces_openvpn.py
index 9ca661e87..ca47c3218 100755
--- a/smoketest/scripts/cli/test_interfaces_openvpn.py
+++ b/smoketest/scripts/cli/test_interfaces_openvpn.py
@@ -123,7 +123,7 @@ class TestInterfacesOpenVPN(VyOSUnitTestSHIM.TestCase):
interface = 'vtun2000'
path = base_path + [interface]
self.cli_set(path + ['mode', 'client'])
- self.cli_set(path + ['encryption', 'ncp-ciphers', 'aes192gcm'])
+ self.cli_set(path + ['encryption', 'data-ciphers', 'aes192gcm'])
# check validate() - cannot specify local-port in client mode
self.cli_set(path + ['local-port', '5000'])
@@ -197,7 +197,7 @@ class TestInterfacesOpenVPN(VyOSUnitTestSHIM.TestCase):
auth_hash = 'sha1'
self.cli_set(path + ['device-type', 'tun'])
- self.cli_set(path + ['encryption', 'ncp-ciphers', 'aes256'])
+ self.cli_set(path + ['encryption', 'data-ciphers', 'aes256'])
self.cli_set(path + ['hash', auth_hash])
self.cli_set(path + ['mode', 'client'])
self.cli_set(path + ['persistent-tunnel'])
@@ -371,7 +371,7 @@ class TestInterfacesOpenVPN(VyOSUnitTestSHIM.TestCase):
port = str(2000 + ii)
self.cli_set(path + ['device-type', 'tun'])
- self.cli_set(path + ['encryption', 'ncp-ciphers', 'aes192'])
+ self.cli_set(path + ['encryption', 'data-ciphers', 'aes192'])
self.cli_set(path + ['hash', auth_hash])
self.cli_set(path + ['mode', 'server'])
self.cli_set(path + ['local-port', port])
@@ -462,8 +462,8 @@ class TestInterfacesOpenVPN(VyOSUnitTestSHIM.TestCase):
self.cli_set(path + ['mode', 'site-to-site'])
- # check validate() - encryption ncp-ciphers cannot be specified in site-to-site mode
- self.cli_set(path + ['encryption', 'ncp-ciphers', 'aes192gcm'])
+ # check validate() - cipher negotiation cannot be enabled in site-to-site mode
+ self.cli_set(path + ['encryption', 'data-ciphers', 'aes192gcm'])
with self.assertRaises(ConfigSessionError):
self.cli_commit()
self.cli_delete(path + ['encryption'])
diff --git a/smoketest/scripts/cli/test_interfaces_vxlan.py b/smoketest/scripts/cli/test_interfaces_vxlan.py
index 18676491b..b2076b43b 100755
--- a/smoketest/scripts/cli/test_interfaces_vxlan.py
+++ b/smoketest/scripts/cli/test_interfaces_vxlan.py
@@ -27,6 +27,13 @@ from vyos.utils.network import get_vxlan_vni_filter
from vyos.template import is_ipv6
from base_interfaces_test import BasicInterfaceTest
+def convert_to_list(ranges_to_convert):
+ result_list = []
+ for r in ranges_to_convert:
+ ranges = r.split('-')
+ result_list.extend([str(i) for i in range(int(ranges[0]), int(ranges[1]) + 1)])
+ return result_list
+
class VXLANInterfaceTest(BasicInterfaceTest.TestCase):
@classmethod
def setUpClass(cls):
@@ -153,6 +160,11 @@ class VXLANInterfaceTest(BasicInterfaceTest.TestCase):
'31': '10031',
}
+ vlan_to_vni_ranges = {
+ '40-43': '10040-10043',
+ '45-47': '10045-10047'
+ }
+
self.cli_set(self._base_path + [interface, 'parameters', 'external'])
self.cli_set(self._base_path + [interface, 'source-address', source_address])
@@ -185,6 +197,26 @@ class VXLANInterfaceTest(BasicInterfaceTest.TestCase):
tmp = get_vxlan_vlan_tunnels('vxlan0')
self.assertEqual(tmp, list(vlan_to_vni))
+ # add ranged VLAN - VNI mapping
+ for vlan, vni in vlan_to_vni_ranges.items():
+ self.cli_set(self._base_path + [interface, 'vlan-to-vni', vlan, 'vni', vni])
+ self.cli_commit()
+
+ tmp = get_vxlan_vlan_tunnels('vxlan0')
+ vlans_list = convert_to_list(vlan_to_vni_ranges.keys())
+ self.assertEqual(tmp, list(vlan_to_vni) + vlans_list)
+
+ # check validate() - cannot map VNI range to a single VLAN id
+ self.cli_set(self._base_path + [interface, 'vlan-to-vni', '100', 'vni', '100-102'])
+ with self.assertRaises(ConfigSessionError):
+ self.cli_commit()
+ self.cli_delete(self._base_path + [interface, 'vlan-to-vni', '100'])
+
+ # check validate() - cannot map VLAN to VNI with different ranges
+ self.cli_set(self._base_path + [interface, 'vlan-to-vni', '100-102', 'vni', '100-105'])
+ with self.assertRaises(ConfigSessionError):
+ self.cli_commit()
+
self.cli_delete(['interfaces', 'bridge', bridge])
def test_vxlan_neighbor_suppress(self):
@@ -287,6 +319,12 @@ class VXLANInterfaceTest(BasicInterfaceTest.TestCase):
'60': '10060',
'69': '10069',
}
+
+ vlan_to_vni_ranges = {
+ '70-73': '10070-10073',
+ '75-77': '10075-10077'
+ }
+
for vlan, vni in vlan_to_vni.items():
self.cli_set(self._base_path + [interface, 'vlan-to-vni', vlan, 'vni', vni])
# we need a bridge ...
@@ -313,6 +351,15 @@ class VXLANInterfaceTest(BasicInterfaceTest.TestCase):
tmp = get_vxlan_vni_filter(interface)
self.assertListEqual(list(vlan_to_vni.values()), tmp)
+ # add ranged VLAN - VNI mapping
+ for vlan, vni in vlan_to_vni_ranges.items():
+ self.cli_set(self._base_path + [interface, 'vlan-to-vni', vlan, 'vni', vni])
+ self.cli_commit()
+
+ tmp = get_vxlan_vni_filter(interface)
+ vnis_list = convert_to_list(vlan_to_vni_ranges.values())
+ self.assertListEqual(list(vlan_to_vni.values()) + vnis_list, tmp)
+
self.cli_delete(['interfaces', 'bridge', bridge])
if __name__ == '__main__':
diff --git a/smoketest/scripts/cli/test_interfaces_wireless.py b/smoketest/scripts/cli/test_interfaces_wireless.py
index 58aef0001..7bfe0d221 100755
--- a/smoketest/scripts/cli/test_interfaces_wireless.py
+++ b/smoketest/scripts/cli/test_interfaces_wireless.py
@@ -22,9 +22,11 @@ from base_interfaces_test import BasicInterfaceTest
from glob import glob
from vyos.configsession import ConfigSessionError
-from vyos.utils.process import process_named_running
-from vyos.utils.kernel import check_kmod
from vyos.utils.file import read_file
+from vyos.utils.kernel import check_kmod
+from vyos.utils.network import interface_exists
+from vyos.utils.process import process_named_running
+from vyos.utils.process import call
from vyos.xml_ref import default_value
def get_config_value(interface, key):
@@ -33,7 +35,7 @@ def get_config_value(interface, key):
return tmp[0]
wifi_cc_path = ['system', 'wireless', 'country-code']
-
+country = 'se'
class WirelessInterfaceTest(BasicInterfaceTest.TestCase):
@classmethod
def setUpClass(cls):
@@ -66,7 +68,8 @@ class WirelessInterfaceTest(BasicInterfaceTest.TestCase):
cls._test_ipv6 = False
cls._test_vlan = False
- cls.cli_set(cls, wifi_cc_path + ['se'])
+ cls.cli_set(cls, wifi_cc_path + [country])
+
def test_wireless_add_single_ip_address(self):
# derived method to check if member interfaces are enslaved properly
@@ -84,7 +87,7 @@ class WirelessInterfaceTest(BasicInterfaceTest.TestCase):
def test_wireless_hostapd_config(self):
# Only set the hostapd (access-point) options
- interface = 'wlan1'
+ interface = self._interfaces[1] # wlan1
ssid = 'ssid'
self.cli_set(self._base_path + [interface, 'ssid', ssid])
@@ -161,7 +164,7 @@ class WirelessInterfaceTest(BasicInterfaceTest.TestCase):
def test_wireless_hostapd_vht_mu_beamformer_config(self):
# Multi-User-Beamformer
- interface = 'wlan1'
+ interface = self._interfaces[1] # wlan1
ssid = 'vht_mu-beamformer'
antennas = '3'
@@ -230,7 +233,7 @@ class WirelessInterfaceTest(BasicInterfaceTest.TestCase):
def test_wireless_hostapd_vht_su_beamformer_config(self):
# Single-User-Beamformer
- interface = 'wlan1'
+ interface = self._interfaces[1] # wlan1
ssid = 'vht_su-beamformer'
antennas = '3'
@@ -299,16 +302,14 @@ class WirelessInterfaceTest(BasicInterfaceTest.TestCase):
def test_wireless_hostapd_he_config(self):
# Only set the hostapd (access-point) options - HE mode for 802.11ax at 6GHz
- interface = 'wlan1'
+ interface = self._interfaces[1] # wlan1
ssid = 'ssid'
channel = '1'
sae_pw = 'VyOSVyOSVyOS'
- country = 'de'
bss_color = '37'
channel_set_width = '134'
center_channel_freq_1 = '15'
- self.cli_set(wifi_cc_path + [country])
self.cli_set(self._base_path + [interface, 'ssid', ssid])
self.cli_set(self._base_path + [interface, 'type', 'access-point'])
self.cli_set(self._base_path + [interface, 'channel', channel])
@@ -353,10 +354,6 @@ class WirelessInterfaceTest(BasicInterfaceTest.TestCase):
tmp = get_config_value(interface, 'he_oper_centr_freq_seg0_idx')
self.assertEqual(center_channel_freq_1, tmp)
- # Country code
- tmp = get_config_value(interface, 'country_code')
- self.assertEqual(country.upper(), tmp)
-
# BSS coloring
tmp = get_config_value(interface, 'he_bss_color')
self.assertEqual(bss_color, tmp)
@@ -386,15 +383,12 @@ class WirelessInterfaceTest(BasicInterfaceTest.TestCase):
def test_wireless_hostapd_wpa_config(self):
# Only set the hostapd (access-point) options
- interface = 'wlan1'
- phy = 'phy0'
+ interface = self._interfaces[1] # wlan1
ssid = 'VyOS-SMOKETEST'
channel = '1'
wpa_key = 'VyOSVyOSVyOS'
mode = 'n'
- country = 'de'
- self.cli_set(self._base_path + [interface, 'physical-device', phy])
self.cli_set(self._base_path + [interface, 'type', 'access-point'])
self.cli_set(self._base_path + [interface, 'mode', mode])
@@ -454,7 +448,7 @@ class WirelessInterfaceTest(BasicInterfaceTest.TestCase):
self.assertTrue(process_named_running('hostapd'))
def test_wireless_access_point_bridge(self):
- interface = 'wlan1'
+ interface = self._interfaces[1] # wlan1
ssid = 'VyOS-Test'
bridge = 'br42477'
@@ -491,7 +485,7 @@ class WirelessInterfaceTest(BasicInterfaceTest.TestCase):
self.cli_delete(bridge_path)
def test_wireless_security_station_address(self):
- interface = 'wlan1'
+ interface = self._interfaces[1] # wlan1
ssid = 'VyOS-ACL'
hostapd_accept_station_conf = f'/run/hostapd/{interface}_station_accept.conf'
@@ -511,6 +505,12 @@ class WirelessInterfaceTest(BasicInterfaceTest.TestCase):
self.cli_commit()
+ self.assertTrue(interface_exists(interface))
+ self.assertTrue(os.path.isfile(f'/run/hostapd/{interface}_station_accept.conf'))
+ self.assertTrue(os.path.isfile(f'/run/hostapd/{interface}_station_deny.conf'))
+
+ self.assertTrue(process_named_running('hostapd'))
+
# in accept mode all addresses are allowed unless specified in the deny list
tmp = get_config_value(interface, 'macaddr_acl')
self.assertEqual(tmp, '0')
@@ -526,6 +526,11 @@ class WirelessInterfaceTest(BasicInterfaceTest.TestCase):
# Switch mode accept -> deny
self.cli_set(self._base_path + [interface, 'security', 'station-address', 'mode', 'deny'])
self.cli_commit()
+
+ self.assertTrue(interface_exists(interface))
+ self.assertTrue(os.path.isfile(f'/run/hostapd/{interface}_station_accept.conf'))
+ self.assertTrue(os.path.isfile(f'/run/hostapd/{interface}_station_deny.conf'))
+
# In deny mode all addresses are denied unless specified in the allow list
tmp = get_config_value(interface, 'macaddr_acl')
self.assertEqual(tmp, '1')
@@ -535,4 +540,9 @@ class WirelessInterfaceTest(BasicInterfaceTest.TestCase):
if __name__ == '__main__':
check_kmod('mac80211_hwsim')
- unittest.main(verbosity=2, failfast=True)
+ # loading the module created two WIFI Interfaces in the background (wlan0 and wlan1)
+ # remove them to have a clean test start
+ for interface in ['wlan0', 'wlan1']:
+ if interface_exists(interface):
+ call(f'sudo iw dev {interface} del')
+ unittest.main(verbosity=2)
diff --git a/smoketest/scripts/cli/test_load-balancing_reverse-proxy.py b/smoketest/scripts/cli/test_load-balancing_reverse-proxy.py
index aa796f59f..34f77b95d 100755
--- a/smoketest/scripts/cli/test_load-balancing_reverse-proxy.py
+++ b/smoketest/scripts/cli/test_load-balancing_reverse-proxy.py
@@ -458,6 +458,45 @@ class TestLoadBalancingReverseProxy(VyOSUnitTestSHIM.TestCase):
config = read_file(HAPROXY_CONF)
self.assertIn(f'option smtpchk', config)
+ def test_09_lb_reverse_proxy_logging(self):
+ # Setup base
+ self.base_config()
+ self.cli_commit()
+
+ # Ensure default logging configuration is present
+ config = read_file(HAPROXY_CONF)
+
+ # Test global-parameters logging options
+ self.cli_set(base_path + ['global-parameters', 'logging', 'facility', 'local1', 'level', 'err'])
+ self.cli_set(base_path + ['global-parameters', 'logging', 'facility', 'local2', 'level', 'warning'])
+ self.cli_commit()
+
+ # Test global logging parameters are generated in configuration file
+ config = read_file(HAPROXY_CONF)
+ self.assertIn('log /dev/log local1 err', config)
+ self.assertIn('log /dev/log local2 warning', config)
+
+ # Test backend logging options
+ backend_path = base_path + ['backend', 'bk-01']
+ self.cli_set(backend_path + ['logging', 'facility', 'local3', 'level', 'debug'])
+ self.cli_set(backend_path + ['logging', 'facility', 'local4', 'level', 'info'])
+ self.cli_commit()
+
+ # Test backend logging parameters are generated in configuration file
+ config = read_file(HAPROXY_CONF)
+ self.assertIn('log /dev/log local3 debug', config)
+ self.assertIn('log /dev/log local4 info', config)
+
+ # Test service logging options
+ service_path = base_path + ['service', 'https_front']
+ self.cli_set(service_path + ['logging', 'facility', 'local5', 'level', 'notice'])
+ self.cli_set(service_path + ['logging', 'facility', 'local6', 'level', 'crit'])
+ self.cli_commit()
+
+ # Test service logging parameters are generated in configuration file
+ config = read_file(HAPROXY_CONF)
+ self.assertIn('log /dev/log local5 notice', config)
+ self.assertIn('log /dev/log local6 crit', config)
if __name__ == '__main__':
unittest.main(verbosity=2)
diff --git a/smoketest/scripts/cli/test_op-mode_show.py b/smoketest/scripts/cli/test_op-mode_show.py
new file mode 100755
index 000000000..fba60cc01
--- /dev/null
+++ b/smoketest/scripts/cli/test_op-mode_show.py
@@ -0,0 +1,39 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2024 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 base_vyostest_shim import VyOSUnitTestSHIM
+
+from vyos.version import get_version
+
+base_path = ['show']
+
+class TestOPModeShow(VyOSUnitTestSHIM.TestCase):
+ def test_op_mode_show_version(self):
+ # Retrieve output of "show version" OP-mode command
+ tmp = self.op_mode(base_path + ['version'])
+ # Validate
+ version = get_version()
+ self.assertIn(f'Version: VyOS {version}', tmp)
+
+ def test_op_mode_show_vrf(self):
+ # Retrieve output of "show version" OP-mode command
+ tmp = self.op_mode(base_path + ['vrf'])
+ # Validate
+ self.assertIn('VRF is not configured', tmp)
+
+if __name__ == '__main__':
+ unittest.main(verbosity=2)
diff --git a/smoketest/scripts/cli/test_policy_route.py b/smoketest/scripts/cli/test_policy_route.py
index 462fc24d0..797ab9770 100755
--- a/smoketest/scripts/cli/test_policy_route.py
+++ b/smoketest/scripts/cli/test_policy_route.py
@@ -25,6 +25,8 @@ conn_mark = '555'
conn_mark_set = '111'
table_mark_offset = 0x7fffffff
table_id = '101'
+vrf = 'PBRVRF'
+vrf_table_id = '102'
interface = 'eth0'
interface_wc = 'ppp*'
interface_ip = '172.16.10.1/24'
@@ -39,11 +41,14 @@ class TestPolicyRoute(VyOSUnitTestSHIM.TestCase):
cls.cli_set(cls, ['interfaces', 'ethernet', interface, 'address', interface_ip])
cls.cli_set(cls, ['protocols', 'static', 'table', table_id, 'route', '0.0.0.0/0', 'interface', interface])
+
+ cls.cli_set(cls, ['vrf', 'name', vrf, 'table', vrf_table_id])
@classmethod
def tearDownClass(cls):
cls.cli_delete(cls, ['interfaces', 'ethernet', interface, 'address', interface_ip])
cls.cli_delete(cls, ['protocols', 'static', 'table', table_id])
+ cls.cli_delete(cls, ['vrf', 'name', vrf])
super(TestPolicyRoute, cls).tearDownClass()
@@ -180,6 +185,50 @@ class TestPolicyRoute(VyOSUnitTestSHIM.TestCase):
self.verify_rules(ip_rule_search)
+ def test_pbr_vrf(self):
+ self.cli_set(['policy', 'route', 'smoketest', 'rule', '1', 'protocol', 'tcp'])
+ self.cli_set(['policy', 'route', 'smoketest', 'rule', '1', 'destination', 'port', '8888'])
+ self.cli_set(['policy', 'route', 'smoketest', 'rule', '1', 'tcp', 'flags', 'syn'])
+ self.cli_set(['policy', 'route', 'smoketest', 'rule', '1', 'tcp', 'flags', 'not', 'ack'])
+ self.cli_set(['policy', 'route', 'smoketest', 'rule', '1', 'set', 'vrf', vrf])
+ self.cli_set(['policy', 'route6', 'smoketest6', 'rule', '1', 'protocol', 'tcp_udp'])
+ self.cli_set(['policy', 'route6', 'smoketest6', 'rule', '1', 'destination', 'port', '8888'])
+ self.cli_set(['policy', 'route6', 'smoketest6', 'rule', '1', 'set', 'vrf', vrf])
+
+ self.cli_set(['policy', 'route', 'smoketest', 'interface', interface])
+ self.cli_set(['policy', 'route6', 'smoketest6', 'interface', interface])
+
+ self.cli_commit()
+
+ mark_hex = "{0:#010x}".format(table_mark_offset - int(vrf_table_id))
+
+ # IPv4
+
+ nftables_search = [
+ [f'iifname "{interface}"', 'jump VYOS_PBR_UD_smoketest'],
+ ['tcp flags syn / syn,ack', 'tcp dport 8888', 'meta mark set ' + mark_hex]
+ ]
+
+ self.verify_nftables(nftables_search, 'ip vyos_mangle')
+
+ # IPv6
+
+ nftables6_search = [
+ [f'iifname "{interface}"', 'jump VYOS_PBR6_UD_smoketest'],
+ ['meta l4proto { tcp, udp }', 'th dport 8888', 'meta mark set ' + mark_hex]
+ ]
+
+ self.verify_nftables(nftables6_search, 'ip6 vyos_mangle')
+
+ # IP rule fwmark -> table
+
+ ip_rule_search = [
+ ['fwmark ' + hex(table_mark_offset - int(vrf_table_id)), 'lookup ' + vrf]
+ ]
+
+ self.verify_rules(ip_rule_search)
+
+
def test_pbr_matching_criteria(self):
self.cli_set(['policy', 'route', 'smoketest', 'default-log'])
self.cli_set(['policy', 'route', 'smoketest', 'rule', '1', 'protocol', 'udp'])
diff --git a/smoketest/scripts/cli/test_protocols_ospf.py b/smoketest/scripts/cli/test_protocols_ospf.py
index 585c1dc89..905eaf2e9 100755
--- a/smoketest/scripts/cli/test_protocols_ospf.py
+++ b/smoketest/scripts/cli/test_protocols_ospf.py
@@ -16,7 +16,6 @@
import unittest
-from time import sleep
from base_vyostest_shim import VyOSUnitTestSHIM
from vyos.configsession import ConfigSessionError
@@ -27,6 +26,7 @@ PROCESS_NAME = 'ospfd'
base_path = ['protocols', 'ospf']
route_map = 'foo-bar-baz10'
+dummy_if = 'dum3562'
class TestProtocolsOSPF(VyOSUnitTestSHIM.TestCase):
@classmethod
@@ -38,6 +38,7 @@ class TestProtocolsOSPF(VyOSUnitTestSHIM.TestCase):
cls.cli_set(cls, ['policy', 'route-map', route_map, 'rule', '10', 'action', 'permit'])
cls.cli_set(cls, ['policy', 'route-map', route_map, 'rule', '20', 'action', 'permit'])
+ cls.cli_set(cls, ['interfaces', 'dummy', dummy_if])
# ensure we can also run this test on a live system - so lets clean
# out the current configuration :)
@@ -46,6 +47,7 @@ class TestProtocolsOSPF(VyOSUnitTestSHIM.TestCase):
@classmethod
def tearDownClass(cls):
cls.cli_delete(cls, ['policy', 'route-map', route_map])
+ cls.cli_delete(cls, ['interfaces', 'dummy', dummy_if])
super(TestProtocolsOSPF, cls).tearDownClass()
def tearDown(self):
@@ -441,14 +443,13 @@ class TestProtocolsOSPF(VyOSUnitTestSHIM.TestCase):
global_block_high = "399"
local_block_low = "400"
local_block_high = "499"
- interface = 'lo'
maximum_stack_size = '5'
prefix_one = '192.168.0.1/32'
prefix_two = '192.168.0.2/32'
prefix_one_value = '1'
prefix_two_value = '2'
- self.cli_set(base_path + ['interface', interface])
+ self.cli_set(base_path + ['interface', dummy_if])
self.cli_set(base_path + ['segment-routing', 'maximum-label-depth', maximum_stack_size])
self.cli_set(base_path + ['segment-routing', 'global-block', 'low-label-value', global_block_low])
self.cli_set(base_path + ['segment-routing', 'global-block', 'high-label-value', global_block_high])
@@ -472,17 +473,14 @@ class TestProtocolsOSPF(VyOSUnitTestSHIM.TestCase):
def test_ospf_15_ldp_sync(self):
holddown = "500"
- interface = 'lo'
interfaces = Section.interfaces('ethernet')
- self.cli_set(base_path + ['interface', interface])
+ self.cli_set(base_path + ['interface', dummy_if])
self.cli_set(base_path + ['ldp-sync', 'holddown', holddown])
# Commit main OSPF changes
self.cli_commit()
- sleep(10)
-
# Verify main OSPF changes
frrconfig = self.getFRRconfig('router ospf', daemon=PROCESS_NAME)
self.assertIn(f'router ospf', frrconfig)
@@ -514,7 +512,7 @@ class TestProtocolsOSPF(VyOSUnitTestSHIM.TestCase):
config = self.getFRRconfig(f'interface {interface}', daemon=PROCESS_NAME)
self.assertIn(f'interface {interface}', config)
self.assertIn(f' ip ospf dead-interval 40', config)
- self.assertIn(f' no ip ospf mpls ldp-sync', config)
+ self.assertNotIn(f' ip ospf mpls ldp-sync', config)
def test_ospf_16_graceful_restart(self):
period = '300'
diff --git a/smoketest/scripts/cli/test_protocols_static.py b/smoketest/scripts/cli/test_protocols_static.py
index c5cf2aab6..f676e2a52 100755
--- a/smoketest/scripts/cli/test_protocols_static.py
+++ b/smoketest/scripts/cli/test_protocols_static.py
@@ -21,6 +21,7 @@ from base_vyostest_shim import VyOSUnitTestSHIM
from vyos.configsession import ConfigSessionError
from vyos.template import is_ipv6
from vyos.utils.network import get_interface_config
+from vyos.utils.network import get_vrf_tableid
base_path = ['protocols', 'static']
vrf_path = ['protocols', 'vrf']
@@ -421,7 +422,7 @@ class TestProtocolsStatic(VyOSUnitTestSHIM.TestCase):
tmp = get_interface_config(vrf)
# Compare VRF table ID
- self.assertEqual(tmp['linkinfo']['info_data']['table'], int(vrf_config['table']))
+ self.assertEqual(get_vrf_tableid(vrf), int(vrf_config['table']))
self.assertEqual(tmp['linkinfo']['info_kind'], 'vrf')
# Verify FRR bgpd configuration
@@ -478,4 +479,4 @@ class TestProtocolsStatic(VyOSUnitTestSHIM.TestCase):
self.assertIn(tmp, frrconfig)
if __name__ == '__main__':
- unittest.main(verbosity=2)
+ unittest.main(verbosity=2, failfast=True)
diff --git a/smoketest/scripts/cli/test_service_snmp.py b/smoketest/scripts/cli/test_service_snmp.py
index b3daa90d0..7d5eaa440 100755
--- a/smoketest/scripts/cli/test_service_snmp.py
+++ b/smoketest/scripts/cli/test_service_snmp.py
@@ -246,5 +246,19 @@ class TestSNMPService(VyOSUnitTestSHIM.TestCase):
for excluded in snmpv3_view_oid_exclude:
self.assertIn(f'view {snmpv3_view} excluded .{excluded}', tmp)
+ def test_snmp_script_extensions(self):
+ extensions = {
+ 'default': 'snmp_smoketest_extension_script.sh',
+ 'external': '/run/external_snmp_smoketest_extension_script.sh'
+ }
+
+ for key, val in extensions.items():
+ self.cli_set(base_path + ['script-extensions', 'extension-name', key, 'script', val])
+ self.cli_commit()
+
+ self.assertEqual(get_config_value('extend default'), f'/config/user-data/{extensions["default"]}')
+ self.assertEqual(get_config_value('extend external'), extensions["external"])
+
+
if __name__ == '__main__':
unittest.main(verbosity=2)
diff --git a/smoketest/scripts/cli/test_service_ssh.py b/smoketest/scripts/cli/test_service_ssh.py
index b09990c92..d8e325eee 100755
--- a/smoketest/scripts/cli/test_service_ssh.py
+++ b/smoketest/scripts/cli/test_service_ssh.py
@@ -304,6 +304,22 @@ class TestServiceSSH(VyOSUnitTestSHIM.TestCase):
for line in ssh_lines:
self.assertIn(line, tmp_sshd_conf)
+ def test_ssh_pubkey_accepted_algorithm(self):
+ algs = ['ssh-ed25519', 'ecdsa-sha2-nistp256', 'ecdsa-sha2-nistp384',
+ 'ecdsa-sha2-nistp521', 'ssh-dss', 'ssh-rsa', 'rsa-sha2-256',
+ 'rsa-sha2-512'
+ ]
+
+ expected = 'PubkeyAcceptedAlgorithms '
+ for alg in algs:
+ self.cli_set(base_path + ['pubkey-accepted-algorithm', alg])
+ expected = f'{expected}{alg},'
+ expected = expected[:-1]
+
+ self.cli_commit()
+ tmp_sshd_conf = read_file(SSHD_CONF)
+ self.assertIn(expected, tmp_sshd_conf)
+
if __name__ == '__main__':
unittest.main(verbosity=2)
diff --git a/smoketest/scripts/cli/test_system_conntrack.py b/smoketest/scripts/cli/test_system_conntrack.py
index 3ae7b6217..c07fdce77 100755
--- a/smoketest/scripts/cli/test_system_conntrack.py
+++ b/smoketest/scripts/cli/test_system_conntrack.py
@@ -20,7 +20,7 @@ import unittest
from base_vyostest_shim import VyOSUnitTestSHIM
from vyos.firewall import find_nftables_rule
-from vyos.utils.file import read_file
+from vyos.utils.file import read_file, read_json
base_path = ['system', 'conntrack']
@@ -28,6 +28,9 @@ def get_sysctl(parameter):
tmp = parameter.replace(r'.', r'/')
return read_file(f'/proc/sys/{tmp}')
+def get_logger_config():
+ return read_json('/run/vyos-conntrack-logger.conf')
+
class TestSystemConntrack(VyOSUnitTestSHIM.TestCase):
@classmethod
def setUpClass(cls):
@@ -280,5 +283,35 @@ class TestSystemConntrack(VyOSUnitTestSHIM.TestCase):
self.verify_nftables(nftables6_search, 'ip6 vyos_conntrack')
self.cli_delete(['firewall'])
+
+ def test_conntrack_log(self):
+ expected_config = {
+ 'event': {
+ 'destroy': {},
+ 'new': {},
+ 'update': {},
+ },
+ 'queue_size': '10000'
+ }
+ self.cli_set(base_path + ['log', 'event', 'destroy'])
+ self.cli_set(base_path + ['log', 'event', 'new'])
+ self.cli_set(base_path + ['log', 'event', 'update'])
+ self.cli_set(base_path + ['log', 'queue-size', '10000'])
+ self.cli_commit()
+ self.assertEqual(expected_config, get_logger_config())
+ self.assertEqual('0', get_sysctl('net.netfilter.nf_conntrack_timestamp'))
+
+ for event in ['destroy', 'new', 'update']:
+ for proto in ['icmp', 'other', 'tcp', 'udp']:
+ self.cli_set(base_path + ['log', 'event', event, proto])
+ expected_config['event'][event][proto] = {}
+ self.cli_set(base_path + ['log', 'timestamp'])
+ expected_config['timestamp'] = {}
+ self.cli_commit()
+
+ self.assertEqual(expected_config, get_logger_config())
+ self.assertEqual('1', get_sysctl('net.netfilter.nf_conntrack_timestamp'))
+
+
if __name__ == '__main__':
unittest.main(verbosity=2)
diff --git a/smoketest/scripts/cli/test_system_syslog.py b/smoketest/scripts/cli/test_system_syslog.py
index 030ec587b..45a5b4087 100755
--- a/smoketest/scripts/cli/test_system_syslog.py
+++ b/smoketest/scripts/cli/test_system_syslog.py
@@ -53,8 +53,8 @@ class TestRSYSLOGService(VyOSUnitTestSHIM.TestCase):
self.assertFalse(process_named_running(PROCESS_NAME))
def test_syslog_basic(self):
- host1 = '198.51.100.1'
- host2 = '192.0.2.1'
+ host1 = '127.0.0.10'
+ host2 = '127.0.0.20'
self.cli_set(base_path + ['host', host1, 'port', '999'])
self.cli_set(base_path + ['host', host1, 'facility', 'all', 'level', 'all'])
@@ -68,7 +68,7 @@ class TestRSYSLOGService(VyOSUnitTestSHIM.TestCase):
# *.* @198.51.100.1:999
# kern.err @192.0.2.1:514
config = [get_config_value('\*.\*'), get_config_value('kern.err'), get_config_value('\*.warning')]
- expected = ['@198.51.100.1:999', '@192.0.2.1:514', '/dev/console']
+ expected = [f'@{host1}:999', f'@{host2}:514', '/dev/console']
for i in range(0,3):
self.assertIn(expected[i], config[i])
diff --git a/smoketest/scripts/cli/test_vpn_ipsec.py b/smoketest/scripts/cli/test_vpn_ipsec.py
index 27356d70e..2dc66485b 100755
--- a/smoketest/scripts/cli/test_vpn_ipsec.py
+++ b/smoketest/scripts/cli/test_vpn_ipsec.py
@@ -252,6 +252,15 @@ class TestVPNIPsec(VyOSUnitTestSHIM.TestCase):
for line in swanctl_conf_lines:
self.assertIn(line, swanctl_conf)
+ # if dpd is not specified it should not be enabled (see T6599)
+ swanctl_unexpected_lines = [
+ f'dpd_timeout'
+ f'dpd_delay'
+ ]
+
+ for unexpected_line in swanctl_unexpected_lines:
+ self.assertNotIn(unexpected_line, swanctl_conf)
+
swanctl_secrets_lines = [
f'id-{regex_uuid4} = "{local_id}"',
f'id-{regex_uuid4} = "{remote_id}"',
@@ -639,8 +648,7 @@ class TestVPNIPsec(VyOSUnitTestSHIM.TestCase):
f'auth = eap-mschapv2',
f'eap_id = %any',
f'esp_proposals = aes256-sha512,aes256-sha384,aes256-sha256,aes256-sha1,aes128gcm128-sha256',
- f'rekey_time = {eap_lifetime}s',
- f'rand_time = 540s',
+ f'life_time = {eap_lifetime}s',
f'dpd_action = clear',
f'replay_window = 32',
f'inactivity = 28800',
@@ -761,8 +769,7 @@ class TestVPNIPsec(VyOSUnitTestSHIM.TestCase):
f'auth = eap-tls',
f'eap_id = %any',
f'esp_proposals = aes256-sha512,aes256-sha384,aes256-sha256,aes256-sha1,aes128gcm128-sha256',
- f'rekey_time = {eap_lifetime}s',
- f'rand_time = 540s',
+ f'life_time = {eap_lifetime}s',
f'dpd_action = clear',
f'inactivity = 28800',
f'local_ts = 0.0.0.0/0,::/0',
@@ -876,8 +883,7 @@ class TestVPNIPsec(VyOSUnitTestSHIM.TestCase):
f'certs = peer1.pem',
f'cacerts = MyVyOS-CA.pem,MyVyOS-IntCA.pem',
f'esp_proposals = aes256-sha512,aes256-sha384,aes256-sha256,aes256-sha1,aes128gcm128-sha256',
- f'rekey_time = {eap_lifetime}s',
- f'rand_time = 540s',
+ f'life_time = {eap_lifetime}s',
f'dpd_action = clear',
f'inactivity = 28800',
f'local_ts = 0.0.0.0/0,::/0',
@@ -968,5 +974,117 @@ class TestVPNIPsec(VyOSUnitTestSHIM.TestCase):
self.tearDownPKI()
+ def test_remote_access_no_rekey(self):
+ # In some RA secnarios, disabling server-initiated rekey of IKE and CHILD SA is desired
+ self.setupPKI()
+
+ ike_group = 'IKE-RW'
+ esp_group = 'ESP-RW'
+
+ conn_name = 'vyos-rw'
+ local_address = '192.0.2.1'
+ ip_pool_name = 'ra-rw-ipv4'
+ ike_lifetime = '7200'
+ eap_lifetime = '3600'
+ local_id = 'ipsec.vyos.net'
+
+ name_servers = ['172.16.254.100', '172.16.254.101']
+ prefix = '172.16.250.0/28'
+
+ # IKE
+ self.cli_set(base_path + ['ike-group', ike_group, 'key-exchange', 'ikev2'])
+ self.cli_set(base_path + ['ike-group', ike_group, 'lifetime', '0'])
+ self.cli_set(base_path + ['ike-group', ike_group, 'proposal', '1', 'dh-group', '14'])
+ self.cli_set(base_path + ['ike-group', ike_group, 'proposal', '1', 'encryption', 'aes256'])
+ self.cli_set(base_path + ['ike-group', ike_group, 'proposal', '1', 'hash', 'sha512'])
+ self.cli_set(base_path + ['ike-group', ike_group, 'proposal', '2', 'dh-group', '14'])
+ 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', 'sha256'])
+ self.cli_set(base_path + ['ike-group', ike_group, 'proposal', '3', 'dh-group', '2'])
+ self.cli_set(base_path + ['ike-group', ike_group, 'proposal', '3', 'encryption', 'aes256'])
+ self.cli_set(base_path + ['ike-group', ike_group, 'proposal', '3', 'hash', 'sha256'])
+ self.cli_set(base_path + ['ike-group', ike_group, 'proposal', '10', 'dh-group', '14'])
+ self.cli_set(base_path + ['ike-group', ike_group, 'proposal', '10', 'encryption', 'aes128gcm128'])
+ self.cli_set(base_path + ['ike-group', ike_group, 'proposal', '10', 'hash', 'sha256'])
+
+ # ESP
+ self.cli_set(base_path + ['esp-group', esp_group, 'lifetime', eap_lifetime])
+ self.cli_set(base_path + ['esp-group', esp_group, 'pfs', 'disable'])
+ self.cli_set(base_path + ['esp-group', esp_group, 'disable-rekey'])
+ self.cli_set(base_path + ['esp-group', esp_group, 'proposal', '1', 'encryption', 'aes256'])
+ self.cli_set(base_path + ['esp-group', esp_group, 'proposal', '1', 'hash', 'sha512'])
+ self.cli_set(base_path + ['esp-group', esp_group, 'proposal', '2', 'encryption', 'aes256'])
+ self.cli_set(base_path + ['esp-group', esp_group, 'proposal', '2', 'hash', 'sha384'])
+ self.cli_set(base_path + ['esp-group', esp_group, 'proposal', '3', 'encryption', 'aes256'])
+ self.cli_set(base_path + ['esp-group', esp_group, 'proposal', '3', 'hash', 'sha256'])
+ self.cli_set(base_path + ['esp-group', esp_group, 'proposal', '4', 'encryption', 'aes256'])
+ self.cli_set(base_path + ['esp-group', esp_group, 'proposal', '4', 'hash', 'sha1'])
+ self.cli_set(base_path + ['esp-group', esp_group, 'proposal', '10', 'encryption', 'aes128gcm128'])
+ self.cli_set(base_path + ['esp-group', esp_group, 'proposal', '10', 'hash', 'sha256'])
+
+ self.cli_set(base_path + ['remote-access', 'connection', conn_name, 'authentication', 'local-id', local_id])
+ # Use client-mode x509 instead of default EAP-MSCHAPv2
+ self.cli_set(base_path + ['remote-access', 'connection', conn_name, 'authentication', 'client-mode', 'x509'])
+ self.cli_set(base_path + ['remote-access', 'connection', conn_name, 'authentication', 'server-mode', 'x509'])
+
+ self.cli_set(base_path + ['remote-access', 'connection', conn_name, 'authentication', 'x509', 'certificate', peer_name])
+ # verify() - CA cert required for x509 auth
+ with self.assertRaises(ConfigSessionError):
+ self.cli_commit()
+ self.cli_set(base_path + ['remote-access', 'connection', conn_name, 'authentication', 'x509', 'ca-certificate', ca_name])
+ self.cli_set(base_path + ['remote-access', 'connection', conn_name, 'authentication', 'x509', 'ca-certificate', int_ca_name])
+
+ self.cli_set(base_path + ['remote-access', 'connection', conn_name, 'esp-group', esp_group])
+ self.cli_set(base_path + ['remote-access', 'connection', conn_name, 'ike-group', ike_group])
+ self.cli_set(base_path + ['remote-access', 'connection', conn_name, 'local-address', local_address])
+ self.cli_set(base_path + ['remote-access', 'connection', conn_name, 'pool', ip_pool_name])
+
+ for ns in name_servers:
+ self.cli_set(base_path + ['remote-access', 'pool', ip_pool_name, 'name-server', ns])
+ self.cli_set(base_path + ['remote-access', 'pool', ip_pool_name, 'prefix', prefix])
+
+ self.cli_commit()
+
+ # verify applied configuration
+ swanctl_conf = read_file(swanctl_file)
+ swanctl_lines = [
+ f'{conn_name}',
+ f'remote_addrs = %any',
+ f'local_addrs = {local_address}',
+ f'proposals = aes256-sha512-modp2048,aes256-sha256-modp2048,aes256-sha256-modp1024,aes128gcm128-sha256-modp2048',
+ f'version = 2',
+ f'send_certreq = no',
+ f'rekey_time = 0s',
+ f'keyingtries = 0',
+ f'pools = {ip_pool_name}',
+ f'id = "{local_id}"',
+ f'auth = pubkey',
+ f'certs = peer1.pem',
+ f'cacerts = MyVyOS-CA.pem,MyVyOS-IntCA.pem',
+ f'esp_proposals = aes256-sha512,aes256-sha384,aes256-sha256,aes256-sha1,aes128gcm128-sha256',
+ f'life_time = {eap_lifetime}s',
+ f'rekey_time = 0s',
+ f'dpd_action = clear',
+ f'inactivity = 28800',
+ f'local_ts = 0.0.0.0/0,::/0',
+ ]
+ for line in swanctl_lines:
+ self.assertIn(line, swanctl_conf)
+
+ swanctl_pool_lines = [
+ f'{ip_pool_name}',
+ f'addrs = {prefix}',
+ f'dns = {",".join(name_servers)}',
+ ]
+ for line in swanctl_pool_lines:
+ self.assertIn(line, swanctl_conf)
+
+ # Check Root CA, Intermediate CA and Peer cert/key pair is present
+ self.assertTrue(os.path.exists(os.path.join(CA_PATH, f'{ca_name}.pem')))
+ self.assertTrue(os.path.exists(os.path.join(CA_PATH, f'{int_ca_name}.pem')))
+ self.assertTrue(os.path.exists(os.path.join(CERT_PATH, f'{peer_name}.pem')))
+
+ self.tearDownPKI()
+
if __name__ == '__main__':
unittest.main(verbosity=2)
diff --git a/smoketest/scripts/cli/test_vrf.py b/smoketest/scripts/cli/test_vrf.py
index 243397dc2..2bb6c91c1 100755
--- a/smoketest/scripts/cli/test_vrf.py
+++ b/smoketest/scripts/cli/test_vrf.py
@@ -19,14 +19,18 @@ import os
import unittest
from base_vyostest_shim import VyOSUnitTestSHIM
+from json import loads
+from jmespath import search
from vyos.configsession import ConfigSessionError
from vyos.ifconfig import Interface
from vyos.ifconfig import Section
from vyos.utils.file import read_file
from vyos.utils.network import get_interface_config
+from vyos.utils.network import get_vrf_tableid
from vyos.utils.network import is_intf_addr_assigned
from vyos.utils.network import interface_exists
+from vyos.utils.process import cmd
from vyos.utils.system import sysctl_read
base_path = ['vrf']
@@ -111,8 +115,7 @@ class VRFTest(VyOSUnitTestSHIM.TestCase):
frrconfig = self.getFRRconfig(f'vrf {vrf}')
self.assertIn(f' vni {table}', frrconfig)
- tmp = get_interface_config(vrf)
- self.assertEqual(int(table), tmp['linkinfo']['info_data']['table'])
+ self.assertEqual(int(table), get_vrf_tableid(vrf))
# Increment table ID for the next run
table = str(int(table) + 1)
@@ -266,8 +269,7 @@ class VRFTest(VyOSUnitTestSHIM.TestCase):
for address in addresses:
self.assertTrue(is_intf_addr_assigned(interface, address))
# Verify VRF table ID
- tmp = get_interface_config(vrf)
- self.assertEqual(int(table), tmp['linkinfo']['info_data']['table'])
+ self.assertEqual(int(table), get_vrf_tableid(vrf))
# Verify interface is assigned to VRF
tmp = get_interface_config(interface)
@@ -558,26 +560,39 @@ class VRFTest(VyOSUnitTestSHIM.TestCase):
self.assertNotIn(f' no ipv6 nht resolve-via-default', frrconfig)
def test_vrf_conntrack(self):
- table = '1000'
+ table = '8710'
nftables_rules = {
'vrf_zones_ct_in': ['ct original zone set iifname map @ct_iface_map'],
'vrf_zones_ct_out': ['ct original zone set oifname map @ct_iface_map']
}
- self.cli_set(base_path + ['name', 'blue', 'table', table])
+ self.cli_set(base_path + ['name', 'randomVRF', 'table', '1000'])
self.cli_commit()
# Conntrack rules should not be present
for chain, rule in nftables_rules.items():
self.verify_nftables_chain(rule, 'inet vrf_zones', chain, inverse=True)
+ # conntrack is only enabled once NAT, NAT66 or firewalling is enabled
self.cli_set(['nat'])
- self.cli_commit()
+
+ for vrf in vrfs:
+ base = base_path + ['name', vrf]
+ self.cli_set(base + ['table', table])
+ table = str(int(table) + 1)
+ # We need the commit inside the loop to trigger the bug in T6603
+ self.cli_commit()
# Conntrack rules should now be present
for chain, rule in nftables_rules.items():
self.verify_nftables_chain(rule, 'inet vrf_zones', chain, inverse=False)
+ # T6603: there should be only ONE entry for the iifname/oifname in the chains
+ tmp = loads(cmd('sudo nft -j list table inet vrf_zones'))
+ num_rules = len(search("nftables[].rule[].chain", tmp))
+ # ['vrf_zones_ct_in', 'vrf_zones_ct_out']
+ self.assertEqual(num_rules, 2)
+
self.cli_delete(['nat'])
if __name__ == '__main__':
diff --git a/smoketest/scripts/system/test_kernel_options.py b/smoketest/scripts/system/test_kernel_options.py
index 4666e98e7..700e4cec7 100755
--- a/smoketest/scripts/system/test_kernel_options.py
+++ b/smoketest/scripts/system/test_kernel_options.py
@@ -19,8 +19,9 @@ import os
import platform
import unittest
-kernel = platform.release()
+from vyos.utils.kernel import check_kmod
+kernel = platform.release()
class TestKernelModules(unittest.TestCase):
""" VyOS makes use of a lot of Kernel drivers, modules and features. The
required modules which are essential for VyOS should be tested that they are
@@ -35,9 +36,8 @@ class TestKernelModules(unittest.TestCase):
super(TestKernelModules, cls).setUpClass()
CONFIG = '/proc/config.gz'
-
if not os.path.isfile(CONFIG):
- call('sudo modprobe configs')
+ check_kmod('configs')
with gzip.open(CONFIG, 'rt') as f:
cls._config_data = f.read()
diff --git a/src/completion/list_login_ttys.py b/src/completion/list_login_ttys.py
new file mode 100644
index 000000000..4d77a1b8b
--- /dev/null
+++ b/src/completion/list_login_ttys.py
@@ -0,0 +1,25 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2024 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 vyos.utils.serial import get_serial_units
+
+if __name__ == '__main__':
+ # Autocomplete uses runtime state rather than the config tree, as a manual
+ # restart/cleanup may be needed for deleted devices.
+ tty_completions = [ '<text>' ] + [ x['device'] for x in get_serial_units() if 'device' in x ]
+ print(' '.join(tty_completions))
+
+
diff --git a/src/conf_mode/firewall.py b/src/conf_mode/firewall.py
index ec6b86ef2..352d5cbb1 100755
--- a/src/conf_mode/firewall.py
+++ b/src/conf_mode/firewall.py
@@ -128,7 +128,49 @@ def get_config(config=None):
return firewall
-def verify_rule(firewall, rule_conf, ipv6):
+def verify_jump_target(firewall, root_chain, jump_target, ipv6, recursive=False):
+ targets_seen = []
+ targets_pending = [jump_target]
+
+ while targets_pending:
+ target = targets_pending.pop()
+
+ if not ipv6:
+ if target not in dict_search_args(firewall, 'ipv4', 'name'):
+ raise ConfigError(f'Invalid jump-target. Firewall name {target} does not exist on the system')
+ target_rules = dict_search_args(firewall, 'ipv4', 'name', target, 'rule')
+ else:
+ if target not in dict_search_args(firewall, 'ipv6', 'name'):
+ raise ConfigError(f'Invalid jump-target. Firewall ipv6 name {target} does not exist on the system')
+ target_rules = dict_search_args(firewall, 'ipv6', 'name', target, 'rule')
+
+ no_ipsec_in = root_chain in ('output', )
+
+ if target_rules:
+ for target_rule_conf in target_rules.values():
+ # Output hook types will not tolerate 'meta ipsec exists' matches even in jump targets:
+ if no_ipsec_in and (dict_search_args(target_rule_conf, 'ipsec', 'match_ipsec_in') is not None \
+ or dict_search_args(target_rule_conf, 'ipsec', 'match_none_in') is not None):
+ if not ipv6:
+ raise ConfigError(f'Invalid jump-target for {root_chain}. Firewall name {target} rules contain incompatible ipsec inbound matches')
+ else:
+ raise ConfigError(f'Invalid jump-target for {root_chain}. Firewall ipv6 name {target} rules contain incompatible ipsec inbound matches')
+ # Make sure we're not looping back on ourselves somewhere:
+ if recursive and 'jump_target' in target_rule_conf:
+ child_target = target_rule_conf['jump_target']
+ if child_target in targets_seen:
+ if not ipv6:
+ raise ConfigError(f'Loop detected in jump-targets, firewall name {target} refers to previously traversed name {child_target}')
+ else:
+ raise ConfigError(f'Loop detected in jump-targets, firewall ipv6 name {target} refers to previously traversed ipv6 name {child_target}')
+ targets_pending.append(child_target)
+ if len(targets_seen) == 7:
+ path_txt = ' -> '.join(targets_seen)
+ Warning(f'Deep nesting of jump targets has reached 8 levels deep, following the path {path_txt} -> {child_target}!')
+
+ targets_seen.append(target)
+
+def verify_rule(firewall, chain_name, rule_conf, ipv6):
if 'action' not in rule_conf:
raise ConfigError('Rule action must be defined')
@@ -139,12 +181,10 @@ def verify_rule(firewall, rule_conf, ipv6):
if 'jump' not in rule_conf['action']:
raise ConfigError('jump-target defined, but action jump needed and it is not defined')
target = rule_conf['jump_target']
- if not ipv6:
- if target not in dict_search_args(firewall, 'ipv4', 'name'):
- raise ConfigError(f'Invalid jump-target. Firewall name {target} does not exist on the system')
+ if chain_name != 'name': # This is a bit clumsy, but consolidates a chunk of code.
+ verify_jump_target(firewall, chain_name, target, ipv6, recursive=True)
else:
- if target not in dict_search_args(firewall, 'ipv6', 'name'):
- raise ConfigError(f'Invalid jump-target. Firewall ipv6 name {target} does not exist on the system')
+ verify_jump_target(firewall, chain_name, target, ipv6, recursive=False)
if rule_conf['action'] == 'offload':
if 'offload_target' not in rule_conf:
@@ -185,8 +225,10 @@ def verify_rule(firewall, rule_conf, ipv6):
raise ConfigError('Limit rate integer cannot be less than 1')
if 'ipsec' in rule_conf:
- if {'match_ipsec', 'match_non_ipsec'} <= set(rule_conf['ipsec']):
- raise ConfigError('Cannot specify both "match-ipsec" and "match-non-ipsec"')
+ if {'match_ipsec_in', 'match_none_in'} <= set(rule_conf['ipsec']):
+ raise ConfigError('Cannot specify both "match-ipsec" and "match-none"')
+ if {'match_ipsec_out', 'match_none_out'} <= set(rule_conf['ipsec']):
+ raise ConfigError('Cannot specify both "match-ipsec" and "match-none"')
if 'recent' in rule_conf:
if not {'count', 'time'} <= set(rule_conf['recent']):
@@ -349,13 +391,11 @@ def verify(firewall):
raise ConfigError('default-jump-target defined, but default-action jump needed and it is not defined')
if name_conf['default_jump_target'] == name_id:
raise ConfigError(f'Loop detected on default-jump-target.')
- ## Now need to check that default-jump-target exists (other firewall chain/name)
- if target not in dict_search_args(firewall['ipv4'], 'name'):
- raise ConfigError(f'Invalid jump-target. Firewall name {target} does not exist on the system')
+ verify_jump_target(firewall, name, target, False, recursive=True)
if 'rule' in name_conf:
for rule_id, rule_conf in name_conf['rule'].items():
- verify_rule(firewall, rule_conf, False)
+ verify_rule(firewall, name, rule_conf, False)
if 'ipv6' in firewall:
for name in ['name','forward','input','output', 'prerouting']:
@@ -369,13 +409,11 @@ def verify(firewall):
raise ConfigError('default-jump-target defined, but default-action jump needed and it is not defined')
if name_conf['default_jump_target'] == name_id:
raise ConfigError(f'Loop detected on default-jump-target.')
- ## Now need to check that default-jump-target exists (other firewall chain/name)
- if target not in dict_search_args(firewall['ipv6'], 'name'):
- raise ConfigError(f'Invalid jump-target. Firewall name {target} does not exist on the system')
+ verify_jump_target(firewall, name, target, True, recursive=True)
if 'rule' in name_conf:
for rule_id, rule_conf in name_conf['rule'].items():
- verify_rule(firewall, rule_conf, True)
+ verify_rule(firewall, name, rule_conf, True)
#### ZONESSSS
local_zone = False
diff --git a/src/conf_mode/interfaces_geneve.py b/src/conf_mode/interfaces_geneve.py
index 769139e0f..007708d4a 100755
--- a/src/conf_mode/interfaces_geneve.py
+++ b/src/conf_mode/interfaces_geneve.py
@@ -24,6 +24,7 @@ from vyos.configverify import verify_mtu_ipv6
from vyos.configverify import verify_bridge_delete
from vyos.configverify import verify_mirror_redirect
from vyos.configverify import verify_bond_bridge_member
+from vyos.configverify import verify_vrf
from vyos.ifconfig import GeneveIf
from vyos.utils.network import interface_exists
from vyos import ConfigError
@@ -59,6 +60,7 @@ def verify(geneve):
verify_mtu_ipv6(geneve)
verify_address(geneve)
+ verify_vrf(geneve)
verify_bond_bridge_member(geneve)
verify_mirror_redirect(geneve)
diff --git a/src/conf_mode/interfaces_l2tpv3.py b/src/conf_mode/interfaces_l2tpv3.py
index e25793543..b9f827bee 100755
--- a/src/conf_mode/interfaces_l2tpv3.py
+++ b/src/conf_mode/interfaces_l2tpv3.py
@@ -24,6 +24,7 @@ from vyos.configverify import verify_bridge_delete
from vyos.configverify import verify_mtu_ipv6
from vyos.configverify import verify_mirror_redirect
from vyos.configverify import verify_bond_bridge_member
+from vyos.configverify import verify_vrf
from vyos.ifconfig import L2TPv3If
from vyos.utils.kernel import check_kmod
from vyos.utils.network import is_addr_assigned
@@ -76,6 +77,7 @@ def verify(l2tpv3):
verify_mtu_ipv6(l2tpv3)
verify_address(l2tpv3)
+ verify_vrf(l2tpv3)
verify_bond_bridge_member(l2tpv3)
verify_mirror_redirect(l2tpv3)
return None
diff --git a/src/conf_mode/interfaces_openvpn.py b/src/conf_mode/interfaces_openvpn.py
index 017010a61..a03bd5959 100755
--- a/src/conf_mode/interfaces_openvpn.py
+++ b/src/conf_mode/interfaces_openvpn.py
@@ -235,10 +235,6 @@ def verify_pki(openvpn):
def verify(openvpn):
if 'deleted' in openvpn:
- # remove totp secrets file if totp is not configured
- if os.path.isfile(otp_file.format(**openvpn)):
- os.remove(otp_file.format(**openvpn))
-
verify_bridge_delete(openvpn)
return None
@@ -326,8 +322,8 @@ def verify(openvpn):
if v4addr in openvpn['local_address'] and 'subnet_mask' not in openvpn['local_address'][v4addr]:
raise ConfigError('Must specify IPv4 "subnet-mask" for local-address')
- if dict_search('encryption.ncp_ciphers', openvpn):
- raise ConfigError('NCP ciphers can only be used in client or server mode')
+ if dict_search('encryption.data_ciphers', openvpn):
+ raise ConfigError('Cipher negotiation can only be used in client or server mode')
else:
# checks for client-server or site-to-site bridged
@@ -432,6 +428,13 @@ def verify(openvpn):
if IPv6Address(client['ipv6_ip'][0]) in v6PoolNet:
print(f'Warning: Client "{client["name"]}" IP {client["ipv6_ip"][0]} is in server IP pool, it is not reserved for this client.')
+ if 'topology' in openvpn['server']:
+ if openvpn['server']['topology'] == 'net30':
+ DeprecationWarning('Topology net30 is deprecated '\
+ 'and will be removed in future VyOS versions. '\
+ 'Switch to "subnet" or "p2p"'
+ )
+
# add mfa users to the file the mfa plugin uses
if dict_search('server.mfa.totp', openvpn):
user_data = ''
@@ -517,7 +520,7 @@ def verify(openvpn):
if dict_search('encryption.cipher', openvpn):
raise ConfigError('"encryption cipher" option is deprecated for TLS mode. '
- 'Use "encryption ncp-ciphers" instead')
+ 'Use "encryption data-ciphers" instead')
if dict_search('encryption.cipher', openvpn) == 'none':
print('Warning: "encryption none" was specified!')
@@ -628,9 +631,19 @@ def generate_pki_files(openvpn):
def generate(openvpn):
+ if 'deleted' in openvpn:
+ # remove totp secrets file if totp is not configured
+ if os.path.isfile(otp_file.format(**openvpn)):
+ os.remove(otp_file.format(**openvpn))
+ return None
+
+ if 'disable' in openvpn:
+ return None
+
interface = openvpn['ifname']
directory = os.path.dirname(cfg_file.format(**openvpn))
openvpn['plugin_dir'] = '/usr/lib/openvpn'
+
# create base config directory on demand
makedir(directory, user, group)
# enforce proper permissions on /run/openvpn
@@ -647,9 +660,6 @@ def generate(openvpn):
if os.path.isdir(service_dir):
rmtree(service_dir, ignore_errors=True)
- if 'deleted' in openvpn or 'disable' in openvpn:
- return None
-
# create client config directory on demand
makedir(ccd_dir, user, group)
diff --git a/src/conf_mode/interfaces_vti.py b/src/conf_mode/interfaces_vti.py
index e6a833df7..20629c6c1 100755
--- a/src/conf_mode/interfaces_vti.py
+++ b/src/conf_mode/interfaces_vti.py
@@ -19,6 +19,7 @@ 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.configverify import verify_vrf
from vyos.ifconfig import VTIIf
from vyos import ConfigError
from vyos import airbag
@@ -38,6 +39,7 @@ def get_config(config=None):
return vti
def verify(vti):
+ verify_vrf(vti)
verify_mirror_redirect(vti)
return None
diff --git a/src/conf_mode/interfaces_vxlan.py b/src/conf_mode/interfaces_vxlan.py
index 39365968a..68646e8ff 100755
--- a/src/conf_mode/interfaces_vxlan.py
+++ b/src/conf_mode/interfaces_vxlan.py
@@ -28,6 +28,7 @@ from vyos.configverify import verify_mtu_ipv6
from vyos.configverify import verify_mirror_redirect
from vyos.configverify import verify_source_interface
from vyos.configverify import verify_bond_bridge_member
+from vyos.configverify import verify_vrf
from vyos.ifconfig import Interface
from vyos.ifconfig import VXLANIf
from vyos.template import is_ipv6
@@ -178,13 +179,36 @@ def verify(vxlan):
'is member of a bridge interface!')
vnis_used = []
+ vlans_used = []
for vif, vif_config in vxlan['vlan_to_vni'].items():
if 'vni' not in vif_config:
raise ConfigError(f'Must define VNI for VLAN "{vif}"!')
vni = vif_config['vni']
- if vni in vnis_used:
- raise ConfigError(f'VNI "{vni}" is already assigned to a different VLAN!')
- vnis_used.append(vni)
+
+ err_msg = f'VLAN range "{vif}" does not match VNI range "{vni}"!'
+ vif_range, vni_range = list(map(int, vif.split('-'))), list(map(int, vni.split('-')))
+
+ if len(vif_range) != len(vni_range):
+ raise ConfigError(err_msg)
+
+ if len(vif_range) > 1:
+ if vni_range[0] > vni_range[-1] or vif_range[0] > vif_range[-1]:
+ raise ConfigError('The upper bound of the range must be greater than the lower bound!')
+ vni_range = range(vni_range[0], vni_range[1] + 1)
+ vif_range = range(vif_range[0], vif_range[1] + 1)
+
+ if len(vif_range) != len(vni_range):
+ raise ConfigError(err_msg)
+
+ for vni_id in vni_range:
+ if vni_id in vnis_used:
+ raise ConfigError(f'VNI "{vni_id}" is already assigned to a different VLAN!')
+ vnis_used.append(vni_id)
+
+ for vif_id in vif_range:
+ if vif_id in vlans_used:
+ raise ConfigError(f'VLAN "{vif_id}" is already in use!')
+ vlans_used.append(vif_id)
if dict_search('parameters.neighbor_suppress', vxlan) != None:
if 'is_bridge_member' not in vxlan:
@@ -193,6 +217,7 @@ def verify(vxlan):
verify_mtu_ipv6(vxlan)
verify_address(vxlan)
+ verify_vrf(vxlan)
verify_bond_bridge_member(vxlan)
verify_mirror_redirect(vxlan)
diff --git a/src/conf_mode/interfaces_wireless.py b/src/conf_mode/interfaces_wireless.py
index 73944dc8b..f35a250cb 100755
--- a/src/conf_mode/interfaces_wireless.py
+++ b/src/conf_mode/interfaces_wireless.py
@@ -19,6 +19,7 @@ import os
from sys import exit
from re import findall
from netaddr import EUI, mac_unix_expanded
+from time import sleep
from vyos.config import Config
from vyos.configdict import get_interface_dict
@@ -34,6 +35,9 @@ from vyos.template import render
from vyos.utils.dict import dict_search
from vyos.utils.kernel import check_kmod
from vyos.utils.process import call
+from vyos.utils.process import is_systemd_service_active
+from vyos.utils.process import is_systemd_service_running
+from vyos.utils.network import interface_exists
from vyos import ConfigError
from vyos import airbag
airbag.enable()
@@ -93,6 +97,11 @@ def get_config(config=None):
if wifi.from_defaults(['security', 'wpa']): # if not set by user
del wifi['security']['wpa']
+ # XXX: Jinja2 can not operate on a dictionary key when it starts of with a number
+ if '40mhz_incapable' in (dict_search('capabilities.ht', wifi) or []):
+ wifi['capabilities']['ht']['fourtymhz_incapable'] = wifi['capabilities']['ht']['40mhz_incapable']
+ del wifi['capabilities']['ht']['40mhz_incapable']
+
if dict_search('security.wpa', wifi) != None:
wpa_cipher = wifi['security']['wpa'].get('cipher')
wpa_mode = wifi['security']['wpa'].get('mode')
@@ -120,7 +129,7 @@ def get_config(config=None):
tmp = find_other_stations(conf, base, wifi['ifname'])
if tmp: wifi['station_interfaces'] = tmp
- # used in hostapt.conf.j2
+ # used in hostapd.conf.j2
wifi['hostapd_accept_station_conf'] = hostapd_accept_station_conf.format(**wifi)
wifi['hostapd_deny_station_conf'] = hostapd_deny_station_conf.format(**wifi)
@@ -184,11 +193,18 @@ def verify(wifi):
if not any(i in ['passphrase', 'radius'] for i in wpa):
raise ConfigError('Misssing WPA key or RADIUS server')
+ if 'username' in wpa:
+ if 'passphrase' not in wpa:
+ raise ConfigError('WPA-Enterprise configured - missing passphrase!')
+ elif 'passphrase' in wpa:
+ # check if passphrase meets the regex .{8,63}
+ if len(wpa['passphrase']) < 8 or len(wpa['passphrase']) > 63:
+ raise ConfigError('WPA passphrase must be between 8 and 63 characters long')
if 'radius' in wpa:
if 'server' in wpa['radius']:
for server in wpa['radius']['server']:
if 'key' not in wpa['radius']['server'][server]:
- raise ConfigError(f'Misssing RADIUS shared secret key for server: {server}')
+ raise ConfigError(f'Missing RADIUS shared secret key for server: {server}')
if 'capabilities' in wifi:
capabilities = wifi['capabilities']
@@ -225,11 +241,6 @@ def verify(wifi):
def generate(wifi):
interface = wifi['ifname']
- # always stop hostapd service first before reconfiguring it
- call(f'systemctl stop hostapd@{interface}.service')
- # always stop wpa_supplicant service first before reconfiguring it
- call(f'systemctl stop wpa_supplicant@{interface}.service')
-
# Delete config files if interface is removed
if 'deleted' in wifi:
if os.path.isfile(hostapd_conf.format(**wifi)):
@@ -265,11 +276,6 @@ def generate(wifi):
mac.dialect = mac_unix_expanded
wifi['mac'] = str(mac)
- # XXX: Jinja2 can not operate on a dictionary key when it starts of with a number
- if '40mhz_incapable' in (dict_search('capabilities.ht', wifi) or []):
- wifi['capabilities']['ht']['fourtymhz_incapable'] = wifi['capabilities']['ht']['40mhz_incapable']
- del wifi['capabilities']['ht']['40mhz_incapable']
-
# render appropriate new config files depending on access-point or station mode
if wifi['type'] == 'access-point':
render(hostapd_conf.format(**wifi), 'wifi/hostapd.conf.j2', wifi)
@@ -283,23 +289,45 @@ def generate(wifi):
def apply(wifi):
interface = wifi['ifname']
+ # From systemd source code:
+ # If there's a stop job queued before we enter the DEAD state, we shouldn't act on Restart=,
+ # in order to not undo what has already been enqueued. */
+ #
+ # It was found that calling restart on hostapd will (4 out of 10 cases) deactivate
+ # the service instead of restarting it, when it was not yet properly stopped
+ # systemd[1]: hostapd@wlan1.service: Deactivated successfully.
+ # Thus kill all WIFI service and start them again after it's ensured nothing lives
+ call(f'systemctl stop hostapd@{interface}.service')
+ call(f'systemctl stop wpa_supplicant@{interface}.service')
+
if 'deleted' in wifi:
- WiFiIf(interface).remove()
- else:
- # Finally create the new interface
- w = WiFiIf(**wifi)
- w.update(wifi)
-
- # Enable/Disable interface - interface is always placed in
- # administrative down state in WiFiIf class
- if 'disable' not in wifi:
- # Physical interface is now configured. Proceed by starting hostapd or
- # wpa_supplicant daemon. When type is monitor we can just skip this.
- if wifi['type'] == 'access-point':
- call(f'systemctl start hostapd@{interface}.service')
-
- elif wifi['type'] == 'station':
- call(f'systemctl start wpa_supplicant@{interface}.service')
+ WiFiIf(**wifi).remove()
+ return None
+
+ while (is_systemd_service_running(f'hostapd@{interface}.service') or \
+ is_systemd_service_active(f'hostapd@{interface}.service')):
+ sleep(0.250) # wait 250ms
+
+ # Finally create the new interface
+ w = WiFiIf(**wifi)
+ w.update(wifi)
+
+ # Enable/Disable interface - interface is always placed in
+ # administrative down state in WiFiIf class
+ if 'disable' not in wifi:
+ # Wait until interface was properly added to the Kernel
+ ii = 0
+ while not (interface_exists(interface) and ii < 20):
+ sleep(0.250) # wait 250ms
+ ii += 1
+
+ # Physical interface is now configured. Proceed by starting hostapd or
+ # wpa_supplicant daemon. When type is monitor we can just skip this.
+ if wifi['type'] == 'access-point':
+ call(f'systemctl start hostapd@{interface}.service')
+
+ elif wifi['type'] == 'station':
+ call(f'systemctl start wpa_supplicant@{interface}.service')
return None
diff --git a/src/conf_mode/policy_route.py b/src/conf_mode/policy_route.py
index c58fe1bce..223175b8a 100755
--- a/src/conf_mode/policy_route.py
+++ b/src/conf_mode/policy_route.py
@@ -25,6 +25,9 @@ from vyos.template import render
from vyos.utils.dict import dict_search_args
from vyos.utils.process import cmd
from vyos.utils.process import run
+from vyos.utils.network import get_vrf_tableid
+from vyos.defaults import rt_global_table
+from vyos.defaults import rt_global_vrf
from vyos import ConfigError
from vyos import airbag
airbag.enable()
@@ -83,6 +86,9 @@ def verify_rule(policy, name, rule_conf, ipv6, rule_id):
if not tcp_flags or 'syn' not in tcp_flags:
raise ConfigError(f'{name} rule {rule_id}: TCP SYN flag must be set to modify TCP-MSS')
+ if 'vrf' in rule_conf['set'] and 'table' in rule_conf['set']:
+ raise ConfigError(f'{name} rule {rule_id}: Cannot set both forwarding route table and VRF')
+
tcp_flags = dict_search_args(rule_conf, 'tcp', 'flags')
if tcp_flags:
if dict_search_args(rule_conf, 'protocol') != 'tcp':
@@ -152,15 +158,26 @@ def apply_table_marks(policy):
for name, pol_conf in policy[route].items():
if 'rule' in pol_conf:
for rule_id, rule_conf in pol_conf['rule'].items():
+ vrf_table_id = None
set_table = dict_search_args(rule_conf, 'set', 'table')
- if set_table:
+ set_vrf = dict_search_args(rule_conf, 'set', 'vrf')
+ if set_vrf:
+ if set_vrf == 'default':
+ vrf_table_id = rt_global_vrf
+ else:
+ vrf_table_id = get_vrf_tableid(set_vrf)
+ elif set_table:
if set_table == 'main':
- set_table = '254'
- if set_table in tables:
+ vrf_table_id = rt_global_table
+ else:
+ vrf_table_id = set_table
+ if vrf_table_id is not None:
+ vrf_table_id = int(vrf_table_id)
+ if vrf_table_id in tables:
continue
- tables.append(set_table)
- table_mark = mark_offset - int(set_table)
- cmd(f'{cmd_str} rule add pref {set_table} fwmark {table_mark} table {set_table}')
+ tables.append(vrf_table_id)
+ table_mark = mark_offset - vrf_table_id
+ cmd(f'{cmd_str} rule add pref {vrf_table_id} fwmark {table_mark} table {vrf_table_id}')
def cleanup_table_marks():
for cmd_str in ['ip', 'ip -6']:
diff --git a/src/conf_mode/service_monitoring_telegraf.py b/src/conf_mode/service_monitoring_telegraf.py
index 9455b6109..db870aae5 100755
--- a/src/conf_mode/service_monitoring_telegraf.py
+++ b/src/conf_mode/service_monitoring_telegraf.py
@@ -86,7 +86,8 @@ def get_config(config=None):
monitoring['custom_scripts_dir'] = custom_scripts_dir
monitoring['hostname'] = get_hostname()
monitoring['interfaces_ethernet'] = Section.interfaces('ethernet', vlan=False)
- monitoring['nft_chains'] = get_nft_filter_chains()
+ if conf.exists('firewall'):
+ monitoring['nft_chains'] = get_nft_filter_chains()
# Redefine azure group-metrics 'single-table' and 'table-per-metric'
if 'azure_data_explorer' in monitoring:
diff --git a/src/conf_mode/service_snmp.py b/src/conf_mode/service_snmp.py
index 6f025cc23..c9c0ed9a0 100755
--- a/src/conf_mode/service_snmp.py
+++ b/src/conf_mode/service_snmp.py
@@ -41,6 +41,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'
+default_script_dir = r'/config/user-data/'
systemd_override = r'/run/systemd/system/snmpd.service.d/override.conf'
systemd_service = 'snmpd.service'
@@ -85,8 +86,20 @@ def get_config(config=None):
tmp = {'::1': {'port': '161'}}
snmp['listen_address'] = dict_merge(tmp, snmp['listen_address'])
+ if 'script_extensions' in snmp and 'extension_name' in snmp['script_extensions']:
+ for key, val in snmp['script_extensions']['extension_name'].items():
+ if 'script' not in val:
+ continue
+ script_path = val['script']
+ # if script has not absolute path, use pre configured path
+ if not os.path.isabs(script_path):
+ script_path = os.path.join(default_script_dir, script_path)
+
+ snmp['script_extensions']['extension_name'][key]['script'] = script_path
+
return snmp
+
def verify(snmp):
if 'deleted' in snmp:
return None
diff --git a/src/conf_mode/system_conntrack.py b/src/conf_mode/system_conntrack.py
index aa290788c..2529445bf 100755
--- a/src/conf_mode/system_conntrack.py
+++ b/src/conf_mode/system_conntrack.py
@@ -13,7 +13,7 @@
#
# 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
from sys import exit
@@ -24,7 +24,8 @@ from vyos.configdep import set_dependents, call_dependents
from vyos.utils.dict import dict_search
from vyos.utils.dict import dict_search_args
from vyos.utils.dict import dict_search_recursive
-from vyos.utils.process import cmd
+from vyos.utils.file import write_file
+from vyos.utils.process import cmd, call
from vyos.utils.process import rc_cmd
from vyos.template import render
from vyos import ConfigError
@@ -34,6 +35,7 @@ airbag.enable()
conntrack_config = r'/etc/modprobe.d/vyatta_nf_conntrack.conf'
sysctl_file = r'/run/sysctl/10-vyos-conntrack.conf'
nftables_ct_file = r'/run/nftables-ct.conf'
+vyos_conntrack_logger_config = r'/run/vyos-conntrack-logger.conf'
# Every ALG (Application Layer Gateway) consists of either a Kernel Object
# also called a Kernel Module/Driver or some rules present in iptables
@@ -113,6 +115,7 @@ def get_config(config=None):
return conntrack
+
def verify(conntrack):
for inet in ['ipv4', 'ipv6']:
if dict_search_args(conntrack, 'ignore', inet, 'rule') != None:
@@ -181,6 +184,11 @@ def generate(conntrack):
if not os.path.exists(nftables_ct_file):
conntrack['first_install'] = True
+ if 'log' not in conntrack:
+ # Remove old conntrack-logger config and return
+ if os.path.exists(vyos_conntrack_logger_config):
+ os.unlink(vyos_conntrack_logger_config)
+
# Determine if conntrack is needed
conntrack['ipv4_firewall_action'] = 'return'
conntrack['ipv6_firewall_action'] = 'return'
@@ -199,6 +207,11 @@ def generate(conntrack):
render(conntrack_config, 'conntrack/vyos_nf_conntrack.conf.j2', conntrack)
render(sysctl_file, 'conntrack/sysctl.conf.j2', conntrack)
render(nftables_ct_file, 'conntrack/nftables-ct.j2', conntrack)
+
+ if 'log' in conntrack:
+ log_conf_json = json.dumps(conntrack['log'], indent=4)
+ write_file(vyos_conntrack_logger_config, log_conf_json)
+
return None
def apply(conntrack):
@@ -243,8 +256,12 @@ def apply(conntrack):
# See: https://bugzilla.redhat.com/show_bug.cgi?id=1264080
cmd(f'sysctl -f {sysctl_file}')
+ if 'log' in conntrack:
+ call(f'systemctl restart vyos-conntrack-logger.service')
+
return None
+
if __name__ == '__main__':
try:
c = get_config()
diff --git a/src/conf_mode/system_console.py b/src/conf_mode/system_console.py
index 19bbb8875..27bf92e0b 100755
--- a/src/conf_mode/system_console.py
+++ b/src/conf_mode/system_console.py
@@ -19,8 +19,10 @@ from pathlib import Path
from vyos.config import Config
from vyos.utils.process import call
+from vyos.utils.serial import restart_login_consoles
from vyos.system import grub_util
from vyos.template import render
+from vyos.defaults import directories
from vyos import ConfigError
from vyos import airbag
airbag.enable()
@@ -74,7 +76,6 @@ def generate(console):
for root, dirs, files in os.walk(base_dir):
for basename in files:
if 'serial-getty' in basename:
- call(f'systemctl stop {basename}')
os.unlink(os.path.join(root, basename))
if not console or 'device' not in console:
@@ -122,6 +123,11 @@ def apply(console):
# Reload systemd manager configuration
call('systemctl daemon-reload')
+ # Service control moved to vyos.utils.serial to unify checks and prompts.
+ # If users are connected, we want to show an informational message on completing
+ # the process, but not halt configuration processing with an interactive prompt.
+ restart_login_consoles(prompt_user=False, quiet=False)
+
if not console:
return None
@@ -129,13 +135,6 @@ def apply(console):
# Configure screen blank powersaving on VGA console
call('/usr/bin/setterm -blank 15 -powersave powerdown -powerdown 60 -term linux </dev/tty1 >/dev/tty1 2>&1')
- # Start getty process on configured serial interfaces
- for device in console['device']:
- # Only start console if it exists on the running system. If a user
- # detaches a USB serial console and reboots - it should not fail!
- if os.path.exists(f'/dev/{device}'):
- call(f'systemctl restart serial-getty@{device}.service')
-
return None
if __name__ == '__main__':
diff --git a/src/conf_mode/system_option.py b/src/conf_mode/system_option.py
index 571ce55ec..180686924 100755
--- a/src/conf_mode/system_option.py
+++ b/src/conf_mode/system_option.py
@@ -31,6 +31,7 @@ from vyos.utils.process import cmd
from vyos.utils.process import is_systemd_service_running
from vyos.utils.network import is_addr_assigned
from vyos.utils.network import is_intf_addr_assigned
+from vyos.configdep import set_dependents, call_dependents
from vyos import ConfigError
from vyos import airbag
airbag.enable()
@@ -55,6 +56,12 @@ def get_config(config=None):
get_first_key=True,
with_recursive_defaults=True)
+ if 'performance' in options:
+ # Update IPv4 and IPv6 options after TuneD reapplies
+ # sysctl from config files
+ for protocol in ['ip', 'ipv6']:
+ set_dependents(protocol, conf)
+
return options
def verify(options):
@@ -145,6 +152,8 @@ def apply(options):
else:
cmd('systemctl stop tuned.service')
+ call_dependents()
+
# Keyboard layout - there will be always the default key inside the dict
# but we check for key existence anyway
if 'keyboard_layout' in options:
diff --git a/src/conf_mode/vpn_ipsec.py b/src/conf_mode/vpn_ipsec.py
index dc78c755e..cf82b767f 100755
--- a/src/conf_mode/vpn_ipsec.py
+++ b/src/conf_mode/vpn_ipsec.py
@@ -24,6 +24,7 @@ from time import sleep
from vyos.base import Warning
from vyos.config import Config
+from vyos.config import config_dict_merge
from vyos.configdep import set_dependents
from vyos.configdep import call_dependents
from vyos.configdict import leaf_node_changed
@@ -86,9 +87,22 @@ def get_config(config=None):
ipsec = conf.get_config_dict(base, key_mangling=('-', '_'),
no_tag_node_value_mangle=True,
get_first_key=True,
- with_recursive_defaults=True,
with_pki=True)
+ # We have to cleanup the default dict, as default values could
+ # enable features which are not explicitly enabled on the
+ # CLI. E.g. dead-peer-detection defaults should not be injected
+ # unless the feature is explicitly opted in to by setting the
+ # top-level node
+ default_values = conf.get_config_defaults(**ipsec.kwargs, recursive=True)
+
+ if 'ike_group' in ipsec:
+ for name, ike in ipsec['ike_group'].items():
+ if 'dead_peer_detection' not in ike:
+ del default_values['ike_group'][name]['dead_peer_detection']
+
+ ipsec = config_dict_merge(default_values, ipsec)
+
ipsec['dhcp_interfaces'] = set()
ipsec['dhcp_no_address'] = {}
ipsec['install_routes'] = 'no' if conf.exists(base + ["options", "disable-route-autoinstall"]) else default_install_routes
diff --git a/src/conf_mode/vrf.py b/src/conf_mode/vrf.py
index 8d8c234c0..72b178c89 100755
--- a/src/conf_mode/vrf.py
+++ b/src/conf_mode/vrf.py
@@ -15,6 +15,7 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from sys import exit
+from jmespath import search
from json import loads
from vyos.config import Config
@@ -26,7 +27,7 @@ from vyos.ifconfig import Interface
from vyos.template import render
from vyos.template import render_to_string
from vyos.utils.dict import dict_search
-from vyos.utils.network import get_interface_config
+from vyos.utils.network import get_vrf_tableid
from vyos.utils.network import get_vrf_members
from vyos.utils.network import interface_exists
from vyos.utils.process import call
@@ -70,6 +71,14 @@ def has_rule(af : str, priority : int, table : str=None):
return True
return False
+def is_nft_vrf_zone_rule_setup() -> bool:
+ """
+ Check if an nftables connection tracking rule already exists
+ """
+ tmp = loads(cmd('sudo nft -j list table inet vrf_zones'))
+ num_rules = len(search("nftables[].rule[].chain", tmp))
+ return bool(num_rules)
+
def vrf_interfaces(c, match):
matched = []
old_level = c.get_level()
@@ -160,8 +169,8 @@ def verify(vrf):
# routing table id can't be changed - OS restriction
if interface_exists(name):
- tmp = str(dict_search('linkinfo.info_data.table', get_interface_config(name)))
- if tmp and tmp != vrf_config['table']:
+ tmp = get_vrf_tableid(name)
+ if tmp and tmp != int(vrf_config['table']):
raise ConfigError(f'VRF "{name}" table id modification not possible!')
# VRF routing table ID must be unique on the system
@@ -264,6 +273,7 @@ def apply(vrf):
if not has_rule(afi, 2000, 'l3mdev'):
call(f'ip {afi} rule add pref 2000 l3mdev unreachable')
+ nft_vrf_zone_rule_setup = False
for name, config in vrf['name'].items():
table = config['table']
if not interface_exists(name):
@@ -302,7 +312,12 @@ def apply(vrf):
nft_add_element = f'add element inet vrf_zones ct_iface_map {{ "{name}" : {table} }}'
cmd(f'nft {nft_add_element}')
- if vrf['conntrack']:
+ # Only call into nftables as long as there is nothing setup to avoid wasting
+ # CPU time and thus lenghten the commit process
+ if not nft_vrf_zone_rule_setup:
+ nft_vrf_zone_rule_setup = is_nft_vrf_zone_rule_setup()
+ # Install nftables conntrack rules only once
+ if vrf['conntrack'] and not nft_vrf_zone_rule_setup:
for chain, rule in nftables_rules.items():
cmd(f'nft add rule inet vrf_zones {chain} {rule}')
diff --git a/src/etc/bash_completion.d/vyatta-op b/src/etc/bash_completion.d/vyatta-op
new file mode 100644
index 000000000..8ac2d9b20
--- /dev/null
+++ b/src/etc/bash_completion.d/vyatta-op
@@ -0,0 +1,685 @@
+# vyatta bash operational mode completion
+# **** License ****
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License version 2 as
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# General Public License for more details.
+#
+# This code was originally developed by Vyatta, Inc.
+# Portions created by Vyatta are Copyright (C) 2006, 2007 Vyatta, Inc.
+# All Rights Reserved.
+#
+# Author: Tom Grennan
+# Date: 2007
+# Description: setup bash completion for Vyatta operational commands
+#
+# **** End License ****
+
+test -z "$_vyatta_less_options" && \
+ declare -r _vyatta_less_options="\
+ --QUIT-AT-EOF\
+ --quit-if-one-screen\
+ --RAW-CONTROL-CHARS\
+ --squeeze-blank-lines\
+ --no-init"
+test -z "$_vyatta_default_pager" && \
+ declare -r _vyatta_default_pager="less \
+ --buffers=64\
+ --auto-buffers\
+ --no-lessopen\
+ $_vyatta_less_options"
+test -z "$VYATTA_PAGER" && \
+ declare -x VYATTA_PAGER=$_vyatta_default_pager
+
+_vyatta_op_do_key_bindings ()
+{
+ if [[ "$SHELL" != "/bin/vbash" && "$SHELL" != "/sbin/radius_shell" ]]; then
+ # only do bindings if vbash and radius_shell
+ return
+ fi
+ nullglob_save=$(shopt -p nullglob)
+ shopt -u nullglob
+ case "$-" in
+ *i*)
+ bind '"?": possible-completions'
+ bind 'set show-all-if-ambiguous on'
+ bind_cmds=$(grep '^bind .* # vyatta key binding$' $HOME/.bashrc)
+ eval $bind_cmds
+ ;;
+ esac
+ eval $nullglob_save
+}
+
+_vyatta_op_do_key_bindings
+
+test -f /etc/default/vyatta && \
+ source /etc/default/vyatta
+
+test ! -d "$vyatta_op_templates" && \
+ return 0
+
+case "$-" in
+ *i*)
+ declare -r _vyatta_op_last_comp_init='>>>>>>LASTCOMP<<<<<<'
+ ;;
+esac
+declare _vyatta_op_last_comp=${_vyatta_op_last_comp_init}
+declare _vyatta_op_node_path
+declare -a _vyatta_op_noncompletions _vyatta_op_completions
+declare -x -a _vyatta_pipe_noncompletions _vyatta_pipe_completions
+declare _vyatta_comptype
+declare -x -a reply
+declare -a _vyatta_operator_allowed
+
+if [[ "$VYATTA_USER_LEVEL_DIR" != "/opt/vyatta/etc/shell/level/admin" ]]; then
+ _vyatta_operator_allowed=( $(cat $VYATTA_USER_LEVEL_DIR/allowed-op) )
+fi
+
+declare -a functions
+functions=( /opt/vyatta/share/vyatta-op/functions/interpreter/* )
+
+for file in "${functions[@]}";do
+ source $file;
+done
+
+# $1: label
+# #2...: strings
+_vyatta_op_debug ()
+{
+ echo -ne \\n$1:
+ shift
+ for s ; do
+ echo -ne " \"$s\""
+ done
+}
+
+# this is needed to provide original "default completion" behavior.
+# see "vyatta-cfg" completion script for details.
+_vyatta_op_default_expand ()
+{
+ local wc=${#COMP_WORDS[@]}
+ if [[ "${COMP_WORDS[0]}" =~ "/" ]]; then
+ # if we are looking for a directory on the first completion then do directory completions
+ _filedir_xspec_vyos
+ elif (( wc < 2 )) ||
+ [[ $COMP_CWORD -eq 0 ]] ||
+ [[ $1 == $2 ]]; then
+ _vyatta_op_expand "$@"
+ else
+ # after the first word => cannot be vyatta command so use original default
+ _filedir_xspec_vyos
+ fi
+}
+
+# $1: label
+# $2...: help
+_vyatta_op_print_help ()
+{
+ local label=$1 help=$2
+ if [ ${#label} -eq 0 ] ; then
+ return
+ elif [ ${#help} -eq 0 ] ; then
+ echo -ne "\n $label"
+ elif [ ${#label} -lt 6 ] ; then
+ echo -ne "\n $label\t\t\t$help"
+ elif [ ${#label} -lt 14 ] ; then
+ echo -ne "\n $label\t\t$help"
+ elif [ ${#label} -lt 21 ] ; then
+ echo -ne "\n $label\t$help"
+ else
+ echo -ne "\n $label\n\t\t\t$help"
+ fi
+}
+
+# $1: $cur
+# $2...: possible completions
+_vyatta_op_help ()
+{
+ local restore_shopts=$( shopt -p extglob nullglob | tr \\n \; )
+ shopt -u nullglob
+ local cur=$1; shift
+ local ndef node_tag_help node_run help last_help
+
+ ndef=${_vyatta_op_node_path}/node.tag/node.def
+ [ -f $ndef ] && \
+ node_tag_help=$( _vyatta_op_get_node_def_field $ndef help )
+
+ ndef=${_vyatta_op_node_path}/node.def
+ [ -f $ndef ] && \
+ node_run=$( _vyatta_op_get_node_def_field $ndef run )
+
+ if [[ "$1" == "<nocomps>" ]]; then
+ eval "$restore_shopts"
+ return
+ fi
+ echo -en "\nPossible completions:"
+ if [ -z "$cur" -a -n "$node_run" ]; then
+ _vyatta_op_print_help '<Enter>' "Execute the current command"
+ fi
+ if [ $# -eq 0 ];then
+ _vyatta_op_print_help '<text>' "$node_tag_help"
+ eval "$restore_shopts"
+ return
+ fi
+ for comp ; do
+ if [[ "$comp" == "<Enter>" ]]; then
+ continue
+ fi
+ if [ -z "$comp" ] ; then
+ if [ "X$node_tag_help" == "X$last_help" ] ; then
+ help=""
+ else
+ last_help=$node_tag_help
+ help=$node_tag_help
+ fi
+ _vyatta_op_print_help '*' "$help"
+ elif [[ -z "$cur" || $comp == ${cur}* ]] ; then
+ ndef=${_vyatta_op_node_path}/$comp/node.def
+ if [ -f $ndef ] ; then
+ help=$( _vyatta_op_get_node_def_field $ndef help )
+ else
+ help=$node_tag_help
+ fi
+ if [ "X$help" == "X$last_help" ] ; then
+ help=""
+ else
+ last_help=$help
+ fi
+ _vyatta_op_print_help "$comp" "$help"
+ fi
+ done
+ eval "$restore_shopts"
+}
+
+_vyatta_op_set_node_path ()
+{
+ local node
+ _vyatta_op_node_path=$vyatta_op_templates
+ for (( i=0 ; i<COMP_CWORD ; i++ )) ; do
+ # expand the command so completion continues to work with short versions
+ if [[ "${COMP_WORDS[i]}" == "*" ]]; then
+ node="node.tag" # user defined wildcars are always tag nodes
+ else
+ node=$(_vyatta_op_conv_node_path $_vyatta_op_node_path ${COMP_WORDS[i]})
+ fi
+ if [ -f "${_vyatta_op_node_path}/$node/node.def" ] ; then
+ _vyatta_op_node_path+=/$node
+ elif [ -f ${_vyatta_op_node_path}/node.tag/node.def ] ; then
+ _vyatta_op_node_path+=/node.tag
+ else
+ return 1
+ fi
+ done
+}
+
+_vyatta_op_set_completions ()
+{
+ local -a allowed completions
+ local cur=$1
+ local restore_shopts=$( shopt -p extglob nullglob | tr \\n \; )
+ for ndef in ${_vyatta_op_node_path}/*/node.def ; do
+ if [[ $ndef == */node.tag/node.def ]] ; then
+ local acmd=$( _vyatta_op_get_node_def_field $ndef allowed )
+ shopt -u extglob nullglob
+ local -a a=($( eval "$acmd" ))
+ eval "$restore_shopts"
+
+ if [ ${#a[@]} -ne 0 ] ; then
+ allowed+=( "${a[@]}" )
+ else
+ allowed+=( "<text>" )
+ fi
+ else
+ local sdir=${ndef%/*}
+ allowed+=( ${sdir##*/} )
+ fi
+ done
+
+ # donot complete entries like <HOSTNAME> or <A.B.C.D>
+ _vyatta_op_noncompletions=( )
+ completions=( )
+
+ # make runable commands have a non-comp
+ ndef=${_vyatta_op_node_path}/node.def
+ [ -f $ndef ] && \
+ node_run=$( _vyatta_op_get_node_def_field $ndef run )
+ if [ -z "$cur" -a -n "$node_run" ]; then
+ _vyatta_op_noncompletions+=('<Enter>')
+ fi
+
+ for (( i=0 ; i<${#allowed[@]} ; i++ )) ; do
+ if [[ "${allowed[i]}" == \<*\> ]] ; then
+ _vyatta_op_noncompletions+=( "${allowed[i]}" )
+ else
+ if [[ "$VYATTA_USER_LEVEL_DIR" == "/opt/vyatta/etc/shell/level/admin" ]]; then
+ completions+=( ${allowed[i]} )
+ elif is_elem_of ${allowed[i]} _vyatta_operator_allowed; then
+ completions+=( ${allowed[i]} )
+ elif [[ $_vyatta_op_node_path == $vyatta_op_templates ]];then
+ continue
+ else
+ completions+=( ${allowed[i]} )
+ fi
+ fi
+ done
+
+ # Prefix filter the non empty completions
+ if [ -n "$cur" ]; then
+ _vyatta_op_completions=()
+ get_prefix_filtered_list "$cur" completions _vyatta_op_completions
+ _vyatta_op_completions=($( printf "%s\n" ${_vyatta_op_completions[@]} | sort -u ))
+ else
+ _vyatta_op_completions=($( printf "%s\n" ${completions[@]} | sort -u ))
+ fi
+ #shopt -s nullglob
+}
+
+_vyatta_op_comprely_needs_ambiguity ()
+{
+ local -a uniq
+
+ [ ${#COMPREPLY[@]} -eq 1 ] && return
+
+ uniq=( `printf "%s\n" ${COMPREPLY[@]} | cut -c1 | sort -u` )
+
+ [ ${#uniq[@]} -eq 1 ] && return
+ false
+}
+
+_vyatta_op_invalid_completion ()
+{
+ local tpath=$vyatta_op_templates
+ local -a args
+ local i=1
+ for arg in "${COMP_WORDS[@]}"; do
+ arg=( $(_vyatta_op_conv_node_path $tpath $arg) ) # expand the arguments
+ # output proper error message based on the above expansion
+ if [[ "${arg[1]}" == "ambiguous" ]]; then
+ echo -ne "\n\n Ambiguous command: ${args[@]} [$arg]\n"
+ local -a cmds=( $(compgen -d $tpath/$arg) )
+ _vyatta_op_node_path=$tpath
+ local comps=$(_vyatta_op_help $arg ${cmds[@]##*/})
+ echo -ne "$comps" | sed -e 's/^P/ P/'
+ break
+ elif [[ "${arg[1]}" == "invalid" ]]; then
+ echo -ne "\n\n Invalid command: ${args[@]} [$arg]"
+ break
+ fi
+
+ if [ -f "$tpath/$arg/node.def" ] ; then
+ tpath+=/$arg
+ elif [ -f $tpath/node.tag/node.def ] ; then
+ tpath+=/node.tag
+ else
+ echo -ne "\n\n Invalid command: ${args[@]} [$arg]" >&2
+ break
+ fi
+ args[$i]=$arg
+ let "i+=1"
+ if [ $[${#COMP_WORDS[@]}+1] -eq $i ];then
+ _vyatta_op_help "" \
+ "${_vyatta_op_noncompletions[@]}" \
+ "${_vyatta_op_completions[@]}" \
+ | ${VYATTA_PAGER:-cat}
+ fi
+ done
+}
+
+_vyatta_op_expand ()
+{
+ # We need nospace here and we have to append our own spaces
+ compopt -o nospace
+
+ local restore_shopts=$( shopt -p extglob nullglob | tr \\n \; )
+ shopt -s extglob nullglob
+ local cur=""
+ local _has_comptype=0
+ local current_prefix=$2
+ local current_word=$3
+ _vyatta_comptype=""
+
+ if (( ${#COMP_WORDS[@]} > 0 )); then
+ cur=${COMP_WORDS[COMP_CWORD]}
+ else
+ (( COMP_CWORD = ${#COMP_WORDS[@]} ))
+ fi
+
+ if _vyatta_pipe_completion "${COMP_WORDS[@]}"; then
+ if [ "${COMP_WORDS[*]}" == "$_vyatta_op_last_comp" ] ||
+ [ ${#_vyatta_pipe_completions[@]} -eq 0 ]; then
+ _vyatta_do_pipe_help
+ COMPREPLY=( "" " " )
+ _vyatta_op_last_comp=${_vyatta_op_last_comp_init}
+ else
+ COMPREPLY=( "${_vyatta_pipe_completions[@]}" )
+ _vyatta_op_last_comp="${COMP_WORDS[*]}"
+ if [ ${#COMPREPLY[@]} -eq 1 ]; then
+ COMPREPLY=( "${COMPREPLY[0]} " )
+ fi
+ fi
+ eval "$restore_shopts"
+ return
+ fi
+
+ # this needs to be done on every completion even if it is the 'same' comp.
+ # The cursor can be at different places in the string.
+ # this will lead to unexpected cases if setting the node path isn't attempted
+ # each time.
+ if ! _vyatta_op_set_node_path ; then
+ echo -ne \\a
+ _vyatta_op_invalid_completion
+ COMPREPLY=( "" " " )
+ eval "$restore_shopts"
+ return 1
+ fi
+
+ if [ "${COMP_WORDS[*]:0:$[$COMP_CWORD+1]}" != "$_vyatta_op_last_comp" ] ; then
+ _vyatta_set_comptype
+ case $_vyatta_comptype in
+ 'imagefiles')
+ _has_comptype=1
+ _vyatta_image_file_complete
+ ;;
+ *)
+ _has_comptype=0
+ if [[ -z "$current_word" ]]; then
+ _vyatta_op_set_completions $cur
+ else
+ _vyatta_op_set_completions $current_prefix
+ fi
+ ;;
+ esac
+ fi
+ if [[ $_has_comptype == 1 ]]; then
+ COMPREPLY=( "${_vyatta_op_completions[@]}" )
+ else
+ COMPREPLY=($( compgen -W "${_vyatta_op_completions[*]}" -- $current_prefix ))
+ fi
+
+ # if the last command line arg is empty and we have
+ # an empty completion option (meaning wild card),
+ # append a blank(s) to the completion array to force ambiguity
+ if [ -z "$current_prefix" -a -n "$current_word" ] ||
+ [[ "${COMPREPLY[0]}" =~ "$cur" ]]; then
+ for comp ; do
+ if [ -z "$comp" ] ; then
+ if [ ${#COMPREPLY[@]} -eq 0 ] ; then
+ COMPREPLY=( " " "" )
+ elif _vyatta_op_comprely_needs_ambiguity ; then
+ COMPREPLY+=( " " )
+ fi
+ fi
+ done
+ fi
+ # Set this environment to enable and disable debugging on the fly
+ if [[ $DBG_OP_COMPS -eq 1 ]]; then
+ echo -e "\nCurrent: '$cur'"
+ echo -e "Current word: '$current_word'"
+ echo -e "Current prefix: '$current_prefix'"
+ echo "Number of comps: ${#_vyatta_op_completions[*]}"
+ echo "Number of non-comps: ${#_vyatta_op_noncompletions[*]}"
+ echo "_vyatta_op_completions: '${_vyatta_op_completions[*]}'"
+ echo "COMPREPLY: '${COMPREPLY[@]}'"
+ echo "CWORD: $COMP_CWORD"
+ echo "Last comp: '$_vyatta_op_last_comp'"
+ echo -e "Current comp: '${COMP_WORDS[*]:0:$[$COMP_CWORD+1]}'\n"
+ fi
+
+ # This is non obvious...
+ # To have completion continue to work when working with words that aren't the last word,
+ # we have to set nospace at the beginning of this script and then append the spaces here.
+ if [ ${#COMPREPLY[@]} -eq 1 ] &&
+ [[ $_has_comptype -ne 1 ]]; then
+ COMPREPLY=( "${COMPREPLY[0]} " )
+ fi
+ # if there are no completions then handle invalid commands
+ if [ ${#_vyatta_op_noncompletions[@]} -eq 0 ] &&
+ [ ${#_vyatta_op_completions[@]} -eq 0 ]; then
+ _vyatta_op_invalid_completion
+ COMPREPLY=( "" " " )
+ _vyatta_op_last_comp=${_vyatta_op_last_comp_init}
+ elif [ ${#COMPREPLY[@]} -eq 0 ] &&
+ [ -n "$current_prefix" ]; then
+ _vyatta_op_invalid_completion
+ COMPREPLY=( "" " " )
+ _vyatta_op_last_comp=${_vyatta_op_last_comp_init}
+ # Stop completions from getting stuck
+ elif [ ${#_vyatta_op_completions[@]} -eq 1 ] &&
+ [ -n "$cur" ] &&
+ [[ "${COMPREPLY[0]}" =~ "$cur" ]]; then
+ _vyatta_op_last_comp=${_vyatta_op_last_comp_init}
+ elif [ ${#_vyatta_op_completions[@]} -eq 1 ] &&
+ [ -n "$current_prefix" ] &&
+ [[ "${COMPREPLY[0]}" =~ "$current_prefix" ]]; then
+ _vyatta_op_last_comp=${_vyatta_op_last_comp_init}
+ # if there are no completions then always show the non-comps
+ elif [ "${COMP_WORDS[*]:0:$[$COMP_CWORD+1]}" == "$_vyatta_op_last_comp" ] ||
+ [ ${#_vyatta_op_completions[@]} -eq 0 ] ||
+ [ -z "$cur" ]; then
+ _vyatta_op_help "$current_prefix" \
+ "${_vyatta_op_noncompletions[@]}" \
+ "${_vyatta_op_completions[@]}" \
+ | ${VYATTA_PAGER:-cat}
+ COMPREPLY=( "" " " )
+ _vyatta_op_last_comp=${_vyatta_op_last_comp_init}
+ else
+ _vyatta_op_last_comp="${COMP_WORDS[*]:0:$[$COMP_CWORD+1]}"
+ fi
+
+ eval "$restore_shopts"
+}
+
+# "pipe" functions
+count ()
+{
+ wc -l
+}
+
+match ()
+{
+ grep -E -e "$1"
+}
+
+no-match ()
+{
+ grep -E -v -e "$1"
+}
+
+no-more ()
+{
+ cat
+}
+
+strip-private ()
+{
+ ${vyos_libexec_dir}/strip-private.py
+}
+
+commands ()
+{
+ if [ "$_OFR_CONFIGURE" != "" ]; then
+ if $(cli-shell-api sessionChanged); then
+ echo "You have uncommited changes, please commit them before using the commands pipe"
+ else
+ vyos-config-to-commands
+ fi
+ else
+ echo "commands pipe is not supported in operational mode"
+ fi
+}
+
+json ()
+{
+ if [ "$_OFR_CONFIGURE" != "" ]; then
+ if $(cli-shell-api sessionChanged); then
+ echo "You have uncommited changes, please commit them before using the JSON pipe"
+ else
+ vyos-config-to-json
+ fi
+ else
+ echo "JSON pipe is not supported in operational mode"
+ fi
+}
+
+# pipe command help
+# $1: command
+_vyatta_pipe_help ()
+{
+ local help="No help text available"
+ case "$1" in
+ count) help="Count the number of lines in the output";;
+ match) help="Only output lines that match specified pattern";;
+ no-match) help="Only output lines that do not match specified pattern";;
+ more) help="Paginate the output";;
+ no-more) help="Do not paginate the output";;
+ strip-private) help="Remove private information from the config";;
+ commands) help="Convert config to set commands";;
+ json) help="Convert config to JSON";;
+ '<pattern>') help="Pattern for matching";;
+ esac
+ echo -n "$help"
+}
+
+_vyatta_do_pipe_help ()
+{
+ local help=''
+ if (( ${#_vyatta_pipe_completions[@]} + ${#_vyatta_pipe_noncompletions[@]}
+ == 0 )); then
+ return
+ fi
+ echo -en "\nPossible completions:"
+ for comp in "${_vyatta_pipe_completions[@]}" \
+ "${_vyatta_pipe_noncompletions[@]}"; do
+ _vyatta_op_print_help "$comp" "$(_vyatta_pipe_help "$comp")"
+ done
+}
+
+# pipe completion
+# $@: words
+_vyatta_pipe_completion ()
+{
+ local -a pipe_cmd=()
+ local -a all_cmds=( 'count' 'match' 'no-match' 'more' 'no-more' 'strip-private' 'commands' 'json' )
+ local found=0
+ _vyatta_pipe_completions=()
+ _vyatta_pipe_noncompletions=()
+
+ for word in "$@"; do
+ if [[ "$found" == "1" || "$word" == "|" ]]; then
+ pipe_cmd+=( "$word" )
+ found=1
+ fi
+ done
+ if (( found == 0 )); then
+ return 1
+ fi
+ if (( ${#pipe_cmd[@]} == 1 )); then
+ # "|" only
+ _vyatta_pipe_completions=( "${all_cmds[@]}" )
+ return 0
+ fi
+ if (( ${#pipe_cmd[@]} == 2 )); then
+ # "|<space, chars, or space+chars>"
+ _vyatta_pipe_completions=($(compgen -W "${all_cmds[*]}" -- ${pipe_cmd[1]}))
+ return 0
+ fi
+ if (( ${#pipe_cmd[@]} == 3 )); then
+ # "|<chars or space+chars><space or space+chars>"
+ case "${pipe_cmd[1]}" in
+ match|no-match) _vyatta_pipe_noncompletions=( '<pattern>' );;
+ esac
+ return 0
+ fi
+ return 0
+}
+
+# comptype
+_vyatta_set_comptype ()
+{
+ local comptype
+ unset _vyatta_comptype
+ for ndef in ${_vyatta_op_node_path}/*/node.def ; do
+ if [[ $ndef == */node.tag/node.def ]] ; then
+ local comptype=$( _vyatta_op_get_node_def_field $ndef comptype )
+ if [[ $comptype == "imagefiles" ]] ; then
+ _vyatta_comptype=$comptype
+ return 0
+ else
+ _vyatta_comptype=""
+ return 1
+ fi
+ else
+ _vyatta_comptype=""
+ return 1
+ fi
+ done
+}
+
+_filedir_xspec_vyos()
+{
+ local cur prev words cword
+ _init_completion || return
+
+ _tilde "$cur" || return 0
+
+ local IFS=$'\n' xspec=${_xspec[${1##*/}]} tmp
+ local -a toks
+
+ toks=( $(
+ compgen -d -- "$(quote_readline "$cur")" | {
+ while read -r tmp; do
+ printf '%s\n' $tmp
+ done
+ }
+ ))
+
+ # Munge xspec to contain uppercase version too
+ # http://thread.gmane.org/gmane.comp.shells.bash.bugs/15294/focus=15306
+ eval xspec="${xspec}"
+ local matchop=!
+ if [[ $xspec == !* ]]; then
+ xspec=${xspec#!}
+ matchop=@
+ fi
+ xspec="$matchop($xspec|${xspec^^})"
+
+ toks+=( $(
+ eval compgen -f -X "!$xspec" -- "\$(quote_readline "\$cur")" | {
+ while read -r tmp; do
+ [[ -n $tmp ]] && printf '%s\n' $tmp
+ done
+ }
+ ))
+
+ if [[ ${#toks[@]} -ne 0 ]]; then
+ compopt -o filenames
+ COMPREPLY=( "${toks[@]}" )
+ fi
+}
+
+nullglob_save=$( shopt -p nullglob )
+shopt -s nullglob
+for f in ${vyatta_datadir}/vyatta-op/functions/allowed/* ; do
+ source $f
+done
+eval $nullglob_save
+unset nullglob_save
+
+# don't initialize if we are in configure mode
+if [ "$_OFR_CONFIGURE" == "ok" ]; then
+ return 0
+fi
+
+if [[ "$VYATTA_USER_LEVEL_DIR" != "/opt/vyatta/etc/shell/level/admin" ]]; then
+ vyatta_unpriv_init $@
+else
+ _vyatta_op_init $@
+fi
+
+### Local Variables:
+### mode: shell-script
+### End:
diff --git a/src/etc/default/vyatta b/src/etc/default/vyatta
new file mode 100644
index 000000000..e5fa3bb30
--- /dev/null
+++ b/src/etc/default/vyatta
@@ -0,0 +1,217 @@
+# **** License ****
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License version 2 as
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# General Public License for more details.
+#
+# This code was originally developed by Vyatta, Inc.
+# Portions created by Vyatta are Copyright (C) 2006, 2007 Vyatta, Inc.
+# All Rights Reserved.
+
+# declare configured Vyatta shell environment variables
+
+# first set vars per args of the "source /etc/default/vyatta VAR=FOO"
+_vyatta_extglob=$(shopt -p extglob)
+shopt -s extglob
+for arg ; do
+ [[ $arg == *=* ]] && \
+ eval declare -x $arg
+done
+eval $_vyatta_extglob
+unset _vyatta_extglob
+
+{
+ # These declarations must go within braces in order to be able to silence
+ # readonly variable errors.
+
+ for var in prefix exec_prefix datarootdir ; do
+ eval test -n \"\$$var\" \&\& _vyatta_save_$var=\$$var
+ done
+
+ prefix=/opt/vyatta
+ exec_prefix=${prefix}
+ datarootdir=${prefix}/share
+
+ if test -z "$vyatta_prefix" ; then
+ if test -n "/opt/vyatta" ; then
+ declare -x -r vyatta_prefix=/opt/vyatta
+ declare -x -r vyos_prefix=/opt/vyatta
+ else
+ declare -x -r vyatta_prefix=/opt/vyatta
+ declare -x -r vyos_prefix=/opt/vyatta
+ fi
+ fi
+ if test -z "$vyatta_exec_prefix" ; then
+ if test -n "${prefix}" ; then
+ declare -x -r vyatta_prefix=${prefix}
+ declare -x -r vyos_prefix=${prefix}
+ else
+ declare -x -r vyatta_prefix=$vyatta_prefix
+ declare -x -r vyos_prefix=$vyatta_prefix
+ fi
+ fi
+ if test -z "$vyatta_datarootdir" ; then
+ if test -n "${prefix}/share" ; then
+ declare -x -r vyatta_datarootdir=${prefix}/share
+ declare -x -r vyos_datarootdir=${prefix}/share
+ else
+ declare -x -r vyatta_datarootdir=$vyatta_prefix/share
+ declare -x -r vyos_datarootdir=$vyatta_prefix/share
+ fi
+ fi
+ if test -z "$vyatta_bindir" ; then
+ if test -n "${exec_prefix}/bin" ; then
+ declare -x -r vyatta_bindir=${exec_prefix}/bin
+ else
+ declare -x -r vyatta_bindir=$vyatta_exec_prefix/bin
+ fi
+ fi
+ if test -z "$vyatta_sbindir" ; then
+ if test -n "${exec_prefix}/sbin" ; then
+ declare -x -r vyatta_sbindir=${exec_prefix}/sbin
+ else
+ declare -x -r vyatta_sbindir=$vyatta_exec_prefix/sbin
+ fi
+ fi
+ if test -z "$vyatta_libdir" ; then
+ if test -n "${exec_prefix}/lib" ; then
+ declare -x -r vyatta_libdir=${exec_prefix}/lib
+ declare -x -r vyos_libdir=${exec_prefix}/lib
+ else
+ declare -x -r vyatta_libdir=$vyatta_exec_prefix/lib
+ declare -x -r vyos_libdir=$vyatta_exec_prefix/lib
+ fi
+ fi
+ if test -z "$vyatta_libexecdir" ; then
+ if test -n "${exec_prefix}/libexec" ; then
+ declare -x -r vyatta_libexecdir=${exec_prefix}/libexec
+ else
+ declare -x -r vyatta_libexecdir=$vyatta_exec_prefix/libexec
+ fi
+ fi
+ if test -z "$vyatta_datadir" ; then
+ if test -n "${datarootdir}" ; then
+ declare -x -r vyatta_datadir=${datarootdir}
+ declare -x -r vyos_datadir=${datarootdir}
+ else
+ declare -x -r vyatta_datadir=$vyatta_datarootdir
+ declare -x -r vyos_datadir=$vyatta_datarootdir
+ fi
+ fi
+ if test -z "$vyatta_htmldir" ; then
+ if test -n "${docdir}" ; then
+ declare -x -r vyatta_htmldir=${docdir}
+ else
+ declare -x -r vyatta_htmldir=$vyatta_datarootdir/html
+ fi
+ fi
+ if test -z "$vyatta_infodir" ; then
+ if test -n "${prefix}/share/info" ; then
+ declare -x -r vyatta_infodir=${prefix}/share/info
+ else
+ declare -x -r vyatta_infodir=$vyatta_datarootdir/info
+ fi
+ fi
+ if test -z "$vyatta_mandir" ; then
+ if test -n "${prefix}/share/man" ; then
+ declare -x -r vyatta_htmldir=${prefix}/share/man
+ else
+ declare -x -r vyatta_htmldir=$vyatta_datarootdir/man
+ fi
+ fi
+ if test -z "$vyatta_localedir" ; then
+ if test -n "${datarootdir}/locale" ; then
+ declare -x -r vyatta_localedir=${datarootdir}/locale
+ else
+ declare -x -r vyatta_localedir=$vyatta_datarootdir/locale
+ fi
+ fi
+ if test -z "$vyatta_localstatedir" ; then
+ if test -n "${prefix}/var" ; then
+ declare -x -r vyatta_localstatedir=${prefix}/var
+ else
+ declare -x -r vyatta_localstatedir=$vyatta_prefix/var
+ fi
+ fi
+ if test -z "$vyatta_sharedstatedir" ; then
+ if test -n "${prefix}/com" ; then
+ declare -x -r vyatta_sharedstatedir=${prefix}/com
+ else
+ declare -x -r vyatta_sharedstatedir=$vyatta_prefix/com
+ fi
+ fi
+ if test -z "$vyatta_sysconfdir" ; then
+ if test -n "${prefix}/etc" ; then
+ declare -x -r vyatta_sysconfdir=${prefix}/etc
+ else
+ declare -x -r vyatta_sysconfdir=$vyatta_prefix/etc
+ fi
+ fi
+ if test -z "$vyatta_op_templates" ; then
+ declare -x -r vyatta_op_templates=$vyatta_datadir/vyatta-op/templates
+ declare -x -r vyos_op_templates=$vyatta_datadir/vyatta-op/templates
+ fi
+ if test -z "$vyatta_cfg_templates" ; then
+ declare -x -r vyatta_cfg_templates=$vyatta_datadir/vyatta-cfg/templates
+ declare -x -r vyos_cfg_templates=$vyatta_datadir/vyatta-cfg/templates
+ fi
+ if test -z "$vyatta_configdir" ; then
+ declare -x -r vyatta_configdir=$vyatta_prefix/config
+ declare -x -r vyos_configdir=$vyatta_prefix/config
+ fi
+
+ for var in prefix exec_prefix datarootdir ; do
+ eval test -n \"\$_vyatta_save_$var\" \&\& $var=\$_vyatta_save_$var
+ done
+
+ # It's not like we do, or should support installing VyOS at a different prefix
+ declare -x -r vyos_libexec_dir=/usr/libexec/vyos
+ declare -x -r vyos_bin_dir=/usr/bin
+ declare -x -r vyos_sbin_dir=/usr/sbin
+ declare -x -r vyos_share_dir=/usr/share
+
+ if test -z "$vyos_conf_scripts_dir" ; then
+ declare -x -r vyos_conf_scripts_dir=$vyos_libexec_dir/conf_mode
+ fi
+ if test -z "$vyos_op_scripts_dir" ; then
+ declare -x -r vyos_op_scripts_dir=$vyos_libexec_dir/op_mode
+ fi
+ if test -z "$vyos_completion_dir" ; then
+ declare -x -r vyos_completion_dir=$vyos_libexec_dir/completion
+ fi
+ if test -z "$vyos_validators_dir" ; then
+ declare -x -r vyos_validators_dir=$vyos_libexec_dir/validators
+ fi
+ if test -z "$vyos_data_dir" ; then
+ declare -x -r vyos_data_dir=$vyos_share_dir/vyos
+ fi
+ if test -z "$vyos_persistence_dir" ; then
+ UNION_NAME=$(cat /proc/cmdline | sed -e s+^.*vyos-union=++ | sed -e 's/ .*$//')
+ declare -x -r vyos_persistence_dir="/usr/lib/live/mount/persistence/${UNION_NAME}"
+ fi
+ if test -z "$vyos_rootfs_dir" ; then
+ ROOTFS=$(mount -t squashfs | grep loop0 | cut -d' ' -f3)
+ declare -x -r vyos_rootfs_dir="${ROOTFS}"
+ fi
+ if test -z "$VRF" ; then
+ VRF=$(ip vrf identify)
+ [ -n "$VRF" ] && declare -x -r VRF="${VRF}"
+ fi
+ if test -z "$NETNS" ; then
+ NETNS=$(ip netns identify)
+ [ -n "$NETNS" ] && declare -x -r NETNS="${NETNS}"
+ fi
+
+} 2>/dev/null || :
+
+[ -r /etc/default/vyatta-cfg ] && source /etc/default/vyatta-cfg
+
+[ -r /etc/default/vyatta-local-env ] && source /etc/default/vyatta-local-env
+
+### Local Variables:
+### mode: shell-script
+### End:
diff --git a/src/helpers/vyos_net_name b/src/helpers/vyos_net_name
index 518e204f9..f5de182c6 100755
--- a/src/helpers/vyos_net_name
+++ b/src/helpers/vyos_net_name
@@ -18,42 +18,35 @@ import os
import re
import time
import logging
+import logging.handlers
import tempfile
-import threading
+from pathlib import Path
from sys import argv
from vyos.configtree import ConfigTree
from vyos.defaults import directories
from vyos.utils.process import cmd
from vyos.utils.boot import boot_configuration_complete
+from vyos.utils.locking import Lock
from vyos.migrate import ConfigMigrate
+# Define variables
vyos_udev_dir = directories['vyos_udev_dir']
-vyos_log_dir = '/run/udev/log'
-vyos_log_file = os.path.join(vyos_log_dir, 'vyos-net-name')
-
config_path = '/opt/vyatta/etc/config/config.boot'
-lock = threading.Lock()
-
-try:
- os.mkdir(vyos_log_dir)
-except FileExistsError:
- pass
-
-logging.basicConfig(filename=vyos_log_file, level=logging.DEBUG)
def is_available(intfs: dict, intf_name: str) -> bool:
- """ Check if interface name is already assigned
- """
+ """Check if interface name is already assigned"""
if intf_name in list(intfs.values()):
return False
return True
+
def find_available(intfs: dict, prefix: str) -> str:
- """ Find lowest indexed iterface name that is not assigned
- """
- index_list = [int(x.replace(prefix, '')) for x in list(intfs.values()) if prefix in x]
+ """Find lowest indexed iterface name that is not assigned"""
+ index_list = [
+ int(x.replace(prefix, '')) for x in list(intfs.values()) if prefix in x
+ ]
index_list.sort()
# find 'holes' in list, if any
missing = sorted(set(range(index_list[0], index_list[-1])) - set(index_list))
@@ -62,21 +55,22 @@ def find_available(intfs: dict, prefix: str) -> str:
return f'{prefix}{len(index_list)}'
+
def mod_ifname(ifname: str) -> str:
- """ Check interface with names eX and return ifname on the next format eth{ifindex} - 2
- """
- if re.match("^e[0-9]+$", ifname):
- intf = ifname.split("e")
+ """Check interface with names eX and return ifname on the next format eth{ifindex} - 2"""
+ if re.match('^e[0-9]+$', ifname):
+ intf = ifname.split('e')
if intf[1]:
if int(intf[1]) >= 2:
- return "eth" + str(int(intf[1]) - 2)
+ return 'eth' + str(int(intf[1]) - 2)
else:
- return "eth" + str(intf[1])
+ return 'eth' + str(intf[1])
return ifname
+
def get_biosdevname(ifname: str) -> str:
- """ Use legacy vyatta-biosdevname to query for name
+ """Use legacy vyatta-biosdevname to query for name
This is carried over for compatability only, and will likely be dropped
going forward.
@@ -95,11 +89,12 @@ def get_biosdevname(ifname: str) -> str:
try:
biosname = cmd(f'/sbin/biosdevname --policy all_ethN -i {ifname}')
except Exception as e:
- logging.error(f'biosdevname error: {e}')
+ logger.error(f'biosdevname error: {e}')
biosname = ''
return intf if biosname == '' else biosname
+
def leave_rescan_hint(intf_name: str, hwid: str):
"""Write interface information reported by udev
@@ -112,18 +107,18 @@ def leave_rescan_hint(intf_name: str, hwid: str):
except FileExistsError:
pass
except Exception as e:
- logging.critical(f"Error creating rescan hint directory: {e}")
+ logger.critical(f'Error creating rescan hint directory: {e}')
exit(1)
try:
with open(os.path.join(vyos_udev_dir, intf_name), 'w') as f:
f.write(hwid)
except OSError as e:
- logging.critical(f"OSError {e}")
+ logger.critical(f'OSError {e}')
+
def get_configfile_interfaces() -> dict:
- """Read existing interfaces from config file
- """
+ """Read existing interfaces from config file"""
interfaces: dict = {}
if not os.path.isfile(config_path):
@@ -134,14 +129,14 @@ def get_configfile_interfaces() -> dict:
with open(config_path) as f:
config_file = f.read()
except OSError as e:
- logging.critical(f"OSError {e}")
+ logger.critical(f'OSError {e}')
exit(1)
try:
config = ConfigTree(config_file)
except Exception:
try:
- logging.debug(f"updating component version string syntax")
+ logger.debug('updating component version string syntax')
# this will update the component version string syntax,
# required for updates 1.2 --> 1.3/1.4
with tempfile.NamedTemporaryFile() as fp:
@@ -157,7 +152,8 @@ def get_configfile_interfaces() -> dict:
config = ConfigTree(config_file)
except Exception as e:
- logging.critical(f"ConfigTree error: {e}")
+ logger.critical(f'ConfigTree error: {e}')
+ exit(1)
base = ['interfaces', 'ethernet']
if config.exists(base):
@@ -165,11 +161,13 @@ def get_configfile_interfaces() -> dict:
for intf in eth_intfs:
path = base + [intf, 'hw-id']
if not config.exists(path):
- logging.warning(f"no 'hw-id' entry for {intf}")
+ logger.warning(f"no 'hw-id' entry for {intf}")
continue
hwid = config.return_value(path)
if hwid in list(interfaces):
- logging.warning(f"multiple entries for {hwid}: {interfaces[hwid]}, {intf}")
+ logger.warning(
+ f'multiple entries for {hwid}: {interfaces[hwid]}, {intf}'
+ )
continue
interfaces[hwid] = intf
@@ -179,21 +177,23 @@ def get_configfile_interfaces() -> dict:
for intf in wlan_intfs:
path = base + [intf, 'hw-id']
if not config.exists(path):
- logging.warning(f"no 'hw-id' entry for {intf}")
+ logger.warning(f"no 'hw-id' entry for {intf}")
continue
hwid = config.return_value(path)
if hwid in list(interfaces):
- logging.warning(f"multiple entries for {hwid}: {interfaces[hwid]}, {intf}")
+ logger.warning(
+ f'multiple entries for {hwid}: {interfaces[hwid]}, {intf}'
+ )
continue
interfaces[hwid] = intf
- logging.debug(f"config file entries: {interfaces}")
+ logger.debug(f'config file entries: {interfaces}')
return interfaces
+
def add_assigned_interfaces(intfs: dict):
- """Add interfaces found by previous invocation of udev rule
- """
+ """Add interfaces found by previous invocation of udev rule"""
if not os.path.isdir(vyos_udev_dir):
return
@@ -203,55 +203,74 @@ def add_assigned_interfaces(intfs: dict):
with open(path) as f:
hwid = f.read().rstrip()
except OSError as e:
- logging.error(f"OSError {e}")
+ logger.error(f'OSError {e}')
continue
intfs[hwid] = intf
+
def on_boot_event(intf_name: str, hwid: str, predefined: str = '') -> str:
- """Called on boot by vyos-router: 'coldplug' in vyatta_net_name
- """
- logging.info(f"lookup {intf_name}, {hwid}")
+ """Called on boot by vyos-router: 'coldplug' in vyatta_net_name"""
+ logger.info(f'lookup {intf_name}, {hwid}')
interfaces = get_configfile_interfaces()
- logging.debug(f"config file interfaces are {interfaces}")
+ logger.debug(f'config file interfaces are {interfaces}')
if hwid in list(interfaces):
- logging.info(f"use mapping from config file: '{hwid}' -> '{interfaces[hwid]}'")
+ logger.info(f"use mapping from config file: '{hwid}' -> '{interfaces[hwid]}'")
return interfaces[hwid]
add_assigned_interfaces(interfaces)
- logging.debug(f"adding assigned interfaces: {interfaces}")
+ logger.debug(f'adding assigned interfaces: {interfaces}')
if predefined:
newname = predefined
- logging.info(f"predefined interface name for '{intf_name}' is '{newname}'")
+ logger.info(f"predefined interface name for '{intf_name}' is '{newname}'")
else:
newname = get_biosdevname(intf_name)
- logging.info(f"biosdevname returned '{newname}' for '{intf_name}'")
+ logger.info(f"biosdevname returned '{newname}' for '{intf_name}'")
if not is_available(interfaces, newname):
prefix = re.sub(r'\d+$', '', newname)
newname = find_available(interfaces, prefix)
- logging.info(f"new name for '{intf_name}' is '{newname}'")
+ logger.info(f"new name for '{intf_name}' is '{newname}'")
leave_rescan_hint(newname, hwid)
return newname
+
def hotplug_event():
# Not yet implemented, since interface-rescan will only be run on boot.
pass
-if len(argv) > 3:
- predef_name = argv[3]
-else:
- predef_name = ''
-
-lock.acquire()
-if not boot_configuration_complete():
- res = on_boot_event(argv[1], argv[2], predefined=predef_name)
- logging.debug(f"on boot, returned name is {res}")
- print(res)
-else:
- logging.debug("boot configuration complete")
-lock.release()
+
+if __name__ == '__main__':
+ # Set up logging to syslog
+ syslog_handler = logging.handlers.SysLogHandler(address='/dev/log')
+ formatter = logging.Formatter(f'{Path(__file__).name}: %(message)s')
+ syslog_handler.setFormatter(formatter)
+
+ logger = logging.getLogger()
+ logger.addHandler(syslog_handler)
+ logger.setLevel(logging.DEBUG)
+
+ logger.debug(f'Started with arguments: {argv}')
+
+ if len(argv) > 3:
+ predef_name = argv[3]
+ else:
+ predef_name = ''
+
+ lock = Lock('vyos_net_name')
+ # Wait 60 seconds for other running scripts to finish
+ lock.acquire(60)
+
+ if not boot_configuration_complete():
+ res = on_boot_event(argv[1], argv[2], predefined=predef_name)
+ logger.debug(f'on boot, returned name is {res}')
+ print(res)
+ else:
+ logger.debug('boot configuration complete')
+
+ lock.release()
+ logger.debug('Finished')
diff --git a/src/migration-scripts/firewall/16-to-17 b/src/migration-scripts/firewall/16-to-17
new file mode 100755
index 000000000..ad0706f04
--- /dev/null
+++ b/src/migration-scripts/firewall/16-to-17
@@ -0,0 +1,60 @@
+# Copyright (C) 2024 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/>.
+
+#
+# T4694: Adding rt ipsec exists/missing match to firewall configs.
+# This involves a syntax change for IPsec matches, reflecting that different
+# nftables expressions are required depending on whether we're matching a
+# decrypted packet or a packet that will be encrypted - it's directional.
+# The old rules only matched decrypted packets, those matches are now *-in:
+ # from: set firewall <family> <chainspec> rule <rule#> ipsec match-ipsec|match-none
+ # to: set firewall <family> <chainspec> rule <rule#> ipsec match-ipsec-in|match-none-in
+#
+# The <chainspec> positions this match allowed were:
+# name (any custom chains), forward filter, input filter, prerouting raw.
+# There are positions where it was possible to set, but it would never commit
+# (nftables rejects 'meta ipsec' in output hooks), they are not considered here.
+#
+
+from vyos.configtree import ConfigTree
+
+firewall_base = ['firewall']
+
+def migrate_chain(config: ConfigTree, path: list[str]) -> None:
+ if not config.exists(path + ['rule']):
+ return
+
+ for rule_num in config.list_nodes(path + ['rule']):
+ tmp_path = path + ['rule', rule_num, 'ipsec']
+ if config.exists(tmp_path + ['match-ipsec']):
+ config.delete(tmp_path + ['match-ipsec'])
+ config.set(tmp_path + ['match-ipsec-in'])
+ elif config.exists(tmp_path + ['match-none']):
+ config.delete(tmp_path + ['match-none'])
+ config.set(tmp_path + ['match-none-in'])
+
+def migrate(config: ConfigTree) -> None:
+ if not config.exists(firewall_base):
+ # Nothing to do
+ return
+
+ for family in ['ipv4', 'ipv6']:
+ tmp_path = firewall_base + [family, 'name']
+ if config.exists(tmp_path):
+ for custom_fwname in config.list_nodes(tmp_path):
+ migrate_chain(config, tmp_path + [custom_fwname])
+
+ for base_hook in [['forward', 'filter'], ['input', 'filter'], ['prerouting', 'raw']]:
+ tmp_path = firewall_base + [family] + base_hook
+ migrate_chain(config, tmp_path)
diff --git a/src/migration-scripts/firewall/7-to-8 b/src/migration-scripts/firewall/7-to-8
index f46994ce2..b8bcc52cc 100644
--- a/src/migration-scripts/firewall/7-to-8
+++ b/src/migration-scripts/firewall/7-to-8
@@ -71,5 +71,11 @@ def migrate(config: ConfigTree) -> None:
config.set_tag(['firewall', 'zone'])
for zone in config.list_nodes(zone_base + ['zone']):
+ if 'interface' in config.list_nodes(zone_base + ['zone', zone]):
+ for iface in config.return_values(zone_base + ['zone', zone, 'interface']):
+ if '+' in iface:
+ config.delete_value(zone_base + ['zone', zone, 'interface'], value=iface)
+ iface = iface.replace('+', '*')
+ config.set(zone_base + ['zone', zone, 'interface'], value=iface, replace=False)
config.copy(zone_base + ['zone', zone], ['firewall', 'zone', zone])
- config.delete(zone_base)
+ config.delete(zone_base) \ No newline at end of file
diff --git a/src/migration-scripts/nat/6-to-7 b/src/migration-scripts/nat/6-to-7
index 81b413e36..e9b90fc98 100644
--- a/src/migration-scripts/nat/6-to-7
+++ b/src/migration-scripts/nat/6-to-7
@@ -47,6 +47,8 @@ def migrate(config: ConfigTree) -> None:
tmp = config.return_value(base + [iface, 'interface-name'])
if tmp != 'any':
config.delete(base + [iface, 'interface-name'])
+ if '+' in tmp:
+ tmp = tmp.replace('+', '*')
config.set(base + [iface, 'name'], value=tmp)
else:
config.delete(base + [iface])
diff --git a/src/migration-scripts/openvpn/1-to-2 b/src/migration-scripts/openvpn/1-to-2
index b7b7d4c77..2baa7302c 100644
--- a/src/migration-scripts/openvpn/1-to-2
+++ b/src/migration-scripts/openvpn/1-to-2
@@ -20,12 +20,8 @@
from vyos.configtree import ConfigTree
def migrate(config: ConfigTree) -> None:
- if not config.exists(['interfaces', 'openvpn']):
- # Nothing to do
- return
-
- ovpn_intfs = config.list_nodes(['interfaces', 'openvpn'])
- for i in ovpn_intfs:
+ ovpn_intfs = config.list_nodes(['interfaces', 'openvpn'], path_must_exist=False)
+ for i in ovpn_intfs:
# Remove 'encryption cipher' and add this value to 'encryption ncp-ciphers'
# for server and client mode.
# Site-to-site mode still can use --cipher option
diff --git a/src/migration-scripts/openvpn/2-to-3 b/src/migration-scripts/openvpn/2-to-3
new file mode 100644
index 000000000..4e6b3c8b7
--- /dev/null
+++ b/src/migration-scripts/openvpn/2-to-3
@@ -0,0 +1,39 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2024 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/>.
+#
+# Adds an explicit old default for 'server topology'
+# to keep old configs working as before even though the default has changed.
+
+from vyos.configtree import ConfigTree
+
+def migrate(config: ConfigTree) -> None:
+ ovpn_intfs = config.list_nodes(['interfaces', 'openvpn'], path_must_exist=False)
+ for i in ovpn_intfs:
+ mode = config.return_value(['interfaces', 'openvpn', i, 'mode'])
+ if mode != 'server':
+ # If it's a client or a site-to-site OpenVPN interface,
+ # the topology setting is not applicable
+ # and will cause commit errors on load,
+ # so we must not change such interfaces.
+ continue
+ else:
+ # The default OpenVPN server topology was changed from net30 to subnet
+ # because net30 is deprecated and causes problems with Windows clients.
+ # We add 'net30' to old configs if topology is not set there
+ # to ensure that if anyone relies on net30, their configs work as before.
+ topology_path = ['interfaces', 'openvpn', i, 'server', 'topology']
+ if not config.exists(topology_path):
+ config.set(topology_path, value='net30', replace=False)
diff --git a/src/migration-scripts/openvpn/3-to-4 b/src/migration-scripts/openvpn/3-to-4
new file mode 100644
index 000000000..0529491c1
--- /dev/null
+++ b/src/migration-scripts/openvpn/3-to-4
@@ -0,0 +1,26 @@
+#!/usr/bin/env python3
+# Copyright 2024 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/>.
+# Renames ncp-ciphers option to data-ciphers
+
+from vyos.configtree import ConfigTree
+
+def migrate(config: ConfigTree) -> None:
+ ovpn_intfs = config.list_nodes(['interfaces', 'openvpn'], path_must_exist=False)
+ for i in ovpn_intfs:
+ #Rename 'encryption ncp-ciphers' with 'encryption data-ciphers'
+ ncp_cipher_path = ['interfaces', 'openvpn', i, 'encryption', 'ncp-ciphers']
+ if config.exists(ncp_cipher_path):
+ config.rename(ncp_cipher_path, 'data-ciphers')
diff --git a/src/op_mode/bridge.py b/src/op_mode/bridge.py
index d04f1541f..e80b1c21d 100755
--- a/src/op_mode/bridge.py
+++ b/src/op_mode/bridge.py
@@ -70,7 +70,7 @@ def _get_raw_data_fdb(bridge):
# From iproute2 fdb.c, fdb_show() will only exit(-1) in case of
# non-existent bridge device; raise error.
if code == 255:
- raise vyos.opmode.UnconfiguredSubsystem(f"no such bridge device {bridge}")
+ raise vyos.opmode.UnconfiguredObject(f"bridge {bridge} does not exist in the system")
data_dict = json.loads(json_data)
return data_dict
diff --git a/src/op_mode/connect_disconnect.py b/src/op_mode/connect_disconnect.py
index 373f9e953..8903f916a 100755
--- a/src/op_mode/connect_disconnect.py
+++ b/src/op_mode/connect_disconnect.py
@@ -95,17 +95,21 @@ def disconnect(interface):
def main():
parser = argparse.ArgumentParser()
group = parser.add_mutually_exclusive_group()
- group.add_argument("--connect", help="Bring up a connection-oriented network interface", action="store")
- group.add_argument("--disconnect", help="Take down connection-oriented network interface", action="store")
+ group.add_argument("--connect", help="Bring up a connection-oriented network interface", action="store_true")
+ group.add_argument("--disconnect", help="Take down connection-oriented network interface", action="store_true")
+ parser.add_argument("--interface", help="Interface name", action="store", required=True)
args = parser.parse_args()
- if args.connect:
- if commit_in_progress():
- print('Cannot connect while a commit is in progress')
- exit(1)
- connect(args.connect)
- elif args.disconnect:
- disconnect(args.disconnect)
+ if args.connect or args.disconnect:
+ if args.disconnect:
+ disconnect(args.interface)
+
+ if args.connect:
+ if commit_in_progress():
+ print('Cannot connect while a commit is in progress')
+ exit(1)
+ connect(args.interface)
+
else:
parser.print_help()
diff --git a/src/op_mode/dhcp.py b/src/op_mode/dhcp.py
index 6f57f22a5..e5455c8af 100755
--- a/src/op_mode/dhcp.py
+++ b/src/op_mode/dhcp.py
@@ -332,7 +332,7 @@ def _verify_client(func):
# Check if config does not exist
if not config.exists(f'interfaces {interface_path} address dhcp{v}'):
- raise vyos.opmode.UnconfiguredSubsystem(unconf_message)
+ raise vyos.opmode.UnconfiguredObject(unconf_message)
return func(*args, **kwargs)
return _wrapper
diff --git a/src/op_mode/interfaces.py b/src/op_mode/interfaces.py
index 14ffdca9f..e7afc4caa 100755
--- a/src/op_mode/interfaces.py
+++ b/src/op_mode/interfaces.py
@@ -445,12 +445,24 @@ def _format_show_counters(data: list):
print (output)
return output
+
+def _show_raw(data: list, intf_name: str):
+ if intf_name is not None and len(data) <= 1:
+ try:
+ return data[0]
+ except IndexError:
+ raise vyos.opmode.UnconfiguredObject(
+ f"Interface {intf_name} does not exist")
+ else:
+ return data
+
+
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 _show_raw(data, intf_name)
return _format_show_data(data)
def show_summary(raw: bool, intf_name: typing.Optional[str],
@@ -458,7 +470,7 @@ def show_summary(raw: bool, intf_name: typing.Optional[str],
vif: bool, vrrp: bool):
data = _get_summary_data(intf_name, intf_type, vif, vrrp)
if raw:
- return data
+ return _show_raw(data, intf_name)
return _format_show_summary(data)
def show_summary_extended(raw: bool, intf_name: typing.Optional[str],
@@ -466,7 +478,7 @@ def show_summary_extended(raw: bool, intf_name: typing.Optional[str],
vif: bool, vrrp: bool):
data = _get_summary_data(intf_name, intf_type, vif, vrrp)
if raw:
- return data
+ return _show_raw(data, intf_name)
return _format_show_summary_extended(data)
def show_counters(raw: bool, intf_name: typing.Optional[str],
@@ -474,7 +486,7 @@ def show_counters(raw: bool, intf_name: typing.Optional[str],
vif: bool, vrrp: bool):
data = _get_counter_data(intf_name, intf_type, vif, vrrp)
if raw:
- return data
+ return _show_raw(data, intf_name)
return _format_show_counters(data)
def clear_counters(intf_name: typing.Optional[str],
diff --git a/src/op_mode/ipsec.py b/src/op_mode/ipsec.py
index 44d41219e..c8f5072da 100755
--- a/src/op_mode/ipsec.py
+++ b/src/op_mode/ipsec.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2022-2023 VyOS maintainers and contributors
+# Copyright (C) 2022-2024 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,6 +13,7 @@
#
# 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 pprint
import re
import sys
import typing
@@ -25,6 +26,7 @@ from vyos.utils.convert import convert_data
from vyos.utils.convert import seconds_to_human
from vyos.utils.process import cmd
from vyos.configquery import ConfigTreeQuery
+from vyos.base import Warning
import vyos.opmode
import vyos.ipsec
@@ -43,7 +45,7 @@ def _get_raw_data_sas():
get_sas = vyos.ipsec.get_vici_sas()
sas = convert_data(get_sas)
return sas
- except (vyos.ipsec.ViciInitiateError) as err:
+ except vyos.ipsec.ViciInitiateError as err:
raise vyos.opmode.UnconfiguredSubsystem(err)
@@ -56,11 +58,10 @@ def _get_output_swanctl_sas_from_list(ra_output_list: list) -> str:
:return: formatted string
:rtype: str
"""
- output = '';
+ output = ''
for sa_val in ra_output_list:
for sa in sa_val.values():
- swanctl_output: str = cmd(
- f'sudo swanctl -l --ike-id {sa["uniqueid"]}')
+ swanctl_output: str = cmd(f'sudo swanctl -l --ike-id {sa["uniqueid"]}')
output = f'{output}{swanctl_output}\n\n'
return output
@@ -72,7 +73,9 @@ def _get_formatted_output_sas(sas):
# 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'
+ 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')
@@ -104,10 +107,8 @@ def _get_formatted_output_sas(sas):
bytes_out = filesize.size(int(sa_bytes_out))
sa_out_bytes = f'{bytes_in}/{bytes_out}'
if sa_packets_in and sa_packets_out:
- packets_in = filesize.size(int(sa_packets_in),
- system=filesize.si)
- packets_out = filesize.size(int(sa_packets_out),
- system=filesize.si)
+ packets_in = filesize.size(int(sa_packets_in), system=filesize.si)
+ packets_out = filesize.size(int(sa_packets_out), system=filesize.si)
packets_str = f'{packets_in}/{packets_out}'
sa_out_packets = re.sub(r'B', r'', packets_str)
if sa_remote_addr:
@@ -119,7 +120,9 @@ def _get_formatted_output_sas(sas):
sa_out_proposal = sa_proposal_encr_alg
if sa_proposal_encr_keysize:
sa_proposal_encr_keysize_str = sa_proposal_encr_keysize
- sa_out_proposal = f'{sa_out_proposal}_{sa_proposal_encr_keysize_str}'
+ 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
sa_out_proposal = f'{sa_out_proposal}/{sa_proposal_integ_alg_str}'
@@ -128,15 +131,28 @@ def _get_formatted_output_sas(sas):
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
- ])
+ 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,
+ ]
+ )
headers = [
- "Connection", "State", "Uptime", "Bytes In/Out", "Packets In/Out",
- "Remote address", "Remote ID", "Proposal"
+ 'Connection',
+ 'State',
+ 'Uptime',
+ 'Bytes In/Out',
+ 'Packets In/Out',
+ 'Remote address',
+ 'Remote ID',
+ 'Proposal',
]
sa_data = sorted(sa_data, key=_alphanum_key)
output = tabulate(sa_data, headers)
@@ -145,14 +161,16 @@ def _get_formatted_output_sas(sas):
# Connections block
+
def _get_convert_data_connections():
try:
get_connections = vyos.ipsec.get_vici_connections()
connections = convert_data(get_connections)
return connections
- except (vyos.ipsec.ViciInitiateError) as err:
+ except vyos.ipsec.ViciInitiateError as err:
raise vyos.opmode.UnconfiguredSubsystem(err)
+
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
@@ -184,7 +202,7 @@ def _get_parent_sa_proposal(connection_name: str, data: list) -> dict:
'mode': mode,
'key_size': encr_keysize,
'hash': integ_alg,
- 'dh': dh_group
+ 'dh': dh_group,
}
return proposal
return {}
@@ -213,8 +231,7 @@ def _get_parent_sa_state(connection_name: str, data: list) -> str:
return ike_state
-def _get_child_sa_state(connection_name: str, tunnel_name: str,
- data: list) -> str:
+def _get_child_sa_state(connection_name: str, tunnel_name: str, data: list) -> str:
"""Get child SA state by connection and tunnel name
Args:
@@ -236,14 +253,12 @@ def _get_child_sa_state(connection_name: str, tunnel_name: str,
# 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
+ 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:
+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:
@@ -264,8 +279,9 @@ def _get_child_sa_info(connection_name: str, tunnel_name: str,
# {'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'
+ 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 {}
@@ -283,7 +299,7 @@ def _get_child_sa_proposal(child_sa_data: dict) -> dict:
'mode': mode,
'key_size': key_size,
'hash': integ_alg,
- 'dh': dh_group
+ 'dh': dh_group,
}
return proposal
return {}
@@ -305,10 +321,10 @@ def _get_raw_data_connections(list_connections: list, list_sas: list) -> 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)
+ 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['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')
@@ -322,22 +338,25 @@ def _get_raw_data_connections(list_connections: list, list_sas: list) -> list:
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_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)
@@ -347,17 +366,16 @@ def _get_raw_connections_summary(list_conn, list_sas):
'tunnels': child,
'total': len(child),
'down': tunnels_down,
- 'up': tunnels_up
+ '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')
@@ -367,15 +385,26 @@ def _get_formatted_output_conections(data):
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
- ])
+ 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')
@@ -384,18 +413,36 @@ def _get_formatted_output_conections(data):
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
- ])
+ 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'
+ 'Connection',
+ 'State',
+ 'Type',
+ 'Remote address',
+ 'Local TS',
+ 'Remote TS',
+ 'Local id',
+ 'Remote id',
+ 'Proposal',
]
output = tabulate(connections, connection_headers, numalign='left')
return output
@@ -421,6 +468,31 @@ def _get_childsa_id_list(ike_sas: list) -> list:
return list_childsa_id
+def _get_con_childsa_name_list(
+ ike_sas: list, filter_dict: typing.Optional[dict] = None
+) -> list:
+ """
+ Generate list of CHILD SA ids based on list of OrderingDict
+ wich is returned by vici
+ :param ike_sas: list of IKE SAs connections generated by vici
+ :type ike_sas: list
+ :param filter_dict: dict of filter options
+ :type filter_dict: dict
+ :return: list of IKE SAs name
+ :rtype: list
+ """
+ list_childsa_name: list = []
+ for ike in ike_sas:
+ for ike_name, ike_values in ike.items():
+ for sa, sa_values in ike_values['children'].items():
+ if filter_dict:
+ if filter_dict.items() <= sa_values.items():
+ list_childsa_name.append(sa)
+ else:
+ list_childsa_name.append(sa)
+ return list_childsa_name
+
+
def _get_all_sitetosite_peers_name_list() -> list:
"""
Return site-to-site peers configuration
@@ -429,53 +501,142 @@ def _get_all_sitetosite_peers_name_list() -> list:
"""
conf: ConfigTreeQuery = ConfigTreeQuery()
config_path = ['vpn', 'ipsec', 'site-to-site', 'peer']
- peers_config = conf.get_config_dict(config_path, key_mangling=('-', '_'),
- get_first_key=True,
- no_tag_node_value_mangle=True)
+ peers_config = conf.get_config_dict(
+ config_path,
+ key_mangling=('-', '_'),
+ get_first_key=True,
+ no_tag_node_value_mangle=True,
+ )
peers_list: list = []
for name in peers_config:
peers_list.append(name)
return peers_list
-def reset_peer(peer: str, tunnel: typing.Optional[str] = None):
- # Convert tunnel to Strongwan format of CHILD_SA
+def _get_tunnel_sw_format(peer: str, tunnel: str) -> str:
+ """
+ Convert tunnel to Strongwan format of CHILD_SA
+ :param peer: Peer name (IKE_SA)
+ :type peer: str
+ :param tunnel: tunnel number (CHILD_SA)
+ :type tunnel: str
+ :return: Converted tunnel name (CHILD_SA)
+ :rtype: str
+ """
tunnel_sw = None
if tunnel:
if tunnel.isnumeric():
tunnel_sw = f'{peer}-tunnel-{tunnel}'
elif tunnel == 'vti':
tunnel_sw = f'{peer}-vti'
+ return tunnel_sw
+
+
+def _initiate_peer_with_childsas(
+ peer: str, tunnel: typing.Optional[str] = None
+) -> None:
+ """
+ Initiate IPSEC peer SAs by vici.
+ If tunnel is None it initiates all peers tunnels
+ :param peer: Peer name (IKE_SA)
+ :type peer: str
+ :param tunnel: tunnel number (CHILD_SA)
+ :type tunnel: str
+ """
+ tunnel_sw = _get_tunnel_sw_format(peer, tunnel)
try:
- sa_list: list = vyos.ipsec.get_vici_sas_by_name(peer, tunnel_sw)
- if not sa_list:
+ con_list: list = vyos.ipsec.get_vici_connection_by_name(peer)
+ if not con_list:
raise vyos.opmode.IncorrectValue(
- f'Peer\'s {peer} SA(s) not found, aborting')
- if tunnel and sa_list:
- childsa_id_list: list = _get_childsa_id_list(sa_list)
- if not childsa_id_list:
- raise vyos.opmode.IncorrectValue(
- f'Peer {peer} tunnel {tunnel} SA(s) not found, aborting')
- vyos.ipsec.terminate_vici_by_name(peer, tunnel_sw)
- print(f'Peer {peer} reset result: success')
- except (vyos.ipsec.ViciInitiateError) as err:
+ f"Peer's {peer} SA(s) not loaded. Initiation was failed"
+ )
+ childsa_name_list: list = _get_con_childsa_name_list(con_list)
+
+ if not tunnel_sw:
+ vyos.ipsec.vici_initiate_all_child_sa_by_ike(peer, childsa_name_list)
+ print(f'Peer {peer} initiate result: success')
+ return
+
+ if tunnel_sw in childsa_name_list:
+ vyos.ipsec.vici_initiate_all_child_sa_by_ike(peer, [tunnel_sw])
+ print(f'Peer {peer} tunnel {tunnel} initiate result: success')
+ return
+
+ raise vyos.opmode.IncorrectValue(f'Peer {peer} SA {tunnel} not found, aborting')
+
+ except vyos.ipsec.ViciInitiateError as err:
raise vyos.opmode.UnconfiguredSubsystem(err)
- except (vyos.ipsec.ViciCommandError) as err:
+ except vyos.ipsec.ViciCommandError as err:
raise vyos.opmode.IncorrectValue(err)
-def reset_all_peers():
+def _terminate_peer(peer: str, tunnel: typing.Optional[str] = None) -> None:
+ """
+ Terminate IPSEC peer SAs by vici.
+ If tunnel is None it terminates all peers tunnels
+ :param peer: Peer name (IKE_SA)
+ :type peer: str
+ :param tunnel: tunnel number (CHILD_SA)
+ :type tunnel: str
+ """
+ # Convert tunnel to Strongwan format of CHILD_SA
+ tunnel_sw = _get_tunnel_sw_format(peer, tunnel)
+ try:
+ sa_list: list = vyos.ipsec.get_vici_sas_by_name(peer, tunnel_sw)
+ if sa_list:
+ if tunnel:
+ childsa_id_list: list = _get_childsa_id_list(sa_list)
+ if childsa_id_list:
+ vyos.ipsec.terminate_vici_by_name(peer, tunnel_sw)
+ print(f'Peer {peer} tunnel {tunnel} terminate result: success')
+ else:
+ Warning(
+ f'Peer {peer} tunnel {tunnel} SA is not initiated. Nothing to terminate'
+ )
+ else:
+ vyos.ipsec.terminate_vici_by_name(peer, tunnel_sw)
+ print(f'Peer {peer} terminate result: success')
+ else:
+ Warning(f"Peer's {peer} SAs are not initiated. Nothing to terminate")
+
+ except vyos.ipsec.ViciInitiateError as err:
+ raise vyos.opmode.UnconfiguredSubsystem(err)
+ except vyos.ipsec.ViciCommandError as err:
+ raise vyos.opmode.IncorrectValue(err)
+
+
+def reset_peer(peer: str, tunnel: typing.Optional[str] = None) -> None:
+ """
+ Reset IPSEC peer SAs.
+ If tunnel is None it resets all peers tunnels
+ :param peer: Peer name (IKE_SA)
+ :type peer: str
+ :param tunnel: tunnel number (CHILD_SA)
+ :type tunnel: str
+ """
+ _terminate_peer(peer, tunnel)
+ peer_config = _get_sitetosite_peer_config(peer)
+ # initiate SAs only if 'connection-type=initiate'
+ if (
+ 'connection_type' in peer_config
+ and peer_config['connection_type'] == 'initiate'
+ ):
+ _initiate_peer_with_childsas(peer, tunnel)
+
+
+def reset_all_peers() -> None:
sitetosite_list = _get_all_sitetosite_peers_name_list()
if sitetosite_list:
for peer_name in sitetosite_list:
try:
reset_peer(peer_name)
- except (vyos.opmode.IncorrectValue) as err:
+ except vyos.opmode.IncorrectValue as err:
print(err)
print('Peers reset result: success')
else:
raise vyos.opmode.UnconfiguredSubsystem(
- 'VPN IPSec site-to-site is not configured, aborting')
+ 'VPN IPSec site-to-site is not configured, aborting'
+ )
def _get_ra_session_list_by_username(username: typing.Optional[str] = None):
@@ -500,7 +661,7 @@ def _get_ra_session_list_by_username(username: typing.Optional[str] = None):
def reset_ra(username: typing.Optional[str] = None):
- #Reset remote-access ipsec sessions
+ # Reset remote-access ipsec sessions
if username:
list_sa_id = _get_ra_session_list_by_username(username)
else:
@@ -514,32 +675,47 @@ def reset_profile_dst(profile: str, tunnel: str, nbma_dst: str):
ike_sa_name = f'dmvpn-{profile}-{tunnel}'
try:
# Get IKE SAs
- sa_list = convert_data(
- vyos.ipsec.get_vici_sas_by_name(ike_sa_name, None))
+ sa_list = convert_data(vyos.ipsec.get_vici_sas_by_name(ike_sa_name, None))
if not sa_list:
raise vyos.opmode.IncorrectValue(
- f'SA(s) for profile {profile} tunnel {tunnel} not found, aborting')
- sa_nbma_list = list([x for x in sa_list if
- ike_sa_name in x and x[ike_sa_name][
- 'remote-host'] == nbma_dst])
+ f'SA(s) for profile {profile} tunnel {tunnel} not found, aborting'
+ )
+ sa_nbma_list = list(
+ [
+ x
+ for x in sa_list
+ if ike_sa_name in x and x[ike_sa_name]['remote-host'] == nbma_dst
+ ]
+ )
if not sa_nbma_list:
raise vyos.opmode.IncorrectValue(
- f'SA(s) for profile {profile} tunnel {tunnel} remote-host {nbma_dst} not found, aborting')
+ f'SA(s) for profile {profile} tunnel {tunnel} remote-host {nbma_dst} not found, aborting'
+ )
# terminate IKE SAs
- vyos.ipsec.terminate_vici_ikeid_list(list(
- [x[ike_sa_name]['uniqueid'] for x in sa_nbma_list if
- ike_sa_name in x]))
+ vyos.ipsec.terminate_vici_ikeid_list(
+ list(
+ [
+ x[ike_sa_name]['uniqueid']
+ for x in sa_nbma_list
+ if ike_sa_name in x
+ ]
+ )
+ )
# initiate IKE SAs
for ike in sa_nbma_list:
if ike_sa_name in ike:
- vyos.ipsec.vici_initiate(ike_sa_name, 'dmvpn',
- ike[ike_sa_name]['local-host'],
- ike[ike_sa_name]['remote-host'])
+ vyos.ipsec.vici_initiate(
+ ike_sa_name,
+ 'dmvpn',
+ ike[ike_sa_name]['local-host'],
+ ike[ike_sa_name]['remote-host'],
+ )
print(
- f'Profile {profile} tunnel {tunnel} remote-host {nbma_dst} reset result: success')
- except (vyos.ipsec.ViciInitiateError) as err:
+ f'Profile {profile} tunnel {tunnel} remote-host {nbma_dst} reset result: success'
+ )
+ except vyos.ipsec.ViciInitiateError as err:
raise vyos.opmode.UnconfiguredSubsystem(err)
- except (vyos.ipsec.ViciCommandError) as err:
+ except vyos.ipsec.ViciCommandError as err:
raise vyos.opmode.IncorrectValue(err)
@@ -549,24 +725,30 @@ def reset_profile_all(profile: str, tunnel: str):
try:
# Get IKE SAs
sa_list: list = convert_data(
- vyos.ipsec.get_vici_sas_by_name(ike_sa_name, None))
+ vyos.ipsec.get_vici_sas_by_name(ike_sa_name, None)
+ )
if not sa_list:
raise vyos.opmode.IncorrectValue(
- f'SA(s) for profile {profile} tunnel {tunnel} not found, aborting')
+ f'SA(s) for profile {profile} tunnel {tunnel} not found, aborting'
+ )
# terminate IKE SAs
vyos.ipsec.terminate_vici_by_name(ike_sa_name, None)
# initiate IKE SAs
for ike in sa_list:
if ike_sa_name in ike:
- vyos.ipsec.vici_initiate(ike_sa_name, 'dmvpn',
- ike[ike_sa_name]['local-host'],
- ike[ike_sa_name]['remote-host'])
+ vyos.ipsec.vici_initiate(
+ ike_sa_name,
+ 'dmvpn',
+ ike[ike_sa_name]['local-host'],
+ ike[ike_sa_name]['remote-host'],
+ )
print(
- f'Profile {profile} tunnel {tunnel} remote-host {ike[ike_sa_name]["remote-host"]} reset result: success')
+ f'Profile {profile} tunnel {tunnel} remote-host {ike[ike_sa_name]["remote-host"]} reset result: success'
+ )
print(f'Profile {profile} tunnel {tunnel} reset result: success')
- except (vyos.ipsec.ViciInitiateError) as err:
+ except vyos.ipsec.ViciInitiateError as err:
raise vyos.opmode.UnconfiguredSubsystem(err)
- except (vyos.ipsec.ViciCommandError) as err:
+ except vyos.ipsec.ViciCommandError as err:
raise vyos.opmode.IncorrectValue(err)
@@ -734,36 +916,56 @@ def _get_formatted_output_ra_summary(ra_output_list: list):
if child_sa_key:
child_sa = sa['child-sas'][child_sa_key]
sa_ipsec_proposal = _get_formatted_ipsec_proposal(child_sa)
- sa_state = "UP"
+ sa_state = 'UP'
sa_uptime = seconds_to_human(sa['established'])
else:
sa_ipsec_proposal = ''
- sa_state = "DOWN"
+ sa_state = 'DOWN'
sa_uptime = ''
sa_data.append(
- [sa_id, sa_username, sa_protocol, sa_state, sa_uptime,
- sa_tunnel_ip,
- sa_remotehost, sa_remoteid, sa_ike_proposal,
- sa_ipsec_proposal])
-
- headers = ["Connection ID", "Username", "Protocol", "State", "Uptime",
- "Tunnel IP", "Remote Host", "Remote ID", "IKE Proposal",
- "IPSec Proposal"]
+ [
+ sa_id,
+ sa_username,
+ sa_protocol,
+ sa_state,
+ sa_uptime,
+ sa_tunnel_ip,
+ sa_remotehost,
+ sa_remoteid,
+ sa_ike_proposal,
+ sa_ipsec_proposal,
+ ]
+ )
+
+ headers = [
+ 'Connection ID',
+ 'Username',
+ 'Protocol',
+ 'State',
+ 'Uptime',
+ 'Tunnel IP',
+ 'Remote Host',
+ 'Remote ID',
+ 'IKE Proposal',
+ 'IPSec Proposal',
+ ]
sa_data = sorted(sa_data, key=_alphanum_key)
output = tabulate(sa_data, headers)
return output
-def show_ra_detail(raw: bool, username: typing.Optional[str] = None,
- conn_id: typing.Optional[str] = None):
+def show_ra_detail(
+ raw: bool,
+ username: typing.Optional[str] = None,
+ conn_id: typing.Optional[str] = None,
+):
list_sa: list = _get_ra_sessions()
if username:
list_sa = _filter_ikesas(list_sa, 'remote-eap-id', username)
elif conn_id:
list_sa = _filter_ikesas(list_sa, 'uniqueid', conn_id)
if not list_sa:
- raise vyos.opmode.IncorrectValue(
- f'No active connections found, aborting')
+ raise vyos.opmode.IncorrectValue('No active connections found, aborting')
if raw:
return list_sa
return _get_output_ra_sas_detail(list_sa)
@@ -772,8 +974,7 @@ def show_ra_detail(raw: bool, username: typing.Optional[str] = None,
def show_ra_summary(raw: bool):
list_sa: list = _get_ra_sessions()
if not list_sa:
- raise vyos.opmode.IncorrectValue(
- f'No active connections found, aborting')
+ raise vyos.opmode.IncorrectValue('No active connections found, aborting')
if raw:
return list_sa
return _get_formatted_output_ra_summary(list_sa)
@@ -783,9 +984,12 @@ def show_ra_summary(raw: bool):
def _get_raw_psk():
conf: ConfigTreeQuery = ConfigTreeQuery()
config_path = ['vpn', 'ipsec', 'authentication', 'psk']
- psk_config = conf.get_config_dict(config_path, key_mangling=('-', '_'),
- get_first_key=True,
- no_tag_node_value_mangle=True)
+ psk_config = conf.get_config_dict(
+ config_path,
+ key_mangling=('-', '_'),
+ get_first_key=True,
+ no_tag_node_value_mangle=True,
+ )
psk_list = []
for psk, psk_data in psk_config.items():
@@ -796,11 +1000,13 @@ def _get_raw_psk():
def _get_formatted_psk(psk_list):
- headers = ["PSK", "Id", "Secret"]
+ headers = ['PSK', 'Id', 'Secret']
formatted_data = []
for psk_data in psk_list:
- formatted_data.append([psk_data["psk"], "\n".join(psk_data["id"]), psk_data["secret"]])
+ formatted_data.append(
+ [psk_data['psk'], '\n'.join(psk_data['id']), psk_data['secret']]
+ )
return tabulate(formatted_data, headers=headers)
@@ -808,16 +1014,36 @@ def _get_formatted_psk(psk_list):
def show_psk(raw: bool):
config = ConfigTreeQuery()
if not config.exists('vpn ipsec authentication psk'):
- raise vyos.opmode.UnconfiguredSubsystem('VPN ipsec psk authentication is not configured')
+ raise vyos.opmode.UnconfiguredSubsystem(
+ 'VPN ipsec psk authentication is not configured'
+ )
psk = _get_raw_psk()
if raw:
return psk
return _get_formatted_psk(psk)
+
# PSK block end
+def _get_sitetosite_peer_config(peer: str):
+ """
+ Return site-to-site peers configuration
+ :return: site-to-site peers configuration
+ :rtype: list
+ """
+ conf: ConfigTreeQuery = ConfigTreeQuery()
+ config_path = ['vpn', 'ipsec', 'site-to-site', 'peer', peer]
+ peers_config = conf.get_config_dict(
+ config_path,
+ key_mangling=('-', '_'),
+ get_first_key=True,
+ no_tag_node_value_mangle=True,
+ )
+ return peers_config
+
+
if __name__ == '__main__':
try:
res = vyos.opmode.run(sys.modules[__name__])
diff --git a/src/op_mode/nat.py b/src/op_mode/nat.py
index 16a545cda..c6cf4770a 100755
--- a/src/op_mode/nat.py
+++ b/src/op_mode/nat.py
@@ -31,6 +31,7 @@ from vyos.utils.dict import dict_search
ArgDirection = typing.Literal['source', 'destination']
ArgFamily = typing.Literal['inet', 'inet6']
+
def _get_xml_translation(direction, family, address=None):
"""
Get conntrack XML output --src-nat|--dst-nat
@@ -99,22 +100,35 @@ def _get_raw_translation(direction, family, address=None):
def _get_formatted_output_rules(data, direction, family):
- def _get_ports_for_output(my_dict):
- # Get and insert all configured ports or port ranges into output string
- for index, port in enumerate(my_dict['set']):
- if 'range' in str(my_dict['set'][index]):
- output = my_dict['set'][index]['range']
- output = '-'.join(map(str, output))
- else:
- output = str(port)
- if index == 0:
- output = str(output)
- else:
- output = ','.join([output,output])
- # Handle case where configured ports are a negated list
- if my_dict['op'] == '!=':
- output = '!' + output
- return(output)
+
+
+ def _get_ports_for_output(rules):
+ """
+ Return: string of configured ports
+ """
+ ports = []
+ if 'set' in rules:
+ for index, port in enumerate(rules['set']):
+ if 'range' in str(rules['set'][index]):
+ output = rules['set'][index]['range']
+ output = '-'.join(map(str, output))
+ else:
+ output = str(port)
+ ports.append(output)
+ # When NAT rule contains port range or single port
+ # JSON will not contain keyword 'set'
+ elif 'range' in rules:
+ output = rules['range']
+ output = '-'.join(map(str, output))
+ ports.append(output)
+ else:
+ output = rules['right']
+ ports.append(str(output))
+ result = ','.join(ports)
+ # Handle case where ports in NAT rule are negated
+ if rules['op'] == '!=':
+ result = '!' + result
+ return(result)
# Add default values before loop
sport, dport, proto = 'any', 'any', 'any'
@@ -132,7 +146,10 @@ def _get_formatted_output_rules(data, direction, family):
if jmespath.search('rule.expr[*].match.left.meta', rule) else 'any'
for index, match in enumerate(jmespath.search('rule.expr[*].match', rule)):
if 'payload' in match['left']:
- if isinstance(match['right'], dict) and ('prefix' in match['right'] or 'set' in match['right']):
+ # Handle NAT rule containing comma-seperated list of ports
+ if (isinstance(match['right'], dict) and
+ ('prefix' in match['right'] or 'set' in match['right'] or
+ 'range' in match['right'])):
# Merge dict src/dst l3_l4 parameters
my_dict = {**match['left']['payload'], **match['right']}
my_dict['op'] = match['op']
@@ -146,6 +163,7 @@ def _get_formatted_output_rules(data, direction, family):
sport = _get_ports_for_output(my_dict)
elif my_dict['field'] == 'dport':
dport = _get_ports_for_output(my_dict)
+ # Handle NAT rule containing a single port
else:
field = jmespath.search('left.payload.field', match)
if field == 'saddr':
@@ -153,9 +171,9 @@ def _get_formatted_output_rules(data, direction, family):
elif field == 'daddr':
daddr = match.get('right')
elif field == 'sport':
- sport = match.get('right')
+ sport = _get_ports_for_output(match)
elif field == 'dport':
- dport = match.get('right')
+ dport = _get_ports_for_output(match)
else:
saddr = '::/0' if family == 'inet6' else '0.0.0.0/0'
daddr = '::/0' if family == 'inet6' else '0.0.0.0/0'
diff --git a/src/op_mode/openconnect.py b/src/op_mode/openconnect.py
index cfa0678a7..62c683ebb 100755
--- a/src/op_mode/openconnect.py
+++ b/src/op_mode/openconnect.py
@@ -42,8 +42,10 @@ def _get_formatted_sessions(data):
ses_list = []
for ses in data:
ses_list.append([
- ses["Device"], ses["Username"], ses["IPv4"], ses["Remote IP"],
- ses["_RX"], ses["_TX"], ses["State"], ses["_Connected at"]
+ ses.get("Device", '(none)'), ses.get("Username", '(none)'),
+ ses.get("IPv4", '(none)'), ses.get("Remote IP", '(none)'),
+ ses.get("_RX", '(none)'), ses.get("_TX", '(none)'),
+ ses.get("State", '(none)'), ses.get("_Connected at", '(none)')
])
if len(ses_list) > 0:
output = tabulate(ses_list, headers)
diff --git a/src/op_mode/pki.py b/src/op_mode/pki.py
index 9ce166c7d..84b080023 100755
--- a/src/op_mode/pki.py
+++ b/src/op_mode/pki.py
@@ -844,7 +844,8 @@ def import_openvpn_secret(name, path):
key_version = '1'
with open(path) as f:
- key_lines = f.read().split("\n")
+ key_lines = f.read().strip().split("\n")
+ key_lines = list(filter(lambda line: not line.strip().startswith('#'), key_lines)) # Remove commented lines
key_data = "".join(key_lines[1:-1]) # Remove wrapper tags and line endings
version_search = re.search(r'BEGIN OpenVPN Static key V(\d+)', key_lines[0]) # Future-proofing (hopefully)
diff --git a/src/op_mode/powerctrl.py b/src/op_mode/powerctrl.py
index cb4a175dd..c32a2be7d 100755
--- a/src/op_mode/powerctrl.py
+++ b/src/op_mode/powerctrl.py
@@ -24,7 +24,6 @@ from time import time
from vyos.utils.io import ask_yes_no
from vyos.utils.process import call
-from vyos.utils.process import cmd
from vyos.utils.process import run
from vyos.utils.process import STDOUT
@@ -117,11 +116,15 @@ def check_unsaved_config():
pass
def execute_shutdown(time, reboot=True, ask=True):
+ from vyos.utils.process import cmd
+
check_unsaved_config()
+ host = cmd("hostname --fqdn")
+
action = "reboot" if reboot else "poweroff"
if not ask:
- if not ask_yes_no(f"Are you sure you want to {action} this system?"):
+ if not ask_yes_no(f"Are you sure you want to {action} this system ({host})?"):
exit(0)
action_cmd = "-r" if reboot else "-P"
diff --git a/src/op_mode/restart.py b/src/op_mode/restart.py
new file mode 100755
index 000000000..813d3a2b7
--- /dev/null
+++ b/src/op_mode/restart.py
@@ -0,0 +1,127 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2024 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 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.configquery import ConfigTreeQuery
+from vyos.utils.process import call
+from vyos.utils.commit import commit_in_progress
+
+config = ConfigTreeQuery()
+
+service_map = {
+ 'dhcp' : {
+ 'systemd_service': 'kea-dhcp4-server',
+ 'path': ['service', 'dhcp-server'],
+ },
+ 'dhcpv6' : {
+ 'systemd_service': 'kea-dhcp6-server',
+ 'path': ['service', 'dhcpv6-server'],
+ },
+ 'dns_dynamic': {
+ 'systemd_service': 'ddclient',
+ 'path': ['service', 'dns', 'dynamic'],
+ },
+ 'dns_forwarding': {
+ 'systemd_service': 'pdns-recursor',
+ 'path': ['service', 'dns', 'forwarding'],
+ },
+ 'igmp_proxy': {
+ 'systemd_service': 'igmpproxy',
+ 'path': ['protocols', 'igmp-proxy'],
+ },
+ 'ipsec': {
+ 'systemd_service': 'strongswan',
+ 'path': ['vpn', 'ipsec'],
+ },
+ 'mdns_repeater': {
+ 'systemd_service': 'avahi-daemon',
+ 'path': ['service', 'mdns', 'repeater'],
+ },
+ 'reverse_proxy': {
+ 'systemd_service': 'haproxy',
+ 'path': ['load-balancing', 'reverse-proxy'],
+ },
+ 'router_advert': {
+ 'systemd_service': 'radvd',
+ 'path': ['service', 'router-advert'],
+ },
+ 'snmp' : {
+ 'systemd_service': 'snmpd',
+ },
+ 'ssh' : {
+ 'systemd_service': 'ssh',
+ },
+ 'suricata' : {
+ 'systemd_service': 'suricata',
+ },
+ 'vrrp' : {
+ 'systemd_service': 'keepalived',
+ 'path': ['high-availability', 'vrrp'],
+ },
+ 'webproxy' : {
+ 'systemd_service': 'squid',
+ },
+}
+services = typing.Literal['dhcp', 'dhcpv6', 'dns_dynamic', 'dns_forwarding', 'igmp_proxy', 'ipsec', 'mdns_repeater', 'reverse_proxy', 'router_advert', 'snmp', 'ssh', 'suricata' 'vrrp', 'webproxy']
+
+def _verify(func):
+ """Decorator checks if DHCP(v6) config exists"""
+ from functools import wraps
+
+ @wraps(func)
+ def _wrapper(*args, **kwargs):
+ config = ConfigTreeQuery()
+ name = kwargs.get('name')
+ human_name = name.replace('_', '-')
+
+ if commit_in_progress():
+ print(f'Cannot restart {human_name} service while a commit is in progress')
+ sys.exit(1)
+
+ # Get optional CLI path from service_mapping dict
+ # otherwise use "service name" CLI path
+ path = ['service', name]
+ if 'path' in service_map[name]:
+ path = service_map[name]['path']
+
+ # Check if config does not exist
+ if not config.exists(path):
+ raise vyos.opmode.UnconfiguredSubsystem(f'Service {human_name} is not configured!')
+ if config.exists(path + ['disable']):
+ raise vyos.opmode.UnconfiguredSubsystem(f'Service {human_name} is disabled!')
+ return func(*args, **kwargs)
+
+ return _wrapper
+
+@_verify
+def restart_service(raw: bool, name: services, vrf: typing.Optional[str]):
+ systemd_service = service_map[name]['systemd_service']
+ if vrf:
+ call(f'systemctl restart "{systemd_service}@{vrf}.service"')
+ else:
+ call(f'systemctl restart "{systemd_service}.service"')
+
+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/serial.py b/src/op_mode/serial.py
new file mode 100644
index 000000000..a5864872b
--- /dev/null
+++ b/src/op_mode/serial.py
@@ -0,0 +1,38 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2024 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, typing
+
+import vyos.opmode
+from vyos.utils.serial import restart_login_consoles as _restart_login_consoles
+
+def restart_console(device_name: typing.Optional[str]):
+ # Service control moved to vyos.utils.serial to unify checks and prompts.
+ # If users are connected, we want to show an informational message and a prompt
+ # to continue, verifying that the user acknowledges possible interruptions.
+ if device_name:
+ _restart_login_consoles(prompt_user=True, quiet=False, devices=[device_name])
+ else:
+ _restart_login_consoles(prompt_user=True, quiet=False)
+
+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/ssh.py b/src/op_mode/ssh.py
index 102becc55..0c51576b0 100755
--- a/src/op_mode/ssh.py
+++ b/src/op_mode/ssh.py
@@ -65,7 +65,7 @@ def show_fingerprints(raw: bool, ascii: bool):
def show_dynamic_protection(raw: bool):
config = ConfigTreeQuery()
if not config.exists(['service', 'ssh', 'dynamic-protection']):
- raise vyos.opmode.UnconfiguredSubsystem("SSH server dynamic-protection is not enabled.")
+ raise vyos.opmode.UnconfiguredObject("SSH server dynamic-protection is not enabled.")
attackers = []
try:
diff --git a/src/op_mode/tech_support.py b/src/op_mode/tech_support.py
new file mode 100644
index 000000000..f60bb87ff
--- /dev/null
+++ b/src/op_mode/tech_support.py
@@ -0,0 +1,394 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2024 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 json
+
+import vyos.opmode
+
+from vyos.utils.process import cmd
+
+def _get_version_data():
+ from vyos.version import get_version_data
+ return get_version_data()
+
+def _get_uptime():
+ from vyos.utils.system import get_uptime_seconds
+
+ return get_uptime_seconds()
+
+def _get_load_average():
+ from vyos.utils.system import get_load_averages
+
+ return get_load_averages()
+
+def _get_cpus():
+ from vyos.utils.cpu import get_cpus
+
+ return get_cpus()
+
+def _get_process_stats():
+ return cmd('top --iterations 1 --batch-mode --accum-time-toggle')
+
+def _get_storage():
+ from vyos.utils.disk import get_persistent_storage_stats
+
+ return get_persistent_storage_stats()
+
+def _get_devices():
+ devices = {}
+ devices["pci"] = cmd("lspci")
+ devices["usb"] = cmd("lsusb")
+
+ return devices
+
+def _get_memory():
+ from vyos.utils.file import read_file
+
+ return read_file("/proc/meminfo")
+
+def _get_processes():
+ res = cmd("ps aux")
+
+ return res
+
+def _get_interrupts():
+ from vyos.utils.file import read_file
+
+ interrupts = read_file("/proc/interrupts")
+ softirqs = read_file("/proc/softirqs")
+
+ return (interrupts, softirqs)
+
+def _get_partitions():
+ # XXX: as of parted 3.5, --json is completely broken
+ # and cannot be used (outputs malformed JSON syntax)
+ res = cmd(f"parted --list")
+
+ return res
+
+def _get_running_config():
+ from os import getpid
+ from vyos.configsession import ConfigSession
+ from vyos.utils.strip_config import strip_config_source
+
+ c = ConfigSession(getpid())
+ return strip_config_source(c.show_config([]))
+
+def _get_boot_config():
+ from vyos.utils.file import read_file
+ from vyos.utils.strip_config import strip_config_source
+
+ config = read_file('/opt/vyatta/etc/config.boot.default')
+
+ return strip_config_source(config)
+
+def _get_config_scripts():
+ from os import listdir
+ from os.path import join
+ from vyos.utils.file import read_file
+
+ scripts = []
+
+ dir = '/config/scripts'
+ for f in listdir(dir):
+ script = {}
+ path = join(dir, f)
+ data = read_file(path)
+ script["path"] = path
+ script["data"] = data
+
+ scripts.append(script)
+
+ return scripts
+
+def _get_nic_data():
+ from vyos.utils.process import ip_cmd
+ link_data = ip_cmd("link show")
+ addr_data = ip_cmd("address show")
+
+ return link_data, addr_data
+
+def _get_routes(proto):
+ from json import loads
+ from vyos.utils.process import ip_cmd
+
+ # Only include complete routing tables if they are not too large
+ # At the moment "too large" is arbitrarily set to 1000
+ MAX_ROUTES = 1000
+
+ data = {}
+
+ summary = cmd(f"vtysh -c 'show {proto} route summary json'")
+ summary = loads(summary)
+
+ data["summary"] = summary
+
+ if summary["routesTotal"] < MAX_ROUTES:
+ rib_routes = cmd(f"vtysh -c 'show {proto} route json'")
+ data["routes"] = loads(rib_routes)
+
+ if summary["routesTotalFib"] < MAX_ROUTES:
+ ip_proto = "-4" if proto == "ip" else "-6"
+ fib_routes = ip_cmd(f"{ip_proto} route show")
+ data["fib_routes"] = fib_routes
+
+ return data
+
+def _get_ip_routes():
+ return _get_routes("ip")
+
+def _get_ipv6_routes():
+ return _get_routes("ipv6")
+
+def _get_ospfv2():
+ # XXX: OSPF output when it's not configured is an empty string,
+ # which is not a valid JSON
+ output = cmd("vtysh -c 'show ip ospf json'")
+ if output:
+ return json.loads(output)
+ else:
+ return {}
+
+def _get_ospfv3():
+ output = cmd("vtysh -c 'show ipv6 ospf6 json'")
+ if output:
+ return json.loads(output)
+ else:
+ return {}
+
+def _get_bgp_summary():
+ output = cmd("vtysh -c 'show bgp summary json'")
+ return json.loads(output)
+
+def _get_isis():
+ output = cmd("vtysh -c 'show isis summary json'")
+ if output:
+ return json.loads(output)
+ else:
+ return {}
+
+def _get_arp_table():
+ from json import loads
+ from vyos.utils.process import cmd
+
+ arp_table = cmd("ip --json -4 neighbor show")
+ return loads(arp_table)
+
+def _get_ndp_table():
+ from json import loads
+
+ arp_table = cmd("ip --json -6 neighbor show")
+ return loads(arp_table)
+
+def _get_nftables_rules():
+ nft_rules = cmd("nft list ruleset")
+ return nft_rules
+
+def _get_connections():
+ from vyos.utils.process import cmd
+
+ return cmd("ss -apO")
+
+def _get_system_packages():
+ from re import split
+ from vyos.utils.process import cmd
+
+ dpkg_out = cmd(''' dpkg-query -W -f='${Package} ${Version} ${Architecture} ${db:Status-Abbrev}\n' ''')
+ pkg_lines = split(r'\n+', dpkg_out)
+
+ # Discard the header, it's five lines long
+ pkg_lines = pkg_lines[5:]
+
+ pkgs = []
+
+ for pl in pkg_lines:
+ parts = split(r'\s+', pl)
+ pkg = {}
+ pkg["name"] = parts[0]
+ pkg["version"] = parts[1]
+ pkg["architecture"] = parts[2]
+ pkg["status"] = parts[3]
+
+ pkgs.append(pkg)
+
+ return pkgs
+
+def _get_image_info():
+ from vyos.system.image import get_images_details
+
+ return get_images_details()
+
+def _get_kernel_modules():
+ from vyos.utils.kernel import lsmod
+
+ return lsmod()
+
+def _get_last_logs(max):
+ from systemd import journal
+
+ r = journal.Reader()
+
+ # Set the reader to use logs from the current boot
+ r.this_boot()
+
+ # Jump to the last logs
+ r.seek_tail()
+
+ # Only get logs of INFO level or more urgent
+ r.log_level(journal.LOG_INFO)
+
+ # Retrieve the entries
+ entries = []
+
+ # I couldn't find a way to just get last/first N entries,
+ # so we'll use the cursor directly.
+ num = max
+ while num >= 0:
+ je = r.get_previous()
+ entry = {}
+
+ # Extract the most useful and serializable fields
+ entry["timestamp"] = je.get("SYSLOG_TIMESTAMP")
+ entry["pid"] = je.get("SYSLOG_PID")
+ entry["identifier"] = je.get("SYSLOG_IDENTIFIER")
+ entry["facility"] = je.get("SYSLOG_FACILITY")
+ entry["systemd_unit"] = je.get("_SYSTEMD_UNIT")
+ entry["message"] = je.get("MESSAGE")
+
+ entries.append(entry)
+
+ num = num - 1
+
+ return entries
+
+
+def _get_raw_data():
+ data = {}
+
+ # VyOS-specific information
+ data["vyos"] = {}
+
+ ## The equivalent of "show version"
+ from vyos.version import get_version_data
+ data["vyos"]["version"] = _get_version_data()
+
+ ## Installed images
+ data["vyos"]["images"] = _get_image_info()
+
+ # System information
+ data["system"] = {}
+
+ ## Uptime and load averages
+ data["system"]["uptime"] = _get_uptime()
+ data["system"]["load_average"] = _get_load_average()
+ data["system"]["process_stats"] = _get_process_stats()
+
+ ## Debian packages
+ data["system"]["packages"] = _get_system_packages()
+
+ ## Kernel modules
+ data["system"]["kernel"] = {}
+ data["system"]["kernel"]["modules"] = _get_kernel_modules()
+
+ ## Processes
+ data["system"]["processes"] = _get_processes()
+
+ ## Interrupts
+ interrupts, softirqs = _get_interrupts()
+ data["system"]["interrupts"] = interrupts
+ data["system"]["softirqs"] = softirqs
+
+ # Hardware
+ data["hardware"] = {}
+ data["hardware"]["cpu"] = _get_cpus()
+ data["hardware"]["storage"] = _get_storage()
+ data["hardware"]["partitions"] = _get_partitions()
+ data["hardware"]["devices"] = _get_devices()
+ data["hardware"]["memory"] = _get_memory()
+
+ # Configuration data
+ data["vyos"]["config"] = {}
+
+ ## Running config text
+ ## We do not encode it so that it's possible to
+ ## see exactly what the user sees and detect any syntax/rendering anomalies —
+ ## exporting the config to JSON could obscure them
+ data["vyos"]["config"]["running"] = _get_running_config()
+
+ ## Default boot config, exactly as in /config/config.boot
+ ## It may be different from the running config
+ ## _and_ may have its own syntax quirks that may point at bugs
+ data["vyos"]["config"]["boot"] = _get_boot_config()
+
+ ## Config scripts
+ data["vyos"]["config"]["scripts"] = _get_config_scripts()
+
+ # Network interfaces
+ data["network_interfaces"] = {}
+
+ # Interface data from iproute2
+ link_data, addr_data = _get_nic_data()
+ data["network_interfaces"]["links"] = link_data
+ data["network_interfaces"]["addresses"] = addr_data
+
+ # Routing table data
+ data["routing"] = {}
+ data["routing"]["ip"] = _get_ip_routes()
+ data["routing"]["ipv6"] = _get_ipv6_routes()
+
+ # Routing protocols
+ data["routing"]["ip"]["ospf"] = _get_ospfv2()
+ data["routing"]["ipv6"]["ospfv3"] = _get_ospfv3()
+
+ data["routing"]["bgp"] = {}
+ data["routing"]["bgp"]["summary"] = _get_bgp_summary()
+
+ data["routing"]["isis"] = _get_isis()
+
+ # ARP and NDP neighbor tables
+ data["neighbor_tables"] = {}
+ data["neighbor_tables"]["arp"] = _get_arp_table()
+ data["neighbor_tables"]["ndp"] = _get_ndp_table()
+
+ # nftables config
+ data["nftables_rules"] = _get_nftables_rules()
+
+ # All connections
+ data["connections"] = _get_connections()
+
+ # Logs
+ data["last_logs"] = _get_last_logs(1000)
+
+ return data
+
+def show(raw: bool):
+ data = _get_raw_data()
+ if raw:
+ return data
+ else:
+ raise vyos.opmode.UnsupportedOperation("Formatted output is not implemented yet")
+
+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)
+ except (KeyboardInterrupt, BrokenPipeError):
+ sys.exit(1)
diff --git a/src/op_mode/zone.py b/src/op_mode/zone.py
index d24b1065b..49fecdf28 100644
--- a/src/op_mode/zone.py
+++ b/src/op_mode/zone.py
@@ -104,7 +104,7 @@ def _convert_config(zones_config: dict, zone: str = None) -> list:
if zones_config:
output = [_convert_one_zone_data(zone, zones_config)]
else:
- raise vyos.opmode.DataUnavailable(f'Zone {zone} not found')
+ raise vyos.opmode.UnconfiguredObject(f'Zone {zone} not found')
else:
if zones_config:
output = _convert_zones_data(zones_config)
@@ -212,4 +212,4 @@ if __name__ == '__main__':
print(res)
except (ValueError, vyos.opmode.Error) as e:
print(e)
- sys.exit(1) \ No newline at end of file
+ sys.exit(1)
diff --git a/src/opt/vyatta/bin/restricted-shell b/src/opt/vyatta/bin/restricted-shell
new file mode 100755
index 000000000..ffcbb53b7
--- /dev/null
+++ b/src/opt/vyatta/bin/restricted-shell
@@ -0,0 +1,11 @@
+#!/bin/bash
+
+if [ $# != 0 ]; then
+ echo "Remote command execution is not allowed for operator level users"
+ args=($@)
+ args_str=$(IFS=" " ; echo "${args[*]}")
+ logger "Operator level user $USER attempted remote command execution: $args_str"
+ exit 1
+fi
+
+exec vbash
diff --git a/src/opt/vyatta/bin/vyatta-op-cmd-wrapper b/src/opt/vyatta/bin/vyatta-op-cmd-wrapper
new file mode 100755
index 000000000..a89211b2b
--- /dev/null
+++ b/src/opt/vyatta/bin/vyatta-op-cmd-wrapper
@@ -0,0 +1,6 @@
+#!/bin/vbash
+shopt -s expand_aliases
+source /etc/default/vyatta
+source /etc/bash_completion.d/vyatta-op
+_vyatta_op_init
+_vyatta_op_run "$@"
diff --git a/src/opt/vyatta/etc/LICENSE b/src/opt/vyatta/etc/LICENSE
new file mode 100644
index 000000000..6d45519c8
--- /dev/null
+++ b/src/opt/vyatta/etc/LICENSE
@@ -0,0 +1,340 @@
+ GNU GENERAL PUBLIC LICENSE
+ Version 2, June 1991
+
+ Copyright (C) 1989, 1991 Free Software Foundation, Inc.
+ 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+ Preamble
+
+ The licenses for most software are designed to take away your
+freedom to share and change it. By contrast, the GNU General Public
+License is intended to guarantee your freedom to share and change free
+software--to make sure the software is free for all its users. This
+General Public License applies to most of the Free Software
+Foundation's software and to any other program whose authors commit to
+using it. (Some other Free Software Foundation software is covered by
+the GNU Library General Public License instead.) You can apply it to
+your programs, too.
+
+ When we speak of free software, we are referring to freedom, not
+price. Our General Public Licenses are designed to make sure that you
+have the freedom to distribute copies of free software (and charge for
+this service if you wish), that you receive source code or can get it
+if you want it, that you can change the software or use pieces of it
+in new free programs; and that you know you can do these things.
+
+ To protect your rights, we need to make restrictions that forbid
+anyone to deny you these rights or to ask you to surrender the rights.
+These restrictions translate to certain responsibilities for you if you
+distribute copies of the software, or if you modify it.
+
+ For example, if you distribute copies of such a program, whether
+gratis or for a fee, you must give the recipients all the rights that
+you have. You must make sure that they, too, receive or can get the
+source code. And you must show them these terms so they know their
+rights.
+
+ We protect your rights with two steps: (1) copyright the software, and
+(2) offer you this license which gives you legal permission to copy,
+distribute and/or modify the software.
+
+ Also, for each author's protection and ours, we want to make certain
+that everyone understands that there is no warranty for this free
+software. If the software is modified by someone else and passed on, we
+want its recipients to know that what they have is not the original, so
+that any problems introduced by others will not reflect on the original
+authors' reputations.
+
+ Finally, any free program is threatened constantly by software
+patents. We wish to avoid the danger that redistributors of a free
+program will individually obtain patent licenses, in effect making the
+program proprietary. To prevent this, we have made it clear that any
+patent must be licensed for everyone's free use or not licensed at all.
+
+ The precise terms and conditions for copying, distribution and
+modification follow.
+
+ GNU GENERAL PUBLIC LICENSE
+ TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
+
+ 0. This License applies to any program or other work which contains
+a notice placed by the copyright holder saying it may be distributed
+under the terms of this General Public License. The "Program", below,
+refers to any such program or work, and a "work based on the Program"
+means either the Program or any derivative work under copyright law:
+that is to say, a work containing the Program or a portion of it,
+either verbatim or with modifications and/or translated into another
+language. (Hereinafter, translation is included without limitation in
+the term "modification".) Each licensee is addressed as "you".
+
+Activities other than copying, distribution and modification are not
+covered by this License; they are outside its scope. The act of
+running the Program is not restricted, and the output from the Program
+is covered only if its contents constitute a work based on the
+Program (independent of having been made by running the Program).
+Whether that is true depends on what the Program does.
+
+ 1. You may copy and distribute verbatim copies of the Program's
+source code as you receive it, in any medium, provided that you
+conspicuously and appropriately publish on each copy an appropriate
+copyright notice and disclaimer of warranty; keep intact all the
+notices that refer to this License and to the absence of any warranty;
+and give any other recipients of the Program a copy of this License
+along with the Program.
+
+You may charge a fee for the physical act of transferring a copy, and
+you may at your option offer warranty protection in exchange for a fee.
+
+ 2. You may modify your copy or copies of the Program or any portion
+of it, thus forming a work based on the Program, and copy and
+distribute such modifications or work under the terms of Section 1
+above, provided that you also meet all of these conditions:
+
+ a) You must cause the modified files to carry prominent notices
+ stating that you changed the files and the date of any change.
+
+ b) You must cause any work that you distribute or publish, that in
+ whole or in part contains or is derived from the Program or any
+ part thereof, to be licensed as a whole at no charge to all third
+ parties under the terms of this License.
+
+ c) If the modified program normally reads commands interactively
+ when run, you must cause it, when started running for such
+ interactive use in the most ordinary way, to print or display an
+ announcement including an appropriate copyright notice and a
+ notice that there is no warranty (or else, saying that you provide
+ a warranty) and that users may redistribute the program under
+ these conditions, and telling the user how to view a copy of this
+ License. (Exception: if the Program itself is interactive but
+ does not normally print such an announcement, your work based on
+ the Program is not required to print an announcement.)
+
+These requirements apply to the modified work as a whole. If
+identifiable sections of that work are not derived from the Program,
+and can be reasonably considered independent and separate works in
+themselves, then this License, and its terms, do not apply to those
+sections when you distribute them as separate works. But when you
+distribute the same sections as part of a whole which is a work based
+on the Program, the distribution of the whole must be on the terms of
+this License, whose permissions for other licensees extend to the
+entire whole, and thus to each and every part regardless of who wrote it.
+
+Thus, it is not the intent of this section to claim rights or contest
+your rights to work written entirely by you; rather, the intent is to
+exercise the right to control the distribution of derivative or
+collective works based on the Program.
+
+In addition, mere aggregation of another work not based on the Program
+with the Program (or with a work based on the Program) on a volume of
+a storage or distribution medium does not bring the other work under
+the scope of this License.
+
+ 3. You may copy and distribute the Program (or a work based on it,
+under Section 2) in object code or executable form under the terms of
+Sections 1 and 2 above provided that you also do one of the following:
+
+ a) Accompany it with the complete corresponding machine-readable
+ source code, which must be distributed under the terms of Sections
+ 1 and 2 above on a medium customarily used for software interchange; or,
+
+ b) Accompany it with a written offer, valid for at least three
+ years, to give any third party, for a charge no more than your
+ cost of physically performing source distribution, a complete
+ machine-readable copy of the corresponding source code, to be
+ distributed under the terms of Sections 1 and 2 above on a medium
+ customarily used for software interchange; or,
+
+ c) Accompany it with the information you received as to the offer
+ to distribute corresponding source code. (This alternative is
+ allowed only for noncommercial distribution and only if you
+ received the program in object code or executable form with such
+ an offer, in accord with Subsection b above.)
+
+The source code for a work means the preferred form of the work for
+making modifications to it. For an executable work, complete source
+code means all the source code for all modules it contains, plus any
+associated interface definition files, plus the scripts used to
+control compilation and installation of the executable. However, as a
+special exception, the source code distributed need not include
+anything that is normally distributed (in either source or binary
+form) with the major components (compiler, kernel, and so on) of the
+operating system on which the executable runs, unless that component
+itself accompanies the executable.
+
+If distribution of executable or object code is made by offering
+access to copy from a designated place, then offering equivalent
+access to copy the source code from the same place counts as
+distribution of the source code, even though third parties are not
+compelled to copy the source along with the object code.
+
+ 4. You may not copy, modify, sublicense, or distribute the Program
+except as expressly provided under this License. Any attempt
+otherwise to copy, modify, sublicense or distribute the Program is
+void, and will automatically terminate your rights under this License.
+However, parties who have received copies, or rights, from you under
+this License will not have their licenses terminated so long as such
+parties remain in full compliance.
+
+ 5. You are not required to accept this License, since you have not
+signed it. However, nothing else grants you permission to modify or
+distribute the Program or its derivative works. These actions are
+prohibited by law if you do not accept this License. Therefore, by
+modifying or distributing the Program (or any work based on the
+Program), you indicate your acceptance of this License to do so, and
+all its terms and conditions for copying, distributing or modifying
+the Program or works based on it.
+
+ 6. Each time you redistribute the Program (or any work based on the
+Program), the recipient automatically receives a license from the
+original licensor to copy, distribute or modify the Program subject to
+these terms and conditions. You may not impose any further
+restrictions on the recipients' exercise of the rights granted herein.
+You are not responsible for enforcing compliance by third parties to
+this License.
+
+ 7. If, as a consequence of a court judgment or allegation of patent
+infringement or for any other reason (not limited to patent issues),
+conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License. If you cannot
+distribute so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you
+may not distribute the Program at all. For example, if a patent
+license would not permit royalty-free redistribution of the Program by
+all those who receive copies directly or indirectly through you, then
+the only way you could satisfy both it and this License would be to
+refrain entirely from distribution of the Program.
+
+If any portion of this section is held invalid or unenforceable under
+any particular circumstance, the balance of the section is intended to
+apply and the section as a whole is intended to apply in other
+circumstances.
+
+It is not the purpose of this section to induce you to infringe any
+patents or other property right claims or to contest validity of any
+such claims; this section has the sole purpose of protecting the
+integrity of the free software distribution system, which is
+implemented by public license practices. Many people have made
+generous contributions to the wide range of software distributed
+through that system in reliance on consistent application of that
+system; it is up to the author/donor to decide if he or she is willing
+to distribute software through any other system and a licensee cannot
+impose that choice.
+
+This section is intended to make thoroughly clear what is believed to
+be a consequence of the rest of this License.
+
+ 8. If the distribution and/or use of the Program is restricted in
+certain countries either by patents or by copyrighted interfaces, the
+original copyright holder who places the Program under this License
+may add an explicit geographical distribution limitation excluding
+those countries, so that distribution is permitted only in or among
+countries not thus excluded. In such case, this License incorporates
+the limitation as if written in the body of this License.
+
+ 9. The Free Software Foundation may publish revised and/or new versions
+of the General Public License from time to time. Such new versions will
+be similar in spirit to the present version, but may differ in detail to
+address new problems or concerns.
+
+Each version is given a distinguishing version number. If the Program
+specifies a version number of this License which applies to it and "any
+later version", you have the option of following the terms and conditions
+either of that version or of any later version published by the Free
+Software Foundation. If the Program does not specify a version number of
+this License, you may choose any version ever published by the Free Software
+Foundation.
+
+ 10. If you wish to incorporate parts of the Program into other free
+programs whose distribution conditions are different, write to the author
+to ask for permission. For software which is copyrighted by the Free
+Software Foundation, write to the Free Software Foundation; we sometimes
+make exceptions for this. Our decision will be guided by the two goals
+of preserving the free status of all derivatives of our free software and
+of promoting the sharing and reuse of software generally.
+
+ NO WARRANTY
+
+ 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
+FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN
+OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
+PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
+OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
+MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS
+TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE
+PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
+REPAIR OR CORRECTION.
+
+ 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
+REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
+INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
+OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
+TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
+YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
+PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
+POSSIBILITY OF SUCH DAMAGES.
+
+ END OF TERMS AND CONDITIONS
+
+ How to Apply These Terms to Your New Programs
+
+ If you develop a new program, and you want it to be of the greatest
+possible use to the public, the best way to achieve this is to make it
+free software which everyone can redistribute and change under these terms.
+
+ To do so, attach the following notices to the program. It is safest
+to attach them to the start of each source file to most effectively
+convey the exclusion of warranty; and each file should have at least
+the "copyright" line and a pointer to where the full notice is found.
+
+ <one line to give the program's name and a brief idea of what it does.>
+ Copyright (C) <year> <name of author>
+
+ This program is free software; you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation; either version 2 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program; if not, write to the Free Software
+ Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
+
+
+Also add information on how to contact you by electronic and paper mail.
+
+If the program is interactive, make it output a short notice like this
+when it starts in an interactive mode:
+
+ Gnomovision version 69, Copyright (C) year name of author
+ Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
+ This is free software, and you are welcome to redistribute it
+ under certain conditions; type `show c' for details.
+
+The hypothetical commands `show w' and `show c' should show the appropriate
+parts of the General Public License. Of course, the commands you use may
+be called something other than `show w' and `show c'; they could even be
+mouse-clicks or menu items--whatever suits your program.
+
+You should also get your employer (if you work as a programmer) or your
+school, if any, to sign a "copyright disclaimer" for the program, if
+necessary. Here is a sample; alter the names:
+
+ Yoyodyne, Inc., hereby disclaims all copyright interest in the program
+ `Gnomovision' (which makes passes at compilers) written by James Hacker.
+
+ <signature of Ty Coon>, 1 April 1989
+ Ty Coon, President of Vice
+
+This General Public License does not permit incorporating your program into
+proprietary programs. If your program is a subroutine library, you may
+consider it more useful to permit linking proprietary applications with the
+library. If this is what you want to do, use the GNU Library General
+Public License instead of this License.
diff --git a/src/opt/vyatta/etc/shell/level/users/allowed-op b/src/opt/vyatta/etc/shell/level/users/allowed-op
new file mode 100644
index 000000000..74c45af37
--- /dev/null
+++ b/src/opt/vyatta/etc/shell/level/users/allowed-op
@@ -0,0 +1,20 @@
+c
+cl
+cle
+clea
+clear
+connect
+delete
+disconnect
+exit
+force
+monitor
+ping
+reset
+release
+renew
+set
+show
+telnet
+traceroute
+update
diff --git a/src/opt/vyatta/etc/shell/level/users/allowed-op.in b/src/opt/vyatta/etc/shell/level/users/allowed-op.in
new file mode 100644
index 000000000..1976904e4
--- /dev/null
+++ b/src/opt/vyatta/etc/shell/level/users/allowed-op.in
@@ -0,0 +1,16 @@
+clear
+connect
+delete
+disconnect
+exit
+force
+monitor
+ping
+reset
+release
+renew
+set
+show
+telnet
+traceroute
+update
diff --git a/src/opt/vyatta/sbin/if-mib-alias b/src/opt/vyatta/sbin/if-mib-alias
new file mode 100755
index 000000000..bc86f999f
--- /dev/null
+++ b/src/opt/vyatta/sbin/if-mib-alias
@@ -0,0 +1,130 @@
+#! /usr/bin/perl
+
+# **** License ****
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License version 2 as
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# General Public License for more details.
+#
+# This code was originally developed by Vyatta, Inc.
+# Portions created by Vyatta are Copyright (C) 2007 Vyatta, Inc.
+# All Rights Reserved.
+#
+# Author: Stephen Hemminger
+# Date: October 2010
+# Description: script is run as net-snmp extension to read interface alias
+#
+# **** End License ****
+
+use strict;
+use warnings;
+use feature "switch";
+no warnings 'experimental::smartmatch';
+
+# Collect interface all alias values
+sub get_alias {
+ my @interfaces;
+
+ open (my $ip, '-|', 'ip li')
+ or die "Can't run ip command\n";
+ my $index;
+ while(<$ip>) {
+ if (/^(\d+): ([^:]*): /) {
+ $index = $1;
+ $interfaces[$index] = $2;
+ } elsif (/^ +alias (.*)$/) {
+ $interfaces[$index] = $1;
+ }
+ }
+ close $ip;
+ return @interfaces;
+}
+
+sub get_oid {
+ my $oid = shift;
+ die "Not a valid Object ID: $oid"
+ unless ($oid =~ /.(\d+)$/);
+
+ my $ifindex = $1;
+ my @interfaces = get_alias();
+
+ my $ifalias = $interfaces[$ifindex];
+ print "$oid\nstring\n$ifalias\n" if $ifalias;
+}
+
+# OID of ifAlias [RFC2863]
+my $BASE = '.1.3.6.1.2.1.31.1.1.1.18';
+
+sub get_next {
+ my $oid = shift;
+
+ return get_next("$BASE.0")
+ if ($oid eq $BASE);
+
+ die "Not a valid Object ID: $oid"
+ unless ($oid =~ /^(\S*)\.(\d+)$/);
+
+ my $base = $1;
+ my $ifindex = $2;
+ my @interfaces = get_alias();
+
+ while (++$ifindex <= $#interfaces) {
+ my $ifalias = $interfaces[$ifindex];
+ if ($ifalias) {
+ print "$base.$ifindex\nstring\n$ifalias\n";
+ last;
+ }
+ }
+}
+
+sub ifindextoname {
+ my $ifindex = shift;
+
+ open (my $ip, '-|', 'ip li')
+ or die "Can't run ip command\n";
+ my $index;
+ while(<$ip>) {
+ next unless (/^(\d+): ([^:]*): /);
+ return $2 if ($1 == $ifindex);
+ }
+ return;
+}
+
+sub set_oid {
+ my ($oid, $target, $value) = @_;
+ die "Not a valid Object ID: $oid"
+ unless ($oid =~ /\.(\d+)$/);
+ my $ifindex = $1;
+ unless ($target eq 'string') {
+ print "wrong-type\n";
+ return;
+ }
+
+ my $ifname = ifindextoname($ifindex);
+ if ($ifname) {
+ system("ip li set $ifname alias '$value' >/dev/null 2>&1");
+ print "not-writeable\n" if ($? != 0);
+ }
+}
+
+sub usage {
+ warn "Usage: $0 {-g|-n} OID\n";
+ warn " $0 -s OID TARGET VALUE\n";
+ exit 1;
+}
+
+usage unless $#ARGV >= 1;
+
+given ($ARGV[0]) {
+ when ('-g') { get_oid ($ARGV[1]); }
+ when ('-n') { get_next ($ARGV[1]); }
+ when ('-s') { set_oid ($ARGV[1], $ARGV[2], $ARGV[3]); }
+ default {
+ warn "$ARGV[0] unknown flag\n";
+ usage;
+ }
+}
diff --git a/src/opt/vyatta/sbin/vyos-persistpath b/src/opt/vyatta/sbin/vyos-persistpath
new file mode 100755
index 000000000..d7199b09a
--- /dev/null
+++ b/src/opt/vyatta/sbin/vyos-persistpath
@@ -0,0 +1,19 @@
+#!/bin/bash
+
+if grep -q -e '^overlay.*/filesystem.squashfs' /proc/mounts; then
+ # Live CD boot
+ exit 2
+
+elif grep -q 'upperdir=/live/persistence/' /proc/mounts && egrep -q 'overlay / overlay ' /proc/mounts; then
+ # union boot
+
+ boot_device=`grep -o 'upperdir=/live/persistence/[^/]*/boot' /proc/mounts | cut -d / -f 4`
+ persist_path="/lib/live/mount/persistence/$boot_device"
+
+ echo $persist_path
+ exit 0
+else
+ # old style boot
+
+ exit 1
+fi \ No newline at end of file
diff --git a/src/opt/vyatta/share/vyatta-op/functions/interpreter/vyatta-common b/src/opt/vyatta/share/vyatta-op/functions/interpreter/vyatta-common
new file mode 100644
index 000000000..e749f0217
--- /dev/null
+++ b/src/opt/vyatta/share/vyatta-op/functions/interpreter/vyatta-common
@@ -0,0 +1,82 @@
+# vyatta bash completion common functions
+
+# **** License ****
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License version 2 as
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# General Public License for more details.
+#
+# A copy of the GNU General Public License is available as
+# `/usr/share/common-licenses/GPL' in the Debian GNU/Linux distribution
+# or on the World Wide Web at `http://www.gnu.org/copyleft/gpl.html'.
+# You can also obtain it by writing to the Free Software Foundation,
+# Free Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston,
+# MA 02110-1301, USA.
+#
+# Author: Vyatta
+# Description: bash completion common functions
+#
+# **** End License ****
+
+get_prefix_filtered_list ()
+{
+ # $1: prefix
+ # $2: \@list
+ # $3: \@filtered
+ declare -a olist
+ local pfx=$1
+ pfx=${pfx#\"}
+ eval "olist=( \"\${$2[@]}\" )"
+ local idx=0
+ for elem in "${olist[@]}"; do
+ local sub="${elem#$pfx}"
+ if [[ "$elem" == "$sub" ]] && [[ -n "$pfx" ]]; then
+ continue
+ fi
+ eval "$3[$idx]=\$elem"
+ (( idx++ ))
+ done
+}
+
+get_prefix_filtered_list2 ()
+{
+ # $1: prefix
+ # $2: \@list
+ # $3: \@filtered
+ # $4: \@list2
+ # $5: \@filtered2
+ declare -a olist
+ local pfx=$1
+ pfx=${pfx#\"}
+ eval "olist=( \"\${$2[@]}\" )"
+ eval "local orig_len=\${#$2[@]}"
+ local orig_idx=0
+ local idx=0
+ for (( orig_idx = 0; orig_idx < orig_len; orig_idx++ )); do
+ eval "local elem=\${$2[$orig_idx]}"
+ eval "local elem2=\${$4[$orig_idx]}"
+ local sub="${elem#$pfx}"
+ if [[ "$elem" == "$sub" ]] && [[ -n "$pfx" ]]; then
+ continue
+ fi
+ eval "$3[$idx]=\$elem"
+ eval "$5[$idx]=\$elem2"
+ (( idx++ ))
+ done
+}
+
+is_elem_of () {
+ local elem="$1"
+ local -a olist
+ eval "olist=( \"\${$2[@]}\" )"
+ for e in "${olist[@]}"; do
+ if [[ "$e" == "$elem" ]]; then
+ return 0
+ fi
+ done
+ return 1
+}
diff --git a/src/opt/vyatta/share/vyatta-op/functions/interpreter/vyatta-op-run b/src/opt/vyatta/share/vyatta-op/functions/interpreter/vyatta-op-run
new file mode 100644
index 000000000..f0479ae88
--- /dev/null
+++ b/src/opt/vyatta/share/vyatta-op/functions/interpreter/vyatta-op-run
@@ -0,0 +1,240 @@
+# **** License ****
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License version 2 as
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# General Public License for more details.
+#
+# This code was originally developed by Vyatta, Inc.
+# Portions created by Vyatta are Copyright (C) 2006, 2007 Vyatta, Inc.
+# All Rights Reserved.
+#
+# Author: Tom Grennan
+# Date: 2007
+# Description: setup bash completion for Vyatta operational commands
+#
+# **** End License ****
+
+_vyatta_op_init ()
+{
+ # empty and default line compeletion
+ complete -E -F _vyatta_op_expand
+ complete -D -F _vyatta_op_default_expand
+
+ # create the top level aliases for the unambiguous portions of the commands
+ # this is the only place we need an entire enumerated list of the subcommands
+ for cmd in $( ls /opt/vyatta/share/vyatta-op/templates/ ); do
+ for pos in $(seq 1 ${#cmd}); do
+ case ${cmd:0:$pos} in
+ for|do|done|if|fi|case|while|tr )
+ continue ;;
+ *) ;;
+ esac
+ complete -F _vyatta_op_expand ${cmd:0:$pos}
+ eval alias ${cmd:0:$pos}=\'_vyatta_op_run ${cmd:0:$pos}\'
+ done
+ done
+
+ shopt -s histverify
+}
+
+_vyatta_op_get_node_def_field ()
+{
+ local file=$1 field=$2
+
+ sed -n '/^'"$field"':/,$ {
+# strip field name and hold rest of line
+ s/[a-z]*: *//
+ h
+ :b
+# at EOF, print hold buffer and quit
+ $ { x; p; q }
+# input next line
+ n
+# if start of another field def, print hold buf and quit
+ /^[a-z]*:/ { x; p; q }
+# add to hold buf and branch to input next line
+ H
+ bb
+ }' $file
+}
+
+_vyatta_op_conv_node_path ()
+{
+ # is the node ok, ambiguous, or invalid
+ local node_path
+ local node
+ local -a ARR
+ node_path=$1
+ node=$2
+ ARR=( $(compgen -d $node_path/$node) )
+ if [[ "${#ARR[@]}" == "1" ]]; then
+ echo ${ARR[0]##*/}
+ elif [[ "${#ARR[@]}" == "0" ]]; then
+ if [[ -d "${node_path}/node.tag" ]]; then
+ echo "$node tag"
+ else
+ echo "$node invalid"
+ fi
+ elif [[ -d "$node_path/$node" ]]; then
+ echo $node
+ elif [[ "$VYATTA_USER_LEVEL_DIR" != "/opt/vyatta/etc/shell/level/admin" ]];then
+ # special handling for unprivledged completions.
+ # Since top level commands are different for unprivledged users
+ # we need a handler to expand them properly.
+ local -a filtered_cmds=()
+ local -a allowed=( $(cat $VYATTA_USER_LEVEL_DIR/allowed-op.in) )
+ get_prefix_filtered_list $node allowed filtered_cmds
+ if [[ "${#filtered_cmds[@]}" == "1" ]];then
+ echo ${filtered_cmds[0]}
+ else
+ echo "${node} ambiguous"
+ fi
+ else
+ echo "$node ambiguous"
+ fi
+}
+
+_vyatta_op_conv_run_cmd ()
+{
+ # Substitue bash positional variables
+ # for the same value in the expanded array
+ local restore_shopts=$( shopt -p extglob nullglob | tr \\n \; )
+ shopt -s extglob
+ shopt -u nullglob
+ local run_cmd="$1"
+ local line outline
+ local -i inquote=0;
+ local outcmd='';
+ local OIFS=$IFS
+ local re="([^']*')(.*)"
+
+ toggle_inquote()
+ {
+ if [[ $inquote == 0 ]]; then
+ inquote=1
+ else
+ inquote=0
+ fi
+ }
+
+ process_subline()
+ {
+ if [[ $inquote == 1 ]]; then
+ outline+="$1"
+ else
+ outline+=$(sed -e 's/\$\([0-9]\)/\$\{args\[\1\]\}/g' <<<"$1")
+ fi
+ }
+
+ run_cmd="${run_cmd/\"\$\@\"/${args[*]}}"
+ run_cmd="${run_cmd/\$\*/${args[*]}}"
+ run_cmd="${run_cmd//\\/\\\\}"
+ IFS=$'\n'
+ for line in ${run_cmd[@]}; do
+ outline=''
+ while [[ -n "$line" ]]; do
+ if [[ "$line" =~ $re ]]; then
+ process_subline "${BASH_REMATCH[1]}"
+ toggle_inquote
+ else
+ process_subline "$line"
+ fi
+ line="${BASH_REMATCH[2]}"
+ done
+ outcmd+="$outline\n"
+ done
+ IFS=$OIFS
+ eval "$restore_shopts"
+ echo -ne "$outcmd"
+}
+
+_vyatta_op_run ()
+{
+ # if run with bash builtin "set -/+*" run set and return
+ # this happens when a different completion script runs eval "set ..."
+ # (VyOS T1604)
+ if [[ "$1" == "set" && "$2" =~ ^(-|\+).* ]]; then
+ set "${@:2}"
+ return
+ fi
+
+ local -i estat
+ local tpath=$vyatta_op_templates
+ local restore_shopts=$( shopt -p extglob nullglob | tr \\n \; )
+ shopt -s extglob nullglob
+
+ _vyatta_op_last_comp=${_vyatta_op_last_comp_init}
+ false; estat=$?
+ stty echo 2> /dev/null # turn echo on, this is a workaround for bug 7570
+ # not a fix we need to look at why the readline library
+ # is getting confused on paged help text.
+
+ i=1
+ declare -a args # array of expanded arguments
+ for arg in "$@"; do
+ local orig_arg=$arg
+ if [[ $arg == "*" ]]; then
+ arg="*" #leave user defined wildcards alone
+ else
+ arg=( $(_vyatta_op_conv_node_path $tpath $arg) ) # expand the arguments
+ fi
+ # output proper error message based on the above expansion
+ if [[ "${arg[1]}" == "ambiguous" ]]; then
+ echo -ne "\n Ambiguous command: ${args[@]} [$arg]\n" >&2
+ local -a cmds=( $(compgen -d $tpath/$arg) )
+ _vyatta_op_node_path=$tpath
+ local comps=$(_vyatta_op_help $arg ${cmds[@]##*/})
+ echo -e "$comps\n" | sed -e 's/^P/ P/'
+ eval $restore_shopts
+ return 1
+ elif [[ "${arg[1]}" == "invalid" ]]; then
+ echo -ne "\n Invalid command: ${args[@]} [$arg]\n\n" >&2
+ eval $restore_shopts
+ return 1
+ fi
+
+ if [ -f "$tpath/$arg/node.def" ] ; then
+ tpath+=/$arg
+ elif [ -f $tpath/node.tag/node.def ] ; then
+ tpath+=/node.tag
+ else
+ echo -ne "\n Invalid command: ${args[@]} [$arg]\n\n" >&2
+ eval $restore_shopts
+ return 1
+ fi
+ if [[ "$arg" == "node.tag" ]]; then
+ args[$i]=$orig_arg
+ else
+ args[$i]=$arg
+ fi
+ let "i+=1"
+ done
+
+ local run_cmd=$(_vyatta_op_get_node_def_field $tpath/node.def run)
+ run_cmd=$(_vyatta_op_conv_run_cmd "$run_cmd") # convert the positional parameters
+ local ret=0
+ # Exception for the `show file` command
+ local file_cmd='\$\{vyos_op_scripts_dir\}\/file\.py'
+ local cmd_regex="^(LESSOPEN=|less|pager|tail|(sudo )?$file_cmd).*"
+ if [ -n "$run_cmd" ]; then
+ eval $restore_shopts
+ if [[ -t 1 && "${args[1]}" == "show" && ! $run_cmd =~ $cmd_regex ]] ; then
+ eval "($run_cmd) | ${VYATTA_PAGER:-cat}"
+ else
+ eval "$run_cmd"
+ fi
+ else
+ echo -ne "\n Incomplete command: ${args[@]}\n\n" >&2
+ eval $restore_shopts
+ ret=1
+ fi
+ return $ret
+}
+
+### Local Variables:
+### mode: shell-script
+### End:
diff --git a/src/opt/vyatta/share/vyatta-op/functions/interpreter/vyatta-unpriv b/src/opt/vyatta/share/vyatta-op/functions/interpreter/vyatta-unpriv
new file mode 100644
index 000000000..1507f4f0d
--- /dev/null
+++ b/src/opt/vyatta/share/vyatta-op/functions/interpreter/vyatta-unpriv
@@ -0,0 +1,97 @@
+#!/bin/bash
+# **** License ****
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License version 2 as
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# General Public License for more details.
+#
+# This code was originally developed by Vyatta, Inc.
+# Portions created by Vyatta are Copyright (C) 2006, 2007 Vyatta, Inc.
+# All Rights Reserved.
+#
+# **** End License ****
+
+source /opt/vyatta/share/vyatta-op/functions/interpreter/vyatta-common
+
+declare -a op_allowed
+declare -a toplevel
+
+op_allowed=( $(cat /opt/vyatta/etc/shell/level/users/allowed-op.in) )
+toplevel=( $(ls /opt/vyatta/share/vyatta-op/templates/) )
+
+vyatta_unpriv_ambiguous ()
+{
+ local -a filtered_cmds=()
+ get_prefix_filtered_list $1 op_allowed filtered_cmds
+ _vyatta_op_node_path=${vyatta_op_templates}
+ comps=$(_vyatta_op_help $1 ${filtered_cmds[@]})
+ echo -ne "\n Ambiguous command: [$1]\n"
+ echo -e "$comps\n" | sed -e 's/^P/ P/'
+}
+
+vyatta_unpriv_init ()
+{
+ # empty and default line compeletion
+ complete -E -F _vyatta_op_expand
+ complete -D -F _vyatta_op_default_expand
+
+ for cmd in "${op_allowed[@]}"; do
+ if is_elem_of ${cmd} toplevel; then
+ for pos in $(seq 1 ${#cmd}); do
+ case ${cmd:0:$pos} in
+ for|do|done|if|fi|case|while|tr )
+ continue ;;
+ *) ;;
+ esac
+ local -a filtered_cmds=()
+ get_prefix_filtered_list ${cmd:0:$pos} op_allowed filtered_cmds
+ local found
+ is_elem_of "${cmd:0:$pos}" op_allowed
+ found=$?
+ if [[ "${#filtered_cmds[@]}" == "1" || "${cmd:0:$pos}" == "$cmd" || "$found" == "0" ]]; then
+ local fcmd
+ if [[ "${#filtered_cmds[@]}" == "1" ]]; then
+ fcmd=${filtered_cmds[0]}
+ elif is_elem_of "${cmd:0:$pos}" op_allowed; then
+ fcmd=${cmd:0:$pos}
+ else
+ fcmd=$cmd
+ fi
+ eval alias ${cmd:0:$pos}=\'_vyatta_op_run $fcmd\'
+ else
+ eval alias ${cmd:0:$pos}=\'vyatta_unpriv_ambiguous ${cmd:0:$pos}\'
+ fi
+ complete -F _vyatta_op_expand ${cmd:0:$pos}
+ done
+ fi
+ done
+ if [[ "$VYATTA_USER_LEVEL_DIR" == "/opt/vyatta/etc/shell/level/users" ]]; then
+ PS1='\u@\h> '
+ fi
+}
+
+vyatta_unpriv_gen_allowed () {
+ local -a allowed_cmds=()
+ rm -rf /opt/vyatta/etc/shell/level/users/allowed-op
+ for cmd in "${op_allowed[@]}"; do
+ if is_elem_of ${cmd} toplevel; then
+ for pos in $(seq 1 ${#cmd}); do
+ case ${cmd:0:$pos} in
+ for|do|done|if|fi|case|while|tr )
+ continue ;;
+ *) ;;
+ esac
+ if ! is_elem_of ${cmd:0:$pos} allowed_cmds; then
+ allowed_cmds+=( ${cmd:0:$pos} )
+ echo ${cmd:0:$pos} >> /opt/vyatta/etc/shell/level/users/allowed-op
+ fi
+ done
+ else
+ echo ${cmd} >> /opt/vyatta/etc/shell/level/users/allowed-op
+ fi
+ done
+}
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 18d555f2d..800767219 100644
--- a/src/services/api/graphql/session/errors/op_mode_errors.py
+++ b/src/services/api/graphql/session/errors/op_mode_errors.py
@@ -1,5 +1,6 @@
op_mode_err_msg = {
"UnconfiguredSubsystem": "subsystem is not configured or not running",
+ "UnconfiguredObject": "object does not exist in the system configuration",
"DataUnavailable": "data currently unavailable",
"PermissionDenied": "client does not have permission",
"InsufficientResources": "insufficient system resources",
@@ -9,6 +10,7 @@ op_mode_err_msg = {
op_mode_err_code = {
"UnconfiguredSubsystem": 2000,
+ "UnconfiguredObject": 2003,
"DataUnavailable": 2001,
"InsufficientResources": 2002,
"PermissionDenied": 1003,
diff --git a/src/services/vyos-configd b/src/services/vyos-configd
index d92b539c8..a4b839a7f 100755
--- a/src/services/vyos-configd
+++ b/src/services/vyos-configd
@@ -30,7 +30,6 @@ from vyos.defaults import directories
from vyos.utils.boot import boot_configuration_complete
from vyos.configsource import ConfigSourceString
from vyos.configsource import ConfigSourceError
-from vyos.configdep import call_dependents
from vyos.config import Config
from vyos import ConfigError
@@ -134,7 +133,8 @@ def explicit_print(path, mode, msg):
except OSError:
logger.critical("error explicit_print")
-def run_script(script, config, args) -> int:
+def run_script(script_name, config, args) -> int:
+ script = conf_mode_scripts[script_name]
script.argv = args
config.set_level([])
try:
@@ -143,7 +143,7 @@ def run_script(script, config, args) -> int:
script.generate(c)
script.apply(c)
except ConfigError as e:
- logger.critical(e)
+ logger.error(e)
explicit_print(session_out, session_mode, str(e))
return R_ERROR_COMMIT
except Exception as e:
@@ -219,6 +219,7 @@ def process_node_data(config, data, last: bool = False) -> int:
script_name = None
args = []
+ config.dependency_list.clear()
res = re.match(r'^(VYOS_TAGNODE_VALUE=[^/]+)?.*\/([^/]+).py(.*)', data)
if res.group(1):
@@ -234,17 +235,10 @@ def process_node_data(config, data, last: bool = False) -> int:
args.insert(0, f'{script_name}.py')
if script_name not in include_set:
- # call dependents now if last element of prio queue is run
- # independent of configd
- if last:
- call_dependents(dependent_func=config.dependent_func)
return R_PASS
with stdout_redirected(session_out, session_mode):
- result = run_script(conf_mode_scripts[script_name], config, args)
-
- if last and result == R_SUCCESS:
- call_dependents(dependent_func=config.dependent_func)
+ result = run_script(script_name, config, args)
return result
diff --git a/src/services/vyos-conntrack-logger b/src/services/vyos-conntrack-logger
new file mode 100755
index 000000000..9c31b465f
--- /dev/null
+++ b/src/services/vyos-conntrack-logger
@@ -0,0 +1,458 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2024 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 grp
+import logging
+import multiprocessing
+import os
+import queue
+import signal
+import socket
+import threading
+from datetime import timedelta
+from pathlib import Path
+from time import sleep
+from typing import Dict, AnyStr
+
+from pyroute2 import conntrack
+from pyroute2.netlink import nfnetlink
+from pyroute2.netlink.nfnetlink import NFNL_SUBSYS_CTNETLINK
+from pyroute2.netlink.nfnetlink.nfctsocket import nfct_msg, \
+ IPCTNL_MSG_CT_DELETE, IPCTNL_MSG_CT_NEW, IPS_SEEN_REPLY, \
+ IPS_OFFLOAD, IPS_ASSURED
+
+from vyos.utils.file import read_json
+
+
+shutdown_event = multiprocessing.Event()
+
+logging.basicConfig(level=logging.INFO, format='%(message)s')
+logger = logging.getLogger(__name__)
+
+
+class DebugFormatter(logging.Formatter):
+ def format(self, record):
+ self._style._fmt = '[%(asctime)s] %(levelname)s: %(message)s'
+ return super().format(record)
+
+
+def set_log_level(level: str) -> None:
+ if level == 'debug':
+ logger.setLevel(logging.DEBUG)
+ logger.parent.handlers[0].setFormatter(DebugFormatter())
+ else:
+ logger.setLevel(logging.INFO)
+
+
+EVENT_NAME_TO_GROUP = {
+ 'new': nfnetlink.NFNLGRP_CONNTRACK_NEW,
+ 'update': nfnetlink.NFNLGRP_CONNTRACK_UPDATE,
+ 'destroy': nfnetlink.NFNLGRP_CONNTRACK_DESTROY
+}
+
+# https://github.com/torvalds/linux/blob/1dfe225e9af5bd3399a1dbc6a4df6a6041ff9c23/include/uapi/linux/netfilter/nf_conntrack_tcp.h#L9
+TCP_CONNTRACK_SYN_SENT = 1
+TCP_CONNTRACK_SYN_RECV = 2
+TCP_CONNTRACK_ESTABLISHED = 3
+TCP_CONNTRACK_FIN_WAIT = 4
+TCP_CONNTRACK_CLOSE_WAIT = 5
+TCP_CONNTRACK_LAST_ACK = 6
+TCP_CONNTRACK_TIME_WAIT = 7
+TCP_CONNTRACK_CLOSE = 8
+TCP_CONNTRACK_LISTEN = 9
+TCP_CONNTRACK_MAX = 10
+TCP_CONNTRACK_IGNORE = 11
+TCP_CONNTRACK_RETRANS = 12
+TCP_CONNTRACK_UNACK = 13
+TCP_CONNTRACK_TIMEOUT_MAX = 14
+
+TCP_CONNTRACK_TO_NAME = {
+ TCP_CONNTRACK_SYN_SENT: "SYN_SENT",
+ TCP_CONNTRACK_SYN_RECV: "SYN_RECV",
+ TCP_CONNTRACK_ESTABLISHED: "ESTABLISHED",
+ TCP_CONNTRACK_FIN_WAIT: "FIN_WAIT",
+ TCP_CONNTRACK_CLOSE_WAIT: "CLOSE_WAIT",
+ TCP_CONNTRACK_LAST_ACK: "LAST_ACK",
+ TCP_CONNTRACK_TIME_WAIT: "TIME_WAIT",
+ TCP_CONNTRACK_CLOSE: "CLOSE",
+ TCP_CONNTRACK_LISTEN: "LISTEN",
+ TCP_CONNTRACK_MAX: "MAX",
+ TCP_CONNTRACK_IGNORE: "IGNORE",
+ TCP_CONNTRACK_RETRANS: "RETRANS",
+ TCP_CONNTRACK_UNACK: "UNACK",
+ TCP_CONNTRACK_TIMEOUT_MAX: "TIMEOUT_MAX",
+}
+
+# https://github.com/torvalds/linux/blob/1dfe225e9af5bd3399a1dbc6a4df6a6041ff9c23/include/uapi/linux/netfilter/nf_conntrack_sctp.h#L8
+SCTP_CONNTRACK_CLOSED = 1
+SCTP_CONNTRACK_COOKIE_WAIT = 2
+SCTP_CONNTRACK_COOKIE_ECHOED = 3
+SCTP_CONNTRACK_ESTABLISHED = 4
+SCTP_CONNTRACK_SHUTDOWN_SENT = 5
+SCTP_CONNTRACK_SHUTDOWN_RECD = 6
+SCTP_CONNTRACK_SHUTDOWN_ACK_SENT = 7
+SCTP_CONNTRACK_HEARTBEAT_SENT = 8
+SCTP_CONNTRACK_HEARTBEAT_ACKED = 9 # no longer used
+SCTP_CONNTRACK_MAX = 10
+
+SCTP_CONNTRACK_TO_NAME = {
+ SCTP_CONNTRACK_CLOSED: 'CLOSED',
+ SCTP_CONNTRACK_COOKIE_WAIT: 'COOKIE_WAIT',
+ SCTP_CONNTRACK_COOKIE_ECHOED: 'COOKIE_ECHOED',
+ SCTP_CONNTRACK_ESTABLISHED: 'ESTABLISHED',
+ SCTP_CONNTRACK_SHUTDOWN_SENT: 'SHUTDOWN_SENT',
+ SCTP_CONNTRACK_SHUTDOWN_RECD: 'SHUTDOWN_RECD',
+ SCTP_CONNTRACK_SHUTDOWN_ACK_SENT: 'SHUTDOWN_ACK_SENT',
+ SCTP_CONNTRACK_HEARTBEAT_SENT: 'HEARTBEAT_SENT',
+ SCTP_CONNTRACK_HEARTBEAT_ACKED: 'HEARTBEAT_ACKED',
+ SCTP_CONNTRACK_MAX: 'MAX',
+}
+
+PROTO_CONNTRACK_TO_NAME = {
+ 'TCP': TCP_CONNTRACK_TO_NAME,
+ 'SCTP': SCTP_CONNTRACK_TO_NAME
+}
+
+SUPPORTED_PROTO_TO_NAME = {
+ socket.IPPROTO_ICMP: 'icmp',
+ socket.IPPROTO_TCP: 'tcp',
+ socket.IPPROTO_UDP: 'udp',
+}
+
+PROTO_TO_NAME = {
+ socket.IPPROTO_ICMPV6: 'icmpv6',
+ socket.IPPROTO_SCTP: 'sctp',
+ socket.IPPROTO_GRE: 'gre',
+}
+
+PROTO_TO_NAME.update(SUPPORTED_PROTO_TO_NAME)
+
+
+def sig_handler(signum, frame):
+ process_name = multiprocessing.current_process().name
+ logger.debug(f'[{process_name}]: {"Shutdown" if signum == signal.SIGTERM else "Reload"} signal received...')
+ shutdown_event.set()
+
+
+def format_flow_data(data: Dict) -> AnyStr:
+ """
+ Formats the flow event data into a string suitable for logging.
+ """
+ key_format = {
+ 'SRC_PORT': 'sport',
+ 'DST_PORT': 'dport'
+ }
+ message = f"src={data['ADDR'].get('SRC')} dst={data['ADDR'].get('DST')}"
+
+ for key in ['SRC_PORT', 'DST_PORT', 'TYPE', 'CODE', 'ID']:
+ tmp = data['PROTO'].get(key)
+ if tmp is not None:
+ key = key_format.get(key, key)
+ message += f" {key.lower()}={tmp}"
+
+ if 'COUNTERS' in data:
+ for key in ['PACKETS', 'BYTES']:
+ tmp = data['COUNTERS'].get(key)
+ if tmp is not None:
+ message += f" {key.lower()}={tmp}"
+
+ return message
+
+
+def format_event_message(event: Dict) -> AnyStr:
+ """
+ Formats the internal parsed event data into a string suitable for logging.
+ """
+ event_type = f"[{event['COMMON']['EVENT_TYPE'].upper()}]"
+ message = f"{event_type:<{9}} {event['COMMON']['ID']} " \
+ f"{event['ORIG']['PROTO'].get('NAME'):<{8}} " \
+ f"{event['ORIG']['PROTO'].get('NUMBER')} "
+
+ tmp = event['COMMON']['TIME_OUT']
+ if tmp is not None: message += f"{tmp} "
+
+ if proto_info := event['COMMON'].get('PROTO_INFO'):
+ message += f"{proto_info.get('STATE_NAME')} "
+
+ for key in ['ORIG', 'REPLY']:
+ message += f"{format_flow_data(event[key])} "
+ if key == 'ORIG' and not (event['COMMON']['STATUS'] & IPS_SEEN_REPLY):
+ message += f"[UNREPLIED] "
+
+ tmp = event['COMMON']['MARK']
+ if tmp is not None: message += f"mark={tmp} "
+
+ if event['COMMON']['STATUS'] & IPS_OFFLOAD: message += f" [OFFLOAD] "
+ elif event['COMMON']['STATUS'] & IPS_ASSURED: message += f" [ASSURED] "
+
+ if tmp := event['COMMON']['PORTID']: message += f"portid={tmp} "
+ if tstamp := event['COMMON'].get('TIMESTAMP'):
+ message += f"start={tstamp['START']} stop={tstamp['STOP']} "
+ delta_ns = tstamp['STOP'] - tstamp['START']
+ delta_s = delta_ns // 1e9
+ remaining_ns = delta_ns % 1e9
+ delta = timedelta(seconds=delta_s, microseconds=remaining_ns / 1000)
+ message += f"delta={delta.total_seconds()} "
+
+ return message
+
+
+def parse_event_type(header: Dict) -> AnyStr:
+ """
+ Extract event type from nfct_msg. new, update, destroy
+ """
+ event_type = 'unknown'
+ if header['type'] == IPCTNL_MSG_CT_DELETE | (NFNL_SUBSYS_CTNETLINK << 8):
+ event_type = 'destroy'
+ elif header['type'] == IPCTNL_MSG_CT_NEW | (NFNL_SUBSYS_CTNETLINK << 8):
+ event_type = 'update'
+ if header['flags']:
+ event_type = 'new'
+ return event_type
+
+
+def parse_proto(cta: nfct_msg.cta_tuple) -> Dict:
+ """
+ Extract proto info from nfct_msg. src/dst port, code, type, id
+ """
+ data = dict()
+
+ cta_proto = cta.get_attr('CTA_TUPLE_PROTO')
+ proto_num = cta_proto.get_attr('CTA_PROTO_NUM')
+
+ data['NUMBER'] = proto_num
+ data['NAME'] = PROTO_TO_NAME.get(proto_num, 'unknown')
+
+ if proto_num in (socket.IPPROTO_ICMP, socket.IPPROTO_ICMPV6):
+ pref = 'CTA_PROTO_ICMP'
+ if proto_num == socket.IPPROTO_ICMPV6: pref += 'V6'
+ keys = ['TYPE', 'CODE', 'ID']
+ else:
+ pref = 'CTA_PROTO'
+ keys = ['SRC_PORT', 'DST_PORT']
+
+ for key in keys:
+ data[key] = cta_proto.get_attr(f'{pref}_{key}')
+
+ return data
+
+
+def parse_proto_info(cta: nfct_msg.cta_protoinfo) -> Dict:
+ """
+ Extract proto state and state name from nfct_msg
+ """
+ data = dict()
+ if not cta:
+ return data
+
+ for proto in ['TCP', 'SCTP']:
+ if proto_info := cta.get_attr(f'CTA_PROTOINFO_{proto}'):
+ data['STATE'] = proto_info.get_attr(f'CTA_PROTOINFO_{proto}_STATE')
+ data['STATE_NAME'] = PROTO_CONNTRACK_TO_NAME.get(proto, {}).get(data['STATE'], 'unknown')
+ return data
+
+
+def parse_timestamp(cta: nfct_msg.cta_timestamp) -> Dict:
+ """
+ Extract timestamp from nfct_msg
+ """
+ data = dict()
+ if not cta:
+ return data
+ data['START'] = cta.get_attr('CTA_TIMESTAMP_START')
+ data['STOP'] = cta.get_attr('CTA_TIMESTAMP_STOP')
+
+ return data
+
+
+def parse_ip_addr(family: int, cta: nfct_msg.cta_tuple) -> Dict:
+ """
+ Extract ip adr from nfct_msg
+ """
+ data = dict()
+ cta_ip = cta.get_attr('CTA_TUPLE_IP')
+
+ if family == socket.AF_INET:
+ pref = 'CTA_IP_V4'
+ elif family == socket.AF_INET6:
+ pref = 'CTA_IP_V6'
+ else:
+ logger.error(f'Undefined INET: {family}')
+ raise NotImplementedError(family)
+
+ for direct in ['SRC', 'DST']:
+ data[direct] = cta_ip.get_attr(f'{pref}_{direct}')
+
+ return data
+
+
+def parse_counters(cta: nfct_msg.cta_counters) -> Dict:
+ """
+ Extract counters from nfct_msg
+ """
+ data = dict()
+ if not cta:
+ return data
+
+ for key in ['PACKETS', 'BYTES']:
+ tmp = cta.get_attr(f'CTA_COUNTERS_{key}')
+ if tmp is None:
+ tmp = cta.get_attr(f'CTA_COUNTERS32_{key}')
+ data['key'] = tmp
+
+ return data
+
+
+def is_need_to_log(event_type: AnyStr, proto_num: int, conf_event: Dict):
+ """
+ Filter message by event type and protocols
+ """
+ conf = conf_event.get(event_type)
+ if conf == {} or conf.get(SUPPORTED_PROTO_TO_NAME.get(proto_num, 'other')) is not None:
+ return True
+ return False
+
+
+def parse_conntrack_event(msg: nfct_msg, conf_event: Dict) -> Dict:
+ """
+ Convert nfct_msg to internal data dict.
+ """
+ data = dict()
+ event_type = parse_event_type(msg['header'])
+ proto_num = msg.get_nested('CTA_TUPLE_ORIG', 'CTA_TUPLE_PROTO', 'CTA_PROTO_NUM')
+
+ if not is_need_to_log(event_type, proto_num, conf_event):
+ return data
+
+ data = {
+ 'COMMON': {
+ 'ID': msg.get_attr('CTA_ID'),
+ 'EVENT_TYPE': event_type,
+ 'TIME_OUT': msg.get_attr('CTA_TIMEOUT'),
+ 'MARK': msg.get_attr('CTA_MARK'),
+ 'PORTID': msg['header'].get('pid'),
+ 'PROTO_INFO': parse_proto_info(msg.get_attr('CTA_PROTOINFO')),
+ 'STATUS': msg.get_attr('CTA_STATUS'),
+ 'TIMESTAMP': parse_timestamp(msg.get_attr('CTA_TIMESTAMP'))
+ },
+ 'ORIG': {},
+ 'REPLY': {},
+ }
+
+ for direct in ['ORIG', 'REPLY']:
+ data[direct]['ADDR'] = parse_ip_addr(msg['nfgen_family'], msg.get_attr(f'CTA_TUPLE_{direct}'))
+ data[direct]['PROTO'] = parse_proto(msg.get_attr(f'CTA_TUPLE_{direct}'))
+ data[direct]['COUNTERS'] = parse_counters(msg.get_attr(f'CTA_COUNTERS_{direct}'))
+
+ return data
+
+
+def worker(ct: conntrack.Conntrack, shutdown_event: multiprocessing.Event, conf_event: Dict):
+ """
+ Main function of parser worker process
+ """
+ process_name = multiprocessing.current_process().name
+ logger.debug(f'[{process_name}] started')
+ timeout = 0.1
+ while not shutdown_event.is_set():
+ if not ct.buffer_queue.empty():
+ try:
+ for msg in ct.get():
+ parsed_event = parse_conntrack_event(msg, conf_event)
+ if parsed_event:
+ message = format_event_message(parsed_event)
+ if logger.level == logging.DEBUG:
+ logger.debug(f"[{process_name}]: {message} raw: {msg}")
+ else:
+ logger.info(message)
+ except queue.Full:
+ logger.error("Conntrack message queue if full.")
+ except Exception as e:
+ logger.error(f"Error in queue: {e.__class__} {e}")
+ else:
+ sleep(timeout)
+
+
+if __name__ == '__main__':
+ parser = argparse.ArgumentParser()
+ parser.add_argument('-c',
+ '--config',
+ action='store',
+ help='Path to vyos-conntrack-logger configuration',
+ required=True,
+ type=Path)
+
+ args = parser.parse_args()
+ try:
+ config = read_json(args.config)
+ except Exception as err:
+ logger.error(f'Configuration file "{args.config}" does not exist or malformed: {err}')
+ exit(1)
+
+ set_log_level(config.get('log_level', 'info'))
+
+ signal.signal(signal.SIGHUP, sig_handler)
+ signal.signal(signal.SIGTERM, sig_handler)
+
+ if 'event' in config:
+ event_groups = list(config.get('event').keys())
+ else:
+ logger.error(f'Configuration is wrong. Event filter is empty.')
+ exit(1)
+
+ conf_event = config['event']
+ qsize = config.get('queue_size')
+ ct = conntrack.Conntrack(async_qsize=int(qsize) if qsize else None)
+ ct.buffer_queue = multiprocessing.Queue(ct.async_qsize)
+ ct.bind(async_cache=True)
+
+ for name in event_groups:
+ if group := EVENT_NAME_TO_GROUP.get(name):
+ ct.add_membership(group)
+ else:
+ logger.error(f'Unexpected event group {name}')
+ processes = list()
+ try:
+ for _ in range(multiprocessing.cpu_count()):
+ p = multiprocessing.Process(target=worker, args=(ct,
+ shutdown_event,
+ conf_event))
+ processes.append(p)
+ p.start()
+ logger.info('Conntrack socket bound and listening for messages.')
+
+ while not shutdown_event.is_set():
+ if not ct.pthread.is_alive():
+ if ct.buffer_queue.qsize()/ct.async_qsize < 0.9:
+ if not shutdown_event.is_set():
+ logger.debug('Restart listener thread')
+ # restart listener thread after queue overloaded when queue size low than 90%
+ ct.pthread = threading.Thread(
+ name="Netlink async cache", target=ct.async_recv
+ )
+ ct.pthread.daemon = True
+ ct.pthread.start()
+ else:
+ sleep(0.1)
+ finally:
+ for p in processes:
+ p.join()
+ if not p.is_alive():
+ logger.debug(f"[{p.name}]: finished")
+ ct.close()
+ logging.info("Conntrack socket closed.")
+ exit()
diff --git a/src/systemd/vyos-conntrack-logger.service b/src/systemd/vyos-conntrack-logger.service
new file mode 100644
index 000000000..9bc1d857b
--- /dev/null
+++ b/src/systemd/vyos-conntrack-logger.service
@@ -0,0 +1,21 @@
+[Unit]
+Description=VyOS conntrack logger daemon
+
+# Seemingly sensible way to say "as early as the system is ready"
+# All vyos-configd needs is read/write mounted root
+After=conntrackd.service
+
+[Service]
+ExecStart=/usr/bin/python3 -u /usr/libexec/vyos/services/vyos-conntrack-logger -c /run/vyos-conntrack-logger.conf
+Type=idle
+
+SyslogIdentifier=vyos-conntrack-logger
+SyslogFacility=daemon
+
+Restart=on-failure
+
+User=root
+Group=vyattacfg
+
+[Install]
+WantedBy=multi-user.target