summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--python/vyos/airbag.py17
-rw-r--r--python/vyos/template.py10
-rw-r--r--python/vyos/util.py87
-rwxr-xr-xsrc/conf_mode/dhcp_relay.py5
-rwxr-xr-xsrc/conf_mode/dhcp_server.py5
-rwxr-xr-xsrc/conf_mode/dhcpv6_relay.py5
-rwxr-xr-xsrc/conf_mode/dhcpv6_server.py5
-rwxr-xr-xsrc/conf_mode/dns_forwarding.py4
-rwxr-xr-xsrc/conf_mode/dynamic_dns.py4
-rwxr-xr-xsrc/conf_mode/interfaces-pppoe.py24
-rwxr-xr-xsrc/conf_mode/interfaces-wirelessmodem.py23
-rwxr-xr-xsrc/conf_mode/salt-minion.py10
-rwxr-xr-xsrc/conf_mode/service_ipoe-server.py4
-rwxr-xr-xsrc/conf_mode/service_pppoe-server.py4
-rwxr-xr-xsrc/conf_mode/vpn_l2tp.py4
-rwxr-xr-xsrc/conf_mode/vpn_pptp.py4
-rwxr-xr-xsrc/conf_mode/vpn_sstp.py4
-rw-r--r--src/etc/dhcp/dhclient-enter-hooks.d/03-vyos-ipwrapper8
-rw-r--r--src/etc/dhcp/dhclient-exit-hooks.d/01-vyos-cleanup64
-rw-r--r--src/etc/dhcp/dhclient-exit-hooks.d/02-vyos-dhcp-renew-rfc3442148
20 files changed, 323 insertions, 116 deletions
diff --git a/python/vyos/airbag.py b/python/vyos/airbag.py
index 6698aa404..b7838d8a2 100644
--- a/python/vyos/airbag.py
+++ b/python/vyos/airbag.py
@@ -26,6 +26,17 @@ from vyos.version import get_full_version_data
DISABLE = False
+_noteworthy = []
+
+def noteworthy(msg):
+ """
+ noteworthy can be use to take note things which we may not want to
+ report to the user may but be worth including in bug report
+ if something goes wrong later on
+ """
+ _noteworthy.append(msg)
+
+
# emulate a file object
class _IO(object):
def __init__(self, std, log):
@@ -58,11 +69,16 @@ def bug_report(dtype, value, trace):
information = get_full_version_data()
trace = '\n'.join(format_exception(dtype, value, trace)).replace('\n\n','\n')
+ note = ''
+ if _noteworthy:
+ note = 'noteworthy:\n'
+ note += '\n'.join(_noteworthy)
information.update({
'date': datetime.now().strftime('%Y-%m-%d %H:%M:%S'),
'trace': trace,
'instructions': COMMUNITY if 'rolling' in get_version() else SUPPORTED,
+ 'note': note,
})
sys.stdout.write(INTRO.format(**information))
@@ -145,6 +161,7 @@ Hardware S/N: {hardware_serial}
Hardware UUID: {hardware_uuid}
{trace}
+{note}
"""
INTRO = """\
diff --git a/python/vyos/template.py b/python/vyos/template.py
index 6c73ce753..e4b253ed3 100644
--- a/python/vyos/template.py
+++ b/python/vyos/template.py
@@ -19,6 +19,7 @@ from jinja2 import Environment
from jinja2 import FileSystemLoader
from vyos.defaults import directories
+from vyos.util import chmod, chown, makedir
# reuse the same Environment to improve performance
@@ -32,7 +33,7 @@ _templates_mem = {
}
-def render(destination, template, content, trim_blocks=False, formater=None):
+def render(destination, template, content, trim_blocks=False, formater=None, permission=None, user=None, group=None):
"""
render a template from the template directory, it will raise on any errors
destination: the file where the rendered template must be saved
@@ -46,6 +47,10 @@ def render(destination, template, content, trim_blocks=False, formater=None):
(recovering the load time and overhead caused by having the file out of the code)
"""
+ # Create the directory if it does not exists
+ folder = os.path.dirname(destination)
+ makedir(folder, user, group)
+
# Setup a renderer for the given template
# This is cached and re-used for performance
if template not in _templates_mem[trim_blocks]:
@@ -63,3 +68,6 @@ def render(destination, template, content, trim_blocks=False, formater=None):
# Write client config file
with open(destination, 'w') as f:
f.write(content)
+
+ chmod(destination, permission)
+ chown(destination, user, group)
diff --git a/python/vyos/util.py b/python/vyos/util.py
index 4340332d3..307decc87 100644
--- a/python/vyos/util.py
+++ b/python/vyos/util.py
@@ -14,6 +14,7 @@
# License along with this library. If not, see <http://www.gnu.org/licenses/>.
import os
+import sys
#
# NOTE: Do not import full classes here, move your import to the function
@@ -25,7 +26,7 @@ import os
# which all have slighty different behaviour
from subprocess import Popen, PIPE, STDOUT, DEVNULL
def popen(command, flag='', shell=None, input=None, timeout=None, env=None,
- stdout=PIPE, stderr=None, decode=None):
+ stdout=PIPE, stderr=PIPE, decode='utf-8'):
"""
popen is a wrapper helper aound subprocess.Popen
with it default setting it will return a tuple (out, err)
@@ -48,12 +49,14 @@ def popen(command, flag='', shell=None, input=None, timeout=None, env=None,
- STDOUT, send the data to be merged with stdout
- DEVNULL, discard the output
decode: specify the expected text encoding (utf-8, ascii, ...)
+ the default is explicitely utf-8 which is python's own default
usage:
to get both stdout, and stderr: popen('command', stdout=PIPE, stderr=STDOUT)
to discard stdout and get stderr: popen('command', stdout=DEVNUL, stderr=PIPE)
"""
from vyos import debug
+ from vyos import airbag
# log if the flag is set, otherwise log if command is set
if not debug.enabled(flag):
flag = 'command'
@@ -77,27 +80,39 @@ def popen(command, flag='', shell=None, input=None, timeout=None, env=None,
stdin=stdin, stdout=stdout, stderr=stderr,
env=env, shell=use_shell,
)
- tmp = p.communicate(input, timeout)
- out1 = b''
- out2 = b''
+
+ pipe = p.communicate(input, timeout)
+
+ pipe_out = b''
if stdout == PIPE:
- out1 = tmp[0]
+ pipe_out = pipe[0]
+
+ pipe_err = b''
if stderr == PIPE:
- out2 += tmp[1]
- decoded1 = out1.decode(decode) if decode else out1.decode()
- decoded2 = out2.decode(decode) if decode else out2.decode()
- decoded1 = decoded1.replace('\r\n', '\n').strip()
- decoded2 = decoded2.replace('\r\n', '\n').strip()
- nl = '\n' if decoded1 and decoded2 else ''
- decoded = decoded1 + nl + decoded2
- if decoded:
- ret_msg = f"returned:\n{decoded}"
- debug.message(ret_msg, flag)
- return decoded, p.returncode
+ pipe_err = pipe[1]
+
+ str_out = pipe_out.decode(decode).replace('\r\n', '\n').strip()
+ str_err = pipe_err.decode(decode).replace('\r\n', '\n').strip()
+
+ out_msg = f"returned (out):\n{str_out}"
+ if str_out:
+ debug.message(out_msg, flag)
+
+ if str_err:
+ err_msg = f"returned (err):\n{str_err}"
+ # this message will also be send to syslog via airbag
+ debug.message(err_msg, flag, destination=sys.stderr)
+
+ # should something go wrong, report this too via airbag
+ airbag.noteworthy(cmd_msg)
+ airbag.noteworthy(out_msg)
+ airbag.noteworthy(err_msg)
+
+ return str_out, p.returncode
def run(command, flag='', shell=None, input=None, timeout=None, env=None,
- stdout=DEVNULL, stderr=None, decode=None):
+ stdout=DEVNULL, stderr=PIPE, decode='utf-8'):
"""
A wrapper around vyos.util.popen, which discard the stdout and
will return the error code of a command
@@ -113,14 +128,15 @@ def run(command, flag='', shell=None, input=None, timeout=None, env=None,
def cmd(command, flag='', shell=None, input=None, timeout=None, env=None,
- stdout=PIPE, stderr=None, decode=None,
- raising=None, message=''):
+ stdout=PIPE, stderr=PIPE, decode='utf-8',
+ raising=None, message='', expect=[0]):
"""
A wrapper around vyos.util.popen, which returns the stdout and
will raise the error code of a command
raising: specify which call should be used when raising (default is OSError)
the class should only require a string as parameter
+ expect: a list of error codes to consider as normal
"""
decoded, code = popen(
command, flag,
@@ -129,7 +145,7 @@ def cmd(command, flag='', shell=None, input=None, timeout=None, env=None,
env=env, shell=shell,
decode=decode,
)
- if code != 0:
+ if code not in expect:
feedback = message + '\n' if message else ''
feedback += f'failed to run command: {command}\n'
feedback += f'returned: {decoded}\n'
@@ -143,7 +159,7 @@ def cmd(command, flag='', shell=None, input=None, timeout=None, env=None,
def call(command, flag='', shell=None, input=None, timeout=None, env=None,
- stdout=PIPE, stderr=None, decode=None):
+ stdout=PIPE, stderr=PIPE, decode='utf-8'):
"""
A wrapper around vyos.util.popen, which print the stdout and
will return the error code of a command
@@ -197,10 +213,24 @@ def chown(path, user, group):
from pwd import getpwnam
from grp import getgrnam
- if os.path.exists(path):
- uid = getpwnam(user).pw_uid
- gid = getgrnam(group).gr_gid
- os.chown(path, uid, gid)
+ if user is None or group is None:
+ return False
+
+ if not os.path.exists(path):
+ return False
+
+ uid = getpwnam(user).pw_uid
+ gid = getgrnam(group).gr_gid
+ os.chown(path, uid, gid)
+ return True
+
+
+def chmod(path, bitmask):
+ if not os.path.exists(path):
+ return
+ if bitmask is None:
+ return
+ os.chmod(path, bitmask)
def chmod_600(path):
@@ -231,6 +261,13 @@ def chmod_755(path):
os.chmod(path, bitmask)
+def makedir(path, user=None, group=None):
+ if not os.path.exists(path):
+ return
+ os.mkdir(path)
+ chown(path, user, group)
+
+
def colon_separated_to_dict(data_string, uniquekeys=False):
""" Converts a string containing newline-separated entries
of colon-separated key-value pairs into a dict.
diff --git a/src/conf_mode/dhcp_relay.py b/src/conf_mode/dhcp_relay.py
index ce0e01308..d24a46220 100755
--- a/src/conf_mode/dhcp_relay.py
+++ b/src/conf_mode/dhcp_relay.py
@@ -98,11 +98,6 @@ def generate(relay):
if not relay:
return None
- # Create configuration directory on demand
- dirname = os.path.dirname(config_file)
- if not os.path.isdir(dirname):
- os.mkdir(dirname)
-
render(config_file, 'dhcp-relay/config.tmpl', relay)
return None
diff --git a/src/conf_mode/dhcp_server.py b/src/conf_mode/dhcp_server.py
index da01f16eb..1849ece0a 100755
--- a/src/conf_mode/dhcp_server.py
+++ b/src/conf_mode/dhcp_server.py
@@ -594,11 +594,6 @@ def generate(dhcp):
if not dhcp or dhcp['disabled']:
return None
- # Create configuration directory on demand
- dirname = os.path.dirname(config_file)
- if not os.path.isdir(dirname):
- os.mkdir(dirname)
-
# Please see: https://phabricator.vyos.net/T1129 for quoting of the raw parameters
# we can pass to ISC DHCPd
render(config_file, 'dhcp-server/dhcpd.conf.tmpl', dhcp,
diff --git a/src/conf_mode/dhcpv6_relay.py b/src/conf_mode/dhcpv6_relay.py
index cb5a4bbfb..ecc739063 100755
--- a/src/conf_mode/dhcpv6_relay.py
+++ b/src/conf_mode/dhcpv6_relay.py
@@ -84,11 +84,6 @@ def generate(relay):
if relay is None:
return None
- # Create configuration directory on demand
- dirname = os.path.dirname(config_file)
- if not os.path.isdir(dirname):
- os.mkdir(dirname)
-
render(config_file, 'dhcpv6-relay/config.tmpl', relay)
return None
diff --git a/src/conf_mode/dhcpv6_server.py b/src/conf_mode/dhcpv6_server.py
index ce98e39c3..07e936906 100755
--- a/src/conf_mode/dhcpv6_server.py
+++ b/src/conf_mode/dhcpv6_server.py
@@ -335,11 +335,6 @@ def generate(dhcpv6):
if not dhcpv6 or dhcpv6['disabled']:
return None
- # Create configuration directory on demand
- dirname = os.path.dirname(config_file)
- if not os.path.isdir(dirname):
- os.mkdir(dirname)
-
render(config_file, 'dhcpv6-server/dhcpdv6.conf.tmpl', dhcpv6)
return None
diff --git a/src/conf_mode/dns_forwarding.py b/src/conf_mode/dns_forwarding.py
index 567dfa4b3..7f7417b00 100755
--- a/src/conf_mode/dns_forwarding.py
+++ b/src/conf_mode/dns_forwarding.py
@@ -152,10 +152,6 @@ def generate(dns):
if dns is None:
return None
- dirname = os.path.dirname(config_file)
- if not os.path.exists(dirname):
- os.mkdir(dirname)
-
render(config_file, 'dns-forwarding/recursor.conf.tmpl', dns, trim_blocks=True)
return None
diff --git a/src/conf_mode/dynamic_dns.py b/src/conf_mode/dynamic_dns.py
index 038f77cf9..3386324ae 100755
--- a/src/conf_mode/dynamic_dns.py
+++ b/src/conf_mode/dynamic_dns.py
@@ -217,10 +217,6 @@ def generate(dyndns):
if dyndns['deleted']:
return None
- dirname = os.path.dirname(config_file)
- if not os.path.exists(dirname):
- os.mkdir(dirname)
-
render(config_file, 'dynamic-dns/ddclient.conf.tmpl', dyndns)
# Config file must be accessible only by its owner
diff --git a/src/conf_mode/interfaces-pppoe.py b/src/conf_mode/interfaces-pppoe.py
index f942b7d2f..dbc40d8d4 100755
--- a/src/conf_mode/interfaces-pppoe.py
+++ b/src/conf_mode/interfaces-pppoe.py
@@ -173,12 +173,6 @@ def generate(pppoe):
config_files = [config_pppoe, script_pppoe_pre_up, script_pppoe_ip_up,
script_pppoe_ip_down, script_pppoe_ipv6_up]
- # Ensure directories for config files exist - otherwise create them on demand
- for file in config_files:
- dirname = os.path.dirname(file)
- if not os.path.isdir(dirname):
- os.mkdir(dirname)
-
# Always hang-up PPPoE connection prior generating new configuration file
cmd(f'systemctl stop ppp@{intf}.service')
@@ -189,27 +183,23 @@ def generate(pppoe):
os.unlink(file)
else:
+ # generated script must be executable
+
# Create PPP configuration files
render(config_pppoe, 'pppoe/peer.tmpl',
- pppoe, trim_blocks=True)
+ pppoe, trim_blocks=True, permision=0o755)
# Create script for ip-pre-up.d
render(script_pppoe_pre_up, 'pppoe/ip-pre-up.script.tmpl',
- pppoe, trim_blocks=True)
+ pppoe, trim_blocks=True, permision=0o755)
# Create script for ip-up.d
render(script_pppoe_ip_up, 'pppoe/ip-up.script.tmpl',
- pppoe, trim_blocks=True)
+ pppoe, trim_blocks=True, permision=0o755)
# Create script for ip-down.d
render(script_pppoe_ip_down, 'pppoe/ip-down.script.tmpl',
- pppoe, trim_blocks=True)
+ pppoe, trim_blocks=True, permision=0o755)
# Create script for ipv6-up.d
render(script_pppoe_ipv6_up, 'pppoe/ipv6-up.script.tmpl',
- pppoe, trim_blocks=True)
-
- # make generated script file executable
- chmod_755(script_pppoe_pre_up)
- chmod_755(script_pppoe_ip_up)
- chmod_755(script_pppoe_ip_down)
- chmod_755(script_pppoe_ipv6_up)
+ pppoe, trim_blocks=True, permision=0o755)
return None
diff --git a/src/conf_mode/interfaces-wirelessmodem.py b/src/conf_mode/interfaces-wirelessmodem.py
index 163778e22..a3a2a2648 100755
--- a/src/conf_mode/interfaces-wirelessmodem.py
+++ b/src/conf_mode/interfaces-wirelessmodem.py
@@ -152,12 +152,6 @@ def generate(wwan):
config_files = [config_wwan, config_wwan_chat, script_wwan_pre_up,
script_wwan_ip_up, script_wwan_ip_down]
- # Ensure directories for config files exist - otherwise create them on demand
- for file in config_files:
- dirname = os.path.dirname(file)
- if not os.path.isdir(dirname):
- os.mkdir(dirname)
-
# Always hang-up WWAN connection prior generating new configuration file
cmd(f'systemctl stop ppp@{intf}.service')
@@ -172,17 +166,18 @@ def generate(wwan):
render(config_wwan, 'wwan/peer.tmpl', wwan)
# Create PPP chat script
render(config_wwan_chat, 'wwan/chat.tmpl', wwan)
+
+ # generated script file must be executable
+
# Create script for ip-pre-up.d
- render(script_wwan_pre_up, 'wwan/ip-pre-up.script.tmpl', wwan)
+ render(script_wwan_pre_up, 'wwan/ip-pre-up.script.tmpl',
+ wwan, permission=0o755)
# Create script for ip-up.d
- render(script_wwan_ip_up, 'wwan/ip-up.script.tmpl', wwan)
+ render(script_wwan_ip_up, 'wwan/ip-up.script.tmpl',
+ wwan, permission=0o755)
# Create script for ip-down.d
- render(script_wwan_ip_down, 'wwan/ip-down.script.tmpl', wwan)
-
- # make generated script file executable
- chmod_755(script_wwan_pre_up)
- chmod_755(script_wwan_ip_up)
- chmod_755(script_wwan_ip_down)
+ render(script_wwan_ip_down, 'wwan/ip-down.script.tmpl',
+ wwan, permission=0o755)
return None
diff --git a/src/conf_mode/salt-minion.py b/src/conf_mode/salt-minion.py
index dffe7fcd4..8bc35bb45 100755
--- a/src/conf_mode/salt-minion.py
+++ b/src/conf_mode/salt-minion.py
@@ -79,14 +79,8 @@ def generate(salt):
if not salt:
return None
- for file in [config_file, master_keyfile]:
- dirname = os.path.dirname(file)
- if not os.path.exists(dirname):
- os.mkdir(dirname)
- chown(dirname, salt['user'], salt['group'])
-
- render(config_file, 'salt-minion/minion.tmpl', salt)
- chown(config_file, salt['user'], salt['group'])
+ render(config_file, 'salt-minion/minion.tmpl', salt,
+ user=salt['user'], group=salt['group'])
if not os.path.exists(master_keyfile):
if salt['master_key']:
diff --git a/src/conf_mode/service_ipoe-server.py b/src/conf_mode/service_ipoe-server.py
index 17fa2c3f0..b53692d37 100755
--- a/src/conf_mode/service_ipoe-server.py
+++ b/src/conf_mode/service_ipoe-server.py
@@ -265,10 +265,6 @@ def generate(ipoe):
if not ipoe:
return None
- dirname = os.path.dirname(ipoe_conf)
- if not os.path.exists(dirname):
- os.mkdir(dirname)
-
render(ipoe_conf, 'accel-ppp/ipoe.config.tmpl', ipoe, trim_blocks=True)
if ipoe['auth_mode'] == 'local':
diff --git a/src/conf_mode/service_pppoe-server.py b/src/conf_mode/service_pppoe-server.py
index 518c7633a..e05b0ab2a 100755
--- a/src/conf_mode/service_pppoe-server.py
+++ b/src/conf_mode/service_pppoe-server.py
@@ -429,10 +429,6 @@ def generate(pppoe):
if not pppoe:
return None
- dirname = os.path.dirname(pppoe_conf)
- if not os.path.exists(dirname):
- os.mkdir(dirname)
-
render(pppoe_conf, 'accel-ppp/pppoe.config.tmpl', pppoe, trim_blocks=True)
if pppoe['local_users']:
diff --git a/src/conf_mode/vpn_l2tp.py b/src/conf_mode/vpn_l2tp.py
index a4ef99d45..f312f2a17 100755
--- a/src/conf_mode/vpn_l2tp.py
+++ b/src/conf_mode/vpn_l2tp.py
@@ -340,10 +340,6 @@ def generate(l2tp):
if not l2tp:
return None
- dirname = os.path.dirname(l2tp_conf)
- if not os.path.exists(dirname):
- os.mkdir(dirname)
-
render(l2tp_conf, 'accel-ppp/l2tp.config.tmpl', l2tp, trim_blocks=True)
if l2tp['auth_mode'] == 'local':
diff --git a/src/conf_mode/vpn_pptp.py b/src/conf_mode/vpn_pptp.py
index 046fc8f9c..085c9c2c6 100755
--- a/src/conf_mode/vpn_pptp.py
+++ b/src/conf_mode/vpn_pptp.py
@@ -247,10 +247,6 @@ def generate(pptp):
if not pptp:
return None
- dirname = os.path.dirname(pptp_conf)
- if not os.path.exists(dirname):
- os.mkdir(dirname)
-
render(pptp_conf, 'accel-ppp/pptp.config.tmpl', pptp, trim_blocks=True)
if pptp['local_users']:
diff --git a/src/conf_mode/vpn_sstp.py b/src/conf_mode/vpn_sstp.py
index e6ce94709..d250cd3b0 100755
--- a/src/conf_mode/vpn_sstp.py
+++ b/src/conf_mode/vpn_sstp.py
@@ -303,10 +303,6 @@ def generate(sstp):
if not sstp:
return None
- dirname = os.path.dirname(sstp_conf)
- if not os.path.exists(dirname):
- os.mkdir(dirname)
-
# accel-cmd reload doesn't work so any change results in a restart of the daemon
render(sstp_conf, 'accel-ppp/sstp.config.tmpl', sstp, trim_blocks=True)
diff --git a/src/etc/dhcp/dhclient-enter-hooks.d/03-vyos-ipwrapper b/src/etc/dhcp/dhclient-enter-hooks.d/03-vyos-ipwrapper
index 59f92703c..f1167fcd2 100644
--- a/src/etc/dhcp/dhclient-enter-hooks.d/03-vyos-ipwrapper
+++ b/src/etc/dhcp/dhclient-enter-hooks.d/03-vyos-ipwrapper
@@ -15,8 +15,11 @@ function frr_alive () {
# convert ip route command to vtysh
function iptovtysh () {
# prepare variables for vtysh command
- VTYSH_DISTANCE="210"
- VTYSH_TAG="210"
+ local VTYSH_DISTANCE="210"
+ local VTYSH_TAG="210"
+ local VTYSH_NETADDR=""
+ local VTYSH_GATEWAY=""
+ local VTYSH_DEV=""
# convert default route to 0.0.0.0/0
if [ "$4" == "default" ] ; then
VTYSH_NETADDR="0.0.0.0/0"
@@ -74,3 +77,4 @@ function ip () {
fi
fi
}
+
diff --git a/src/etc/dhcp/dhclient-exit-hooks.d/01-vyos-cleanup b/src/etc/dhcp/dhclient-exit-hooks.d/01-vyos-cleanup
index ce846f6c3..88a4d9db9 100644
--- a/src/etc/dhcp/dhclient-exit-hooks.d/01-vyos-cleanup
+++ b/src/etc/dhcp/dhclient-exit-hooks.d/01-vyos-cleanup
@@ -1,12 +1,74 @@
+# NOTE: here we use 'ip' wrapper, therefore a route will be actually deleted via /usr/sbin/ip or vtysh, according to the system state
+
if [[ $reason =~ (EXPIRE|FAIL|RELEASE|STOP) ]]; then
# delete dynamic nameservers from a configuration if lease was deleted
logmsg info "Deleting nameservers with tag \"dhcp-${interface}\" via vyos-hostsd-client"
vyos-hostsd-client --delete-name-servers --tag dhcp-${interface}
- # try to delete default ip route (NOTE: here we use 'ip' wrapper, therefore a route will be actually deleted via /usr/sbin/ip or vtysh, according to the system state)
+ # try to delete default ip route
for router in $old_routers; do
logmsg info "Deleting default route: via $router dev ${interface}"
ip -4 route del default via $router dev ${interface}
done
+ # delete rfc3442 routes
+ if [ -n "$old_rfc3442_classless_static_routes" ]; then
+ set -- $old_rfc3442_classless_static_routes
+ while [ $# -gt 0 ]; do
+ net_length=$1
+ via_arg=''
+ case $net_length in
+ 32|31|30|29|28|27|26|25)
+ if [ $# -lt 9 ]; then
+ return 1
+ fi
+ net_address="${2}.${3}.${4}.${5}"
+ gateway="${6}.${7}.${8}.${9}"
+ shift 9
+ ;;
+ 24|23|22|21|20|19|18|17)
+ if [ $# -lt 8 ]; then
+ return 1
+ fi
+ net_address="${2}.${3}.${4}.0"
+ gateway="${5}.${6}.${7}.${8}"
+ shift 8
+ ;;
+ 16|15|14|13|12|11|10|9)
+ if [ $# -lt 7 ]; then
+ return 1
+ fi
+ net_address="${2}.${3}.0.0"
+ gateway="${4}.${5}.${6}.${7}"
+ shift 7
+ ;;
+ 8|7|6|5|4|3|2|1)
+ if [ $# -lt 6 ]; then
+ return 1
+ fi
+ net_address="${2}.0.0.0"
+ gateway="${3}.${4}.${5}.${6}"
+ shift 6
+ ;;
+ 0) # default route
+ if [ $# -lt 5 ]; then
+ return 1
+ fi
+ net_address="0.0.0.0"
+ gateway="${2}.${3}.${4}.${5}"
+ shift 5
+ ;;
+ *) # error
+ return 1
+ ;;
+ esac
+ # take care of link-local routes
+ if [ "${gateway}" != '0.0.0.0' ]; then
+ via_arg="via ${gateway}"
+ fi
+ # delete route (ip detects host routes automatically)
+ ip -4 route del "${net_address}/${net_length}" \
+ ${via_arg} dev "${interface}" >/dev/null 2>&1
+ done
+ fi
fi
if [[ $reason =~ (EXPIRE6|RELEASE6|STOP6) ]]; then
diff --git a/src/etc/dhcp/dhclient-exit-hooks.d/02-vyos-dhcp-renew-rfc3442 b/src/etc/dhcp/dhclient-exit-hooks.d/02-vyos-dhcp-renew-rfc3442
new file mode 100644
index 000000000..9202fe72d
--- /dev/null
+++ b/src/etc/dhcp/dhclient-exit-hooks.d/02-vyos-dhcp-renew-rfc3442
@@ -0,0 +1,148 @@
+# support for RFC3442 routes in DHCP RENEW
+
+function convert_to_cidr () {
+ cidr=""
+ set -- $1
+ while [ $# -gt 0 ]; do
+ net_length=$1
+
+ case $net_length in
+ 32|31|30|29|28|27|26|25)
+ if [ $# -lt 9 ]; then
+ return 1
+ fi
+ net_address="${2}.${3}.${4}.${5}"
+ gateway="${6}.${7}.${8}.${9}"
+ shift 9
+ ;;
+ 24|23|22|21|20|19|18|17)
+ if [ $# -lt 8 ]; then
+ return 1
+ fi
+ net_address="${2}.${3}.${4}.0"
+ gateway="${5}.${6}.${7}.${8}"
+ shift 8
+ ;;
+ 16|15|14|13|12|11|10|9)
+ if [ $# -lt 7 ]; then
+ return 1
+ fi
+ net_address="${2}.${3}.0.0"
+ gateway="${4}.${5}.${6}.${7}"
+ shift 7
+ ;;
+ 8|7|6|5|4|3|2|1)
+ if [ $# -lt 6 ]; then
+ return 1
+ fi
+ net_address="${2}.0.0.0"
+ gateway="${3}.${4}.${5}.${6}"
+ shift 6
+ ;;
+ 0) # default route
+ if [ $# -lt 5 ]; then
+ return 1
+ fi
+ net_address="0.0.0.0"
+ gateway="${2}.${3}.${4}.${5}"
+ shift 5
+ ;;
+ *) # error
+ return 1
+ ;;
+ esac
+
+ cidr+="${net_address}/${net_length}:${gateway} "
+ done
+}
+
+# main script starts here
+
+RUN="yes"
+
+if [ "$RUN" = "yes" ]; then
+ convert_to_cidr "$old_rfc3442_classless_static_routes"
+ old_cidr=$cidr
+ convert_to_cidr "$new_rfc3442_classless_static_routes"
+ new_cidr=$cidr
+
+ if [ "$reason" = "RENEW" ]; then
+ if [ "$new_rfc3442_classless_static_routes" != "$old_rfc3442_classless_static_routes" ]; then
+ logmsg info "RFC3442 route change detected, old_routes: $old_rfc3442_classless_static_routes"
+ logmsg info "RFC3442 route change detected, new_routes: $new_rfc3442_classless_static_routes"
+ if [ -z "$new_rfc3442_classless_static_routes" ]; then
+ # delete all routes from the old_rfc3442_classless_static_routes
+ for route in $old_cidr; do
+ network=$(printf "${route}" | awk -F ":" '{print $1}')
+ gateway=$(printf "${route}" | awk -F ":" '{print $2}')
+ # take care of link-local routes
+ if [ "${gateway}" != '0.0.0.0' ]; then
+ via_arg="via ${gateway}"
+ else
+ via_arg=""
+ fi
+ ip -4 route del "${network}" "${via_arg}" dev "${interface}" >/dev/null 2>&1
+ done
+ elif [ -z "$old_rfc3442_classless_static_routes" ]; then
+ # add all routes from the new_rfc3442_classless_static_routes
+ for route in $new_cidr; do
+ network=$(printf "${route}" | awk -F ":" '{print $1}')
+ gateway=$(printf "${route}" | awk -F ":" '{print $2}')
+ # take care of link-local routes
+ if [ "${gateway}" != '0.0.0.0' ]; then
+ via_arg="via ${gateway}"
+ else
+ via_arg=""
+ fi
+ ip -4 route add "${network}" "${via_arg}" dev "${interface}" >/dev/null 2>&1
+ done
+ else
+ # update routes
+ # delete old
+ for old_route in $old_cidr; do
+ match="false"
+ for new_route in $new_cidr; do
+ if [[ "$old_route" == "$new_route" ]]; then
+ match="true"
+ break
+ fi
+ done
+ if [[ "$match" == "false" ]]; then
+ # delete old_route
+ network=$(printf "${old_route}" | awk -F ":" '{print $1}')
+ gateway=$(printf "${old_route}" | awk -F ":" '{print $2}')
+ # take care of link-local routes
+ if [ "${gateway}" != '0.0.0.0' ]; then
+ via_arg="via ${gateway}"
+ else
+ via_arg=""
+ fi
+ ip -4 route del "${network}" "${via_arg}" dev "${interface}" >/dev/null 2>&1
+ fi
+ done
+ # add new
+ for new_route in $new_cidr; do
+ match="false"
+ for old_route in $old_cidr; do
+ if [[ "$new_route" == "$old_route" ]]; then
+ match="true"
+ break
+ fi
+ done
+ if [[ "$match" == "false" ]]; then
+ # add new_route
+ network=$(printf "${new_route}" | awk -F ":" '{print $1}')
+ gateway=$(printf "${new_route}" | awk -F ":" '{print $2}')
+ # take care of link-local routes
+ if [ "${gateway}" != '0.0.0.0' ]; then
+ via_arg="via ${gateway}"
+ else
+ via_arg=""
+ fi
+ ip -4 route add "${network}" "${via_arg}" dev "${interface}" >/dev/null 2>&1
+ fi
+ done
+ fi
+ fi
+ fi
+fi