天天看點

IPv6位址自動配置

預設情況下,配置項autoconf都是1,即開啟位址自動配置。

$ cat /proc/sys/net/ipv6/conf/all/autoconf     
1
$ cat /proc/sys/net/ipv6/conf/default/autoconf    
1
$ cat /proc/sys/net/ipv6/conf/ens33/autoconf        
1

$ cat /proc/sys/net/ipv6/conf/all/accept_ra_pinfo
1
$ cat /proc/sys/net/ipv6/conf/default/accept_ra_pinfo
1
$ cat /proc/sys/net/ipv6/conf/ens33/accept_ra_pinfo
1
           

如下代碼可見,autoconf都初始化為1。

static struct ipv6_devconf ipv6_devconf __read_mostly = {
    .autoconf       = 1,
    .accept_ra_pinfo    = 1,

static struct ipv6_devconf ipv6_devconf_dflt __read_mostly = {

    .autoconf       = 1,
    .accept_ra_pinfo    = 1,
           

另外,在IPv6子產品加載時,可通過參數autoconf指定autoconf的預設值。

struct ipv6_params ipv6_defaults = {
    .disable_ipv6 = 0,
    .autoconf = 1,
};

module_param_named(autoconf, ipv6_defaults.autoconf, int, 0444);
MODULE_PARM_DESC(autoconf, "Enable IPv6 address autoconfiguration on all interfaces");

static int __net_init addrconf_init_net(struct net *net)
{

    /* these will be inherited by all namespaces */
    dflt->autoconf = ipv6_defaults.autoconf;
    dflt->disable_ipv6 = ipv6_defaults.disable_ipv6;
           

字首資訊處理

接收到RA封包之後,如果其中包含字首資訊,并且目前接口配置項accept_ra_pinfo為真,周遊其中的字首資訊,由函數addrconf_prefix_rcv處理其中的每一項。

static void ndisc_router_discovery(struct sk_buff *skb)
{

    if (in6_dev->cnf.accept_ra_pinfo && ndopts.nd_opts_pi) {
        struct nd_opt_hdr *p;
        for (p = ndopts.nd_opts_pi;
             p;
             p = ndisc_next_option(p, ndopts.nd_opts_pi_end)) {
            addrconf_prefix_rcv(skb->dev, (u8 *)p,
                        (p->nd_opt_len) << 3,
                        ndopts.nd_opts_src_lladdr != NULL);
        }
    }
           

首先,檢查字首位址類型,對于多點傳播或者鍊路本地類型,不進行位址自動配置。字首的prefered時長不應大于valid有效時長。

void addrconf_prefix_rcv(struct net_device *dev, u8 *opt, int len, bool sllao)
{
    struct prefix_info *pinfo;

    pinfo = (struct prefix_info *) opt;

    /*  Validation checks ([ADDRCONF], page 19)
     */
    addr_type = ipv6_addr_type(&pinfo->prefix);

    if (addr_type & (IPV6_ADDR_MULTICAST|IPV6_ADDR_LINKLOCAL))
        return;

    valid_lft = ntohl(pinfo->valid);
    prefered_lft = ntohl(pinfo->prefered);
    if (prefered_lft > valid_lft) {
        net_warn_ratelimited("addrconf: prefix option has invalid lifetime\n");
        return;
    }

    in6_dev = in6_dev_get(dev);
    if (!in6_dev) {
        net_dbg_ratelimited("addrconf: device %s not configured\n", dev->name);
        return;
    }
           

對于鍊路本地字首資訊,增加路由表項。由于字首資訊的有效valid時長機關為秒值,而路由項的計時機關為jiffies,參見路由回收函數fib6_age中的處理,這裡需要進行轉換。将u32類型的valid_lft秒值,轉換為unsigned long類型的rt_expires值。

如果valid_lft值為0xffffffff,表示永不逾時。對于32位架構的處理器,将u32秒值轉換為同為32位的long型jiffies值,很可能導緻溢出,由addrconf_timeout_fixup進行處理。

if (pinfo->onlink) {
        struct fib6_info *rt;
        unsigned long rt_expires;

        /* Avoid arithmetic overflow. Really, we could save rt_expires in seconds, likely valid_lft,
         * but it would require division in fib gc, that it not good.
         */
        if (HZ > USER_HZ)
            rt_expires = addrconf_timeout_fixup(valid_lft, HZ);
        else
            rt_expires = addrconf_timeout_fixup(valid_lft, USER_HZ);
        if (addrconf_finite_timeout(rt_expires))
            rt_expires *= HZ;
           

如果字首對應的路由項已經存在,更新其逾時時間,如果valid_lft為零,删除路由表項。如果valid_lft為永不逾時,停止路由表項的逾時檢測。另外,如果表項不存在,并且valid_lft有效時間不為零,為字首建立新的路由表項。

rt = addrconf_get_prefix_route(&pinfo->prefix, pinfo->prefix_len, dev,
                           RTF_ADDRCONF | RTF_PREFIX_RT, RTF_DEFAULT, true);
        if (rt) {
            /* Autoconf prefix route */
            if (valid_lft == 0) {
                ip6_del_rt(net, rt, false);
                rt = NULL;
            } else if (addrconf_finite_timeout(rt_expires)) {
                fib6_set_expires(rt, jiffies + rt_expires); /* not infinity */
            } else {
                fib6_clean_expires(rt);
            }
        } else if (valid_lft) {
            clock_t expires = 0;
            int flags = RTF_ADDRCONF | RTF_PREFIX_RT;
            if (addrconf_finite_timeout(rt_expires)) {
                /* not infinity */
                flags |= RTF_EXPIRES;
                expires = jiffies_to_clock_t(rt_expires);
            }
            addrconf_prefix_route(&pinfo->prefix, pinfo->prefix_len,
                          0, dev, expires, flags, GFP_ATOMIC);
        }
        fib6_info_release(rt);
    }
           

如果字首資訊設定了autoconf标志,并且裝置的autoconf配置項為真,在字首長度為64的情況下,根據字首資訊生成接口位址。由以下幾種情況:

1) 接口的token有值(ip token set指令),将token位址的後8位元組最為接口ID,生成位址;

2) 接口位址生成模式為STABLE,由函數ipv6_generate_stable_address生成位址;

3) 否則,生成EUI64位址;

4) 如果以上都不成立,使用上次上次的EUI64格式的接口ID,生成位址。

/* Try to figure out our local address for this prefix */
   if (pinfo->autoconf && in6_dev->cnf.autoconf) {
       struct in6_addr addr;
       bool tokenized = false, dev_addr_generated = false;

       if (pinfo->prefix_len == 64) {
           memcpy(&addr, &pinfo->prefix, 8);

           if (!ipv6_addr_any(&in6_dev->token)) {
               read_lock_bh(&in6_dev->lock);
               memcpy(addr.s6_addr + 8, in6_dev->token.s6_addr + 8, 8);
               read_unlock_bh(&in6_dev->lock);
               tokenized = true;
           } else if (is_addr_mode_generate_stable(in6_dev) &&
                  !ipv6_generate_stable_address(&addr, 0, in6_dev)) {
               addr_flags |= IFA_F_STABLE_PRIVACY;
               goto ok;
           } else if (ipv6_generate_eui64(addr.s6_addr + 8, dev) &&
                  ipv6_inherit_eui64(addr.s6_addr + 8, in6_dev)) {
               goto put;
           } else {
               dev_addr_generated = true;
           }
           goto ok;
       }
       net_dbg_ratelimited("IPv6 addrconf: prefix with wrong length %d\n", pinfo->prefix_len);
       goto put;
           

以下配置生成的位址。

ok:
        err = addrconf_prefix_rcv_add_addr(net, dev, pinfo, in6_dev,
                           &addr, addr_type, addr_flags, sllao,
                           tokenized, valid_lft, prefered_lft);
        if (err) goto put;

        /* Ignore error case here because previous prefix add addr was
         * successful which will be notified.
         */
        ndisc_ops_prefix_rcv_add_addr(net, dev, pinfo, in6_dev, &addr,
                          addr_type, addr_flags, sllao, tokenized, valid_lft,
                          prefered_lft, dev_addr_generated);
    }
           

字首位址配置

在上節生成字首位址之後,由函數addrconf_prefix_rcv_add_addr進行配置處理。首先,檢查是否已經配置了此位址,如果沒有,并且要配置位址的有效時間valid_lft不為零,嘗試配置此位址。

int addrconf_prefix_rcv_add_addr(struct net *net, struct net_device *dev,
                 const struct prefix_info *pinfo, struct inet6_dev *in6_dev,
                 const struct in6_addr *addr, int addr_type,
                 u32 addr_flags, bool sllao, bool tokenized,
                 __u32 valid_lft, u32 prefered_lft)
{
    struct inet6_ifaddr *ifp = ipv6_get_ifaddr(net, addr, dev, 1);
    int create = 0;

    if (!ifp && valid_lft) {
        int max_addresses = in6_dev->cnf.max_addresses;
        struct ifa6_config cfg = {
            .pfx = addr,
            .plen = pinfo->prefix_len,
            .ifa_flags = addr_flags,
            .valid_lft = valid_lft,
            .preferred_lft = prefered_lft,
            .scope = addr_type & IPV6_ADDR_SCOPE_MASK,
        };

#ifdef CONFIG_IPV6_OPTIMISTIC_DAD
        if ((net->ipv6.devconf_all->optimistic_dad || in6_dev->cnf.optimistic_dad) &&
            !net->ipv6.devconf_all->forwarding && sllao)
            cfg.ifa_flags |= IFA_F_OPTIMISTIC;
#endif
           

隻有在接口目前的位址數量小于配置的最大數量max_addresses時,才能新增位址。對于新增位址,需要啟動DAD檢測。

/* Do not allow to create too much of autoconfigured
         * addresses; this would be too easy way to crash kernel.
         */
        if (!max_addresses || ipv6_count_addresses(in6_dev) < max_addresses)
            ifp = ipv6_add_addr(in6_dev, &cfg, false, NULL);

        if (IS_ERR_OR_NULL(ifp)) return -1;

        create = 1;
        spin_lock_bh(&ifp->lock);
        ifp->flags |= IFA_F_MANAGETEMPADDR;
        ifp->cstamp = jiffies;
        ifp->tokenized = tokenized;
        spin_unlock_bh(&ifp->lock);
        addrconf_dad_start(ifp);
    }
           

以下,如果位址存在,或者以上操作建立了位址(create等于1),首先看一下是否需要更新以存在位址的valid有效時長。如果位址項自身的valid時長大于已經逝去的時長,位址還在有效期内,将新的valid和prefered時長更新到位址項中。清除DEPRECATED标記,如果位址為非TENTATIVE臨時位址,發送RTM_NEWADDR事件通知。

以上的位址valid時間更新操作與RFC4862中表述的不一緻,RFC4862為了防止僞造的RA包含過短的valid時長,造成位址的過早失效,定義了以下三種情況:

1) 如果字首中的valid時長大于2個小時,或者大于接口已有位址中剩餘的有效時長,将字首中valid更新到位址中的valid_lft字段;

2) 如果位址中剩餘有效時長小于等于2個小時,忽略字首中的valid值,不更新。除非字首所在的RA是經過認證的封包(如SEND);

3) 以上情況都不成立,将位址中的valid時長設定為2個小時。

但是,核心中為了及時響應位址重新配置,盡快去除舊位址,未采用RFC中的做法。也許這裡可以設定一個配置項,來決定采用哪種方式。

if (ifp) {
        /* Update lifetime (RFC4862 5.5.3 e)
         * We deviate from RFC4862 by honoring all Valid Lifetimes to
         * improve the reaction of SLAAC to renumbering events
         * (draft-gont-6man-slaac-renum-06, Section 4.2)
         */
        now = jiffies;
        if (ifp->valid_lft > (now - ifp->tstamp) / HZ)
            stored_lft = ifp->valid_lft - (now - ifp->tstamp) / HZ;
        else
            stored_lft = 0;

        if (!create && stored_lft) {
            ifp->valid_lft = valid_lft;
            ifp->prefered_lft = prefered_lft;
            ifp->tstamp = now;
            flags = ifp->flags;
            ifp->flags &= ~IFA_F_DEPRECATED;

            if (!(flags&IFA_F_TENTATIVE)) ipv6_ifa_notify(0, ifp);
        }
        manage_tempaddrs(in6_dev, ifp, valid_lft, prefered_lft, create, now);

        in6_ifa_put(ifp);
        addrconf_verify();
           

接口token設定

如下ip指令所示。

# ip token set ::0102:0304 dev ens33     
# 
# ip token list                          
token ::1.2.3.4 dev ens33
token :: dev ens34
token :: dev ens35
           

核心版本 5.10

繼續閱讀