summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.github/workflows/pr-conflicts.yml18
-rw-r--r--data/templates/frr/isisd.frr.tmpl8
-rw-r--r--interface-definitions/include/isis/password.xml.i20
-rw-r--r--interface-definitions/include/isis/protocol-common-config.xml.i54
-rw-r--r--python/vyos/base.py6
-rw-r--r--python/vyos/ifconfig/interface.py19
-rw-r--r--python/vyos/ifconfig/loopback.py13
-rw-r--r--python/vyos/util.py4
-rwxr-xr-xsmoketest/scripts/cli/test_protocols_isis.py69
-rwxr-xr-xsmoketest/scripts/cli/test_service_ssh.py73
-rwxr-xr-xsmoketest/scripts/cli/test_system_ipv6.py31
-rwxr-xr-xsmoketest/scripts/cli/test_system_login.py8
-rwxr-xr-xsmoketest/scripts/cli/test_vrf.py92
-rwxr-xr-xsrc/conf_mode/dns_forwarding.py25
-rwxr-xr-xsrc/conf_mode/system-ipv6.py4
-rwxr-xr-xsrc/conf_mode/system-login.py21
-rwxr-xr-xsrc/conf_mode/vrf.py8
-rw-r--r--src/tests/test_util.py16
18 files changed, 357 insertions, 132 deletions
diff --git a/.github/workflows/pr-conflicts.yml b/.github/workflows/pr-conflicts.yml
new file mode 100644
index 000000000..72ff3969b
--- /dev/null
+++ b/.github/workflows/pr-conflicts.yml
@@ -0,0 +1,18 @@
+name: "PR Conflicts checker"
+on:
+ pull_request_target:
+ types: [synchronize]
+
+jobs:
+ Conflict_Check:
+ name: 'Check PR status: conflicts and resolution'
+ runs-on: ubuntu-18.04
+ steps:
+ - name: check if PRs are dirty
+ uses: eps1lon/actions-label-merge-conflict@releases/2.x
+ with:
+ dirtyLabel: "state: conflict"
+ removeOnDirtyLabel: "state: conflict resolved"
+ repoToken: "${{ secrets.GITHUB_TOKEN }}"
+ commentOnDirty: "This pull request has conflicts, please resolve those before we can evaluate the pull request."
+ commentOnClean: "Conflicts have been resolved. A maintainer will review the pull request shortly."
diff --git a/data/templates/frr/isisd.frr.tmpl b/data/templates/frr/isisd.frr.tmpl
index 6cfa076d0..324419456 100644
--- a/data/templates/frr/isisd.frr.tmpl
+++ b/data/templates/frr/isisd.frr.tmpl
@@ -99,8 +99,8 @@ router isis VyOS {{ 'vrf ' + vrf if vrf is defined and vrf is not none }}
{% endfor %}
{% endif %}
{% endif %}
-{% if spf_delay_ietf is defined and spf_delay_ietf.init_delay is defined and spf_delay_ietf.init_delay is not none %}
- spf-delay-ietf init-delay {{ spf_delay_ietf.init_delay }}
+{% if spf_delay_ietf is defined and spf_delay_ietf.init_delay is defined and spf_delay_ietf.short_delay is defined and spf_delay_ietf.long_delay is defined and spf_delay_ietf.holddown is defined and spf_delay_ietf.time_to_learn is defined %}
+ spf-delay-ietf init-delay {{ spf_delay_ietf.init_delay }} short-delay {{ spf_delay_ietf.short_delay }} long-delay {{ spf_delay_ietf.long_delay }} holddown {{ spf_delay_ietf.holddown }} time-to-learn {{ spf_delay_ietf.time_to_learn }}
{% endif %}
{% if area_password is defined and area_password is not none %}
{% if area_password.md5 is defined and area_password.md5 is not none %}
@@ -181,7 +181,9 @@ interface {{ iface }} {{ 'vrf ' + vrf if vrf is defined and vrf is not none }}
{% if iface_config.passive is defined %}
isis passive
{% endif %}
-{% if iface_config.password is defined and iface_config.password.plaintext_password is defined and iface_config.password.plaintext_password is not none %}
+{% if iface_config.password is defined and iface_config.password.md5 is defined and iface_config.password.md5 is not none %}
+ isis password md5 {{ iface_config.password.md5 }}
+{% elif iface_config.password is defined and iface_config.password.plaintext_password is defined and iface_config.password.plaintext_password is not none %}
isis password clear {{ iface_config.password.plaintext_password }}
{% endif %}
{% if iface_config.priority is defined and iface_config.priority is not none %}
diff --git a/interface-definitions/include/isis/password.xml.i b/interface-definitions/include/isis/password.xml.i
new file mode 100644
index 000000000..27c3b0fa0
--- /dev/null
+++ b/interface-definitions/include/isis/password.xml.i
@@ -0,0 +1,20 @@
+<!-- include start from isis/password.xml.i -->
+<leafNode name="plaintext-password">
+ <properties>
+ <help>Plain-text authentication type</help>
+ <valueHelp>
+ <format>txt</format>
+ <description>Circuit password</description>
+ </valueHelp>
+ </properties>
+</leafNode>
+<leafNode name="md5">
+ <properties>
+ <help>MD5 authentication type</help>
+ <valueHelp>
+ <format>txt</format>
+ <description>Level-wide password</description>
+ </valueHelp>
+ </properties>
+</leafNode>
+<!-- include end -->
diff --git a/interface-definitions/include/isis/protocol-common-config.xml.i b/interface-definitions/include/isis/protocol-common-config.xml.i
index 84e2f7bb2..190fa6149 100644
--- a/interface-definitions/include/isis/protocol-common-config.xml.i
+++ b/interface-definitions/include/isis/protocol-common-config.xml.i
@@ -4,24 +4,7 @@
<help>Configure the authentication password for an area</help>
</properties>
<children>
- <leafNode name="plaintext-password">
- <properties>
- <help>Plain-text authentication type</help>
- <valueHelp>
- <format>txt</format>
- <description>Level-wide password</description>
- </valueHelp>
- </properties>
- </leafNode>
- <leafNode name="md5">
- <properties>
- <help>MD5 authentication type</help>
- <valueHelp>
- <format>txt</format>
- <description>Level-wide password</description>
- </valueHelp>
- </properties>
- </leafNode>
+ #include <include/isis/password.xml.i>
</children>
</node>
<node name="default-information">
@@ -59,24 +42,7 @@
<help>Set the authentication password for a routing domain</help>
</properties>
<children>
- <leafNode name="plaintext-password">
- <properties>
- <help>Plain-text authentication type</help>
- <valueHelp>
- <format>txt</format>
- <description>Level-wide password</description>
- </valueHelp>
- </properties>
- </leafNode>
- <leafNode name="md5">
- <properties>
- <help>MD5 authentication type</help>
- <valueHelp>
- <format>txt</format>
- <description>Level-wide password</description>
- </valueHelp>
- </properties>
- </leafNode>
+ #include <include/isis/password.xml.i>
</children>
</node>
<leafNode name="dynamic-hostname">
@@ -104,7 +70,7 @@
<description>Act as an area router</description>
</valueHelp>
<constraint>
- <regex>^(level-1|level-1-2|level-2)$</regex>
+ <regex>(level-1|level-1-2|level-2)</regex>
</constraint>
</properties>
</leafNode>
@@ -182,7 +148,7 @@
<description>Use new style of TLVs to carry wider metric</description>
</valueHelp>
<constraint>
- <regex>^(narrow|transition|wide)$</regex>
+ <regex>(narrow|transition|wide)</regex>
</constraint>
</properties>
</leafNode>
@@ -668,7 +634,7 @@
<description>Level-2 only adjacencies are formed</description>
</valueHelp>
<constraint>
- <regex>^(level-1|level-1-2|level-2-only)$</regex>
+ <regex>(level-1|level-1-2|level-2-only)</regex>
</constraint>
</properties>
</leafNode>
@@ -722,15 +688,7 @@
<help>Configure the authentication password for a circuit</help>
</properties>
<children>
- <leafNode name="plaintext-password">
- <properties>
- <help>Plain-text authentication type</help>
- <valueHelp>
- <format>txt</format>
- <description>Circuit password</description>
- </valueHelp>
- </properties>
- </leafNode>
+ #include <include/isis/password.xml.i>
</children>
</node>
<leafNode name="priority">
diff --git a/python/vyos/base.py b/python/vyos/base.py
index c78045548..fd22eaccd 100644
--- a/python/vyos/base.py
+++ b/python/vyos/base.py
@@ -15,6 +15,12 @@
from textwrap import fill
+class DeprecationWarning():
+ def __init__(self, message):
+ # Reformat the message and trim it to 72 characters in length
+ message = fill(message, width=72)
+ print(f'\nDEPRECATION WARNING: {message}\n')
+
class ConfigError(Exception):
def __init__(self, message):
# Reformat the message and trim it to 72 characters in length
diff --git a/python/vyos/ifconfig/interface.py b/python/vyos/ifconfig/interface.py
index 73c602cf9..9eed3acb9 100644
--- a/python/vyos/ifconfig/interface.py
+++ b/python/vyos/ifconfig/interface.py
@@ -38,7 +38,7 @@ from vyos.util import dict_search
from vyos.util import read_file
from vyos.util import get_interface_config
from vyos.util import is_systemd_service_active
-from vyos.util import sysctl_read
+from vyos.util import is_ipv6_enabled
from vyos.template import is_ipv4
from vyos.template import is_ipv6
from vyos.validate import is_intf_addr_assigned
@@ -990,6 +990,10 @@ class Interface(Control):
"Can't configure both static IPv4 and DHCP address "
"on the same interface"))
+ # Failsave - do not add IPv6 address if IPv6 is disabled
+ if is_ipv6(addr) and not is_ipv6_enabled():
+ return False
+
# add to interface
if addr == 'dhcp':
self.set_dhcp(True)
@@ -1359,8 +1363,15 @@ class Interface(Control):
value = tmp if (tmp != None) else '0'
self.set_ipv4_source_validation(value)
+ # MTU - Maximum Transfer Unit has a default value. It must ALWAYS be set
+ # before mangling any IPv6 option. If MTU is less then 1280 IPv6 will be
+ # automatically disabled by the kernel. Also MTU must be increased before
+ # configuring any IPv6 address on the interface.
+ if 'mtu' in config:
+ self.set_mtu(config.get('mtu'))
+
# Only change IPv6 parameters if IPv6 was not explicitly disabled
- if sysctl_read('net.ipv6.conf.all.disable_ipv6') == '0':
+ if is_ipv6_enabled():
# IPv6 forwarding
tmp = dict_search('ipv6.disable_forwarding', config)
value = '0' if (tmp != None) else '1'
@@ -1383,10 +1394,6 @@ class Interface(Control):
value = tmp if (tmp != None) else '1'
self.set_ipv6_dad_messages(value)
- # MTU - Maximum Transfer Unit
- if 'mtu' in config:
- self.set_mtu(config.get('mtu'))
-
# Delete old IPv6 EUI64 addresses before changing MAC
for addr in (dict_search('ipv6.address.eui64_old', config) or []):
self.del_ipv6_eui64_address(addr)
diff --git a/python/vyos/ifconfig/loopback.py b/python/vyos/ifconfig/loopback.py
index de554ef44..30c890fdf 100644
--- a/python/vyos/ifconfig/loopback.py
+++ b/python/vyos/ifconfig/loopback.py
@@ -13,9 +13,8 @@
# You should have received a copy of the GNU Lesser General Public
# License along with this library. If not, see <http://www.gnu.org/licenses/>.
-import vyos.util
-
from vyos.ifconfig.interface import Interface
+from vyos.util import is_ipv6_enabled
@Interface.register
class LoopbackIf(Interface):
@@ -34,8 +33,6 @@ class LoopbackIf(Interface):
}
}
- name = 'loopback'
-
def remove(self):
"""
Loopback interface can not be deleted from operating system. We can
@@ -62,11 +59,11 @@ class LoopbackIf(Interface):
on any interface. """
addr = config.get('address', [])
- # We must ensure that the loopback addresses are never deleted from the system
- addr += ['127.0.0.1/8']
- if (vyos.util.sysctl_read('net.ipv6.conf.all.disable_ipv6') == '0'):
- addr += ['::1/128']
+ # We must ensure that the loopback addresses are never deleted from the system
+ addr.append('127.0.0.1/8')
+ if is_ipv6_enabled():
+ addr.append('::1/128')
# Update IP address entry in our dictionary
config.update({'address' : addr})
diff --git a/python/vyos/util.py b/python/vyos/util.py
index b5d81fba5..4283e604c 100644
--- a/python/vyos/util.py
+++ b/python/vyos/util.py
@@ -827,3 +827,7 @@ def sysctl_write(name, value):
call(f'sysctl -wq {name}={value}')
return True
return False
+
+def is_ipv6_enabled() -> bool:
+ """ Check if IPv6 support on the system is enabled or not """
+ return (sysctl_read('net.ipv6.conf.all.disable_ipv6') == '0')
diff --git a/smoketest/scripts/cli/test_protocols_isis.py b/smoketest/scripts/cli/test_protocols_isis.py
index 8abdd6d37..167cd05f8 100755
--- a/smoketest/scripts/cli/test_protocols_isis.py
+++ b/smoketest/scripts/cli/test_protocols_isis.py
@@ -71,13 +71,13 @@ class TestProtocolsISIS(VyOSUnitTestSHIM.TestCase):
self.cli_commit()
# Verify all changes
- tmp = self.getFRRconfig(f'router isis {domain}')
+ tmp = self.getFRRconfig(f'router isis {domain}', daemon='isisd')
self.assertIn(f' net {net}', tmp)
self.assertIn(f' log-adjacency-changes', tmp)
self.assertIn(f' redistribute ipv4 connected level-2 route-map {route_map}', tmp)
for interface in self._interfaces:
- tmp = self.getFRRconfig(f'interface {interface}')
+ tmp = self.getFRRconfig(f'interface {interface}', daemon='isisd')
self.assertIn(f' ip router isis {domain}', tmp)
self.assertIn(f' ipv6 router isis {domain}', tmp)
@@ -93,22 +93,26 @@ class TestProtocolsISIS(VyOSUnitTestSHIM.TestCase):
self.isis_base_config()
self.cli_set(base_path + ['redistribute', 'ipv4', 'connected', 'level-2', 'route-map', route_map])
self.cli_set(base_path + ['route-map', route_map])
+ self.cli_set(base_path + ['level', 'level-2'])
# commit changes
self.cli_commit()
# Verify FRR configuration
zebra_route_map = f'ip protocol isis route-map {route_map}'
- frrconfig = self.getFRRconfig(zebra_route_map)
+ frrconfig = self.getFRRconfig(zebra_route_map, daemon='zebra')
self.assertIn(zebra_route_map, frrconfig)
+ tmp = self.getFRRconfig(f'router isis {domain}', daemon='isisd')
+ self.assertIn(' is-type level-2-only', tmp)
+
# Remove the route-map again
self.cli_delete(base_path + ['route-map'])
# commit changes
self.cli_commit()
# Verify FRR configuration
- frrconfig = self.getFRRconfig(zebra_route_map)
+ frrconfig = self.getFRRconfig(zebra_route_map, daemon='zebra')
self.assertNotIn(zebra_route_map, frrconfig)
self.cli_delete(['policy', 'route-map', route_map])
@@ -128,7 +132,7 @@ class TestProtocolsISIS(VyOSUnitTestSHIM.TestCase):
self.cli_commit()
# Verify all changes
- tmp = self.getFRRconfig(f'router isis {domain}')
+ tmp = self.getFRRconfig(f'router isis {domain}', daemon='isisd')
self.assertIn(f' net {net}', tmp)
for afi in ['ipv4', 'ipv6']:
@@ -140,6 +144,8 @@ class TestProtocolsISIS(VyOSUnitTestSHIM.TestCase):
password = 'foo'
self.isis_base_config()
+ for interface in self._interfaces:
+ self.cli_set(base_path + ['interface', interface, 'password', 'plaintext-password', f'{password}-{interface}'])
self.cli_set(base_path + ['area-password', 'plaintext-password', password])
self.cli_set(base_path + ['area-password', 'md5', password])
@@ -160,10 +166,61 @@ class TestProtocolsISIS(VyOSUnitTestSHIM.TestCase):
self.cli_commit()
# Verify all changes
- tmp = self.getFRRconfig(f'router isis {domain}')
+ tmp = self.getFRRconfig(f'router isis {domain}', daemon='isisd')
self.assertIn(f' net {net}', tmp)
self.assertIn(f' domain-password clear {password}', tmp)
self.assertIn(f' area-password clear {password}', tmp)
+ for interface in self._interfaces:
+ tmp = self.getFRRconfig(f'interface {interface}', daemon='isisd')
+ self.assertIn(f' isis password clear {password}-{interface}', tmp)
+
+ def test_isis_06_spf_delay(self):
+ network = 'point-to-point'
+ holddown = '10'
+ init_delay = '50'
+ long_delay = '200'
+ short_delay = '100'
+ time_to_learn = '75'
+
+ self.cli_set(base_path + ['net', net])
+ for interface in self._interfaces:
+ self.cli_set(base_path + ['interface', interface, 'network', network])
+
+ self.cli_set(base_path + ['spf-delay-ietf', 'holddown', holddown])
+ # verify() - All types of spf-delay must be configured
+ with self.assertRaises(ConfigSessionError):
+ self.cli_commit()
+
+ self.cli_set(base_path + ['spf-delay-ietf', 'init-delay', init_delay])
+ # verify() - All types of spf-delay must be configured
+ with self.assertRaises(ConfigSessionError):
+ self.cli_commit()
+
+ self.cli_set(base_path + ['spf-delay-ietf', 'long-delay', long_delay])
+ # verify() - All types of spf-delay must be configured
+ with self.assertRaises(ConfigSessionError):
+ self.cli_commit()
+
+ self.cli_set(base_path + ['spf-delay-ietf', 'short-delay', short_delay])
+ # verify() - All types of spf-delay must be configured
+ with self.assertRaises(ConfigSessionError):
+ self.cli_commit()
+ self.cli_set(base_path + ['spf-delay-ietf', 'time-to-learn', time_to_learn])
+
+ # Commit all changes
+ self.cli_commit()
+
+ # Verify all changes
+ tmp = self.getFRRconfig(f'router isis {domain}', daemon='isisd')
+ self.assertIn(f' net {net}', tmp)
+ self.assertIn(f' spf-delay-ietf init-delay {init_delay} short-delay {short_delay} long-delay {long_delay} holddown {holddown} time-to-learn {time_to_learn}', tmp)
+
+ for interface in self._interfaces:
+ tmp = self.getFRRconfig(f'interface {interface}', daemon='isisd')
+ self.assertIn(f' ip router isis {domain}', tmp)
+ self.assertIn(f' ipv6 router isis {domain}', tmp)
+ self.assertIn(f' isis network {network}', tmp)
+
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 6f58ce3d3..49a167e04 100755
--- a/smoketest/scripts/cli/test_service_ssh.py
+++ b/smoketest/scripts/cli/test_service_ssh.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2019-2020 VyOS maintainers and contributors
+# Copyright (C) 2019-2022 VyOS maintainers and contributors
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License version 2 or later as
@@ -14,14 +14,19 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
+import os
+import paramiko
import re
import os
import unittest
+from pwd import getpwall
+
from base_vyostest_shim import VyOSUnitTestSHIM
from vyos.configsession import ConfigSessionError
from vyos.util import cmd
+from vyos.util import is_systemd_service_running
from vyos.util import process_named_running
from vyos.util import read_file
@@ -42,10 +47,18 @@ class TestServiceSSH(VyOSUnitTestSHIM.TestCase):
self.cli_delete(base_path)
def tearDown(self):
+ # Check for running process
+ self.assertTrue(process_named_running(PROCESS_NAME))
+
# delete testing SSH config
self.cli_delete(base_path)
self.cli_commit()
+ # Established SSH connections remains running after service is stopped.
+ # We can not use process_named_running here - we rather need to check
+ # that the systemd service is no longer running
+ self.assertFalse(is_systemd_service_running(PROCESS_NAME))
+
def test_ssh_default(self):
# Check if SSH service runs with default settings - used for checking
# behavior of <defaultValue> in XML definition
@@ -58,9 +71,6 @@ class TestServiceSSH(VyOSUnitTestSHIM.TestCase):
port = get_config_value('Port')[0]
self.assertEqual('22', port)
- # Check for running process
- self.assertTrue(process_named_running(PROCESS_NAME))
-
def test_ssh_single_listen_address(self):
# Check if SSH service can be configured and runs
self.cli_set(base_path + ['port', '1234'])
@@ -97,9 +107,6 @@ class TestServiceSSH(VyOSUnitTestSHIM.TestCase):
keepalive = get_config_value('ClientAliveInterval')[0]
self.assertTrue("100" in keepalive)
- # Check for running process
- self.assertTrue(process_named_running(PROCESS_NAME))
-
def test_ssh_multiple_listen_addresses(self):
# Check if SSH service can be configured and runs with multiple
# listen ports and listen-addresses
@@ -124,9 +131,6 @@ class TestServiceSSH(VyOSUnitTestSHIM.TestCase):
for address in addresses:
self.assertIn(address, tmp)
- # Check for running process
- self.assertTrue(process_named_running(PROCESS_NAME))
-
def test_ssh_vrf(self):
# Check if SSH service can be bound to given VRF
port = '22'
@@ -146,9 +150,6 @@ class TestServiceSSH(VyOSUnitTestSHIM.TestCase):
tmp = get_config_value('Port')
self.assertIn(port, tmp)
- # Check for running process
- self.assertTrue(process_named_running(PROCESS_NAME))
-
# Check for process in VRF
tmp = cmd(f'ip vrf pids {vrf}')
self.assertIn(PROCESS_NAME, tmp)
@@ -156,5 +157,51 @@ class TestServiceSSH(VyOSUnitTestSHIM.TestCase):
# delete VRF
self.cli_delete(['vrf', 'name', vrf])
+ def test_ssh_login(self):
+ # Perform SSH login and command execution with a predefined user. The
+ # result (output of uname -a) must match the output if the command is
+ # run natively.
+ #
+ # We also try to login as an invalid user - this is not allowed to work.
+
+ def ssh_send_cmd(command, username, password, host='localhost'):
+ """ SSH command execution helper """
+ # Try to login via SSH
+ ssh_client = paramiko.SSHClient()
+ ssh_client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
+ ssh_client.connect(hostname='localhost', username=username, password=password)
+ _, stdout, stderr = ssh_client.exec_command(command)
+ output = stdout.read().decode().strip()
+ error = stderr.read().decode().strip()
+ ssh_client.close()
+ return output, error
+
+ test_user = 'ssh_test'
+ test_pass = 'v2i57DZs8idUwMN3VC92'
+ test_command = 'uname -a'
+
+ self.cli_set(base_path)
+ self.cli_set(['system', 'login', 'user', test_user, 'authentication', 'plaintext-password', test_pass])
+
+ # commit changes
+ self.cli_commit()
+
+ # Login with proper credentials
+ output, error = ssh_send_cmd(test_command, test_user, test_pass)
+ # verify login
+ self.assertFalse(error)
+ self.assertEqual(output, cmd(test_command))
+
+ # Login with invalid credentials
+ with self.assertRaises(paramiko.ssh_exception.AuthenticationException):
+ output, error = ssh_send_cmd(test_command, 'invalid_user', 'invalid_password')
+
+ self.cli_delete(['system', 'login', 'user', test_user])
+ self.cli_commit()
+
+ # After deletion the test user is not allowed to remain in /etc/passwd
+ usernames = [x[0] for x in getpwall()]
+ self.assertNotIn(test_user, usernames)
+
if __name__ == '__main__':
unittest.main(verbosity=2)
diff --git a/smoketest/scripts/cli/test_system_ipv6.py b/smoketest/scripts/cli/test_system_ipv6.py
index 3112d2e46..837d1dc12 100755
--- a/smoketest/scripts/cli/test_system_ipv6.py
+++ b/smoketest/scripts/cli/test_system_ipv6.py
@@ -17,7 +17,12 @@
import unittest
from base_vyostest_shim import VyOSUnitTestSHIM
+
+from vyos.template import is_ipv4
from vyos.util import read_file
+from vyos.util import is_ipv6_enabled
+from vyos.util import get_interface_config
+from vyos.validate import is_intf_addr_assigned
base_path = ['system', 'ipv6']
@@ -42,6 +47,14 @@ class TestSystemIPv6(VyOSUnitTestSHIM.TestCase):
self.assertEqual(read_file(file_forwarding), '0')
def test_system_ipv6_disable(self):
+ # Verify previous "enable" state
+ self.assertEqual(read_file(file_disable), '0')
+ self.assertTrue(is_ipv6_enabled())
+
+ loopbacks = ['127.0.0.1', '::1']
+ for addr in loopbacks:
+ self.assertTrue(is_intf_addr_assigned('lo', addr))
+
# Do not assign any IPv6 address on interfaces, this requires a reboot
# which can not be tested, but we can read the config file :)
self.cli_set(base_path + ['disable'])
@@ -49,6 +62,24 @@ class TestSystemIPv6(VyOSUnitTestSHIM.TestCase):
# Verify configuration file
self.assertEqual(read_file(file_disable), '1')
+ self.assertFalse(is_ipv6_enabled())
+
+ for addr in loopbacks:
+ if is_ipv4(addr):
+ self.assertTrue(is_intf_addr_assigned('lo', addr))
+ else:
+ self.assertFalse(is_intf_addr_assigned('lo', addr))
+
+ # T4330: Verify MTU can be changed with IPv6 disabled
+ mtu = '1600'
+ eth_if = 'eth0'
+ self.cli_set(['interfaces', 'ethernet', eth_if, 'mtu', mtu])
+ self.cli_commit()
+
+ tmp = get_interface_config(eth_if)
+ self.assertEqual(tmp['mtu'], int(mtu))
+
+ self.cli_delete(['interfaces', 'ethernet', eth_if, 'mtu'])
def test_system_ipv6_strict_dad(self):
# This defaults to 1
diff --git a/smoketest/scripts/cli/test_system_login.py b/smoketest/scripts/cli/test_system_login.py
index 69a06eeac..bc76de0ad 100755
--- a/smoketest/scripts/cli/test_system_login.py
+++ b/smoketest/scripts/cli/test_system_login.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2019-2020 VyOS maintainers and contributors
+# Copyright (C) 2019-2022 VyOS maintainers and contributors
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License version 2 or later as
@@ -23,6 +23,7 @@ from base_vyostest_shim import VyOSUnitTestSHIM
from distutils.version import LooseVersion
from platform import release as kernel_version
from subprocess import Popen, PIPE
+from pwd import getpwall
from vyos.configsession import ConfigSessionError
from vyos.util import cmd
@@ -52,6 +53,11 @@ class TestSystemLogin(VyOSUnitTestSHIM.TestCase):
self.cli_commit()
+ # After deletion, a user is not allowed to remain in /etc/passwd
+ usernames = [x[0] for x in getpwall()]
+ for user in users:
+ self.assertNotIn(user, usernames)
+
def test_add_linux_system_user(self):
# We are not allowed to re-use a username already taken by the Linux
# base system
diff --git a/smoketest/scripts/cli/test_vrf.py b/smoketest/scripts/cli/test_vrf.py
index 5a44fdc66..6614aeb06 100755
--- a/smoketest/scripts/cli/test_vrf.py
+++ b/smoketest/scripts/cli/test_vrf.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2020-2021 VyOS maintainers and contributors
+# Copyright (C) 2020-2022 VyOS maintainers and contributors
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License version 2 or later as
@@ -27,6 +27,7 @@ from vyos.configsession import ConfigSessionError
from vyos.ifconfig import Interface
from vyos.ifconfig import Section
from vyos.template import is_ipv6
+from vyos.template import is_ipv4
from vyos.util import cmd
from vyos.util import read_file
from vyos.util import get_interface_config
@@ -62,6 +63,8 @@ class VRFTest(VyOSUnitTestSHIM.TestCase):
def tearDown(self):
# delete all VRFs
self.cli_delete(base_path)
+ self.cli_delete(['interfaces', 'dummy'])
+ self.cli_delete(['protocols', 'vrf'])
self.cli_commit()
for vrf in vrfs:
self.assertNotIn(vrf, interfaces())
@@ -109,9 +112,14 @@ class VRFTest(VyOSUnitTestSHIM.TestCase):
# ...
regex = f'{table}\s+{vrf}\s+#\s+{description}'
self.assertTrue(re.findall(regex, iproute2_config))
+
+ tmp = get_interface_config(vrf)
+ self.assertEqual(int(table), tmp['linkinfo']['info_data']['table'])
+
+ # Increment table ID for the next run
table = str(int(table) + 1)
- def test_vrf_loopback_ips(self):
+ def test_vrf_loopbacks_ips(self):
table = '2000'
for vrf in vrfs:
base = base_path + ['name', vrf]
@@ -122,10 +130,48 @@ class VRFTest(VyOSUnitTestSHIM.TestCase):
self.cli_commit()
# Verify VRF configuration
+ loopbacks = ['127.0.0.1', '::1']
for vrf in vrfs:
- self.assertTrue(vrf in interfaces())
- self.assertTrue(is_intf_addr_assigned(vrf, '127.0.0.1'))
- self.assertTrue(is_intf_addr_assigned(vrf, '::1'))
+ # Ensure VRF was created
+ self.assertIn(vrf, interfaces())
+ # Test for proper loopback IP assignment
+ for addr in loopbacks:
+ self.assertTrue(is_intf_addr_assigned(vrf, addr))
+
+ def test_vrf_loopbacks_no_ipv6(self):
+ table = '2002'
+ for vrf in vrfs:
+ base = base_path + ['name', vrf]
+ self.cli_set(base + ['table', str(table)])
+ table = str(int(table) + 1)
+
+ # Globally disable IPv6 - this will remove all IPv6 interface addresses
+ self.cli_set(['system', 'ipv6', 'disable'])
+
+ # commit changes
+ self.cli_commit()
+
+ # Verify VRF configuration
+ table = '2002'
+ loopbacks = ['127.0.0.1', '::1']
+ for vrf in vrfs:
+ # Ensure VRF was created
+ self.assertIn(vrf, interfaces())
+
+ # Verify VRF table ID
+ tmp = get_interface_config(vrf)
+ self.assertEqual(int(table), tmp['linkinfo']['info_data']['table'])
+
+ # Test for proper loopback IP assignment
+ for addr in loopbacks:
+ if is_ipv4(addr):
+ self.assertTrue(is_intf_addr_assigned(vrf, addr))
+ else:
+ self.assertFalse(is_intf_addr_assigned(vrf, addr))
+
+ table = str(int(table) + 1)
+
+ self.cli_delete(['system', 'ipv6'])
def test_vrf_bind_all(self):
table = '2000'
@@ -205,14 +251,14 @@ class VRFTest(VyOSUnitTestSHIM.TestCase):
},
}
+ # required interface for leaking to default table
+ self.cli_set(['interfaces', 'ethernet', 'eth0', 'address', '192.0.2.1/24'])
+
table = '2000'
for vrf in vrfs:
base = base_path + ['name', vrf]
self.cli_set(base + ['table', str(table)])
- # required interface for leaking to default table
- self.cli_set(['interfaces', 'ethernet', 'eth0', 'address', '192.0.2.1/24'])
-
# we also need an interface in "UP" state to install routes
self.cli_set(['interfaces', 'dummy', f'dum{table}', 'vrf', vrf])
self.cli_set(['interfaces', 'dummy', f'dum{table}', 'address', '192.0.2.1/24'])
@@ -234,28 +280,24 @@ class VRFTest(VyOSUnitTestSHIM.TestCase):
self.cli_commit()
# Verify routes
- table = '2000'
for vrf in vrfs:
- for route, route_config in routes.items():
- if is_ipv6(route):
- tmp = get_vrf_ipv6_routes(vrf)
- else:
- tmp = get_vrf_ipv4_routes(vrf)
+ self.assertIn(vrf, interfaces())
+ frrconfig = self.getFRRconfig(f'vrf {vrf}')
+ for prefix, prefix_config in routes.items():
+ tmp = 'ip'
+ if is_ipv6(prefix):
+ tmp += 'v6'
- found = False
- for result in tmp:
- if 'dst' in result and result['dst'] == route:
- if 'gateway' in result and result['gateway'] == route_config['next_hop']:
- found = True
+ tmp += f' route {prefix} {prefix_config["next_hop"]}'
+ if 'distance' in prefix_config:
+ tmp += ' ' + prefix_config['distance']
+ if 'next_hop_vrf' in prefix_config:
+ tmp += ' nexthop-vrf ' + prefix_config['next_hop_vrf']
- self.assertTrue(found)
+ self.assertIn(tmp, frrconfig)
- # Cleanup
- self.cli_delete(['protocols', 'vrf', vrf])
- self.cli_delete(['interfaces', 'dummy', f'dum{table}'])
- self.cli_delete(['interfaces', 'ethernet', 'eth0', 'address', '192.0.2.1/24'])
+ self.cli_delete(['interfaces', 'ethernet', 'eth0', 'address'])
- table = str(int(table) + 1)
def test_vrf_link_local_ip_addresses(self):
# Testcase for issue T4331
diff --git a/src/conf_mode/dns_forwarding.py b/src/conf_mode/dns_forwarding.py
index 06366362a..bc3821f61 100755
--- a/src/conf_mode/dns_forwarding.py
+++ b/src/conf_mode/dns_forwarding.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2018-2020 VyOS maintainers and contributors
+# Copyright (C) 2018-2022 VyOS maintainers and contributors
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License version 2 or later as
@@ -16,6 +16,7 @@
import os
+from netifaces import interfaces
from sys import exit
from vyos.config import Config
@@ -62,10 +63,6 @@ def get_config(config=None):
if conf.exists(base_nameservers):
dns.update({'system_name_server': conf.return_values(base_nameservers)})
- base_nameservers_dhcp = ['system', 'name-servers-dhcp']
- if conf.exists(base_nameservers_dhcp):
- dns.update({'system_name_server_dhcp': conf.return_values(base_nameservers_dhcp)})
-
return dns
def verify(dns):
@@ -87,9 +84,8 @@ def verify(dns):
raise ConfigError(f'No server configured for domain {domain}!')
if 'system' in dns:
- if not ('system_name_server' in dns or 'system_name_server_dhcp' in dns):
- print("Warning: No 'system name-server' or 'system " \
- "name-servers-dhcp' configured")
+ if not 'system_name_server' in dns:
+ print('Warning: No "system name-server" configured')
return None
@@ -142,10 +138,15 @@ def apply(dns):
hc.delete_name_server_tags_recursor(['system'])
# add dhcp nameserver tags for configured interfaces
- if 'system_name_server_dhcp' in dns:
- for interface in dns['system_name_server_dhcp']:
- hc.add_name_server_tags_recursor(['dhcp-' + interface,
- 'dhcpv6-' + interface ])
+ if 'system_name_server' in dns:
+ for interface in dns['system_name_server']:
+ # system_name_server key contains both IP addresses and interface
+ # names (DHCP) to use DNS servers. We need to check if the
+ # value is an interface name - only if this is the case, add the
+ # interface based DNS forwarder.
+ if interface in interfaces():
+ hc.add_name_server_tags_recursor(['dhcp-' + interface,
+ 'dhcpv6-' + interface ])
# hostsd will generate the forward-zones file
# the list and keys() are required as get returns a dict, not list
diff --git a/src/conf_mode/system-ipv6.py b/src/conf_mode/system-ipv6.py
index 7fb2dd1cf..e6bcc12ad 100755
--- a/src/conf_mode/system-ipv6.py
+++ b/src/conf_mode/system-ipv6.py
@@ -17,6 +17,7 @@
import os
from sys import exit
+from vyos.base import DeprecationWarning
from vyos.config import Config
from vyos.configdict import dict_merge
from vyos.configdict import leaf_node_changed
@@ -49,6 +50,9 @@ def get_config(config=None):
return opt
def verify(opt):
+ if 'disable' in opt:
+ DeprecationWarning('VyOS 1.4 (sagitta) will remove the CLI command to '\
+ 'disable IPv6 address family in the Linux Kernel!')
pass
def generate(opt):
diff --git a/src/conf_mode/system-login.py b/src/conf_mode/system-login.py
index 8aa43dd32..aba10689d 100755
--- a/src/conf_mode/system-login.py
+++ b/src/conf_mode/system-login.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2020-2021 VyOS maintainers and contributors
+# Copyright (C) 2020-2022 VyOS maintainers and contributors
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License version 2 or later as
@@ -23,6 +23,7 @@ from pwd import getpwall
from pwd import getpwnam
from spwd import getspnam
from sys import exit
+from time import sleep
from vyos.config import Config
from vyos.configdict import dict_merge
@@ -31,6 +32,7 @@ from vyos.template import render
from vyos.template import is_ipv4
from vyos.util import cmd
from vyos.util import call
+from vyos.util import run
from vyos.util import DEVNULL
from vyos.util import dict_search
from vyos.xml import defaults
@@ -256,13 +258,22 @@ def apply(login):
if 'rm_users' in login:
for user in login['rm_users']:
try:
+ # Disable user to prevent re-login
+ call(f'usermod -s /sbin/nologin {user}')
+
# Logout user if he is still logged in
if user in list(set([tmp[0] for tmp in users()])):
print(f'{user} is logged in, forcing logout!')
- call(f'pkill -HUP -u {user}')
-
- # Remove user account but leave home directory to be safe
- call(f'userdel -r {user}', stderr=DEVNULL)
+ # re-run command until user is logged out
+ while run(f'pkill -HUP -u {user}'):
+ sleep(0.250)
+
+ # Remove user account but leave home directory in place. Re-run
+ # command until user is removed - userdel might return 8 as
+ # SSH sessions are not all yet properly cleaned away, thus we
+ # simply re-run the command until the account wen't away
+ while run(f'userdel --remove {user}', stderr=DEVNULL):
+ sleep(0.250)
except Exception as e:
raise ConfigError(f'Deleting user "{user}" raised exception: {e}')
diff --git a/src/conf_mode/vrf.py b/src/conf_mode/vrf.py
index dd1739087..fb2182fff 100755
--- a/src/conf_mode/vrf.py
+++ b/src/conf_mode/vrf.py
@@ -27,6 +27,7 @@ from vyos.util import call
from vyos.util import cmd
from vyos.util import dict_search
from vyos.util import sysctl_write
+from vyos.util import is_ipv6_enabled
from vyos import ConfigError
from vyos import airbag
airbag.enable()
@@ -194,10 +195,11 @@ def apply(vrf):
# set VRF description for e.g. SNMP monitoring
vrf_if = Interface(name)
- # We also should add proper loopback IP addresses to the newly
- # created VRFs for services bound to the loopback address (SNMP, NTP)
+ # We also should add proper loopback IP addresses to the newly added
+ # VRF for services bound to the loopback address (SNMP, NTP)
vrf_if.add_addr('127.0.0.1/8')
- vrf_if.add_addr('::1/128')
+ if is_ipv6_enabled():
+ vrf_if.add_addr('::1/128')
# add VRF description if available
vrf_if.set_alias(config.get('description', ''))
diff --git a/src/tests/test_util.py b/src/tests/test_util.py
index 22bc085c5..91890262c 100644
--- a/src/tests/test_util.py
+++ b/src/tests/test_util.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2020 VyOS maintainers and contributors
+# Copyright (C) 2020-2022 VyOS maintainers and contributors
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License version 2 or later as
@@ -15,7 +15,7 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from unittest import TestCase
-from vyos.util import mangle_dict_keys
+from vyos.util import *
class TestVyOSUtil(TestCase):
def test_key_mangline(self):
@@ -24,3 +24,15 @@ class TestVyOSUtil(TestCase):
new_data = mangle_dict_keys(data, '-', '_')
self.assertEqual(new_data, expected_data)
+ def test_sysctl_read(self):
+ self.assertEqual(sysctl_read('net.ipv4.conf.lo.forwarding'), '1')
+
+ def test_ipv6_enabled(self):
+ tmp = sysctl_read('net.ipv6.conf.all.disable_ipv6')
+ # We need to test for both variants as this depends on how the
+ # Docker container is started (with or without IPv6 support) - so we
+ # will simply check both cases to not make the users life miserable.
+ if tmp == '0':
+ self.assertTrue(is_ipv6_enabled())
+ else:
+ self.assertFalse(is_ipv6_enabled())