diff --git a/interface-definitions/protocols-static-arp.xml.in b/interface-definitions/protocols-static-arp.xml.in index e5e8a9ad9..8b1b3b5e1 100644 --- a/interface-definitions/protocols-static-arp.xml.in +++ b/interface-definitions/protocols-static-arp.xml.in @@ -1,37 +1,51 @@ <?xml version="1.0"?> <interfaceDefinition> <node name="protocols"> <children> <node name="static"> <children> - <tagNode name="arp" owner="${vyos_conf_scripts_dir}/arp.py"> + <node name="arp" owner="${vyos_conf_scripts_dir}/arp.py"> <properties> <help>Static ARP translation</help> - <valueHelp> - <format>ipv4</format> - <description>IPv4 destination address</description> - </valueHelp> - <constraint> - <validator name="ipv4-address"/> - </constraint> </properties> <children> - <leafNode name="hwaddr"> + <tagNode name="interface"> <properties> - <help>Translation MAC address</help> + <help>Interface configuration</help> + <completionHelp> + <script>${vyos_completion_dir}/list_interfaces.py</script> + </completionHelp> <valueHelp> - <format>macaddr</format> - <description>Hardware (MAC) address</description> + <format>txt</format> + <description>Interface name</description> </valueHelp> <constraint> - <validator name="mac-address"/> + <validator name="interface-name"/> </constraint> </properties> - </leafNode> + <children> + <tagNode name="address"> + <properties> + <help>IP address for static ARP entry</help> + <valueHelp> + <format>ipv4</format> + <description>IPv4 destination address</description> + </valueHelp> + <constraint> + <validator name="ipv4-address"/> + </constraint> + </properties> + <children> + #include <include/generic-description.xml.i> + #include <include/interface/mac.xml.i> + </children> + </tagNode> + </children> + </tagNode> </children> - </tagNode> + </node> </children> </node> </children> </node> </interfaceDefinition> diff --git a/smoketest/configs/basic-vyos b/smoketest/configs/basic-vyos index 3d62d269c..e6f89954f 100644 --- a/smoketest/configs/basic-vyos +++ b/smoketest/configs/basic-vyos @@ -1,112 +1,140 @@ interfaces { ethernet eth0 { address 192.168.0.1/24 duplex auto smp-affinity auto speed auto } ethernet eth1 { - address 100.64.0.0/31 duplex auto smp-affinity auto speed auto } ethernet eth2 { - address 100.100.0.1/24 duplex auto smp-affinity auto speed auto + vif 100 { + address 100.100.0.1/24 + } + vif-s 200 { + address 100.64.200.254/24 + vif-c 201 { + address 100.64.201.254/24 + } + vif-c 202 { + address 100.64.202.254/24 + } + } } loopback lo { } } protocols { static { arp 192.168.0.20 { hwaddr 00:50:00:00:00:20 } arp 192.168.0.30 { hwaddr 00:50:00:00:00:30 } arp 192.168.0.40 { hwaddr 00:50:00:00:00:40 } arp 100.100.0.2 { hwaddr 00:50:00:00:02:02 } arp 100.100.0.3 { hwaddr 00:50:00:00:02:03 } arp 100.100.0.4 { hwaddr 00:50:00:00:02:04 } + arp 100.64.200.1 { + hwaddr 00:50:00:00:00:01 + } + arp 100.64.200.2 { + hwaddr 00:50:00:00:00:02 + } + arp 100.64.201.10 { + hwaddr 00:50:00:00:00:10 + } + arp 100.64.201.20 { + hwaddr 00:50:00:00:00:20 + } + arp 100.64.202.30 { + hwaddr 00:50:00:00:00:30 + } + arp 100.64.202.40 { + hwaddr 00:50:00:00:00:40 + } route 0.0.0.0/0 { next-hop 100.64.0.1 { } } } } service { dhcp-server { shared-network-name LAN { authoritative subnet 192.168.0.0/24 { default-router 192.168.0.1 dns-server 192.168.0.1 domain-name vyos.net domain-search vyos.net range LANDynamic { start 192.168.0.20 stop 192.168.0.240 } } } } dns { forwarding { allow-from 192.168.0.0/16 cache-size 10000 dnssec off listen-address 192.168.0.1 } } ssh { ciphers aes128-ctr,aes192-ctr,aes256-ctr ciphers chacha20-poly1305@openssh.com,rijndael-cbc@lysator.liu.se listen-address 192.168.0.1 key-exchange curve25519-sha256@libssh.org key-exchange diffie-hellman-group1-sha1,diffie-hellman-group-exchange-sha1,diffie-hellman-group-exchange-sha256 port 22 } } system { config-management { commit-revisions 100 } console { device ttyS0 { speed 115200 } } host-name vyos login { user vyos { authentication { encrypted-password $6$O5gJRlDYQpj$MtrCV9lxMnZPMbcxlU7.FI793MImNHznxGoMFgm3Q6QP3vfKJyOSRCt3Ka/GzFQyW1yZS4NS616NLHaIPPFHc0 plaintext-password "" } } } name-server 192.168.0.1 syslog { global { facility all { level info } } } time-zone Europe/Berlin } /* Warning: Do not remove the following line. */ /* === vyatta-config-version: "broadcast-relay@1:cluster@1:config-management@1:conntrack-sync@1:conntrack@1:dhcp-relay@2:dhcp-server@5:dns-forwarding@1:firewall@5:ipsec@5:l2tp@1:mdns@1:nat@4:ntp@1:pptp@1:qos@1:quagga@6:snmp@1:ssh@1:system@9:vrrp@2:wanloadbalance@3:webgui@1:webproxy@1:webproxy@2:zone-policy@1" === */ /* Release version: 1.2.6 */ diff --git a/smoketest/scripts/cli/test_protocols_static_arp.py b/smoketest/scripts/cli/test_protocols_static_arp.py index 6663ade96..b61d8f854 100755 --- a/smoketest/scripts/cli/test_protocols_static_arp.py +++ b/smoketest/scripts/cli/test_protocols_static_arp.py @@ -1,88 +1,88 @@ #!/usr/bin/env python3 # # Copyright (C) 2022 VyOS maintainers and contributors # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 or later as # published by the Free Software Foundation. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see <http://www.gnu.org/licenses/>. import json import unittest from base_vyostest_shim import VyOSUnitTestSHIM from vyos.util import cmd base_path = ['protocols', 'static', 'arp'] interface = 'eth0' address = '192.0.2.1/24' class TestARP(VyOSUnitTestSHIM.TestCase): @classmethod def setUpClass(cls): super(TestARP, cls).setUpClass() # ensure we can also run this test on a live system - so lets clean # out the current configuration :) cls.cli_delete(cls, base_path) # we need a L2 interface with a L3 address to properly configure ARP entries cls.cli_set(cls, ['interfaces', 'ethernet', interface, 'address', address]) @classmethod def tearDownClass(cls): # cleanuop L2 interface cls.cli_delete(cls, ['interfaces', 'ethernet', interface, 'address', address]) cls.cli_commit(cls) super(TestARP, cls).tearDownClass() def tearDown(self): # delete test config self.cli_delete(base_path) self.cli_commit() def test_static_arp(self): test_data = { - '192.0.2.10' : { 'lladdr' : '00:01:02:03:04:0a' }, - '192.0.2.11' : { 'lladdr' : '00:01:02:03:04:0b' }, - '192.0.2.12' : { 'lladdr' : '00:01:02:03:04:0c' }, - '192.0.2.13' : { 'lladdr' : '00:01:02:03:04:0d' }, - '192.0.2.14' : { 'lladdr' : '00:01:02:03:04:0e' }, - '192.0.2.15' : { 'lladdr' : '00:01:02:03:04:0f' }, + '192.0.2.10' : { 'mac' : '00:01:02:03:04:0a' }, + '192.0.2.11' : { 'mac' : '00:01:02:03:04:0b' }, + '192.0.2.12' : { 'mac' : '00:01:02:03:04:0c' }, + '192.0.2.13' : { 'mac' : '00:01:02:03:04:0d' }, + '192.0.2.14' : { 'mac' : '00:01:02:03:04:0e' }, + '192.0.2.15' : { 'mac' : '00:01:02:03:04:0f' }, } for host, host_config in test_data.items(): - self.cli_set(base_path + [host, 'hwaddr', host_config['lladdr']]) + self.cli_set(base_path + ['interface', interface, 'address', host, 'mac', host_config['mac']]) self.cli_commit() arp_table = json.loads(cmd('ip -j -4 neigh show')) for host, host_config in test_data.items(): # As we search within a list of hosts we need to mark if it was # found or not. This ensures all hosts from test_data are processed found = False for entry in arp_table: # Other ARP entry - not related to this testcase if entry['dst'] not in list(test_data): continue if entry['dst'] == host: - self.assertEqual(entry['lladdr'], host_config['lladdr']) + self.assertEqual(entry['lladdr'], host_config['mac']) self.assertEqual(entry['dev'], interface) found = True if found == False: print(entry) self.assertTrue(found) if __name__ == '__main__': unittest.main(verbosity=2) diff --git a/src/conf_mode/arp.py b/src/conf_mode/arp.py index 51a08bee5..1cd8f5451 100755 --- a/src/conf_mode/arp.py +++ b/src/conf_mode/arp.py @@ -1,66 +1,74 @@ #!/usr/bin/env python3 # # Copyright (C) 2018-2022 VyOS maintainers and contributors # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 or later as # published by the Free Software Foundation. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see <http://www.gnu.org/licenses/>. from sys import exit from vyos.config import Config from vyos.configdict import node_changed from vyos.util import call from vyos import ConfigError from vyos import airbag airbag.enable() def get_config(config=None): if config: conf = config else: conf = Config() base = ['protocols', 'static', 'arp'] - arp = conf.get_config_dict(base) - tmp = node_changed(conf, base) - if tmp: arp.update({'removed' : node_changed(conf, base)}) + arp = conf.get_config_dict(base, get_first_key=True) + + if 'interface' in arp: + for interface in arp['interface']: + tmp = node_changed(conf, base + ['interface', interface, 'address'], recursive=True) + if tmp: arp['interface'][interface].update({'address_old' : tmp}) return arp def verify(arp): pass def generate(arp): pass def apply(arp): if not arp: return None - if 'removed' in arp: - for host in arp['removed']: - call(f'arp --delete {host}') + if 'interface' in arp: + for interface, interface_config in arp['interface'].items(): + # Delete old static ARP assignments first + if 'address_old' in interface_config: + for address in interface_config['address_old']: + call(f'ip neigh del {address} dev {interface}') - if 'arp' in arp: - for host, host_config in arp['arp'].items(): - mac = host_config['hwaddr'] - call(f'arp --set {host} {mac}') + # Add new static ARP entries to interface + if 'address' not in interface_config: + continue + for address, address_config in interface_config['address'].items(): + mac = address_config['mac'] + call(f'ip neigh add {address} lladdr {mac} dev {interface}') if __name__ == '__main__': try: c = get_config() verify(c) generate(c) apply(c) except ConfigError as e: print(e) exit(1) diff --git a/src/migration-scripts/system/23-to-24 b/src/migration-scripts/system/23-to-24 new file mode 100755 index 000000000..5ea71d51a --- /dev/null +++ b/src/migration-scripts/system/23-to-24 @@ -0,0 +1,85 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2022 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +import os + +from ipaddress import ip_interface +from ipaddress import ip_address +from sys import exit, argv +from vyos.configtree import ConfigTree + +if (len(argv) < 1): + print("Must specify file name!") + exit(1) + +file_name = argv[1] +with open(file_name, 'r') as f: + config_file = f.read() + +base = ['protocols', 'static', 'arp'] +tmp_base = ['protocols', 'static', 'arp-tmp'] +config = ConfigTree(config_file) + +def fixup_cli(config, path, interface): + if config.exists(path + ['address']): + for address in config.return_values(path + ['address']): + tmp = ip_interface(address) + if ip_address(host) in tmp.network.hosts(): + mac = config.return_value(tmp_base + [host, 'hwaddr']) + iface_path = ['protocols', 'static', 'arp', 'interface'] + config.set(iface_path + [interface, 'address', host, 'mac'], value=mac) + config.set_tag(iface_path) + config.set_tag(iface_path + [interface, 'address']) + continue + +if not config.exists(base): + # Nothing to do + exit(0) + +# We need a temporary copy of the config tree as the original one needs to be +# deleted first due to a change iun thge tagNode structure. +config.copy(base, tmp_base) +config.delete(base) + +for host in config.list_nodes(tmp_base): + for type in config.list_nodes(['interfaces']): + for interface in config.list_nodes(['interfaces', type]): + if_base = ['interfaces', type, interface] + fixup_cli(config, if_base, interface) + + if config.exists(if_base + ['vif']): + for vif in config.list_nodes(if_base + ['vif']): + vif_base = ['interfaces', type, interface, 'vif', vif] + fixup_cli(config, vif_base, f'{interface}.{vif}') + + if config.exists(if_base + ['vif-s']): + for vif_s in config.list_nodes(if_base + ['vif-s']): + vif_s_base = ['interfaces', type, interface, 'vif-s', vif_s] + fixup_cli(config, vif_s_base, f'{interface}.{vif_s}') + + if config.exists(if_base + ['vif-s', vif_s, 'vif-c']): + for vif_c in config.list_nodes(if_base + ['vif-s', vif_s, 'vif-c']): + vif_c_base = ['interfaces', type, interface, 'vif-s', vif_s, 'vif-c', vif_c] + fixup_cli(config, vif_c_base, f'{interface}.{vif_s}.{vif_c}') + +config.delete(tmp_base) + +try: + with open(file_name, 'w') as f: + f.write(config.to_string()) +except OSError as e: + print(f'Failed to save the modified config: {e}') + exit(1)