Page Menu
Home
VyOS Platform
Search
Configure Global Search
Log In
Files
F119063217
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Flag For Later
Award Token
Size
137 KB
Referenced Files
None
Subscribers
None
View Options
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 8458d3208..e74cf3d0e 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -1,165 +1,165 @@
# Contributing to VyOS
You wan't to help us improve VyOS? This is awesome!
We accept any kind of Pull Requests on GitHub. In order to get your changes into
the main repository as smooth as possible please take yourself some time and
review this contribution guideline.
The following paragraphs are an excerpt from our Documentation.
## Submit a Patch
Patches are always more than welcome. To have a clean and easy to maintain
repository we have some guidelines when working with Git. A clean repository
eases the automatic generation of a changelog file.
A good approach for writing commit messages is actually to have a look at the
file(s) history by invoking git log path/to/file.txt.
### Prepare patch/commit
In a big system, such as VyOS, that is comprised of multiple components, it’s
impossible to keep track of all the changes and bugs/feature requests in one’s
head. We use a bugtracker known as Phabricator for it (“issue tracker” would
be a better term, but this one stuck).
The information is used in three ways:
* Keep track of the progress (what we have already done in this branch and
what we still need to do).
* Prepare automatic release notes for upcoming releases
* Help future maintainers of VyOS (it could be you!) to find out why certain
things have been changed in the codebase or why certain features have been
added
To make this approach work, every change must be associated with a task number
(prefixed with **T**) and a component. If there is no bug report/feature
request for the changes you are going to make, you have to create a Phabricator
task first. Once there is an entry in Phabricator, you should reference its id
in your commit message, as shown below:
* `ddclient: T1030: auto create runtime directories`
* `Jenkins: add current Git commit ID to build description`
-If there is no [Phabricator](https://phabricator.vyos.net) reference in the
+If there is no [Phabricator](https://vyos.dev) reference in the
commits of your pull request, we have to ask you to amend the commit message.
Otherwise we will have to reject it.
## Writing good commit messages
The format should be and is inspired by this very good and detailed
[Git documentation](https://git-scm.com/book/ch5-2.html), it is also worth
reading https://chris.beams.io/posts/git-commit/.
This is nothing VyOS specific - it is more a general topic for distributed
development environments.
* A single, short, summary of the commit (recommended 50 characters or less,
not exceeding 80 characters) containing a prefix of the changed component
and the corresponding Phabricator reference e.g. `snmp: T1111:` or
`ethernet: T2222:` - multiple components could be concatenated as in `snmp:
ethernet: T3333`
* In some contexts, the first line is treated as the subject of an email and
the rest of the text as the body. The blank line separating the summary from
the body is critical (unless you omit the body entirely); tools like rebase
can get confused if you run the two together.
* Followed by a message which describes all the details like:
* What/why/how something has been changed, makes everyone’s life easier when
working with `git bisect`
* All text of the commit message should be wrapped at 72 characters if
possible which makes reading commit logs easier with git log on a standard
terminal (which happens to be 80x25)
* If applicable a reference to a previous commit should be made linking those
commits nicely when browsing the history: `After commit abcd12ef ("snmp:
this is a headline") a Python import statement is missing, throwing the
following exception: ABCDEF`
* Always use the `-x` option to the `git cherry-pick` command when back or
forward porting an individual commit. This automatically appends the line:
`(cherry picked from commit <ID>)` to the original authors commit message
making it easier when bisecting problems.
* Every change set must be consistent (self containing)! Do not fix multiple
bugs in a single commit. If you already worked on multiple fixes in the same
file use git add –patch to only add the parts related to the one issue into
your upcoming commit.
## Bug Report/Issue
Issues or bugs are found in any software project. VyOS is not an exception.
All issues should be reported to the developers. This lets the developers know
what is not working properly. Without this sort of feedback every developer
will believe that everything is working correctly.
### I have found a bug, what should I do?
When you believe you have found a bug, it is always a good idea to verify the
issue prior to opening a bug request.
* Consult our [Documentation](https://docs.vyos.io) to ensure that you have
configured your system correctly
* Get community support via [Slack](https://slack.vyos.io) or our online
[Forum](https://forum.vyos.io)
#### Ensure the problem is reproducible
When you are able to verify that it is actually a bug, spend some time to
document how to reproduce the issue. This documentation can be invaluable.
When you wish to have a developer fix a bug that you found, helping them
reproduce the issue is beneficial to everyone. Be sure to include information
about the hardware you are using, commands that you were running, any other
activities that you may have been doing at the time. This additional
information can be very useful.
* What were you attempting to achieve?
* What was the configuration prior to the change?
* What commands did you use? Use e.g. ``show configuration commands``
#### Include output
The output you get when you find a bug can provide lots of information. If you
get an error message on the screen, copy it exactly. Having the exact message
can provide detail that the developers can use. Like wise if you have any log
messages that also are from the time of the issue, include those. They may
also contain information that is helpful for the development team.
### Reporting
In order to open up a bug-report/feature request you need to create yourself
-an account on [Phabricator](https://phabricator.vyos.net). On the left
+an account on [Phabricator](https://vyos.dev). On the left
side of the specific project (VyOS 1.2 or VyOS 1.3) you will find quick-links
for opening a bug-report/feature request.
* Provide as much information as you can
* Which version of VyOS are you using? Use operational level command:
``show version``
* How can we reproduce this Bug? Please include a CLI configuration, you can
use ``show configuration command | strip-private`` to remove sensitive
information before publishing.
## Feature Request
You have an idea of how to make VyOS better or you are in need of a specific
feature which all users of VyOS would benefit from? To send a feature request
-please search [Phabricator](https://phabricator.vyos.net) if there is already a
+please search [Phabricator](https://vyos.dev) if there is already a
request pending. You can enhance it or if you don't find one, create a new one
by use the quick link in the left side under the specific project.
## Code Contribution
For contributing code to VyOS please take a short moment and review the guideline
outlined in our Documentation at
https://docs.vyos.io/en/latest/contributing/development.html#submit-a-patch
### Coding Guidelines
We have some small coding guidelines which are defined in a separate section of
at https://docs.vyos.io/en/latest/contributing/development.html#coding-guidelines.
The guidelines cover how to create the necessary XML structure for new features
and also how to read in the code from the CLI into the Python based scripting
backend.
Thank you for taking the time reading this guide.
It might also worth browsing our [Blog](https://blog.vyos.io) for additional
info and updates.
diff --git a/data/templates/pppoe/ip-down.script.tmpl b/data/templates/pppoe/ip-down.script.tmpl
index c1d1132b3..9db3219b3 100644
--- a/data/templates/pppoe/ip-down.script.tmpl
+++ b/data/templates/pppoe/ip-down.script.tmpl
@@ -1,38 +1,38 @@
#!/bin/sh
# As PPPoE is an "on demand" interface we need to re-configure it when it
# becomes up
if [ "$6" != "{{ ifname }}" ]; then
exit
fi
# add some info to syslog
DIALER_PID=$(cat /var/run/{{ ifname }}.pid)
logger -t pppd[$DIALER_PID] "executing $0"
{% if connect_on_demand is not defined %}
-# See https://phabricator.vyos.net/T2248. Determine if we are enslaved to a
+# See https://vyos.dev/T2248. Determine if we are enslaved to a
# VRF, this is needed to properly insert the default route.
VRF_NAME=""
if [ -d /sys/class/net/{{ ifname }}/upper_* ]; then
# Determine upper (VRF) interface
VRF=$(basename $(ls -d /sys/class/net/{{ ifname }}/upper_*))
# Remove upper_ prefix from result string
VRF=${VRF#"upper_"}
# Populate variable to run in VR context
VRF_NAME="vrf ${VRF_NAME}"
fi
{% if default_route != 'none' %}
# Always delete default route when interface goes down if we installed it
vtysh -c "conf t" ${VRF_NAME} -c "no ip route 0.0.0.0/0 {{ ifname }} ${VRF_NAME}"
{% if ipv6 is defined and ipv6.address is defined and ipv6.address.autoconf is defined %}
vtysh -c "conf t" ${VRF_NAME} -c "no ipv6 route ::/0 {{ ifname }} ${VRF_NAME}"
{% endif %}
{% endif %}
{% endif %}
{% if dhcpv6_options is defined and dhcpv6_options.pd is defined %}
# Stop wide dhcpv6 client without blocking (by default the ip-down script can only run up to 5 seconds)
systemctl stop --no-block dhcp6c@{{ ifname }}.service
{% endif %}
diff --git a/data/templates/pppoe/ip-up.script.tmpl b/data/templates/pppoe/ip-up.script.tmpl
index 302756960..7f0c234fe 100644
--- a/data/templates/pppoe/ip-up.script.tmpl
+++ b/data/templates/pppoe/ip-up.script.tmpl
@@ -1,49 +1,49 @@
#!/bin/sh
# As PPPoE is an "on demand" interface we need to re-configure it when it
# becomes up
if [ "$6" != "{{ ifname }}" ]; then
exit
fi
{% if connect_on_demand is not defined %}
# add some info to syslog
DIALER_PID=$(cat /var/run/{{ ifname }}.pid)
logger -t pppd[$DIALER_PID] "executing $0"
{% if default_route != 'none' %}
-# See https://phabricator.vyos.net/T2248 & T2220. Determine if we are enslaved
+# See https://vyos.dev/T2248 & T2220. Determine if we are enslaved
# to a VRF, this is needed to properly insert the default route.
SED_OPT="^ip route"
VRF_NAME=""
if [ -d /sys/class/net/{{ ifname }}/upper_* ]; then
# Determine upper (VRF) interface
VRF=$(basename $(ls -d /sys/class/net/{{ ifname }}/upper_*))
# Remove upper_ prefix from result string
VRF=${VRF#"upper_"}
# generate new SED command
SED_OPT="vrf ${VRF}"
# generate vtysh option
VRF_NAME="vrf ${VRF}"
fi
{% if default_route == 'auto' %}
# Only insert a new default route if there is no default route configured
routes=$(vtysh -c "show running-config" | sed -n "/${SED_OPT}/,/!/p" | grep 0.0.0.0/0 | wc -l)
if [ "$routes" -ne 0 ]; then
exit 1
fi
{% elif default_route == 'force' %}
# Retrieve current static default routes and remove it from the routing table
vtysh -c "show running-config" | sed -n "/${SED_OPT}/,/!/p" | grep 0.0.0.0/0 | while read route ; do
vtysh -c "conf t" ${VTY_OPT} -c "no ${route} ${VRF_NAME}"
done
{% endif %}
# Add default route to default or VRF routing table
vtysh -c "conf t" ${VTY_OPT} -c "ip route 0.0.0.0/0 {{ ifname }} ${VRF_NAME}"
logger -t pppd[$DIALER_PID] "added default route via {{ ifname }} ${VRF_NAME}"
{% endif %}
{% endif %}
diff --git a/data/templates/pppoe/ipv6-up.script.tmpl b/data/templates/pppoe/ipv6-up.script.tmpl
index 238f8c28e..245ef1b87 100644
--- a/data/templates/pppoe/ipv6-up.script.tmpl
+++ b/data/templates/pppoe/ipv6-up.script.tmpl
@@ -1,91 +1,91 @@
#!/bin/sh
# As PPPoE is an "on demand" interface we need to re-configure it when it
# becomes up
if [ "$6" != "{{ ifname }}" ]; then
exit
fi
{% if ipv6 is defined and ipv6.address is defined and ipv6.address.autoconf is defined %}
# add some info to syslog
DIALER_PID=$(cat /var/run/{{ ifname }}.pid)
logger -t pppd[$DIALER_PID] "executing $0"
logger -t pppd[$DIALER_PID] "configuring interface {{ ifname }} via {{ source_interface }}"
# Configure interface-specific Host/Router behaviour.
# Note: It is recommended to have the same setting on all interfaces; mixed
# router/host scenarios are rather uncommon. Possible values are:
#
# 0 Forwarding disabled
# 1 Forwarding enabled
#
echo 1 > /proc/sys/net/ipv6/conf/{{ ifname }}/forwarding
# Accept Router Advertisements; autoconfigure using them.
#
# It also determines whether or not to transmit Router
# Solicitations. If and only if the functional setting is to
# accept Router Advertisements, Router Solicitations will be
# transmitted. Possible values are:
#
# 0 Do not accept Router Advertisements.
# 1 Accept Router Advertisements if forwarding is disabled.
# 2 Overrule forwarding behaviour. Accept Router Advertisements
# even if forwarding is enabled.
#
echo 2 > /proc/sys/net/ipv6/conf/{{ ifname }}/accept_ra
{% if default_route == 'none' %}
# Prevent learning of default router from router advertisements
echo 0 > /proc/sys/net/ipv6/conf/{{ ifname }}/accept_ra_defrtr
{% else %}
# Enable learning of default router from router advertisements
echo 1 > /proc/sys/net/ipv6/conf/{{ ifname }}/accept_ra_defrtr
{% endif %}
# Autoconfigure addresses using Prefix Information in Router Advertisements.
echo 1 > /proc/sys/net/ipv6/conf/{{ ifname }}/autoconf
{% endif %}
{% if dhcpv6_options is defined and dhcpv6_options.pd is defined %}
# Start wide dhcpv6 client
systemctl restart dhcp6c@{{ ifname }}.service
{% endif %}
{% if default_route != 'none' %}
-# See https://phabricator.vyos.net/T2248 & T2220. Determine if we are enslaved
+# See https://vyos.dev/T2248 & T2220. Determine if we are enslaved
# to a VRF, this is needed to properly insert the default route.
SED_OPT="^ipv6 route"
VRF_NAME=""
if [ -d /sys/class/net/{{ ifname }}/upper_* ]; then
# Determine upper (VRF) interface
VRF=$(basename $(ls -d /sys/class/net/{{ ifname }}/upper_*))
# Remove upper_ prefix from result string
VRF=${VRF#"upper_"}
# generate new SED command
SED_OPT="vrf ${VRF}"
# generate vtysh option
VRF_NAME="vrf ${VRF}"
fi
{% if default_route == 'auto' %}
# Only insert a new default route if there is no default route configured
routes=$(vtysh -c "show running-config" | sed -n "/${SED_OPT}/,/!/p" | grep ::/0 | wc -l)
if [ "$routes" -ne 0 ]; then
exit 1
fi
{% elif default_route == 'force' %}
# Retrieve current static default routes and remove it from the routing table
vtysh -c "show running-config" | sed -n "/${SED_OPT}/,/!/p" | grep ::/0 | while read route ; do
vtysh -c "conf t" ${VTY_OPT} -c "no ${route} ${VRF_NAME}"
done
{% endif %}
# Add default route to default or VRF routing table
vtysh -c "conf t" ${VTY_OPT} -c "ipv6 route ::/0 {{ ifname }} ${VRF_NAME}"
logger -t pppd[$DIALER_PID] "added default route via {{ ifname }} ${VRF_NAME}"
{% endif %}
diff --git a/python/vyos/ifconfig/loopback.py b/python/vyos/ifconfig/loopback.py
index 30c890fdf..1b8161ce5 100644
--- a/python/vyos/ifconfig/loopback.py
+++ b/python/vyos/ifconfig/loopback.py
@@ -1,72 +1,72 @@
# Copyright 2019-2022 VyOS maintainers and contributors <maintainers@vyos.io>
#
# This library is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public
# License as published by the Free Software Foundation; either
# version 2.1 of the License, or (at your option) any later version.
#
# This library is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public
# License along with this library. If not, see <http://www.gnu.org/licenses/>.
from vyos.ifconfig.interface import Interface
from vyos.util import is_ipv6_enabled
@Interface.register
class LoopbackIf(Interface):
"""
The loopback device is a special, virtual network interface that your router
uses to communicate with itself.
"""
_persistent_addresses = ['127.0.0.1/8', '::1/128']
iftype = 'loopback'
definition = {
**Interface.definition,
**{
'section': 'loopback',
'prefixes': ['lo', ],
'bridgeable': True,
}
}
def remove(self):
"""
Loopback interface can not be deleted from operating system. We can
only remove all assigned IP addresses.
Example:
>>> from vyos.ifconfig import Interface
>>> i = LoopbackIf('lo').remove()
"""
# remove all assigned IP addresses from interface
for addr in self.get_addr():
if addr in self._persistent_addresses:
# Do not allow deletion of the default loopback addresses as
# this will cause weird system behavior like snmp/ssh no longer
- # operating as expected, see https://phabricator.vyos.net/T2034.
+ # operating as expected, see https://vyos.dev/T2034.
continue
self.del_addr(addr)
def update(self, config):
""" General helper function which works on a dictionary retrived by
get_config_dict(). It's main intention is to consolidate the scattered
interface setup code and provide a single point of entry when workin
on any interface. """
addr = config.get('address', [])
# 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})
# call base class
super().update(config)
diff --git a/smoketest/scripts/cli/base_interfaces_test.py b/smoketest/scripts/cli/base_interfaces_test.py
index b3492b074..856471c94 100644
--- a/smoketest/scripts/cli/base_interfaces_test.py
+++ b/smoketest/scripts/cli/base_interfaces_test.py
@@ -1,727 +1,727 @@
# 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
# published by the Free Software Foundation.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import os
import unittest
from binascii import hexlify
from netifaces import AF_INET
from netifaces import AF_INET6
from netifaces import ifaddresses
from netifaces import interfaces
from base_vyostest_shim import VyOSUnitTestSHIM
from vyos.configsession import ConfigSession
from vyos.configsession import ConfigSessionError
from vyos.ifconfig import Interface
from vyos.ifconfig import Section
from vyos.util import read_file
from vyos.util import cmd
from vyos.util import dict_search
from vyos.util import process_named_running
from vyos.util import get_interface_config
from vyos.validate import is_intf_addr_assigned
from vyos.validate import is_ipv6_link_local
def is_mirrored_to(interface, mirror_if, qdisc):
"""
Ask TC if we are mirroring traffic to a discrete interface.
interface: source interface
mirror_if: destination where we mirror our data to
qdisc: must be ffff or 1 for ingress/egress
"""
if qdisc not in ['ffff', '1']:
raise ValueError()
ret_val = False
tmp = cmd(f'tc -s -p filter ls dev {interface} parent {qdisc}: | grep mirred')
tmp = tmp.lower()
if mirror_if in tmp:
ret_val = True
return ret_val
class BasicInterfaceTest:
class TestCase(VyOSUnitTestSHIM.TestCase):
_test_dhcp = False
_test_ip = False
_test_mtu = False
_test_vlan = False
_test_qinq = False
_test_ipv6 = False
_test_ipv6_pd = False
_test_ipv6_dhcpc6 = False
_test_mirror = False
_base_path = []
_options = {}
_interfaces = []
_qinq_range = ['10', '20', '30']
_vlan_range = ['100', '200', '300', '2000']
_test_addr = ['192.0.2.1/26', '192.0.2.255/31', '192.0.2.64/32',
'2001:db8:1::ffff/64', '2001:db8:101::1/128']
_mirror_interfaces = []
# choose IPv6 minimum MTU value for tests - this must always work
_mtu = '1280'
def setUp(self):
# Setup mirror interfaces for SPAN (Switch Port Analyzer)
for span in self._mirror_interfaces:
section = Section.section(span)
self.cli_set(['interfaces', section, span])
def tearDown(self):
# Tear down mirror interfaces for SPAN (Switch Port Analyzer)
for span in self._mirror_interfaces:
section = Section.section(span)
self.cli_delete(['interfaces', section, span])
self.cli_delete(self._base_path)
self.cli_commit()
# Verify that no previously interface remained on the system
for intf in self._interfaces:
self.assertNotIn(intf, interfaces())
# No daemon that was started during a test should remain running
for daemon in ['dhcp6c', 'dhclient']:
self.assertFalse(process_named_running(daemon))
def test_dhcp_disable_interface(self):
if not self._test_dhcp:
self.skipTest('not supported')
# When interface is configured as admin down, it must be admin down
# even when dhcpc starts on the given interface
for interface in self._interfaces:
self.cli_set(self._base_path + [interface, 'disable'])
for option in self._options.get(interface, []):
self.cli_set(self._base_path + [interface] + option.split())
self.cli_set(self._base_path + [interface, 'disable'])
# Also enable DHCP (ISC DHCP always places interface in admin up
# state so we check that we do not start DHCP client.
- # https://phabricator.vyos.net/T2767
+ # https://vyos.dev/T2767
self.cli_set(self._base_path + [interface, 'address', 'dhcp'])
self.cli_commit()
# Validate interface state
for interface in self._interfaces:
flags = read_file(f'/sys/class/net/{interface}/flags')
self.assertEqual(int(flags, 16) & 1, 0)
def test_span_mirror(self):
if not self._mirror_interfaces:
self.skipTest('not supported')
# Check the two-way mirror rules of ingress and egress
for mirror in self._mirror_interfaces:
for interface in self._interfaces:
self.cli_set(self._base_path + [interface, 'mirror', 'ingress', mirror])
self.cli_set(self._base_path + [interface, 'mirror', 'egress', mirror])
self.cli_commit()
# Verify config
for mirror in self._mirror_interfaces:
for interface in self._interfaces:
self.assertTrue(is_mirrored_to(interface, mirror, 'ffff'))
self.assertTrue(is_mirrored_to(interface, mirror, '1'))
def test_interface_disable(self):
# Check if description can be added to interface and
# can be read back
for intf in self._interfaces:
self.cli_set(self._base_path + [intf, 'disable'])
for option in self._options.get(intf, []):
self.cli_set(self._base_path + [intf] + option.split())
self.cli_commit()
# Validate interface description
for intf in self._interfaces:
self.assertEqual(Interface(intf).get_admin_state(), 'down')
def test_interface_description(self):
# Check if description can be added to interface and
# can be read back
for intf in self._interfaces:
test_string=f'Description-Test-{intf}'
self.cli_set(self._base_path + [intf, 'description', test_string])
for option in self._options.get(intf, []):
self.cli_set(self._base_path + [intf] + option.split())
self.cli_commit()
# Validate interface description
for intf in self._interfaces:
test_string=f'Description-Test-{intf}'
tmp = read_file(f'/sys/class/net/{intf}/ifalias')
self.assertEqual(tmp, test_string)
self.assertEqual(Interface(intf).get_alias(), test_string)
self.cli_delete(self._base_path + [intf, 'description'])
self.cli_commit()
# Validate remove interface description "empty"
for intf in self._interfaces:
tmp = read_file(f'/sys/class/net/{intf}/ifalias')
self.assertEqual(tmp, str())
self.assertEqual(Interface(intf).get_alias(), str())
def test_add_single_ip_address(self):
addr = '192.0.2.0/31'
for intf in self._interfaces:
self.cli_set(self._base_path + [intf, 'address', addr])
for option in self._options.get(intf, []):
self.cli_set(self._base_path + [intf] + option.split())
self.cli_commit()
for intf in self._interfaces:
self.assertTrue(is_intf_addr_assigned(intf, addr))
self.assertEqual(Interface(intf).get_admin_state(), 'up')
def test_add_multiple_ip_addresses(self):
# Add address
for intf in self._interfaces:
for option in self._options.get(intf, []):
self.cli_set(self._base_path + [intf] + option.split())
for addr in self._test_addr:
self.cli_set(self._base_path + [intf, 'address', addr])
self.cli_commit()
# Validate address
for intf in self._interfaces:
for af in AF_INET, AF_INET6:
for addr in ifaddresses(intf)[af]:
# checking link local addresses makes no sense
if is_ipv6_link_local(addr['addr']):
continue
self.assertTrue(is_intf_addr_assigned(intf, addr['addr']))
def test_ipv6_link_local_address(self):
# Common function for IPv6 link-local address assignemnts
if not self._test_ipv6:
self.skipTest('not supported')
for interface in self._interfaces:
base = self._base_path + [interface]
for option in self._options.get(interface, []):
self.cli_set(base + option.split())
# after commit we must have an IPv6 link-local address
self.cli_commit()
for interface in self._interfaces:
for addr in ifaddresses(interface)[AF_INET6]:
self.assertTrue(is_ipv6_link_local(addr['addr']))
# disable IPv6 link-local address assignment
for interface in self._interfaces:
base = self._base_path + [interface]
self.cli_set(base + ['ipv6', 'address', 'no-default-link-local'])
# after commit we must have no IPv6 link-local address
self.cli_commit()
for interface in self._interfaces:
self.assertNotIn(AF_INET6, ifaddresses(interface))
def test_interface_mtu(self):
if not self._test_mtu:
self.skipTest('not supported')
for intf in self._interfaces:
base = self._base_path + [intf]
self.cli_set(base + ['mtu', self._mtu])
for option in self._options.get(intf, []):
self.cli_set(base + option.split())
# commit interface changes
self.cli_commit()
# verify changed MTU
for intf in self._interfaces:
tmp = read_file(f'/sys/class/net/{intf}/mtu')
self.assertEqual(tmp, self._mtu)
def test_mtu_1200_no_ipv6_interface(self):
# Testcase if MTU can be changed to 1200 on non IPv6
# enabled interfaces
if not self._test_mtu:
self.skipTest('not supported')
old_mtu = self._mtu
self._mtu = '1200'
for intf in self._interfaces:
base = self._base_path + [intf]
self.cli_set(base + ['mtu', self._mtu])
for option in self._options.get(intf, []):
self.cli_set(base + option.split())
# check validate() - can not set low MTU if 'no-default-link-local'
# is not set on CLI
with self.assertRaises(ConfigSessionError):
self.cli_commit()
for intf in self._interfaces:
base = self._base_path + [intf]
self.cli_set(base + ['ipv6', 'address', 'no-default-link-local'])
# commit interface changes
self.cli_commit()
# verify changed MTU
for intf in self._interfaces:
tmp = read_file(f'/sys/class/net/{intf}/mtu')
self.assertEqual(tmp, self._mtu)
self._mtu = old_mtu
def test_vif_8021q_interfaces(self):
# XXX: This testcase is not allowed to run as first testcase, reason
# is the Wireless test will first load the wifi kernel hwsim module
# which creates a wlan0 and wlan1 interface which will fail the
# tearDown() test in the end that no interface is allowed to survive!
if not self._test_vlan:
self.skipTest('not supported')
for interface in self._interfaces:
base = self._base_path + [interface]
for option in self._options.get(interface, []):
self.cli_set(base + option.split())
for vlan in self._vlan_range:
base = self._base_path + [interface, 'vif', vlan]
self.cli_set(base + ['mtu', self._mtu])
for address in self._test_addr:
self.cli_set(base + ['address', address])
self.cli_commit()
for intf in self._interfaces:
for vlan in self._vlan_range:
vif = f'{intf}.{vlan}'
for address in self._test_addr:
self.assertTrue(is_intf_addr_assigned(vif, address))
tmp = read_file(f'/sys/class/net/{vif}/mtu')
self.assertEqual(tmp, self._mtu)
self.assertEqual(Interface(vif).get_admin_state(), 'up')
# T4064: Delete interface addresses, keep VLAN interface
for interface in self._interfaces:
base = self._base_path + [interface]
for vlan in self._vlan_range:
base = self._base_path + [interface, 'vif', vlan]
self.cli_delete(base + ['address'])
self.cli_commit()
# Verify no IP address is assigned
for interface in self._interfaces:
for vlan in self._vlan_range:
vif = f'{intf}.{vlan}'
for address in self._test_addr:
self.assertFalse(is_intf_addr_assigned(vif, address))
def test_vif_8021q_mtu_limits(self):
# XXX: This testcase is not allowed to run as first testcase, reason
# is the Wireless test will first load the wifi kernel hwsim module
# which creates a wlan0 and wlan1 interface which will fail the
# tearDown() test in the end that no interface is allowed to survive!
if not self._test_vlan:
self.skipTest('not supported')
mtu_1500 = '1500'
mtu_9000 = '9000'
for interface in self._interfaces:
base = self._base_path + [interface]
self.cli_set(base + ['mtu', mtu_1500])
for option in self._options.get(interface, []):
self.cli_set(base + option.split())
if 'source-interface' in option:
iface = option.split()[-1]
iface_type = Section.section(iface)
self.cli_set(['interfaces', iface_type, iface, 'mtu', mtu_9000])
for vlan in self._vlan_range:
base = self._base_path + [interface, 'vif', vlan]
self.cli_set(base + ['mtu', mtu_9000])
# check validate() - VIF MTU must not be larger the parent interface
# MTU size.
with self.assertRaises(ConfigSessionError):
self.cli_commit()
# Change MTU on base interface to be the same as on the VIF interface
for interface in self._interfaces:
base = self._base_path + [interface]
self.cli_set(base + ['mtu', mtu_9000])
self.cli_commit()
# Verify MTU on base and VIF interfaces
for interface in self._interfaces:
tmp = get_interface_config(interface)
self.assertEqual(tmp['mtu'], int(mtu_9000))
for vlan in self._vlan_range:
tmp = get_interface_config(f'{interface}.{vlan}')
self.assertEqual(tmp['mtu'], int(mtu_9000))
def test_vif_8021q_lower_up_down(self):
- # Testcase for https://phabricator.vyos.net/T3349
+ # Testcase for https://vyos.dev/T3349
if not self._test_vlan:
self.skipTest('not supported')
for interface in self._interfaces:
base = self._base_path + [interface]
for option in self._options.get(interface, []):
self.cli_set(base + option.split())
# disable the lower interface
self.cli_set(base + ['disable'])
for vlan in self._vlan_range:
vlan_base = self._base_path + [interface, 'vif', vlan]
# disable the vlan interface
self.cli_set(vlan_base + ['disable'])
self.cli_commit()
# re-enable all lower interfaces
for interface in self._interfaces:
base = self._base_path + [interface]
self.cli_delete(base + ['disable'])
self.cli_commit()
# verify that the lower interfaces are admin up and the vlan
# interfaces are all admin down
for interface in self._interfaces:
self.assertEqual(Interface(interface).get_admin_state(), 'up')
for vlan in self._vlan_range:
ifname = f'{interface}.{vlan}'
self.assertEqual(Interface(ifname).get_admin_state(), 'down')
def test_vif_s_8021ad_vlan_interfaces(self):
# XXX: This testcase is not allowed to run as first testcase, reason
# is the Wireless test will first load the wifi kernel hwsim module
# which creates a wlan0 and wlan1 interface which will fail the
# tearDown() test in the end that no interface is allowed to survive!
if not self._test_qinq:
self.skipTest('not supported')
for interface in self._interfaces:
base = self._base_path + [interface]
for option in self._options.get(interface, []):
self.cli_set(base + option.split())
for vif_s in self._qinq_range:
for vif_c in self._vlan_range:
base = self._base_path + [interface, 'vif-s', vif_s, 'vif-c', vif_c]
self.cli_set(base + ['mtu', self._mtu])
for address in self._test_addr:
self.cli_set(base + ['address', address])
self.cli_commit()
for interface in self._interfaces:
for vif_s in self._qinq_range:
tmp = get_interface_config(f'{interface}.{vif_s}')
self.assertEqual(dict_search('linkinfo.info_data.protocol', tmp), '802.1ad')
for vif_c in self._vlan_range:
vif = f'{interface}.{vif_s}.{vif_c}'
for address in self._test_addr:
self.assertTrue(is_intf_addr_assigned(vif, address))
tmp = read_file(f'/sys/class/net/{vif}/mtu')
self.assertEqual(tmp, self._mtu)
# T4064: Delete interface addresses, keep VLAN interface
for interface in self._interfaces:
base = self._base_path + [interface]
for vif_s in self._qinq_range:
for vif_c in self._vlan_range:
self.cli_delete(self._base_path + [interface, 'vif-s', vif_s, 'vif-c', vif_c, 'address'])
self.cli_commit()
# Verify no IP address is assigned
for interface in self._interfaces:
base = self._base_path + [interface]
for vif_s in self._qinq_range:
for vif_c in self._vlan_range:
vif = f'{interface}.{vif_s}.{vif_c}'
for address in self._test_addr:
self.assertFalse(is_intf_addr_assigned(vif, address))
# T3972: remove vif-c interfaces from vif-s
for interface in self._interfaces:
base = self._base_path + [interface]
for vif_s in self._qinq_range:
base = self._base_path + [interface, 'vif-s', vif_s, 'vif-c']
self.cli_delete(base)
self.cli_commit()
def test_interface_ip_options(self):
if not self._test_ip:
self.skipTest('not supported')
for interface in self._interfaces:
arp_tmo = '300'
path = self._base_path + [interface]
for option in self._options.get(interface, []):
self.cli_set(path + option.split())
# Options
self.cli_set(path + ['ip', 'arp-cache-timeout', arp_tmo])
self.cli_set(path + ['ip', 'disable-arp-filter'])
self.cli_set(path + ['ip', 'disable-forwarding'])
self.cli_set(path + ['ip', 'enable-arp-accept'])
self.cli_set(path + ['ip', 'enable-arp-announce'])
self.cli_set(path + ['ip', 'enable-arp-ignore'])
self.cli_set(path + ['ip', 'enable-proxy-arp'])
self.cli_set(path + ['ip', 'proxy-arp-pvlan'])
self.cli_set(path + ['ip', 'source-validation', 'loose'])
self.cli_commit()
for interface in self._interfaces:
tmp = read_file(f'/proc/sys/net/ipv4/neigh/{interface}/base_reachable_time_ms')
self.assertEqual(tmp, str((int(arp_tmo) * 1000))) # tmo value is in milli seconds
proc_base = f'/proc/sys/net/ipv4/conf/{interface}'
tmp = read_file(f'{proc_base}/arp_filter')
self.assertEqual('0', tmp)
tmp = read_file(f'{proc_base}/arp_accept')
self.assertEqual('1', tmp)
tmp = read_file(f'{proc_base}/arp_announce')
self.assertEqual('1', tmp)
tmp = read_file(f'{proc_base}/arp_ignore')
self.assertEqual('1', tmp)
tmp = read_file(f'{proc_base}/forwarding')
self.assertEqual('0', tmp)
tmp = read_file(f'{proc_base}/proxy_arp')
self.assertEqual('1', tmp)
tmp = read_file(f'{proc_base}/proxy_arp_pvlan')
self.assertEqual('1', tmp)
tmp = read_file(f'{proc_base}/rp_filter')
self.assertEqual('2', tmp)
def test_interface_ipv6_options(self):
if not self._test_ipv6:
self.skipTest('not supported')
for interface in self._interfaces:
dad_transmits = '10'
path = self._base_path + [interface]
for option in self._options.get(interface, []):
self.cli_set(path + option.split())
# Options
self.cli_set(path + ['ipv6', 'disable-forwarding'])
self.cli_set(path + ['ipv6', 'dup-addr-detect-transmits', dad_transmits])
self.cli_commit()
for interface in self._interfaces:
proc_base = f'/proc/sys/net/ipv6/conf/{interface}'
tmp = read_file(f'{proc_base}/forwarding')
self.assertEqual('0', tmp)
tmp = read_file(f'{proc_base}/dad_transmits')
self.assertEqual(dad_transmits, tmp)
def test_dhcpv6_client_options(self):
if not self._test_ipv6_dhcpc6:
self.skipTest('not supported')
duid_base = 10
for interface in self._interfaces:
duid = '00:01:00:01:27:71:db:f0:00:50:00:00:00:{}'.format(duid_base)
path = self._base_path + [interface]
for option in self._options.get(interface, []):
self.cli_set(path + option.split())
# Enable DHCPv6 client
self.cli_set(path + ['address', 'dhcpv6'])
self.cli_set(path + ['dhcpv6-options', 'rapid-commit'])
self.cli_set(path + ['dhcpv6-options', 'parameters-only'])
self.cli_set(path + ['dhcpv6-options', 'duid', duid])
duid_base += 1
self.cli_commit()
duid_base = 10
for interface in self._interfaces:
duid = '00:01:00:01:27:71:db:f0:00:50:00:00:00:{}'.format(duid_base)
dhcpc6_config = read_file(f'/run/dhcp6c/dhcp6c.{interface}.conf')
self.assertIn(f'interface {interface} ' + '{', dhcpc6_config)
self.assertIn(f' request domain-name-servers;', dhcpc6_config)
self.assertIn(f' request domain-name;', dhcpc6_config)
self.assertIn(f' information-only;', dhcpc6_config)
self.assertIn(f' send ia-na 0;', dhcpc6_config)
self.assertIn(f' send rapid-commit;', dhcpc6_config)
self.assertIn(f' send client-id {duid};', dhcpc6_config)
self.assertIn('};', dhcpc6_config)
duid_base += 1
# Check for running process
self.assertTrue(process_named_running('dhcp6c'))
def test_dhcpv6pd_auto_sla_id(self):
if not self._test_ipv6_pd:
self.skipTest('not supported')
prefix_len = '56'
sla_len = str(64 - int(prefix_len))
delegatees = ['dum2340', 'dum2341', 'dum2342', 'dum2343', 'dum2344']
for interface in self._interfaces:
path = self._base_path + [interface]
for option in self._options.get(interface, []):
self.cli_set(path + option.split())
address = '1'
# prefix delegation stuff
pd_base = path + ['dhcpv6-options', 'pd', '0']
self.cli_set(pd_base + ['length', prefix_len])
for delegatee in delegatees:
section = Section.section(delegatee)
self.cli_set(['interfaces', section, delegatee])
self.cli_set(pd_base + ['interface', delegatee, 'address', address])
# increment interface address
address = str(int(address) + 1)
self.cli_commit()
for interface in self._interfaces:
dhcpc6_config = read_file(f'/run/dhcp6c/dhcp6c.{interface}.conf')
# verify DHCPv6 prefix delegation
self.assertIn(f'prefix ::/{prefix_len} infinity;', dhcpc6_config)
address = '1'
sla_id = '0'
for delegatee in delegatees:
self.assertIn(f'prefix-interface {delegatee}' + r' {', dhcpc6_config)
self.assertIn(f'ifid {address};', dhcpc6_config)
self.assertIn(f'sla-id {sla_id};', dhcpc6_config)
self.assertIn(f'sla-len {sla_len};', dhcpc6_config)
# increment sla-id
sla_id = str(int(sla_id) + 1)
# increment interface address
address = str(int(address) + 1)
# Check for running process
self.assertTrue(process_named_running('dhcp6c'))
for delegatee in delegatees:
# we can already cleanup the test delegatee interface here
# as until commit() is called, nothing happens
section = Section.section(delegatee)
self.cli_delete(['interfaces', section, delegatee])
def test_dhcpv6pd_manual_sla_id(self):
if not self._test_ipv6_pd:
self.skipTest('not supported')
prefix_len = '56'
sla_len = str(64 - int(prefix_len))
delegatees = ['dum3340', 'dum3341', 'dum3342', 'dum3343', 'dum3344']
for interface in self._interfaces:
path = self._base_path + [interface]
for option in self._options.get(interface, []):
self.cli_set(path + option.split())
# prefix delegation stuff
address = '1'
sla_id = '1'
pd_base = path + ['dhcpv6-options', 'pd', '0']
self.cli_set(pd_base + ['length', prefix_len])
for delegatee in delegatees:
section = Section.section(delegatee)
self.cli_set(['interfaces', section, delegatee])
self.cli_set(pd_base + ['interface', delegatee, 'address', address])
self.cli_set(pd_base + ['interface', delegatee, 'sla-id', sla_id])
# increment interface address
address = str(int(address) + 1)
sla_id = str(int(sla_id) + 1)
self.cli_commit()
# Verify dhcpc6 client configuration
for interface in self._interfaces:
address = '1'
sla_id = '1'
dhcpc6_config = read_file(f'/run/dhcp6c/dhcp6c.{interface}.conf')
# verify DHCPv6 prefix delegation
self.assertIn(f'prefix ::/{prefix_len} infinity;', dhcpc6_config)
for delegatee in delegatees:
self.assertIn(f'prefix-interface {delegatee}' + r' {', dhcpc6_config)
self.assertIn(f'ifid {address};', dhcpc6_config)
self.assertIn(f'sla-id {sla_id};', dhcpc6_config)
self.assertIn(f'sla-len {sla_len};', dhcpc6_config)
# increment sla-id
sla_id = str(int(sla_id) + 1)
# increment interface address
address = str(int(address) + 1)
# Check for running process
self.assertTrue(process_named_running('dhcp6c'))
for delegatee in delegatees:
# we can already cleanup the test delegatee interface here
# as until commit() is called, nothing happens
section = Section.section(delegatee)
self.cli_delete(['interfaces', section, delegatee])
diff --git a/sonar-project.properties b/sonar-project.properties
index 1258da817..eddfd5e1e 100644
--- a/sonar-project.properties
+++ b/sonar-project.properties
@@ -1,21 +1,21 @@
sonar.projectKey=vyos:vyos-1x
sonar.projectName=vyos-1x
sonar.projectVersion=1.2.0
sonar.organization=vyos
sonar.sources=src/conf_mode,src/op_mode,src/completion,src/helpers,src/validators
sonar.language=py
sonar.sourceEncoding=UTF-8
sonar.links.homepage=https://github.com/vyos/vyos-1x
sonar.links.ci=https://ci.vyos.net/job/vyos-1x/
sonar.links.scm=https://github.com/vyos/vyos-1x
-sonar.links.issue=https://phabricator.vyos.net/
+sonar.links.issue=https://vyos.dev/
sonar.host.url=https://sonarcloud.io
sonar.python.pylint=/usr/local/bin/pylint
sonar.python.pylint_config=.pylintrc
sonar.python.pylint.reportPath=pylint-report.txt
sonar.python.xunit.reportPath=nosetests.xml
sonar.python.coverage.reportPath=coverage.xml
diff --git a/src/conf_mode/dhcp_server.py b/src/conf_mode/dhcp_server.py
index d27f8d995..11ee6b60a 100755
--- a/src/conf_mode/dhcp_server.py
+++ b/src/conf_mode/dhcp_server.py
@@ -1,327 +1,327 @@
#!/usr/bin/env python3
#
# Copyright (C) 2018-2022 VyOS maintainers and contributors
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License version 2 or later as
# published by the Free Software Foundation.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import os
from ipaddress import ip_address
from ipaddress import ip_network
from netaddr import IPAddress
from netaddr import IPRange
from sys import exit
from vyos.config import Config
from vyos.configdict import dict_merge
from vyos.template import render
from vyos.util import call
from vyos.util import dict_search
from vyos.util import run
from vyos.validate import is_subnet_connected
from vyos.validate import is_addr_assigned
from vyos.xml import defaults
from vyos import ConfigError
from vyos import airbag
airbag.enable()
config_file = '/run/dhcp-server/dhcpd.conf'
def dhcp_slice_range(exclude_list, range_dict):
"""
This function is intended to slice a DHCP range. What does it mean?
Lets assume we have a DHCP range from '192.0.2.1' to '192.0.2.100'
but want to exclude address '192.0.2.74' and '192.0.2.75'. We will
pass an input 'range_dict' in the format:
{'start' : '192.0.2.1', 'stop' : '192.0.2.100' }
and we will receive an output list of:
[{'start' : '192.0.2.1' , 'stop' : '192.0.2.73' },
{'start' : '192.0.2.76', 'stop' : '192.0.2.100' }]
The resulting list can then be used in turn to build the proper dhcpd
configuration file.
"""
output = []
# exclude list must be sorted for this to work
exclude_list = sorted(exclude_list)
range_start = range_dict['start']
range_stop = range_dict['stop']
range_last_exclude = ''
for e in exclude_list:
if (ip_address(e) >= ip_address(range_start)) and \
(ip_address(e) <= ip_address(range_stop)):
range_last_exclude = e
for e in exclude_list:
if (ip_address(e) >= ip_address(range_start)) and \
(ip_address(e) <= ip_address(range_stop)):
# Build new address range ending one address before exclude address
r = {
'start' : range_start,
'stop' : str(ip_address(e) -1)
}
# On the next run our address range will start one address after
# the exclude address
range_start = str(ip_address(e) + 1)
# on subsequent exclude addresses we can not
# append them to our output
if not (ip_address(r['start']) > ip_address(r['stop'])):
# Everything is fine, add range to result
output.append(r)
# Take care of last IP address range spanning from the last exclude
# address (+1) to the end of the initial configured range
if ip_address(e) == ip_address(range_last_exclude):
r = {
'start': str(ip_address(e) + 1),
'stop': str(range_stop)
}
if not (ip_address(r['start']) > ip_address(r['stop'])):
output.append(r)
else:
# if the excluded address was not part of the range, we simply return
# the entire ranga again
if not range_last_exclude:
if range_dict not in output:
output.append(range_dict)
return output
def get_config(config=None):
if config:
conf = config
else:
conf = Config()
base = ['service', 'dhcp-server']
if not conf.exists(base):
return None
dhcp = conf.get_config_dict(base, key_mangling=('-', '_'), get_first_key=True, no_tag_node_value_mangle=True)
# T2665: defaults include lease time per TAG node which need to be added to
# individual subnet definitions
default_values = defaults(base + ['shared-network-name', 'subnet'])
if 'shared_network_name' in dhcp:
for network, network_config in dhcp['shared_network_name'].items():
if 'subnet' in network_config:
for subnet, subnet_config in network_config['subnet'].items():
if 'lease' not in subnet_config:
dhcp['shared_network_name'][network]['subnet'][subnet] = dict_merge(
default_values, dhcp['shared_network_name'][network]['subnet'][subnet])
# If exclude IP addresses are defined we need to slice them out of
# the defined ranges
if {'exclude', 'range'} <= set(subnet_config):
new_range_id = 0
new_range_dict = {}
for r, r_config in subnet_config['range'].items():
for slice in dhcp_slice_range(subnet_config['exclude'], r_config):
new_range_dict.update({new_range_id : slice})
new_range_id +=1
dhcp['shared_network_name'][network]['subnet'][subnet].update(
{'range' : new_range_dict})
return dhcp
def verify(dhcp):
# bail out early - looks like removal from running config
if not dhcp or 'disable' in dhcp:
return None
# If DHCP is enabled we need one share-network
if 'shared_network_name' not in dhcp:
raise ConfigError('No DHCP shared networks configured.\n' \
'At least one DHCP shared network must be configured.')
# Inspect shared-network/subnet
listen_ok = False
subnets = []
failover_ok = False
shared_networks = len(dhcp['shared_network_name'])
disabled_shared_networks = 0
# A shared-network requires a subnet definition
for network, network_config in dhcp['shared_network_name'].items():
if 'disable' in network_config:
disabled_shared_networks += 1
if 'subnet' not in network_config:
raise ConfigError(f'No subnets defined for {network}. At least one\n' \
'lease subnet must be configured.')
for subnet, subnet_config in network_config['subnet'].items():
# All delivered static routes require a next-hop to be set
if 'static_route' in subnet_config:
for route, route_option in subnet_config['static_route'].items():
if 'next_hop' not in route_option:
raise ConfigError(f'DHCP static-route "{route}" requires router to be defined!')
# DHCP failover needs at least one subnet that uses it
if 'enable_failover' in subnet_config:
if 'failover' not in dhcp:
raise ConfigError(f'Can not enable failover for "{subnet}" in "{network}".\n' \
'Failover is not configured globally!')
failover_ok = True
# Check if DHCP address range is inside configured subnet declaration
if 'range' in subnet_config:
networks = []
for range, range_config in subnet_config['range'].items():
if not {'start', 'stop'} <= set(range_config):
raise ConfigError(f'DHCP range "{range}" start and stop address must be defined!')
# Start/Stop address must be inside network
for key in ['start', 'stop']:
if ip_address(range_config[key]) not in ip_network(subnet):
raise ConfigError(f'DHCP range "{range}" {key} address not within shared-network "{network}, {subnet}"!')
# Stop address must be greater or equal to start address
if ip_address(range_config['stop']) < ip_address(range_config['start']):
raise ConfigError(f'DHCP range "{range}" stop address must be greater or equal\n' \
'to the ranges start address!')
for network in networks:
start = range_config['start']
stop = range_config['stop']
if start in network:
raise ConfigError(f'Range "{range}" start address "{start}" already part of another range!')
if stop in network:
raise ConfigError(f'Range "{range}" stop address "{stop}" already part of another range!')
tmp = IPRange(range_config['start'], range_config['stop'])
networks.append(tmp)
# Exclude addresses must be in bound
if 'exclude' in subnet_config:
for exclude in subnet_config['exclude']:
if ip_address(exclude) not in ip_network(subnet):
raise ConfigError(f'Excluded IP address "{exclude}" not within shared-network "{network}, {subnet}"!')
# At least one DHCP address range or static-mapping required
if 'range' not in subnet_config and 'static_mapping' not in subnet_config:
raise ConfigError(f'No DHCP address range or active static-mapping configured\n' \
f'within shared-network "{network}, {subnet}"!')
if 'static_mapping' in subnet_config:
# Static mappings require just a MAC address (will use an IP from the dynamic pool if IP is not set)
for mapping, mapping_config in subnet_config['static_mapping'].items():
if 'ip_address' in mapping_config:
if ip_address(mapping_config['ip_address']) not in ip_network(subnet):
raise ConfigError(f'Configured static lease address for mapping "{mapping}" is\n' \
f'not within shared-network "{network}, {subnet}"!')
if 'mac_address' not in mapping_config:
raise ConfigError(f'MAC address required for static mapping "{mapping}"\n' \
f'within shared-network "{network}, {subnet}"!')
# There must be one subnet connected to a listen interface.
# This only counts if the network itself is not disabled!
if 'disable' not in network_config:
if is_subnet_connected(subnet, primary=False):
listen_ok = True
# Subnets must be non overlapping
if subnet in subnets:
raise ConfigError(f'Configured subnets must be unique! Subnet "{subnet}"\n'
'defined multiple times!')
subnets.append(subnet)
# Check for overlapping subnets
net = ip_network(subnet)
for n in subnets:
net2 = ip_network(n)
if (net != net2):
if net.overlaps(net2):
raise ConfigError('Conflicting subnet ranges: "{net}" overlaps "{net2}"!')
# Prevent 'disable' for shared-network if only one network is configured
if (shared_networks - disabled_shared_networks) < 1:
raise ConfigError(f'At least one shared network must be active!')
if 'failover' in dhcp:
if not failover_ok:
raise ConfigError('DHCP failover must be enabled for at least one subnet!')
for key in ['name', 'remote', 'source_address', 'status']:
if key not in dhcp['failover']:
tmp = key.replace('_', '-')
raise ConfigError(f'DHCP failover requires "{tmp}" to be specified!')
for address in (dict_search('listen_address', dhcp) or []):
if is_addr_assigned(address):
listen_ok = True
# no need to probe further networks, we have one that is valid
continue
else:
raise ConfigError(f'listen-address "{address}" not configured on any interface')
if not listen_ok:
raise ConfigError('None of the configured subnets have an appropriate primary IP address on any\n'
'broadcast interface configured, nor was there an explicit listen-address\n'
'configured for serving DHCP relay packets!')
return None
def generate(dhcp):
# bail out early - looks like removal from running config
if not dhcp or 'disable' in dhcp:
return None
- # Please see: https://phabricator.vyos.net/T1129 for quoting of the raw
+ # Please see: https://vyos.dev/T1129 for quoting of the raw
# parameters we can pass to ISC DHCPd
tmp_file = '/tmp/dhcpd.conf'
render(tmp_file, 'dhcp-server/dhcpd.conf.tmpl', dhcp,
formater=lambda _: _.replace(""", '"'))
# XXX: as we have the ability for a user to pass in "raw" options via VyOS
# CLI (see T3544) we now ask ISC dhcpd to test the newly rendered
# configuration
tmp = run(f'/usr/sbin/dhcpd -4 -q -t -cf {tmp_file}')
if tmp > 0:
if os.path.exists(tmp_file):
os.unlink(tmp_file)
raise ConfigError('Configuration file errors encountered - check your options!')
# Now that we know that the newly rendered configuration is "good" we can
# render the "real" configuration
render(config_file, 'dhcp-server/dhcpd.conf.tmpl', dhcp,
formater=lambda _: _.replace(""", '"'))
return None
def apply(dhcp):
# bail out early - looks like removal from running config
if not dhcp or 'disable' in dhcp:
call('systemctl stop isc-dhcp-server.service')
if os.path.exists(config_file):
os.unlink(config_file)
return None
call('systemctl restart isc-dhcp-server.service')
return None
if __name__ == '__main__':
try:
c = get_config()
verify(c)
generate(c)
apply(c)
except ConfigError as e:
print(e)
exit(1)
diff --git a/src/conf_mode/interfaces-openvpn.py b/src/conf_mode/interfaces-openvpn.py
index fe5898282..7e2e025ba 100755
--- a/src/conf_mode/interfaces-openvpn.py
+++ b/src/conf_mode/interfaces-openvpn.py
@@ -1,551 +1,551 @@
#!/usr/bin/env python3
#
# 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
# published by the Free Software Foundation.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import os
import re
from glob import glob
from sys import exit
from ipaddress import IPv4Address
from ipaddress import IPv4Network
from ipaddress import IPv6Address
from ipaddress import IPv6Network
from ipaddress import summarize_address_range
from netifaces import interfaces
from shutil import rmtree
from vyos.config import Config
from vyos.configdict import get_interface_dict
from vyos.configdict import is_node_changed
from vyos.configverify import verify_vrf
from vyos.configverify import verify_bridge_delete
from vyos.configverify import verify_diffie_hellman_length
from vyos.configverify import verify_bond_bridge_member
from vyos.ifconfig import VTunIf
from vyos.template import render
from vyos.template import is_ipv4
from vyos.template import is_ipv6
from vyos.util import call
from vyos.util import chown
from vyos.util import chmod_600
from vyos.util import cmd
from vyos.util import dict_search
from vyos.util import is_list_equal
from vyos.util import makedir
from vyos.validate import is_addr_assigned
from vyos import ConfigError
from vyos import airbag
airbag.enable()
user = 'openvpn'
group = 'openvpn'
cfg_file = '/run/openvpn/{ifname}.conf'
service_file = '/run/systemd/system/openvpn@{ifname}.service.d/20-override.conf'
def checkCertHeader(header, filename):
"""
Verify if filename contains specified header.
Returns True if match is found, False if no match or file is not found
"""
if not os.path.isfile(filename):
return False
with open(filename, 'r') as f:
for line in f:
if re.match(header, line):
return True
return False
def get_config(config=None):
"""
Retrive CLI config as dictionary. Dictionary can never be empty, as at least the
interface name will be added or a deleted flag
"""
if config:
conf = config
else:
conf = Config()
base = ['interfaces', 'openvpn']
openvpn = get_interface_dict(conf, base)
if 'deleted' not in openvpn:
if is_node_changed(conf, ['openvpn-option']):
openvpn.update({'restart_required': {}})
openvpn['auth_user_pass_file'] = '/run/openvpn/{ifname}.pw'.format(**openvpn)
return openvpn
def verify(openvpn):
if 'deleted' in openvpn:
verify_bridge_delete(openvpn)
return None
if 'mode' not in openvpn:
raise ConfigError('Must specify OpenVPN operation mode!')
#
# OpenVPN client mode - VERIFY
#
if openvpn['mode'] == 'client':
if 'local_port' in openvpn:
raise ConfigError('Cannot specify "local-port" in client mode')
if 'local_host' in openvpn:
raise ConfigError('Cannot specify "local-host" in client mode')
if 'remote_host' not in openvpn:
raise ConfigError('Must specify "remote-host" in client mode')
if openvpn['protocol'] == 'tcp-passive':
raise ConfigError('Protocol "tcp-passive" is not valid in client mode')
if dict_search('tls.dh_file', openvpn):
raise ConfigError('Cannot specify "tls dh-file" in client mode')
#
# OpenVPN site-to-site - VERIFY
#
elif openvpn['mode'] == 'site-to-site':
if 'local_address' not in openvpn and 'is_bridge_member' not in openvpn:
raise ConfigError('Must specify "local-address" or add interface to bridge')
if 'local_address' in openvpn:
if len([addr for addr in openvpn['local_address'] if is_ipv4(addr)]) > 1:
raise ConfigError('Only one IPv4 local-address can be specified')
if len([addr for addr in openvpn['local_address'] if is_ipv6(addr)]) > 1:
raise ConfigError('Only one IPv6 local-address can be specified')
if openvpn['device_type'] == 'tun':
if 'remote_address' not in openvpn:
raise ConfigError('Must specify "remote-address"')
if 'remote_address' in openvpn:
if len([addr for addr in openvpn['remote_address'] if is_ipv4(addr)]) > 1:
raise ConfigError('Only one IPv4 remote-address can be specified')
if len([addr for addr in openvpn['remote_address'] if is_ipv6(addr)]) > 1:
raise ConfigError('Only one IPv6 remote-address can be specified')
if not 'local_address' in openvpn:
raise ConfigError('"remote-address" requires "local-address"')
v4loAddr = [addr for addr in openvpn['local_address'] if is_ipv4(addr)]
v4remAddr = [addr for addr in openvpn['remote_address'] if is_ipv4(addr)]
if v4loAddr and not v4remAddr:
raise ConfigError('IPv4 "local-address" requires IPv4 "remote-address"')
elif v4remAddr and not v4loAddr:
raise ConfigError('IPv4 "remote-address" requires IPv4 "local-address"')
v6remAddr = [addr for addr in openvpn['remote_address'] if is_ipv6(addr)]
v6loAddr = [addr for addr in openvpn['local_address'] if is_ipv6(addr)]
if v6loAddr and not v6remAddr:
raise ConfigError('IPv6 "local-address" requires IPv6 "remote-address"')
elif v6remAddr and not v6loAddr:
raise ConfigError('IPv6 "remote-address" requires IPv6 "local-address"')
if is_list_equal(v4loAddr, v4remAddr) or is_list_equal(v6loAddr, v6remAddr):
raise ConfigError('"local-address" and "remote-address" cannot be the same')
if dict_search('local_host', openvpn) in dict_search('local_address', openvpn):
raise ConfigError('"local-address" cannot be the same as "local-host"')
if dict_search('remote_host', openvpn) in dict_search('remote_address', openvpn):
raise ConfigError('"remote-address" and "remote-host" can not be the same')
if openvpn['device_type'] == 'tap' and 'local_address' in openvpn:
# we can only have one local_address, this is ensured above
v4addr = None
for laddr in openvpn['local_address']:
if is_ipv4(laddr):
v4addr = laddr
break
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')
else:
# checks for client-server or site-to-site bridged
if 'local_address' in openvpn or 'remote_address' in openvpn:
raise ConfigError('Cannot specify "local-address" or "remote-address" ' \
'in client/server or bridge mode')
#
# OpenVPN server mode - VERIFY
#
if openvpn['mode'] == 'server':
if openvpn['protocol'] == 'tcp-active':
raise ConfigError('Protocol "tcp-active" is not valid in server mode')
if dict_search('authentication.username', openvpn) or dict_search('authentication.password', openvpn):
raise ConfigError('Cannot specify "authentication" in server mode')
if 'remote_port' in openvpn:
raise ConfigError('Cannot specify "remote-port" in server mode')
if 'remote_host' in openvpn:
raise ConfigError('Cannot specify "remote-host" in server mode')
if 'tls' in openvpn:
if 'dh_file' not in openvpn['tls']:
if 'key_file' in openvpn['tls'] and not checkCertHeader('-----BEGIN EC PRIVATE KEY-----', openvpn['tls']['key_file']):
raise ConfigError('Must specify "tls dh-file" when not using EC keys in server mode')
tmp = dict_search('server.subnet', openvpn)
if tmp:
v4_subnets = len([subnet for subnet in tmp if is_ipv4(subnet)])
v6_subnets = len([subnet for subnet in tmp if is_ipv6(subnet)])
if v4_subnets > 1:
raise ConfigError('Cannot specify more than 1 IPv4 server subnet')
if v6_subnets > 1:
raise ConfigError('Cannot specify more than 1 IPv6 server subnet')
if v6_subnets > 0 and v4_subnets == 0:
raise ConfigError('IPv6 server requires an IPv4 server subnet')
for subnet in tmp:
if is_ipv4(subnet):
subnet = IPv4Network(subnet)
if openvpn['device_type'] == 'tun' and subnet.prefixlen > 29:
raise ConfigError('Server subnets smaller than /29 with device type "tun" are not supported')
elif openvpn['device_type'] == 'tap' and subnet.prefixlen > 30:
raise ConfigError('Server subnets smaller than /30 with device type "tap" are not supported')
for client in (dict_search('client', openvpn) or []):
if client['ip'] and not IPv4Address(client['ip'][0]) in subnet:
raise ConfigError(f'Client "{client["name"]}" IP {client["ip"][0]} not in server subnet {subnet}')
else:
if 'is_bridge_member' not in openvpn:
raise ConfigError('Must specify "server subnet" or add interface to bridge in server mode')
for client in (dict_search('client', openvpn) or []):
if len(client['ip']) > 1 or len(client['ipv6_ip']) > 1:
raise ConfigError(f'Server client "{client["name"]}": cannot specify more than 1 IPv4 and 1 IPv6 IP')
if dict_search('server.client_ip_pool', openvpn):
if not (dict_search('server.client_ip_pool.start', openvpn) and dict_search('server.client_ip_pool.stop', openvpn)):
raise ConfigError('Server client-ip-pool requires both start and stop addresses')
else:
v4PoolStart = IPv4Address(dict_search('server.client_ip_pool.start', openvpn))
v4PoolStop = IPv4Address(dict_search('server.client_ip_pool.stop', openvpn))
if v4PoolStart > v4PoolStop:
raise ConfigError(f'Server client-ip-pool start address {v4PoolStart} is larger than stop address {v4PoolStop}')
v4PoolSize = int(v4PoolStop) - int(v4PoolStart)
if v4PoolSize >= 65536:
raise ConfigError(f'Server client-ip-pool is too large [{v4PoolStart} -> {v4PoolStop} = {v4PoolSize}], maximum is 65536 addresses.')
v4PoolNets = list(summarize_address_range(v4PoolStart, v4PoolStop))
for client in (dict_search('client', openvpn) or []):
if client['ip']:
for v4PoolNet in v4PoolNets:
if IPv4Address(client['ip'][0]) in v4PoolNet:
print(f'Warning: Client "{client["name"]}" IP {client["ip"][0]} is in server IP pool, it is not reserved for this client.')
for subnet in (dict_search('server.subnet', openvpn) or []):
if is_ipv6(subnet):
tmp = dict_search('client_ipv6_pool.base', openvpn)
if tmp:
if not dict_search('server.client_ip_pool', openvpn):
raise ConfigError('IPv6 server pool requires an IPv4 server pool')
if int(tmp.split('/')[1]) >= 112:
raise ConfigError('IPv6 server pool must be larger than /112')
#
# todo - weird logic
#
v6PoolStart = IPv6Address(tmp)
v6PoolStop = IPv6Network((v6PoolStart, openvpn['server_ipv6_pool_prefixlen']), strict=False)[-1] # don't remove the parentheses, it's a 2-tuple
v6PoolSize = int(v6PoolStop) - int(v6PoolStart) if int(openvpn['server_ipv6_pool_prefixlen']) > 96 else 65536
if v6PoolSize < v4PoolSize:
raise ConfigError(f'IPv6 server pool must be at least as large as the IPv4 pool (current sizes: IPv6={v6PoolSize} IPv4={v4PoolSize})')
v6PoolNets = list(summarize_address_range(v6PoolStart, v6PoolStop))
for client in (dict_search('client', openvpn) or []):
if client['ipv6_ip']:
for v6PoolNet in v6PoolNets:
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.')
else:
# checks for both client and site-to-site go here
if dict_search('server.reject_unconfigured_clients', openvpn):
raise ConfigError('Option reject-unconfigured-clients only supported in server mode')
if 'replace_default_route' in openvpn and 'remote_host' not in openvpn:
raise ConfigError('Cannot set "replace-default-route" without "remote-host"')
#
# OpenVPN common verification section
# not depending on any operation mode
#
# verify specified IP address is present on any interface on this system
if 'local_host' in openvpn:
if not is_addr_assigned(openvpn['local_host']):
print('local-host IP address "{local_host}" not assigned' \
' to any interface'.format(**openvpn))
# TCP active
if openvpn['protocol'] == 'tcp-active':
if 'local_port' in openvpn:
raise ConfigError('Cannot specify "local-port" with "tcp-active"')
if 'remote_host' not in openvpn:
raise ConfigError('Must specify "remote-host" with "tcp-active"')
# shared secret and TLS
if not ('shared_secret_key_file' in openvpn or 'tls' in openvpn):
raise ConfigError('Must specify one of "shared-secret-key-file" and "tls"')
if {'shared_secret_key_file', 'tls'} <= set(openvpn):
raise ConfigError('Can only specify one of "shared-secret-key-file" and "tls"')
if openvpn['mode'] in ['client', 'server']:
if 'tls' not in openvpn:
raise ConfigError('Must specify "tls" for server and client mode')
#
# TLS/encryption
#
if 'shared_secret_key_file' in openvpn:
if dict_search('encryption.cipher', openvpn) in ['aes128gcm', 'aes192gcm', 'aes256gcm']:
raise ConfigError('GCM encryption with shared-secret-key-file not supported')
file = dict_search('shared_secret_key_file', openvpn)
if file and not checkCertHeader('-----BEGIN OpenVPN Static key V1-----', file):
raise ConfigError(f'Specified shared-secret-key-file "{file}" is not valid')
if 'tls' in openvpn:
if 'ca_cert_file' not in openvpn['tls']:
raise ConfigError('Must specify "tls ca-cert-file"')
if not (openvpn['mode'] == 'client' and 'authentication' in openvpn):
if 'cert_file' not in openvpn['tls']:
raise ConfigError('Missing "tls cert-file"')
if 'key_file' not in openvpn['tls']:
raise ConfigError('Missing "tls key-file"')
if {'auth_file', 'crypt_file'} <= set(openvpn['tls']):
raise ConfigError('TLS auth and crypt are mutually exclusive')
file = dict_search('tls.ca_cert_file', openvpn)
if file and not checkCertHeader('-----BEGIN CERTIFICATE-----', file):
raise ConfigError(f'Specified ca-cert-file "{file}" is invalid')
file = dict_search('tls.auth_file', openvpn)
if file and not checkCertHeader('-----BEGIN OpenVPN Static key V1-----', file):
raise ConfigError(f'Specified auth-file "{file}" is invalid')
file = dict_search('tls.cert_file', openvpn)
if file and not checkCertHeader('-----BEGIN CERTIFICATE-----', file):
raise ConfigError(f'Specified cert-file "{file}" is invalid')
file = dict_search('tls.key_file', openvpn)
if file and not checkCertHeader('-----BEGIN (?:RSA |EC )?PRIVATE KEY-----', file):
raise ConfigError(f'Specified key-file "{file}" is not valid')
file = dict_search('tls.crypt_file', openvpn)
if file and not checkCertHeader('-----BEGIN OpenVPN Static key V1-----', file):
raise ConfigError(f'Specified TLS crypt-file "{file}" is invalid')
file = dict_search('tls.crl_file', openvpn)
if file and not checkCertHeader('-----BEGIN X509 CRL-----', file):
raise ConfigError(f'Specified crl-file "{file} not valid')
file = dict_search('tls.dh_file', openvpn)
if file and not checkCertHeader('-----BEGIN DH PARAMETERS-----', file):
raise ConfigError(f'Specified dh-file "{file}" is not valid')
if file and not verify_diffie_hellman_length(file, 2048):
raise ConfigError(f'Minimum DH key-size is 2048 bits')
tmp = dict_search('tls.role', openvpn)
if tmp:
if openvpn['mode'] in ['client', 'server']:
if not dict_search('tls.auth_file', openvpn):
raise ConfigError('Cannot specify "tls role" in client-server mode')
if tmp == 'active':
if openvpn['protocol'] == 'tcp-passive':
raise ConfigError('Cannot specify "tcp-passive" when "tls role" is "active"')
if dict_search('tls.dh_file', openvpn):
raise ConfigError('Cannot specify "tls dh-file" when "tls role" is "active"')
elif tmp == 'passive':
if openvpn['protocol'] == 'tcp-active':
raise ConfigError('Cannot specify "tcp-active" when "tls role" is "passive"')
if not dict_search('tls.dh_file', openvpn):
raise ConfigError('Must specify "tls dh-file" when "tls role" is "passive"')
file = dict_search('tls.key_file', openvpn)
if file and checkCertHeader('-----BEGIN EC PRIVATE KEY-----', file):
if dict_search('tls.dh_file', openvpn):
print('Warning: using dh-file and EC keys simultaneously will ' \
'lead to DH ciphers being used instead of ECDH')
if dict_search('encryption.cipher', openvpn) == 'none':
print('Warning: "encryption none" was specified!')
print('No encryption will be performed and data is transmitted in ' \
'plain text over the network!')
#
# Auth user/pass
#
if (dict_search('authentication.username', openvpn) and not
dict_search('authentication.password', openvpn)):
raise ConfigError('Password for authentication is missing')
if (dict_search('authentication.password', openvpn) and not
dict_search('authentication.username', openvpn)):
raise ConfigError('Username for authentication is missing')
verify_vrf(openvpn)
verify_bond_bridge_member(openvpn)
return None
def generate(openvpn):
interface = openvpn['ifname']
directory = os.path.dirname(cfg_file.format(**openvpn))
# create base config directory on demand
makedir(directory, user, group)
# enforce proper permissions on /run/openvpn
chown(directory, user, group)
# we can't know in advance which clients have been removed,
# thus all client configs will be removed and re-added on demand
ccd_dir = os.path.join(directory, 'ccd', interface)
if os.path.isdir(ccd_dir):
rmtree(ccd_dir, ignore_errors=True)
# Remove systemd directories with overrides
service_dir = os.path.dirname(service_file.format(**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)
# Fix file permissons for site2site shared secret
if dict_search('shared_secret_key_file', openvpn):
chmod_600(openvpn['shared_secret_key_file'])
chown(openvpn['shared_secret_key_file'], user, group)
# Fix file permissons for TLS certificate and keys
for tls in ['auth_file', 'ca_cert_file', 'cert_file', 'crl_file',
'crypt_file', 'dh_file', 'key_file']:
if dict_search(f'tls.{tls}', openvpn):
chmod_600(openvpn['tls'][tls])
chown(openvpn['tls'][tls], user, group)
# Generate User/Password authentication file
if 'authentication' in openvpn:
render(openvpn['auth_user_pass_file'], 'openvpn/auth.pw.tmpl', openvpn,
user=user, group=group, permission=0o600)
else:
# delete old auth file if present
if os.path.isfile(openvpn['auth_user_pass_file']):
os.remove(openvpn['auth_user_pass_file'])
# Generate client specific configuration
if dict_search('server.client', openvpn):
for client, client_config in dict_search('server.client', openvpn).items():
client_file = os.path.join(ccd_dir, client)
# Our client need's to know its subnet mask ...
client_config['server_subnet'] = dict_search('server.subnet', openvpn)
render(client_file, 'openvpn/client.conf.tmpl', client_config,
user=user, group=group)
# we need to support quoting of raw parameters from OpenVPN CLI
- # see https://phabricator.vyos.net/T1632
+ # see https://vyos.dev/T1632
render(cfg_file.format(**openvpn), 'openvpn/server.conf.tmpl', openvpn,
formater=lambda _: _.replace(""", '"'), user=user, group=group)
# Render 20-override.conf for OpenVPN service
render(service_file.format(**openvpn), 'openvpn/service-override.conf.tmpl', openvpn,
formater=lambda _: _.replace(""", '"'), user=user, group=group)
# Reload systemd services config to apply an override
call(f'systemctl daemon-reload')
return None
def apply(openvpn):
interface = openvpn['ifname']
# Do some cleanup when OpenVPN is disabled/deleted
if 'deleted' in openvpn or 'disable' in openvpn:
call(f'systemctl stop openvpn@{interface}.service')
for cleanup_file in glob(f'/run/openvpn/{interface}.*'):
if os.path.isfile(cleanup_file):
os.unlink(cleanup_file)
if interface in interfaces():
VTunIf(interface).remove()
return None
# verify specified IP address is present on any interface on this system
# Allow to bind service to nonlocal address, if it virtaual-vrrp address
# or if address will be assign later
if 'local_host' in openvpn:
if not is_addr_assigned(openvpn['local_host']):
cmd('sysctl -w net.ipv4.ip_nonlocal_bind=1')
# No matching OpenVPN process running - maybe it got killed or none
# existed - nevertheless, spawn new OpenVPN process
action = 'reload-or-restart'
if 'restart_required' in openvpn:
action = 'restart'
call(f'systemctl {action} openvpn@{interface}.service')
conf = VTunIf.get_config()
conf['device_type'] = openvpn['device_type']
o = VTunIf(interface, **conf)
o.update(openvpn)
return None
if __name__ == '__main__':
try:
c = get_config()
verify(c)
generate(c)
apply(c)
except ConfigError as e:
print(e)
exit(1)
diff --git a/src/conf_mode/snmp.py b/src/conf_mode/snmp.py
index 25ff9d0dd..0be0da3d1 100755
--- a/src/conf_mode/snmp.py
+++ b/src/conf_mode/snmp.py
@@ -1,596 +1,596 @@
#!/usr/bin/env python3
#
# Copyright (C) 2018-2022 VyOS maintainers and contributors
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License version 2 or later as
# published by the Free Software Foundation.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import os
from sys import exit
from vyos.config import Config
from vyos.configverify import verify_vrf
from vyos.snmpv3_hashgen import plaintext_to_md5
from vyos.snmpv3_hashgen import plaintext_to_sha1
from vyos.snmpv3_hashgen import random
from vyos.template import render
from vyos.template import is_ipv4
from vyos.util import call
from vyos.util import chmod_755
from vyos.validate import is_addr_assigned
from vyos.version import get_version_data
from vyos import ConfigError
from vyos import airbag
airbag.enable()
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'/etc/systemd/system/snmpd.service.d/override.conf'
# SNMP OIDs used to mark auth/priv type
OIDs = {
'md5' : '.1.3.6.1.6.3.10.1.1.2',
'sha' : '.1.3.6.1.6.3.10.1.1.3',
'aes' : '.1.3.6.1.6.3.10.1.2.4',
'des' : '.1.3.6.1.6.3.10.1.2.2',
'none': '.1.3.6.1.6.3.10.1.2.1'
}
default_config_data = {
'listen_on': [],
'listen_address': [],
'ipv6_enabled': 'True',
'communities': [],
'smux_peers': [],
'location' : '',
'protocol' : 'udp',
'description' : '',
'contact' : '',
'trap_source': '',
'trap_targets': [],
'vyos_user': '',
'vyos_user_pass': '',
'version': '',
'v3_enabled': 'False',
'v3_engineid': '',
'v3_groups': [],
'v3_traps': [],
'v3_users': [],
'v3_views': [],
'script_ext': []
}
def rmfile(file):
if os.path.isfile(file):
os.unlink(file)
def get_config():
snmp = default_config_data
conf = Config()
if not conf.exists('service snmp'):
return None
else:
if conf.exists('system ipv6 disable'):
snmp['ipv6_enabled'] = False
conf.set_level('service snmp')
version_data = get_version_data()
snmp['version'] = version_data['version']
# create an internal snmpv3 user of the form 'vyosxxxxxxxxxxxxxxxx'
snmp['vyos_user'] = 'vyos' + random(8)
snmp['vyos_user_pass'] = random(16)
if conf.exists('community'):
for name in conf.list_nodes('community'):
community = {
'name': name,
'authorization': 'ro',
'network_v4': [],
'network_v6': [],
'has_source' : False
}
if conf.exists('community {0} authorization'.format(name)):
community['authorization'] = conf.return_value('community {0} authorization'.format(name))
# Subnet of SNMP client(s) allowed to contact system
if conf.exists('community {0} network'.format(name)):
for addr in conf.return_values('community {0} network'.format(name)):
if is_ipv4(addr):
community['network_v4'].append(addr)
else:
community['network_v6'].append(addr)
# IP address of SNMP client allowed to contact system
if conf.exists('community {0} client'.format(name)):
for addr in conf.return_values('community {0} client'.format(name)):
if is_ipv4(addr):
community['network_v4'].append(addr)
else:
community['network_v6'].append(addr)
if (len(community['network_v4']) > 0) or (len(community['network_v6']) > 0):
community['has_source'] = True
snmp['communities'].append(community)
if conf.exists('contact'):
snmp['contact'] = conf.return_value('contact')
if conf.exists('description'):
snmp['description'] = conf.return_value('description')
if conf.exists('listen-address'):
for addr in conf.list_nodes('listen-address'):
port = '161'
if conf.exists('listen-address {0} port'.format(addr)):
port = conf.return_value('listen-address {0} port'.format(addr))
snmp['listen_address'].append((addr, port))
# Always listen on localhost if an explicit address has been configured
# This is a safety measure to not end up with invalid listen addresses
- # that are not configured on this system. See https://phabricator.vyos.net/T850
+ # that are not configured on this system. See https://vyos.dev/T850
if not '127.0.0.1' in conf.list_nodes('listen-address'):
snmp['listen_address'].append(('127.0.0.1', '161'))
if not '::1' in conf.list_nodes('listen-address'):
snmp['listen_address'].append(('::1', '161'))
if conf.exists('location'):
snmp['location'] = conf.return_value('location')
if conf.exists('protocol'):
snmp['protocol'] = conf.return_value('protocol')
if conf.exists('smux-peer'):
snmp['smux_peers'] = conf.return_values('smux-peer')
if conf.exists('trap-source'):
snmp['trap_source'] = conf.return_value('trap-source')
if conf.exists('trap-target'):
for target in conf.list_nodes('trap-target'):
trap_tgt = {
'target': target,
'community': '',
'port': ''
}
if conf.exists('trap-target {0} community'.format(target)):
trap_tgt['community'] = conf.return_value('trap-target {0} community'.format(target))
if conf.exists('trap-target {0} port'.format(target)):
trap_tgt['port'] = conf.return_value('trap-target {0} port'.format(target))
snmp['trap_targets'].append(trap_tgt)
if conf.exists('script-extensions'):
for extname in conf.list_nodes('script-extensions extension-name'):
conf_script = conf.return_value('script-extensions extension-name {} script'.format(extname))
# if script has not absolute path, use pre configured path
if "/" not in conf_script:
conf_script = default_script_dir + conf_script
extension = {
'name': extname,
'script' : conf_script
}
snmp['script_ext'].append(extension)
if conf.exists('vrf'):
# Append key to dict but don't place it in the default dictionary.
# This is required to make the override.conf.tmpl work until we
# migrate to get_config_dict().
snmp['vrf'] = conf.return_value('vrf')
#########################################################################
# ____ _ _ __ __ ____ _____ #
# / ___|| \ | | \/ | _ \ __ _|___ / #
# \___ \| \| | |\/| | |_) | \ \ / / |_ \ #
# ___) | |\ | | | | __/ \ V / ___) | #
# |____/|_| \_|_| |_|_| \_/ |____/ #
# #
# now take care about the fancy SNMP v3 stuff, or bail out eraly #
#########################################################################
if not conf.exists('v3'):
return snmp
else:
snmp['v3_enabled'] = True
# 'set service snmp v3 engineid'
if conf.exists('v3 engineid'):
snmp['v3_engineid'] = conf.return_value('v3 engineid')
# 'set service snmp v3 group'
if conf.exists('v3 group'):
for group in conf.list_nodes('v3 group'):
v3_group = {
'name': group,
'mode': 'ro',
'seclevel': 'auth',
'view': ''
}
if conf.exists('v3 group {0} mode'.format(group)):
v3_group['mode'] = conf.return_value('v3 group {0} mode'.format(group))
if conf.exists('v3 group {0} seclevel'.format(group)):
v3_group['seclevel'] = conf.return_value('v3 group {0} seclevel'.format(group))
if conf.exists('v3 group {0} view'.format(group)):
v3_group['view'] = conf.return_value('v3 group {0} view'.format(group))
snmp['v3_groups'].append(v3_group)
# 'set service snmp v3 trap-target'
if conf.exists('v3 trap-target'):
for trap in conf.list_nodes('v3 trap-target'):
trap_cfg = {
'ipAddr': trap,
'secName': '',
'authProtocol': 'md5',
'authPassword': '',
'authMasterKey': '',
'privProtocol': 'des',
'privPassword': '',
'privMasterKey': '',
'ipProto': 'udp',
'ipPort': '162',
'type': '',
'secLevel': 'noAuthNoPriv'
}
if conf.exists('v3 trap-target {0} user'.format(trap)):
# Set the securityName used for authenticated SNMPv3 messages.
trap_cfg['secName'] = conf.return_value('v3 trap-target {0} user'.format(trap))
if conf.exists('v3 trap-target {0} auth type'.format(trap)):
# Set the authentication protocol (MD5 or SHA) used for authenticated SNMPv3 messages
# cmdline option '-a'
trap_cfg['authProtocol'] = conf.return_value('v3 trap-target {0} auth type'.format(trap))
if conf.exists('v3 trap-target {0} auth plaintext-password'.format(trap)):
# Set the authentication pass phrase used for authenticated SNMPv3 messages.
# cmdline option '-A'
trap_cfg['authPassword'] = conf.return_value('v3 trap-target {0} auth plaintext-password'.format(trap))
if conf.exists('v3 trap-target {0} auth encrypted-password'.format(trap)):
# Sets the keys to be used for SNMPv3 transactions. These options allow you to set the master authentication keys.
# cmdline option '-3m'
trap_cfg['authMasterKey'] = conf.return_value('v3 trap-target {0} auth encrypted-password'.format(trap))
if conf.exists('v3 trap-target {0} privacy type'.format(trap)):
# Set the privacy protocol (DES or AES) used for encrypted SNMPv3 messages.
# cmdline option '-x'
trap_cfg['privProtocol'] = conf.return_value('v3 trap-target {0} privacy type'.format(trap))
if conf.exists('v3 trap-target {0} privacy plaintext-password'.format(trap)):
# Set the privacy pass phrase used for encrypted SNMPv3 messages.
# cmdline option '-X'
trap_cfg['privPassword'] = conf.return_value('v3 trap-target {0} privacy plaintext-password'.format(trap))
if conf.exists('v3 trap-target {0} privacy encrypted-password'.format(trap)):
# Sets the keys to be used for SNMPv3 transactions. These options allow you to set the master encryption keys.
# cmdline option '-3M'
trap_cfg['privMasterKey'] = conf.return_value('v3 trap-target {0} privacy encrypted-password'.format(trap))
if conf.exists('v3 trap-target {0} protocol'.format(trap)):
trap_cfg['ipProto'] = conf.return_value('v3 trap-target {0} protocol'.format(trap))
if conf.exists('v3 trap-target {0} port'.format(trap)):
trap_cfg['ipPort'] = conf.return_value('v3 trap-target {0} port'.format(trap))
if conf.exists('v3 trap-target {0} type'.format(trap)):
trap_cfg['type'] = conf.return_value('v3 trap-target {0} type'.format(trap))
# Determine securityLevel used for SNMPv3 messages (noAuthNoPriv|authNoPriv|authPriv).
# Appropriate pass phrase(s) must provided when using any level higher than noAuthNoPriv.
if trap_cfg['authPassword'] or trap_cfg['authMasterKey']:
if trap_cfg['privProtocol'] or trap_cfg['privPassword']:
trap_cfg['secLevel'] = 'authPriv'
else:
trap_cfg['secLevel'] = 'authNoPriv'
snmp['v3_traps'].append(trap_cfg)
# 'set service snmp v3 user'
if conf.exists('v3 user'):
for user in conf.list_nodes('v3 user'):
user_cfg = {
'name': user,
'authMasterKey': '',
'authPassword': '',
'authProtocol': 'md5',
'authOID': 'none',
'group': '',
'mode': 'ro',
'privMasterKey': '',
'privPassword': '',
'privOID': '',
'privProtocol': 'des'
}
# v3 user {0} auth
if conf.exists('v3 user {0} auth encrypted-password'.format(user)):
user_cfg['authMasterKey'] = conf.return_value('v3 user {0} auth encrypted-password'.format(user))
if conf.exists('v3 user {0} auth plaintext-password'.format(user)):
user_cfg['authPassword'] = conf.return_value('v3 user {0} auth plaintext-password'.format(user))
# load default value
type = user_cfg['authProtocol']
if conf.exists('v3 user {0} auth type'.format(user)):
type = conf.return_value('v3 user {0} auth type'.format(user))
# (re-)update with either default value or value from CLI
user_cfg['authProtocol'] = type
user_cfg['authOID'] = OIDs[type]
# v3 user {0} group
if conf.exists('v3 user {0} group'.format(user)):
user_cfg['group'] = conf.return_value('v3 user {0} group'.format(user))
# v3 user {0} mode
if conf.exists('v3 user {0} mode'.format(user)):
user_cfg['mode'] = conf.return_value('v3 user {0} mode'.format(user))
# v3 user {0} privacy
if conf.exists('v3 user {0} privacy encrypted-password'.format(user)):
user_cfg['privMasterKey'] = conf.return_value('v3 user {0} privacy encrypted-password'.format(user))
if conf.exists('v3 user {0} privacy plaintext-password'.format(user)):
user_cfg['privPassword'] = conf.return_value('v3 user {0} privacy plaintext-password'.format(user))
# load default value
type = user_cfg['privProtocol']
if conf.exists('v3 user {0} privacy type'.format(user)):
type = conf.return_value('v3 user {0} privacy type'.format(user))
# (re-)update with either default value or value from CLI
user_cfg['privProtocol'] = type
user_cfg['privOID'] = OIDs[type]
snmp['v3_users'].append(user_cfg)
# 'set service snmp v3 view'
if conf.exists('v3 view'):
for view in conf.list_nodes('v3 view'):
view_cfg = {
'name': view,
'oids': []
}
if conf.exists('v3 view {0} oid'.format(view)):
for oid in conf.list_nodes('v3 view {0} oid'.format(view)):
oid_cfg = {
'oid': oid
}
view_cfg['oids'].append(oid_cfg)
snmp['v3_views'].append(view_cfg)
return snmp
def verify(snmp):
if snmp is None:
# we can not delete SNMP when LLDP is configured with SNMP
conf = Config()
if conf.exists('service lldp snmp enable'):
raise ConfigError('Can not delete SNMP service, as LLDP still uses SNMP!')
return None
### check if the configured script actually exist
if snmp['script_ext']:
for ext in snmp['script_ext']:
if not os.path.isfile(ext['script']):
print ("WARNING: script: {} doesn't exist".format(ext['script']))
else:
chmod_755(ext['script'])
for listen in snmp['listen_address']:
addr = listen[0]
port = listen[1]
protocol = snmp['protocol']
tmp = None
if is_ipv4(addr):
# example: udp:127.0.0.1:161
tmp = f'{protocol}:{addr}:{port}'
elif snmp['ipv6_enabled']:
# example: udp6:[::1]:161
tmp = f'{protocol}6:[{addr}]:{port}'
# We only wan't to configure addresses that exist on the system.
# Hint the user if they don't exist
if is_addr_assigned(addr):
if tmp: snmp['listen_on'].append(tmp)
else:
print(f'WARNING: SNMP listen address {addr} not configured!')
verify_vrf(snmp)
# bail out early if SNMP v3 is not configured
if not snmp['v3_enabled']:
return None
if 'v3_groups' in snmp.keys():
for group in snmp['v3_groups']:
#
# A view must exist prior to mapping it into a group
#
if 'view' in group.keys():
error = True
if 'v3_views' in snmp.keys():
for view in snmp['v3_views']:
if view['name'] == group['view']:
error = False
if error:
raise ConfigError('You must create view "{0}" first'.format(group['view']))
else:
raise ConfigError('"view" must be specified')
if not 'mode' in group.keys():
raise ConfigError('"mode" must be specified')
if not 'seclevel' in group.keys():
raise ConfigError('"seclevel" must be specified')
if 'v3_traps' in snmp.keys():
for trap in snmp['v3_traps']:
if trap['authPassword'] and trap['authMasterKey']:
raise ConfigError('Must specify only one of encrypted-password/plaintext-key for trap auth')
if trap['authPassword'] == '' and trap['authMasterKey'] == '':
raise ConfigError('Must specify encrypted-password or plaintext-key for trap auth')
if trap['privPassword'] and trap['privMasterKey']:
raise ConfigError('Must specify only one of encrypted-password/plaintext-key for trap privacy')
if trap['privPassword'] == '' and trap['privMasterKey'] == '':
raise ConfigError('Must specify encrypted-password or plaintext-key for trap privacy')
if not 'type' in trap.keys():
raise ConfigError('v3 trap: "type" must be specified')
if not 'authPassword' and 'authMasterKey' in trap.keys():
raise ConfigError('v3 trap: "auth" must be specified')
if not 'authProtocol' in trap.keys():
raise ConfigError('v3 trap: "protocol" must be specified')
if not 'privPassword' and 'privMasterKey' in trap.keys():
raise ConfigError('v3 trap: "user" must be specified')
if 'v3_users' in snmp.keys():
for user in snmp['v3_users']:
#
# Group must exist prior to mapping it into a group
# seclevel will be extracted from group
#
if 'group' not in user or user['group'] == '':
username = user['name']
raise ConfigError(f'Group membership required for user "{username}"!')
if user['group']:
error = True
if 'v3_groups' in snmp.keys():
for group in snmp['v3_groups']:
if group['name'] == user['group']:
seclevel = group['seclevel']
error = False
if error:
raise ConfigError('You must create group "{0}" first'.format(user['group']))
# Depending on the configured security level the user has to provide additional info
if (not user['authPassword'] and not user['authMasterKey']):
raise ConfigError('Must specify encrypted-password or plaintext-key for user auth')
if user['privPassword'] == '' and user['privMasterKey'] == '':
raise ConfigError('Must specify encrypted-password or plaintext-key for user privacy')
if user['mode'] == '':
raise ConfigError('Must specify user mode ro/rw')
if 'v3_views' in snmp.keys():
for view in snmp['v3_views']:
if not view['oids']:
raise ConfigError('Must configure an oid')
return None
def generate(snmp):
#
# As we are manipulating the snmpd user database we have to stop it first!
# This is even save if service is going to be removed
call('systemctl stop snmpd.service')
config_files = [config_file_client, config_file_daemon, config_file_access,
config_file_user, systemd_override]
for file in config_files:
rmfile(file)
if not snmp:
return None
if 'v3_users' in snmp.keys():
# net-snmp is now regenerating the configuration file in the background
# thus we need to re-open and re-read the file as the content changed.
# After that we can no read the encrypted password from the config and
# replace the CLI plaintext password with its encrypted version.
os.environ["vyos_libexec_dir"] = "/usr/libexec/vyos"
for user in snmp['v3_users']:
if user['authProtocol'] == 'sha':
hash = plaintext_to_sha1
else:
hash = plaintext_to_md5
if user['authPassword']:
user['authMasterKey'] = hash(user['authPassword'], snmp['v3_engineid'])
user['authPassword'] = ''
call('/opt/vyatta/sbin/my_set service snmp v3 user "{name}" auth encrypted-password "{authMasterKey}" > /dev/null'.format(**user))
call('/opt/vyatta/sbin/my_delete service snmp v3 user "{name}" auth plaintext-password > /dev/null'.format(**user))
if user['privPassword']:
user['privMasterKey'] = hash(user['privPassword'], snmp['v3_engineid'])
user['privPassword'] = ''
call('/opt/vyatta/sbin/my_set service snmp v3 user "{name}" privacy encrypted-password "{privMasterKey}" > /dev/null'.format(**user))
call('/opt/vyatta/sbin/my_delete service snmp v3 user "{name}" privacy plaintext-password > /dev/null'.format(**user))
# Write client config file
render(config_file_client, 'snmp/etc.snmp.conf.tmpl', snmp)
# Write server config file
render(config_file_daemon, 'snmp/etc.snmpd.conf.tmpl', snmp)
# Write access rights config file
render(config_file_access, 'snmp/usr.snmpd.conf.tmpl', snmp)
# Write access rights config file
render(config_file_user, 'snmp/var.snmpd.conf.tmpl', snmp)
# Write daemon configuration file
render(systemd_override, 'snmp/override.conf.tmpl', snmp)
return None
def apply(snmp):
# Always reload systemd manager configuration
call('systemctl daemon-reload')
if not snmp:
return None
# start SNMP daemon
call('systemctl restart snmpd.service')
# Enable AgentX in FRR
call('vtysh -c "configure terminal" -c "agentx" >/dev/null')
return None
if __name__ == '__main__':
try:
c = get_config()
verify(c)
generate(c)
apply(c)
except ConfigError as e:
print(e)
exit(1)
diff --git a/src/etc/commit/post-hooks.d/00vyos-sync b/src/etc/commit/post-hooks.d/00vyos-sync
index e3bde3abb..8ec732df0 100755
--- a/src/etc/commit/post-hooks.d/00vyos-sync
+++ b/src/etc/commit/post-hooks.d/00vyos-sync
@@ -1,7 +1,7 @@
#!/bin/sh
# When power is lost right after a commit modified files, the
# system can be corrupted and e.g. login is no longer possible.
# Always sync files to the backend storage after a commit.
-# https://phabricator.vyos.net/T4975
+# https://vyos.dev/T4975
sync
diff --git a/src/migration-scripts/interfaces/0-to-1 b/src/migration-scripts/interfaces/0-to-1
index ee4d6b82c..c7f324661 100755
--- a/src/migration-scripts/interfaces/0-to-1
+++ b/src/migration-scripts/interfaces/0-to-1
@@ -1,118 +1,118 @@
#!/usr/bin/env python3
# Change syntax of bridge interface
# - move interface based bridge-group to actual bridge (de-nest)
# - make stp and igmp-snooping nodes valueless
-# https://phabricator.vyos.net/T1556
+# https://vyos.dev/T1556
import sys
from vyos.configtree import ConfigTree
def migrate_bridge(config, tree, intf):
# check if bridge-group exists
tree_bridge = tree + ['bridge-group']
if config.exists(tree_bridge):
bridge = config.return_value(tree_bridge + ['bridge'])
# create new bridge member interface
config.set(base + [bridge, 'member', 'interface', intf])
# format as tag node to avoid loading problems
config.set_tag(base + [bridge, 'member', 'interface'])
# cost: migrate if configured
tree_cost = tree + ['bridge-group', 'cost']
if config.exists(tree_cost):
cost = config.return_value(tree_cost)
# set new node
config.set(base + [bridge, 'member', 'interface', intf, 'cost'], value=cost)
# priority: migrate if configured
tree_priority = tree + ['bridge-group', 'priority']
if config.exists(tree_priority):
priority = config.return_value(tree_priority)
# set new node
config.set(base + [bridge, 'member', 'interface', intf, 'priority'], value=priority)
# Delete the old bridge-group assigned to an interface
config.delete(tree_bridge)
if __name__ == '__main__':
if (len(sys.argv) < 1):
print("Must specify file name!")
sys.exit(1)
file_name = sys.argv[1]
with open(file_name, 'r') as f:
config_file = f.read()
config = ConfigTree(config_file)
base = ['interfaces', 'bridge']
if not config.exists(base):
# Nothing to do
sys.exit(0)
else:
#
# make stp and igmp-snooping nodes valueless
#
for br in config.list_nodes(base):
# STP: check if enabled
if config.exists(base + [br, 'stp']):
stp_val = config.return_value(base + [br, 'stp'])
# STP: delete node with old syntax
config.delete(base + [br, 'stp'])
# STP: set new node - if enabled
if stp_val == "true":
config.set(base + [br, 'stp'], value=None)
# igmp-snooping: check if enabled
if config.exists(base + [br, 'igmp-snooping', 'querier']):
igmp_val = config.return_value(base + [br, 'igmp-snooping', 'querier'])
# igmp-snooping: delete node with old syntax
config.delete(base + [br, 'igmp-snooping', 'querier'])
# igmp-snooping: set new node - if enabled
if igmp_val == "enable":
config.set(base + [br, 'igmp', 'querier'], value=None)
#
# move interface based bridge-group to actual bridge (de-nest)
#
bridge_types = ['bonding', 'ethernet', 'l2tpv3', 'openvpn', 'vxlan', 'wireless']
for type in bridge_types:
if not config.exists(['interfaces', type]):
continue
for interface in config.list_nodes(['interfaces', type]):
# check if bridge-group exists
bridge_group = ['interfaces', type, interface]
if config.exists(bridge_group + ['bridge-group']):
migrate_bridge(config, bridge_group, interface)
# We also need to migrate VLAN interfaces
vlan_base = ['interfaces', type, interface, 'vif']
if config.exists(vlan_base):
for vlan in config.list_nodes(vlan_base):
intf = "{}.{}".format(interface, vlan)
migrate_bridge(config, vlan_base + [vlan], intf)
# And then we have service VLANs (vif-s) interfaces
vlan_base = ['interfaces', type, interface, 'vif-s']
if config.exists(vlan_base):
for vif_s in config.list_nodes(vlan_base):
intf = "{}.{}".format(interface, vif_s)
migrate_bridge(config, vlan_base + [vif_s], intf)
# Every service VLAN can have multiple customer VLANs (vif-c)
vlan_c = ['interfaces', type, interface, 'vif-s', vif_s, 'vif-c']
if config.exists(vlan_c):
for vif_c in config.list_nodes(vlan_c):
intf = "{}.{}.{}".format(interface, vif_s, vif_c)
migrate_bridge(config, vlan_c + [vif_c], intf)
try:
with open(file_name, 'w') as f:
f.write(config.to_string())
except OSError as e:
print("Failed to save the modified config: {}".format(e))
sys.exit(1)
diff --git a/src/migration-scripts/interfaces/1-to-2 b/src/migration-scripts/interfaces/1-to-2
index 050137318..c75404d85 100755
--- a/src/migration-scripts/interfaces/1-to-2
+++ b/src/migration-scripts/interfaces/1-to-2
@@ -1,63 +1,63 @@
#!/usr/bin/env python3
# Change syntax of bond interface
# - move interface based bond-group to actual bond (de-nest)
-# https://phabricator.vyos.net/T1614
+# https://vyos.dev/T1614
import sys
from vyos.configtree import ConfigTree
if (len(sys.argv) < 1):
print("Must specify file name!")
sys.exit(1)
file_name = sys.argv[1]
with open(file_name, 'r') as f:
config_file = f.read()
config = ConfigTree(config_file)
base = ['interfaces', 'bonding']
if not config.exists(base):
# Nothing to do
sys.exit(0)
else:
#
# move interface based bond-group to actual bond (de-nest)
#
for intf in config.list_nodes(['interfaces', 'ethernet']):
# check if bond-group exists
if config.exists(['interfaces', 'ethernet', intf, 'bond-group']):
# get configured bond interface
bond = config.return_value(['interfaces', 'ethernet', intf, 'bond-group'])
# delete old interface asigned (nested) bond group
config.delete(['interfaces', 'ethernet', intf, 'bond-group'])
# create new bond member interface
config.set(base + [bond, 'member', 'interface'], value=intf, replace=False)
#
# some combinations were allowed in the past from a CLI perspective
# but the kernel overwrote them - remove from CLI to not confuse the users.
# In addition new consitency checks are in place so users can't repeat the
- # mistake. One of those nice issues is https://phabricator.vyos.net/T532
+ # mistake. One of those nice issues is https://vyos.dev/T532
for bond in config.list_nodes(base):
if config.exists(base + [bond, 'arp-monitor', 'interval']) and config.exists(base + [bond, 'mode']):
mode = config.return_value(base + [bond, 'mode'])
if mode in ['802.3ad', 'transmit-load-balance', 'adaptive-load-balance']:
intvl = int(config.return_value(base + [bond, 'arp-monitor', 'interval']))
if intvl > 0:
# this is not allowed and the linux kernel replies with:
# option arp_interval: mode dependency failed, not supported in mode 802.3ad(4)
# option arp_interval: mode dependency failed, not supported in mode balance-alb(6)
# option arp_interval: mode dependency failed, not supported in mode balance-tlb(5)
#
# so we simply disable arp_interval by setting it to 0 and miimon will take care about the link
config.set(base + [bond, 'arp-monitor', 'interval'], value='0')
try:
with open(file_name, 'w') as f:
f.write(config.to_string())
except OSError as e:
print("Failed to save the modified config: {}".format(e))
sys.exit(1)
diff --git a/src/migration-scripts/interfaces/16-to-17 b/src/migration-scripts/interfaces/16-to-17
index a6b4c7663..d123be06f 100755
--- a/src/migration-scripts/interfaces/16-to-17
+++ b/src/migration-scripts/interfaces/16-to-17
@@ -1,52 +1,52 @@
#!/usr/bin/env python3
#
# Copyright (C) 2020 VyOS maintainers and contributors
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License version 2 or later as
# published by the Free Software Foundation.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
# Command line migration of port mirroring
-# https://phabricator.vyos.net/T3089
+# https://vyos.dev/T3089
import sys
from vyos.configtree import ConfigTree
if __name__ == '__main__':
if (len(sys.argv) < 1):
print("Must specify file name!")
sys.exit(1)
file_name = sys.argv[1]
with open(file_name, 'r') as f:
config_file = f.read()
config = ConfigTree(config_file)
base = ['interfaces', 'ethernet']
if not config.exists(base):
# Nothing to do
sys.exit(0)
for interface in config.list_nodes(base):
mirror_old_base = base + [interface, 'mirror']
if config.exists(mirror_old_base):
intf = config.return_values(mirror_old_base)
if config.exists(mirror_old_base):
config.delete(mirror_old_base)
config.set(mirror_old_base + ['ingress'],intf[0])
try:
with open(file_name, 'w') as f:
f.write(config.to_string())
except OSError as e:
print("Failed to save the modified config: {}".format(e))
sys.exit(1)
diff --git a/src/migration-scripts/interfaces/2-to-3 b/src/migration-scripts/interfaces/2-to-3
index a63a54cdf..68d41de39 100755
--- a/src/migration-scripts/interfaces/2-to-3
+++ b/src/migration-scripts/interfaces/2-to-3
@@ -1,43 +1,43 @@
#!/usr/bin/env python3
# Change syntax of openvpn encryption settings
# - move cipher from encryption to encryption cipher
-# https://phabricator.vyos.net/T1704
+# https://vyos.dev/T1704
import sys
from vyos.configtree import ConfigTree
if (len(sys.argv) < 1):
print("Must specify file name!")
sys.exit(1)
file_name = sys.argv[1]
with open(file_name, 'r') as f:
config_file = f.read()
config = ConfigTree(config_file)
base = ['interfaces', 'openvpn']
if not config.exists(base):
# Nothing to do
sys.exit(0)
else:
#
# move cipher from "encryption" to "encryption cipher"
#
for intf in config.list_nodes(['interfaces', 'openvpn']):
# Check if encryption is set
if config.exists(['interfaces', 'openvpn', intf, 'encryption']):
# Get cipher used
cipher = config.return_value(['interfaces', 'openvpn', intf, 'encryption'])
# Delete old syntax
config.delete(['interfaces', 'openvpn', intf, 'encryption'])
# Add new syntax to config
config.set(['interfaces', 'openvpn', intf, 'encryption', 'cipher'], value=cipher)
try:
with open(file_name, 'w') as f:
f.write(config.to_string())
except OSError as e:
print("Failed to save the modified config: {}".format(e))
sys.exit(1)
diff --git a/src/migration-scripts/interfaces/20-to-21 b/src/migration-scripts/interfaces/20-to-21
index 0bd858760..cb1c36882 100755
--- a/src/migration-scripts/interfaces/20-to-21
+++ b/src/migration-scripts/interfaces/20-to-21
@@ -1,120 +1,120 @@
#!/usr/bin/env python3
#
# Copyright (C) 2021 VyOS maintainers and contributors
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License version 2 or later as
# published by the Free Software Foundation.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
# T3619: mirror Linux Kernel defaults for ethernet offloading options into VyOS
-# CLI. See https://phabricator.vyos.net/T3619#102254 for all the details.
+# CLI. See https://vyos.dev/T3619#102254 for all the details.
# T3787: Remove deprecated UDP fragmentation offloading option
from sys import argv
from vyos.ethtool import Ethtool
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 = ['interfaces', 'ethernet']
config = ConfigTree(config_file)
if not config.exists(base):
exit(0)
for ifname in config.list_nodes(base):
eth = Ethtool(ifname)
# If GRO is enabled by the Kernel - we reflect this on the CLI. If GRO is
# enabled via CLI but not supported by the NIC - we remove it from the CLI
configured = config.exists(base + [ifname, 'offload', 'gro'])
enabled, fixed = eth.get_generic_receive_offload()
if configured and fixed:
config.delete(base + [ifname, 'offload', 'gro'])
elif enabled and not fixed:
config.set(base + [ifname, 'offload', 'gro'])
# If GSO is enabled by the Kernel - we reflect this on the CLI. If GSO is
# enabled via CLI but not supported by the NIC - we remove it from the CLI
configured = config.exists(base + [ifname, 'offload', 'gso'])
enabled, fixed = eth.get_generic_segmentation_offload()
if configured and fixed:
config.delete(base + [ifname, 'offload', 'gso'])
elif enabled and not fixed:
config.set(base + [ifname, 'offload', 'gso'])
# If LRO is enabled by the Kernel - we reflect this on the CLI. If LRO is
# enabled via CLI but not supported by the NIC - we remove it from the CLI
configured = config.exists(base + [ifname, 'offload', 'lro'])
enabled, fixed = eth.get_large_receive_offload()
if configured and fixed:
config.delete(base + [ifname, 'offload', 'lro'])
elif enabled and not fixed:
config.set(base + [ifname, 'offload', 'lro'])
# If SG is enabled by the Kernel - we reflect this on the CLI. If SG is
# enabled via CLI but not supported by the NIC - we remove it from the CLI
configured = config.exists(base + [ifname, 'offload', 'sg'])
enabled, fixed = eth.get_scatter_gather()
if configured and fixed:
config.delete(base + [ifname, 'offload', 'sg'])
elif enabled and not fixed:
config.set(base + [ifname, 'offload', 'sg'])
# If TSO is enabled by the Kernel - we reflect this on the CLI. If TSO is
# enabled via CLI but not supported by the NIC - we remove it from the CLI
configured = config.exists(base + [ifname, 'offload', 'tso'])
enabled, fixed = eth.get_tcp_segmentation_offload()
if configured and fixed:
config.delete(base + [ifname, 'offload', 'tso'])
elif enabled and not fixed:
config.set(base + [ifname, 'offload', 'tso'])
# Remove deprecated UDP fragmentation offloading option
if config.exists(base + [ifname, 'offload', 'ufo']):
config.delete(base + [ifname, 'offload', 'ufo'])
# Also while processing the interface configuration, not all adapters support
# changing the speed and duplex settings. If the desired speed and duplex
# values do not work for the NIC driver, we change them back to the default
# value of "auto" - which will be applied if the CLI node is deleted.
speed_path = base + [ifname, 'speed']
duplex_path = base + [ifname, 'duplex']
# speed and duplex must always be set at the same time if not set to "auto"
if config.exists(speed_path) and config.exists(duplex_path):
speed = config.return_value(speed_path)
duplex = config.return_value(duplex_path)
if speed != 'auto' and duplex != 'auto':
if not eth.check_speed_duplex(speed, duplex):
config.delete(speed_path)
config.delete(duplex_path)
# Also while processing the interface configuration, not all adapters support
# changing disabling flow-control - or change this setting. If disabling
# flow-control is not supported by the NIC, we remove the setting from CLI
flow_control_path = base + [ifname, 'disable-flow-control']
if config.exists(flow_control_path):
if not eth.check_flow_control():
config.delete(flow_control_path)
try:
with open(file_name, 'w') as f:
f.write(config.to_string())
except OSError as e:
print("Failed to save the modified config: {}".format(e))
exit(1)
diff --git a/src/migration-scripts/interfaces/4-to-5 b/src/migration-scripts/interfaces/4-to-5
index 2a42c60ff..f645c5aeb 100755
--- a/src/migration-scripts/interfaces/4-to-5
+++ b/src/migration-scripts/interfaces/4-to-5
@@ -1,112 +1,112 @@
#!/usr/bin/env python3
# De-nest PPPoE interfaces
# Migrate boolean nodes to valueless
import sys
from vyos.configtree import ConfigTree
def migrate_dialer(config, tree, intf):
for pppoe in config.list_nodes(tree):
# assemble string, 0 -> pppoe0
new_base = ['interfaces', 'pppoe']
pppoe_base = new_base + ['pppoe' + pppoe]
config.set(new_base)
# format as tag node to avoid loading problems
config.set_tag(new_base)
# Copy the entire old node to the new one before migrating individual
# parts
config.copy(tree + [pppoe], pppoe_base)
# Instead of letting the user choose between auto and none
# where auto is default, it makes more sesne to just offer
# an option to disable the default behavior (declutter CLI)
if config.exists(pppoe_base + ['name-server']):
tmp = config.return_value(pppoe_base + ['name-server'])
if tmp == "none":
config.set(pppoe_base + ['no-peer-dns'])
config.delete(pppoe_base + ['name-server'])
# Migrate user-id and password nodes under an 'authentication'
# node
if config.exists(pppoe_base + ['user-id']):
user = config.return_value(pppoe_base + ['user-id'])
config.set(pppoe_base + ['authentication', 'user'], value=user)
config.delete(pppoe_base + ['user-id'])
if config.exists(pppoe_base + ['password']):
pwd = config.return_value(pppoe_base + ['password'])
config.set(pppoe_base + ['authentication', 'password'], value=pwd)
config.delete(pppoe_base + ['password'])
# remove enable-ipv6 node and rather place it under ipv6 node
if config.exists(pppoe_base + ['enable-ipv6']):
config.set(pppoe_base + ['ipv6', 'enable'])
config.delete(pppoe_base + ['enable-ipv6'])
# Source interface migration
config.set(pppoe_base + ['source-interface'], value=intf)
# Remove IPv6 router-advert nodes as this makes no sense on a
# client diale rinterface to send RAs back into the network
- # https://phabricator.vyos.net/T2055
+ # https://vyos.dev/T2055
ipv6_ra = pppoe_base + ['ipv6', 'router-advert']
if config.exists(ipv6_ra):
config.delete(ipv6_ra)
if __name__ == '__main__':
if (len(sys.argv) < 1):
print("Must specify file name!")
exit(1)
file_name = sys.argv[1]
with open(file_name, 'r') as f:
config_file = f.read()
config = ConfigTree(config_file)
pppoe_links = ['bonding', 'ethernet']
for link_type in pppoe_links:
if not config.exists(['interfaces', link_type]):
continue
for interface in config.list_nodes(['interfaces', link_type]):
# check if PPPoE exists
base_if = ['interfaces', link_type, interface]
pppoe_if = base_if + ['pppoe']
if config.exists(pppoe_if):
for dialer in config.list_nodes(pppoe_if):
migrate_dialer(config, pppoe_if, interface)
# Delete old PPPoE interface
config.delete(pppoe_if)
# bail out early if there are no VLAN interfaces to migrate
if not config.exists(base_if + ['vif']):
continue
# Migrate PPPoE interfaces attached to a VLAN
for vlan in config.list_nodes(base_if + ['vif']):
vlan_if = base_if + ['vif', vlan]
pppoe_if = vlan_if + ['pppoe']
if config.exists(pppoe_if):
for dialer in config.list_nodes(pppoe_if):
intf = "{}.{}".format(interface, vlan)
migrate_dialer(config, pppoe_if, intf)
# Delete old PPPoE interface
config.delete(pppoe_if)
# Add interface description that this is required for PPPoE
if not config.exists(vlan_if + ['description']):
config.set(vlan_if + ['description'], value='PPPoE link interface')
try:
with open(file_name, 'w') as f:
f.write(config.to_string())
except OSError as e:
print("Failed to save the modified config: {}".format(e))
sys.exit(1)
diff --git a/src/migration-scripts/snmp/0-to-1 b/src/migration-scripts/snmp/0-to-1
index a836f7011..096ba779d 100755
--- a/src/migration-scripts/snmp/0-to-1
+++ b/src/migration-scripts/snmp/0-to-1
@@ -1,56 +1,56 @@
#!/usr/bin/env python3
#
# Copyright (C) 2019 VyOS maintainers and contributors
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License version 2 or later as
# published by the Free Software Foundation.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import sys
from vyos.configtree import ConfigTree
if (len(sys.argv) < 1):
print("Must specify file name!")
sys.exit(1)
file_name = sys.argv[1]
with open(file_name, 'r') as f:
config_file = f.read()
config = ConfigTree(config_file)
config_base = ['service', 'snmp', 'v3']
if not config.exists(config_base):
# Nothing to do
sys.exit(0)
else:
- # we no longer support a per trap target engine ID (https://phabricator.vyos.net/T818)
+ # we no longer support a per trap target engine ID (https://vyos.dev/T818)
if config.exists(config_base + ['v3', 'trap-target']):
for target in config.list_nodes(config_base + ['v3', 'trap-target']):
config.delete(config_base + ['v3', 'trap-target', target, 'engineid'])
- # we no longer support a per user engine ID (https://phabricator.vyos.net/T818)
+ # we no longer support a per user engine ID (https://vyos.dev/T818)
if config.exists(config_base + ['v3', 'user']):
for user in config.list_nodes(config_base + ['v3', 'user']):
config.delete(config_base + ['v3', 'user', user, 'engineid'])
# we drop TSM support as there seem to be no users and this code is untested
- # https://phabricator.vyos.net/T1769
+ # https://vyos.dev/T1769
if config.exists(config_base + ['v3', 'tsm']):
config.delete(config_base + ['v3', 'tsm'])
try:
with open(file_name, 'w') as f:
f.write(config.to_string())
except OSError as e:
print("Failed to save the modified config: {}".format(e))
sys.exit(1)
File Metadata
Details
Attached
Mime Type
text/x-diff
Expires
Fri, Nov 6, 8:12 AM (1 d, 15 h)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
4653366
Default Alt Text
(137 KB)
Attached To
Mode
rVYOSONEX vyos-1x
Attached
Detach File
Event Timeline
Log In to Comment