diff --git a/etc/network/ifupdown2/addons.conf b/etc/network/ifupdown2/addons.conf index 726d63a0..67de25fe 100644 --- a/etc/network/ifupdown2/addons.conf +++ b/etc/network/ifupdown2/addons.conf @@ -15,6 +15,7 @@ pre-up,mstpctl pre-up,tunnel pre-up,vrf pre-up,ethtool +pre-up,auto pre-up,address up,dhcp up,address @@ -28,6 +29,7 @@ pre-down,usercmds pre-down,vxrd pre-down,dhcp down,ppp +down,auto down,addressvirtual down,address down,usercmds diff --git a/ifupdown2/addons/address.py b/ifupdown2/addons/address.py index e71a26f7..473a0897 100644 --- a/ifupdown2/addons/address.py +++ b/ifupdown2/addons/address.py @@ -188,6 +188,19 @@ class address(AddonWithIpBlackList, moduleBase): 'default': 'off', 'example': ['arp-accept on'] }, + 'accept-ra': { + 'help': 'accept ipv6 router advertisement', + 'validvals': ['0', '1', '2'], + 'default': '0', + 'example': ['accept-ra 1'] + }, + 'autoconf': { + 'help': 'enable ipv6 slaac autoconfiguration', + 'validvals': ['0', '1'], + 'default': '0', + 'example': ['autoconf 1'] + }, + } } @@ -256,6 +269,16 @@ def __init__(self, *args, **kargs): attr="check_l3_svi_ip_forwarding") ) + try: + self.default_accept_ra = str(self.sysctl_get('net.ipv6.conf.all.accept_ra')) + except Exception: + self.default_accept_ra = 1 + + try: + self.default_autoconf = str(self.sysctl_get('net.ipv6.conf.all.autoconf')) + except Exception: + self.default_autoconf = 1 + def __policy_get_default_mtu(self): default_mtu = policymanager.policymanager_api.get_attr_default( module_name=self.__class__.__name__, @@ -627,21 +650,31 @@ def process_addresses(self, ifaceobj, ifaceobj_getfunc=None, force_reapply=False if force_reapply: self.__add_ip_addresses_with_attributes(ifaceobj, ifname, user_config_ip_addrs_list) return + + purge_dynamic_v6_addresses = True + running_autoconf = self.cache.get_link_inet6_autoconf(ifaceobj) + if running_autoconf == '1' and not squash_addr_config: + purge_dynamic_v6_addresses = False + try: - # if primary address is not same, there is no need to keep any, reset all addresses. - if ordered_user_configured_ips and running_ip_addrs and ordered_user_configured_ips[0] != running_ip_addrs[0]: - self.logger.info("%s: primary ip changed (from %s to %s) we need to purge all ip addresses and re-add them" - % (ifname, ordered_user_configured_ips[0], running_ip_addrs[0])) - skip_addrs = [] + # if primary ipv4 address is not same, there is no need to keep any, reset all ipv4 addresses. + if user_ip4 and running_ip_addrs and running_ip_addrs[0].version == 4 and user_ip4[0] != running_ip_addrs[0]: + self.logger.info("%s: primary ipv4 changed (from %s to %s) we need to purge all ipv4 addresses and re-add them" + % (ifname, user_ip4[0], running_ip_addrs[0])) + skip_addrs = user_ip6 else: skip_addrs = ordered_user_configured_ips if anycast_ip: skip_addrs.append(anycast_ip) + ip_flags = self.cache.get_ip_addresses_flags(ifname) for addr in running_ip_addrs: if addr in skip_addrs: continue + # don't purge dynamic ipv6 ip if autoconf is enabled + if addr.version == 6 and not purge_dynamic_v6_addresses and addr in ip_flags and not ip_flags[addr] & 0x80: + continue self.netlink.addr_del(ifname, addr) except Exception as e: self.log_warn(str(e)) @@ -872,7 +905,9 @@ def _set_bridge_forwarding(self, ifaceobj): netconf_ipv4_forwarding = self.cache.get_netconf_forwarding(socket.AF_INET, ifname) netconf_ipv6_forwarding = self.cache.get_netconf_forwarding(socket.AF_INET6, ifname) - if not ifaceobj.upperifaces and not ifaceobj.get_attr_value('address') and (ifaceobj.addr_method and "dhcp" not in ifaceobj.addr_method): + if ( not ifaceobj.upperifaces and not ifaceobj.get_attr_value('address') and + ifaceobj.addr_method and "dhcp" not in ifaceobj.addr_method and "auto" not in ifaceobj.addr_method): + if netconf_ipv4_forwarding: self.sysctl_write_forwarding_value_to_proc(ifname, "ipv4", 0) if netconf_ipv6_forwarding: @@ -886,6 +921,43 @@ def _set_bridge_forwarding(self, ifaceobj): def sysctl_write_forwarding_value_to_proc(self, ifname, family, value): self.write_file("/proc/sys/net/%s/conf/%s/forwarding" % (family, ifname), "%s\n" % value) + def _sysctl_slaac(self, ifaceobj): + addr_method = ifaceobj.addr_method + if addr_method not in ["auto"]: + + try: + running_accept_ra = self.cache.get_link_inet6_accept_ra(ifaceobj) + if running_accept_ra == '': + running_accept_ra = self.default_accept_ra + accept_ra = ifaceobj.get_attr_value_first('accept-ra') + if accept_ra is None: + accept_ra = self.default_accept_ra + + if running_accept_ra != accept_ra: + self.sysctl_set('net.ipv6.conf.%s.accept_ra' + %('/'.join(ifaceobj.name.split("."))), + accept_ra) + self.cache.update_link_inet6_accept_ra(ifaceobj.name, accept_ra) + + running_autoconf = self.cache.get_link_inet6_autoconf(ifaceobj) + if running_autoconf == '': + running_autoconf = self.default_autoconf + autoconf = ifaceobj.get_attr_value_first('autoconf') + if autoconf is None: + autoconf = self.default_autoconf + + if running_autoconf != autoconf: + self.sysctl_set('net.ipv6.conf.%s.autoconf' + %('/'.join(ifaceobj.name.split("."))), + autoconf) + self.cache.update_link_inet6_autoconf(ifaceobj.name, autoconf) + + except Exception as e: + if not setting_default_value: + ifaceobj.status = ifaceStatus.ERROR + self.logger.error('%s: %s' %(ifaceobj.name, str(e))) + + def _sysctl_config(self, ifaceobj): setting_default_value = False mpls_enable = ifaceobj.get_attr_value_first('mpls-enable'); @@ -912,6 +984,7 @@ def _sysctl_config(self, ifaceobj): if (ifaceobj.link_kind & ifaceLinkKind.BRIDGE): self._set_bridge_forwarding(ifaceobj) + self._sysctl_slaac(ifaceobj) return if not self.syntax_check_sysctls(ifaceobj): return @@ -979,6 +1052,8 @@ def _sysctl_config(self, ifaceobj): ifaceobj.status = ifaceStatus.ERROR self.logger.error('%s: %s' %(ifaceobj.name, str(e))) + self._sysctl_slaac(ifaceobj) + def process_mtu(self, ifaceobj, ifaceobj_getfunc): if ifaceobj.link_privflags & ifaceLinkPrivFlags.OPENVSWITCH: @@ -1016,7 +1091,7 @@ def up_ipv6_addrgen(self, ifaceobj): # no need to go further during perfmode (boot) return - if not user_configured_ipv6_addrgen and ifaceobj.addr_method in ["dhcp", "ppp"]: + if not user_configured_ipv6_addrgen and ifaceobj.addr_method in ["dhcp", "ppp", "auto"]: return if not user_configured_ipv6_addrgen: @@ -1213,7 +1288,7 @@ def _down(self, ifaceobj, ifaceobj_getfunc=None): if not self.cache.link_exists(ifaceobj.name): return addr_method = ifaceobj.addr_method - if addr_method not in ["dhcp", "ppp"]: + if addr_method not in ["dhcp", "ppp", "auto"]: if ifaceobj.get_attr_value_first('address-purge')=='no': addrlist = ifaceobj.get_attr_value('address') for addr in addrlist or []: @@ -1326,6 +1401,22 @@ def _query_sysctl(self, ifaceobj, ifaceobjcurr): ifaceobjcurr.update_config_with_status('mpls-enable', running_mpls_enable, mpls_enable != running_mpls_enable) + + accept_ra = ifaceobj.get_attr_value_first('accept-ra') + if accept_ra: + running_accept_ra = self.cache.get_link_inet6_accept_ra(ifaceobj) + + ifaceobjcurr.update_config_with_status('accept_ra', + running_accept_ra, + accept_ra != running_accept_ra) + + autoconf = ifaceobj.get_attr_value_first('autoconf') + if autoconf: + running_autoconf = self.cache.get_link_inet6_autoconf(ifaceobj) + + ifaceobjcurr.update_config_with_status('autoconf', + running_autoconf, + autoconf != running_autoconf) return def query_check_ipv6_addrgen(self, ifaceobj, ifaceobjcurr): @@ -1380,7 +1471,7 @@ def _query_check(self, ifaceobj, ifaceobjcurr, ifaceobj_getfunc=None): def _query_check_address(self, ifaceobj, ifaceobjcurr, ifaceobj_getfunc): """ ifquery-check: attribute: "address" """ - if ifaceobj.addr_method in ["dhcp", "ppp"]: + if ifaceobj.addr_method in ["dhcp", "ppp", "auto"]: return if ifaceobj_getfunc: diff --git a/ifupdown2/addons/auto.py b/ifupdown2/addons/auto.py new file mode 100644 index 00000000..02e6ca48 --- /dev/null +++ b/ifupdown2/addons/auto.py @@ -0,0 +1,168 @@ +#!/usr/bin/env python3 +# + +import re +import time +import socket + +try: + from ifupdown2.lib.addon import Addon + from ifupdown2.lib.log import LogManager + + import ifupdown2.ifupdown.policymanager as policymanager + import ifupdown2.ifupdown.ifupdownflags as ifupdownflags + + from ifupdown2.ifupdown.iface import * + from ifupdown2.ifupdown.utils import utils + + from ifupdown2.ifupdownaddons.modulebase import moduleBase +except (ImportError, ModuleNotFoundError): + from lib.addon import Addon + from lib.log import LogManager + + import ifupdown.policymanager as policymanager + import ifupdown.ifupdownflags as ifupdownflags + + from ifupdown.iface import * + from ifupdown.utils import utils + + from ifupdownaddons.modulebase import moduleBase + + +class auto(Addon, moduleBase): + """ ifupdown2 addon module to configure slaac on inet6 interface """ + + def __init__(self, *args, **kargs): + Addon.__init__(self) + moduleBase.__init__(self, *args, **kargs) + + def syntax_check(self, ifaceobj, ifaceobj_getfunc): + return self.is_auto_allowed_on(ifaceobj, syntax_check=True) + + def is_auto_allowed_on(self, ifaceobj, syntax_check): + if ifaceobj.addr_method and 'auto' in ifaceobj.addr_method: + return utils.is_addr_ip_allowed_on(ifaceobj, syntax_check=True) + return True + + def _up(self, ifaceobj): + + if ifaceobj.link_privflags & ifaceLinkPrivFlags.KEEP_LINK_DOWN: + self.logger.info("%s: skipping auto configuration: link-down yes" % ifaceobj.name) + return + + try: + if 'inet6' in ifaceobj.addr_family: + running_accept_ra = self.cache.get_link_inet6_accept_ra(ifaceobj) + if running_accept_ra != '2': + accept_ra = '2' + self.sysctl_set('net.ipv6.conf.%s.accept_ra' + %('/'.join(ifaceobj.name.split("."))), + accept_ra) + self.cache.update_link_inet6_accept_ra(ifaceobj.name, accept_ra) + + running_autoconf = self.cache.get_link_inet6_autoconf(ifaceobj) + if running_autoconf != '1': + autoconf = '1' + self.sysctl_set('net.ipv6.conf.%s.autoconf' + %('/'.join(ifaceobj.name.split("."))), + autoconf) + self.cache.update_link_inet6_autoconf(ifaceobj.name, autoconf) + + except Exception as e: + self.logger.error("%s: %s" % (ifaceobj.name, str(e))) + ifaceobj.set_status(ifaceStatus.ERROR) + + def _down(self, ifaceobj): + if 'inet6' in ifaceobj.addr_family: + self.cache.force_address_flush_family(ifaceobj.name, socket.AF_INET6) + self.netlink.link_down(ifaceobj.name) + + def _query_check(self, ifaceobj, ifaceobjcurr): + if not self.cache.link_exists(ifaceobj.name): + return + ifaceobjcurr.addr_family = ifaceobj.addr_family + ifaceobjcurr.addr_method = 'auto' + + inet6conf = self.cache.get_link_inet6_conf(ifaceobj.name) + if inet6conf['accept_ra'] == 2 and inet6conf['autoconf'] == 1: + ifaceobjcurr.status = ifaceStatus.SUCCESS + else: + ifaceobjcurr.status = ifaceStatus.ERROR + + def _query_running(self, ifaceobjrunning): + pass + + _run_ops = {'pre-up' : _up, + 'up' : _up, + 'down' : _down, + 'pre-down' : _down, + 'query-checkcurr' : _query_check, + 'query-running' : _query_running } + + def get_ops(self): + """ returns list of ops supported by this module """ + return list(self._run_ops.keys()) + + def run(self, ifaceobj, operation, query_ifaceobj=None, **extra_args): + """ run dhcp configuration on the interface object passed as argument + + Args: + **ifaceobj** (object): iface object + + **operation** (str): any of 'up', 'down', 'query-checkcurr', + 'query-running' + + Kwargs: + **query_ifaceobj** (object): query check ifaceobject. This is only + valid when op is 'query-checkcurr'. It is an object same as + ifaceobj, but contains running attribute values and its config + status. The modules can use it to return queried running state + of interfaces. status is success if the running state is same + as user required state in ifaceobj. error otherwise. + """ + op_handler = self._run_ops.get(operation) + if not op_handler: + return + try: + if (operation != 'query-running' and ifaceobj.addr_method != 'auto'): + return + except Exception: + return + if not self.is_auto_allowed_on(ifaceobj, syntax_check=False): + return + + log_manager = LogManager.get_instance() + + syslog_log_level = logging.INFO + disable_syslog_on_exit = None + + if operation in ["up", "down"]: + # if syslog is already enabled we shouldn't disable it + if log_manager.is_syslog_enabled(): + # save current syslog level + syslog_log_level = log_manager.get_syslog_log_level() + # prevent syslog from being disabled on exit + disable_syslog_on_exit = False + else: + # enabling syslog + log_manager.enable_syslog() + # syslog will be disabled once we are done + disable_syslog_on_exit = True + + # update the current syslog handler log level if higher than INFO + if syslog_log_level >= logging.INFO: + log_manager.set_level_syslog(logging.INFO) + + self.logger.info("%s: enabling syslog for auto configuration" % ifaceobj.name) + + try: + if operation == 'query-checkcurr': + op_handler(self, ifaceobj, query_ifaceobj) + else: + op_handler(self, ifaceobj) + finally: + # disable syslog handler or re-set the proper log-level + if disable_syslog_on_exit is True: + log_manager.get_instance().disable_syslog() + elif disable_syslog_on_exit is False: + log_manager.set_level_syslog(syslog_log_level) diff --git a/ifupdown2/addons/dhcp.py b/ifupdown2/addons/dhcp.py index a5bf8605..22bbdb4b 100644 --- a/ifupdown2/addons/dhcp.py +++ b/ifupdown2/addons/dhcp.py @@ -193,20 +193,10 @@ def _up(self, ifaceobj): self.logger.info('dhclient6 already running on %s. ' 'Not restarting.' % ifaceobj.name) else: - accept_ra = ifaceobj.get_attr_value_first('accept_ra') - if accept_ra: - # XXX: Validate value - self.sysctl_set('net.ipv6.conf.%s' %ifaceobj.name + - '.accept_ra', accept_ra) - autoconf = ifaceobj.get_attr_value_first('autoconf') - if autoconf: - # XXX: Validate value - self.sysctl_set('net.ipv6.conf.%s' %ifaceobj.name + - '.autoconf', autoconf) - try: - self.dhclientcmd.stop6(ifaceobj.name, duid=dhcp6_duid) - except Exception: - pass + try: + self.dhclientcmd.stop6(ifaceobj.name, duid=dhcp6_duid) + except Exception: + pass #add delay before starting IPv6 dhclient to #make sure the configured interface/link is up. if timeout > 1: diff --git a/ifupdown2/ifupdown/iface.py b/ifupdown2/ifupdown/iface.py index 07bd067a..325e6c3b 100644 --- a/ifupdown2/ifupdown/iface.py +++ b/ifupdown2/ifupdown/iface.py @@ -289,6 +289,8 @@ def default(self, o): if o.addr_method: if 'inet' in o.addr_family and 'dhcp' in o.addr_method: retifacedict['addr_method'] = 'dhcp' + elif 'inet6' in o.addr_family and 'auto' in o.addr_method: + retifacedict['addr_method'] = 'auto' else: retifacedict['addr_method'] = o.addr_method if o.addr_family: @@ -843,6 +845,8 @@ def dump_pretty(self, with_status=False, use_realname=False): # both inet and inet6 addr_family if addr_method and family == 'inet' and 'dhcp' in addr_method: addr_method = 'dhcp' + elif addr_method and family == 'inet6' and 'auto' in addr_method: + addr_method = 'auto' self._dump_pretty(family, first, addr_method=addr_method, with_status=with_status, diff --git a/ifupdown2/ifupdown/networkinterfaces.py b/ifupdown2/ifupdown/networkinterfaces.py index 55f93fae..bfdc3499 100644 --- a/ifupdown2/ifupdown/networkinterfaces.py +++ b/ifupdown2/ifupdown/networkinterfaces.py @@ -30,7 +30,7 @@ class networkInterfaces(): """ debian ifupdown /etc/network/interfaces file parser """ _addrfams = {'inet' : ['static', 'manual', 'loopback', 'dhcp', 'dhcp6', 'ppp', 'tunnel'], - 'inet6' : ['static', 'manual', 'loopback', 'dhcp', 'dhcp6', 'ppp', 'tunnel']} + 'inet6' : ['static', 'manual', 'loopback', 'dhcp', 'dhcp6', 'ppp', 'tunnel', 'auto']} # tunnel is part of the address family for backward compatibility but is not required. def __init__(self, interfacesfile='/etc/network/interfaces', diff --git a/ifupdown2/lib/nlcache.py b/ifupdown2/lib/nlcache.py index 0b1c6d2b..0d2f6241 100644 --- a/ifupdown2/lib/nlcache.py +++ b/ifupdown2/lib/nlcache.py @@ -152,7 +152,7 @@ class _NetlinkCache: Address.IFA_ANYCAST, # Address.IFA_CACHEINFO, Address.IFA_MULTICAST, - # Address.IFA_FLAGS + Address.IFA_FLAGS ) def __init__(self): @@ -1179,6 +1179,52 @@ def get_link_ipv6_addrgen_mode(self, ifname): except TypeError as e: return self.__handle_type_error(inspect.currentframe().f_code.co_name, ifname, str(e), return_value=0) + def get_link_inet6_conf(self, ifname): + try: + with self._cache_lock: + return self._link_cache[ifname].attributes[Link.IFLA_AF_SPEC].value[socket.AF_INET6][Link.IFLA_INET6_CONF] + except (KeyError, AttributeError): + return False + except TypeError as e: + return self.__handle_type_error(inspect.currentframe().f_code.co_name, ifname, str(e), return_value=False) + + def get_link_inet6_accept_ra(self, ifaceobj): + inet6conf = self.get_link_inet6_conf(ifaceobj.name) + if inet6conf and 'accept_ra' in inet6conf: + accept_ra = str(inet6conf['accept_ra']) + else: + accept_ra = '' + return accept_ra + + def get_link_inet6_autoconf(self, ifaceobj): + inet6conf = self.get_link_inet6_conf(ifaceobj.name) + if inet6conf and 'autoconf' in inet6conf: + autoconf = str(inet6conf['autoconf']) + else: + autoconf = '' + return autoconf + + def update_link_inet6_accept_ra(self, ifname, accept_ra): + try: + with self._cache_lock: + try: + self._link_cache[ifname].attributes[Link.IFLA_AF_SPEC].value[socket.AF_INET6][Link.IFLA_INET6_CONF]['accept_ra'] = accept_ra + except Exception as e: + pass + except Exception: + pass + + def update_link_inet6_autoconf(self, ifname, autoconf): + try: + with self._cache_lock: + try: + self._link_cache[ifname].attributes[Link.IFLA_AF_SPEC].value[socket.AF_INET6][Link.IFLA_INET6_CONF]['autoconf'] = autoconf + except Exception as e: + pass + except Exception: + pass + + ##################################################### ##################################################### ##################################################### @@ -1745,6 +1791,21 @@ def get_ip_addresses(self, ifname: str) -> list: except (KeyError, AttributeError): return addresses + def get_ip_addresses_flags(self, ifname: str) -> dict: + addresses = {} + try: + with self._cache_lock: + intf_addresses = self._addr_cache[ifname] + for addr in intf_addresses.get(4, []): + addresses[addr.attributes[Address.IFA_ADDRESS].value] = addr.attributes[Address.IFA_FLAGS].value + + for addr in intf_addresses.get(6, []): + addresses[addr.attributes[Address.IFA_ADDRESS].value] = addr.attributes[Address.IFA_FLAGS].value + + return addresses + except (KeyError, AttributeError): + return addresses + def link_has_ip(self, ifname): try: with self._cache_lock: diff --git a/ifupdown2/man/interfaces.5.rst b/ifupdown2/man/interfaces.5.rst index 262d7265..ca461eae 100644 --- a/ifupdown2/man/interfaces.5.rst +++ b/ifupdown2/man/interfaces.5.rst @@ -106,6 +106,12 @@ METHODS The dhcp Method This method may be used to obtain an address via DHCP. + **inet6** address family interfaces can use the following method: + + The auto Method + This method may be used to obtain an address via SLAAC. + + BUILTIN INTERFACES ================== **iface** sections for some interfaces like physical interfaces or vlan @@ -131,6 +137,9 @@ EXAMPLES address 192.168.2.0/24 address 2001:dee:eeee:1::4/128 + auto eth3 + iface eth3 inet auto + # source files from a directory /etc/network/interfaces.d source /etc/network/interfaces.d/* diff --git a/ifupdown2/nlmanager/nlpacket.py b/ifupdown2/nlmanager/nlpacket.py index 8972c761..00905291 100644 --- a/ifupdown2/nlmanager/nlpacket.py +++ b/ifupdown2/nlmanager/nlpacket.py @@ -1818,6 +1818,15 @@ def decode(self, parent_msg, data): */ """ + #only first attributes used in any kernel. + ipv6_devconf = ['forwarding', + 'hop_limit', + 'mtu6', + 'accept_ra', + 'accept_redirects', + 'autoconf', + ] + self.decode_length_type(data) self.value = {} @@ -1896,8 +1905,21 @@ def decode(self, parent_msg, data): (inet6_attr_length, inet6_attr_type) = unpack('=HH', sub_attr_data[:4]) inet6_attr_end = padded_length(inet6_attr_length) + if inet6_attr_type == Link.IFLA_INET6_CONF: + inet6conf_data = sub_attr_data[4:inet6_attr_end] + index = 0 + result = {} + while inet6conf_data: + (value, undef) = unpack('=HH', inet6conf_data[:4]) + result[ipv6_devconf[index]] = value + inet6conf_data = inet6conf_data[4:] + index = index + 1 + if index >= len(ipv6_devconf): + inet6_attr[inet6_attr_type] = result + break + # 1 byte attr - if inet6_attr_type == Link.IFLA_INET6_ADDR_GEN_MODE: + elif inet6_attr_type == Link.IFLA_INET6_ADDR_GEN_MODE: inet6_attr[inet6_attr_type] = self.decode_one_byte_attribute(sub_attr_data) # nlmanager doesn't support multiple kernel version