Index | Thread | Search

From:
Alexander Bluhm <bluhm@openbsd.org>
Subject:
Re: Allow rad(8) to advertise shorter lifetimes
To:
Ryan Vogt <rvogt.ca@gmail.com>, tech@openbsd.org
Date:
Sun, 14 Sep 2025 23:13:29 +0200

Download raw body.

Thread
On Sun, Sep 14, 2025 at 01:05:33PM +0200, Florian Obser wrote:
> Apologies for my silence and very late response, life happened...
> 
> I fully agree that lifetimes should be limited and a excessively long
> lifetimes handed out by a DHCPv6 server are a problem.
> 
> I don't think we need a config option for this. The minimum of what we
> find on an interface and what we have in the config file (including
> defaults) should win.
> 
> The following diff, inspired by your work, implements that.
> Does this work for you?
> Florian

OK bluhm@

> diff --git frontend.c frontend.c
> index 2a519dac0b1..97f4d0ba708 100644
> --- frontend.c
> +++ frontend.c
> @@ -110,7 +110,7 @@ struct ra_iface {
>  	int				 removed;
>  	int				 link_state;
>  	int				 prefix_count;
> -	int				 ltime_decaying;
> +	int				 has_autoconf_prefix;
>  	size_t				 datalen;
>  	uint8_t				 data[RA_MAX_SIZE];
>  };
> @@ -151,11 +151,12 @@ void			 unref_icmp6ev(struct ra_iface *);
>  void			 set_icmp6sock(int, int);
>  void			 add_new_prefix_to_ra_iface(struct ra_iface *r,
>  			    struct in6_addr *, int, struct ra_prefix_conf *,
> -			    uint32_t, uint32_t);
> +			    int, uint32_t, uint32_t);
>  void			 free_ra_iface(struct ra_iface *);
>  int			 in6_mask2prefixlen(struct in6_addr *);
>  void			 get_interface_prefixes(struct ra_iface *,
>  			     struct ra_prefix_conf *, struct ifaddrs *);
> +uint32_t		 calc_autoconf_ltime(time_t, uint32_t, uint32_t);
>  int			 build_packet(struct ra_iface *);
>  void			 build_leaving_packet(struct ra_iface *);
>  void			 ra_output(struct ra_iface *, struct sockaddr_in6 *);
> @@ -979,7 +980,7 @@ merge_ra_interfaces(void)
>  		    entry) {
>  			add_new_prefix_to_ra_iface(ra_iface,
>  			    &ra_prefix_conf->prefix,
> -			    ra_prefix_conf->prefixlen, ra_prefix_conf,
> +			    ra_prefix_conf->prefixlen, ra_prefix_conf, 0,
>  			    ND6_INFINITE_LIFETIME, ND6_INFINITE_LIFETIME);
>  		}
>  
> @@ -1046,7 +1047,7 @@ get_interface_prefixes(struct ra_iface *ra_iface, struct ra_prefix_conf
>  	struct in6_ifreq	 ifr6;
>  	struct ifaddrs		*ifa;
>  	struct sockaddr_in6	*sin6;
> -	uint32_t		 decaying_vltime, decaying_pltime;
> +	uint32_t		 if_vltime, if_pltime;
>  	int			 prefixlen;
>  
>  	for (ifa = ifap; ifa != NULL; ifa = ifa->ifa_next) {
> @@ -1066,8 +1067,8 @@ get_interface_prefixes(struct ra_iface *ra_iface, struct ra_prefix_conf
>  		strlcpy(ifr6.ifr_name, ra_iface->name, sizeof(ifr6.ifr_name));
>  		memcpy(&ifr6.ifr_addr, sin6, sizeof(ifr6.ifr_addr));
>  
> -		decaying_vltime = ND6_INFINITE_LIFETIME;
> -		decaying_pltime = ND6_INFINITE_LIFETIME;
> +		if_vltime = ND6_INFINITE_LIFETIME;
> +		if_pltime = ND6_INFINITE_LIFETIME;
>  
>  		if (ioctl(ioctlsock, SIOCGIFALIFETIME_IN6,
>  		    (caddr_t)&ifr6) != -1) {
> @@ -1075,9 +1076,9 @@ get_interface_prefixes(struct ra_iface *ra_iface, struct ra_prefix_conf
>  
>  			lifetime = &ifr6.ifr_ifru.ifru_lifetime;
>  			if (lifetime->ia6t_preferred)
> -				decaying_pltime = lifetime->ia6t_preferred;
> +				if_pltime = lifetime->ia6t_preferred;
>  			if (lifetime->ia6t_expire)
> -				decaying_vltime = lifetime->ia6t_expire;
> +				if_vltime = lifetime->ia6t_expire;
>  		}
>  
>  		memset(&ifr6, 0, sizeof(ifr6));
> @@ -1096,8 +1097,7 @@ get_interface_prefixes(struct ra_iface *ra_iface, struct ra_prefix_conf
>  		mask_prefix(&sin6->sin6_addr, prefixlen);
>  
>  		add_new_prefix_to_ra_iface(ra_iface, &sin6->sin6_addr,
> -		    prefixlen, autoprefix_conf, decaying_vltime,
> -		    decaying_pltime);
> +		    prefixlen, autoprefix_conf, 1, if_vltime, if_pltime);
>  	}
>  }
>  
> @@ -1118,41 +1118,30 @@ find_ra_prefix_conf(struct ra_prefix_conf_head* head, struct in6_addr *prefix,
>  
>  void
>  add_new_prefix_to_ra_iface(struct ra_iface *ra_iface, struct in6_addr *addr,
> -    int prefixlen, struct ra_prefix_conf *ra_prefix_conf,
> -    uint32_t decaying_vltime, uint32_t decaying_pltime)
> +    int prefixlen, struct ra_prefix_conf *ra_prefix_conf, int autoconf,
> +    uint32_t if_vltime, uint32_t if_pltime)
>  {
>  	struct ra_prefix_conf	*new_ra_prefix_conf;
>  
>  	if ((new_ra_prefix_conf = find_ra_prefix_conf(&ra_iface->prefixes, addr,
>  	    prefixlen)) != NULL) {
> -		if (decaying_vltime != ND6_INFINITE_LIFETIME ||
> -		    decaying_pltime != ND6_INFINITE_LIFETIME) {
> -			ra_iface->ltime_decaying = 1;
> -			new_ra_prefix_conf->ltime_decaying = 0;
> -			if (decaying_vltime != ND6_INFINITE_LIFETIME) {
> -				new_ra_prefix_conf->vltime = decaying_vltime;
> -				new_ra_prefix_conf->ltime_decaying |=
> -				    VLTIME_DECAYING;
> -			}
> -			if (decaying_pltime != ND6_INFINITE_LIFETIME) {
> -				new_ra_prefix_conf->pltime = decaying_pltime;
> -				new_ra_prefix_conf->ltime_decaying |=
> -				    PLTIME_DECAYING;
> -			}
> -		} else if (new_ra_prefix_conf->ltime_decaying) {
> +		int changed = new_ra_prefix_conf->autoconf != autoconf;
> +
> +		new_ra_prefix_conf->autoconf = autoconf;
> +		new_ra_prefix_conf->if_vltime = if_vltime;
> +		new_ra_prefix_conf->if_pltime = if_pltime;
> +
> +		if (changed) {
>  			struct ra_prefix_conf *pc;
>  
> -			new_ra_prefix_conf->ltime_decaying = 0;
> -			ra_iface->ltime_decaying = 0;
> +			ra_iface->has_autoconf_prefix = 0;
>  			SIMPLEQ_FOREACH(pc, &ra_iface->prefixes, entry) {
> -				if (pc->ltime_decaying) {
> -					ra_iface->ltime_decaying = 1;
> +				if (pc->autoconf) {
> +					ra_iface->has_autoconf_prefix = 1;
>  					break;
>  				}
>  			}
> -		} else
> -			log_debug("ignoring duplicate %s/%d prefix",
> -			    in6_to_str(addr), prefixlen);
> +		}
>  		return;
>  	}
>  
> @@ -1164,24 +1153,36 @@ add_new_prefix_to_ra_iface(struct ra_iface *ra_iface, struct in6_addr *addr,
>  	new_ra_prefix_conf->prefixlen = prefixlen;
>  	new_ra_prefix_conf->vltime = ra_prefix_conf->vltime;
>  	new_ra_prefix_conf->pltime = ra_prefix_conf->pltime;
> -	if (decaying_vltime != ND6_INFINITE_LIFETIME ||
> -	    decaying_pltime != ND6_INFINITE_LIFETIME) {
> -		ra_iface->ltime_decaying = 1;
> -		if (decaying_vltime != ND6_INFINITE_LIFETIME) {
> -			new_ra_prefix_conf->vltime = decaying_vltime;
> -			new_ra_prefix_conf->ltime_decaying |= VLTIME_DECAYING;
> -		}
> -		if (decaying_pltime != ND6_INFINITE_LIFETIME) {
> -			new_ra_prefix_conf->pltime = decaying_pltime;
> -			new_ra_prefix_conf->ltime_decaying |= PLTIME_DECAYING;
> -		}
> -	}
> +	new_ra_prefix_conf->autoconf = autoconf;
> +	if (autoconf)
> +		ra_iface->has_autoconf_prefix = 1;
> +	new_ra_prefix_conf->if_vltime = if_vltime;
> +	new_ra_prefix_conf->if_pltime = if_pltime;
>  	new_ra_prefix_conf->aflag = ra_prefix_conf->aflag;
>  	new_ra_prefix_conf->lflag = ra_prefix_conf->lflag;
>  	SIMPLEQ_INSERT_TAIL(&ra_iface->prefixes, new_ra_prefix_conf, entry);
>  	ra_iface->prefix_count++;
>  }
>  
> +uint32_t
> +calc_autoconf_ltime(time_t t, uint32_t conf_ltime, uint32_t if_ltime)
> +{
> +	uint32_t ltime;
> +
> +	if (if_ltime == ND6_INFINITE_LIFETIME)
> +		return conf_ltime;
> +
> +	if (if_ltime > t)
> +		ltime = if_ltime - t;
> +	else
> +		ltime = 0;
> +
> +	if (ltime < conf_ltime)
> +		return ltime;
> +	else
> +		return conf_ltime;
> +}
> +
>  int
>  build_packet(struct ra_iface *ra_iface)
>  {
> @@ -1294,16 +1295,15 @@ build_packet(struct ra_iface *ra_iface)
>  			ndopt_pi->nd_opt_pi_flags_reserved |=
>  			    ND_OPT_PI_FLAG_AUTO;
>  
> -		if (ra_prefix_conf->ltime_decaying & VLTIME_DECAYING)
> -			vltime = ra_prefix_conf->vltime < t ? 0 :
> -			    ra_prefix_conf->vltime - t;
> -		else
> -			vltime = ra_prefix_conf->vltime;
> -		if (ra_prefix_conf->ltime_decaying & PLTIME_DECAYING)
> -			pltime = ra_prefix_conf->pltime < t ? 0 :
> -			    ra_prefix_conf->pltime - t;
> -		else
> +		if (ra_prefix_conf->autoconf) {
> +			pltime = calc_autoconf_ltime(t,
> +			    ra_prefix_conf->pltime, ra_prefix_conf->if_pltime);
> +			vltime = calc_autoconf_ltime(t,
> +			    ra_prefix_conf->vltime, ra_prefix_conf->if_vltime);
> +		} else {
>  			pltime = ra_prefix_conf->pltime;
> +			vltime = ra_prefix_conf->vltime;
> +		}
>  
>  		ndopt_pi->nd_opt_pi_valid_time = htonl(vltime);
>  		ndopt_pi->nd_opt_pi_preferred_time = htonl(pltime);
> @@ -1430,7 +1430,7 @@ ra_output(struct ra_iface *ra_iface, struct sockaddr_in6 *to)
>  	if (!LINK_STATE_IS_UP(ra_iface->link_state))
>  		return;
>  
> -	if (ra_iface->ltime_decaying)
> +	if (ra_iface->has_autoconf_prefix)
>  		/* update vltime & pltime */
>  		build_packet(ra_iface);
>  
> diff --git rad.conf.5 rad.conf.5
> index d03bcc139aa..7211c48d0a1 100644
> --- rad.conf.5
> +++ rad.conf.5
> @@ -207,13 +207,13 @@ The preferred lifetime (pltime) in seconds for addresses generated from this
>  prefix.
>  The default is 2700.
>  This option is ignored if the prefix is discovered from a network interface
> -and it has a preferred lifetime configured.
> +and it has a lower preferred lifetime.
>  .It Cm valid lifetime Ar seconds
>  The valid lifetime (vltime) in seconds for addresses generated from this
>  prefix.
>  The default is 5400.
>  This option is ignored if the prefix is discovered from a network interface
> -and it has a valid lifetime configured.
> +and it has a lower valid lifetime.
>  .El
>  .El
>  .El
> diff --git rad.h rad.h
> index 7775ac4a0aa..00b4f1bfeb0 100644
> --- rad.h
> +++ rad.h
> @@ -35,8 +35,6 @@
>  #define	MIN_DELAY_BETWEEN_RAS	3	/* 3 seconds */
>  #define	MAX_SEARCH		1025	/* MAXDNAME in arpa/nameser.h */
>  #define	DEFAULT_RDNS_LIFETIME	3 * MAX_RTR_ADV_INTERVAL
> -#define	PLTIME_DECAYING		1
> -#define	VLTIME_DECAYING		2
>  
>  #define	IMSG_DATA_SIZE(imsg)	((imsg).hdr.len - IMSG_HEADER_SIZE)
>  
> @@ -116,7 +114,9 @@ struct ra_prefix_conf {
>  	int				 prefixlen;	/* prefix length */
>  	uint32_t			 vltime;	/* valid lifetime */
>  	uint32_t			 pltime;	/* preferred lifetime */
> -	int				 ltime_decaying;
> +	uint32_t			 if_vltime;	/* valid lifetime */
> +	uint32_t			 if_pltime;	/* preferred lifetime */
> +	int				 autoconf;
>  	int				 lflag;		/* on-link flag*/
>  	int				 aflag;		/* autonom. addr flag */
>  };
> 
> 
> 
> On 2025-08-07 17:09 -07, Ryan Vogt <rvogt.ca@gmail.com> wrote:
> > Hi tech@,
> >
> > I'd like to ping one final time, to see if there's any feedback about
> > this proposed patch.
> >
> > I also wanted to elaborate on one comment that I made in my previous
> > message (inline below), which I explained very poorly.
> >
> > On Sun, Jul 20, 2025 at 01:43:06PM -0700, Ryan Vogt wrote:
> >> On Tue, Jul 01, 2025 at 04:11:48PM -0700, Ryan Vogt wrote:
> >> > Hi tech@,
> >> > 
> >> > I've attached a proposed patch for rad(8). It allows rad to send
> >> > advertisements with shorter valid and preferred lifetimes than the
> >> > lifetimes otherwise already configured for a prefix discovered on an
> >> > interface.
> >> > 
> >> > The motivation for the patch is for debugging network setups.
> >> > Currently, let's say you have dhcp6leased(8) getting an upstream
> >> > prefix-delegation lease, good for 7 days. When rad starts advertising
> >> > the prefix, it'll advertise it as having a lifetime of 7 days. In
> >> > general, this is good behaviour. But, for debugging, I'd like to be
> >> > able to tell rad to advertise a lifetime of *no longer* than
> >> > 10 minutes, even if the upstream lease is good for more than 10 min.
> >> 
> >> I'm less confident in this statement now than I was a few weeks ago:
> >> that the current default behaviour of rad(8) is ideal (providing de
> >> facto unbounded lifetimes where they exist).
> >> 
> >> Someone sent me a message off-list where they commented about their
> >> situation with Google Fiber/Webpass. From what I understood, this
> >> Google service gives 1-year valid lifetimes and 1/2-year preferred
> >> lifetimes on DHCPv6 prefix delegations. Discussing extra-big numbers
> >> like that made me think more about the "ecosystem" (for lack of a
> >> better word) formed by dhcp6leased(8) and rad(8).
> >> 
> >> As it stands right now, in -current with the default behaviour of both
> >> rad(8) and dhcpd6leased(6), firing up those two programs on an OpenBSD
> >> gateway would send 1-year valid / half-year preferred lifetimes into
> >> the network behind the gateway. For comparison, the default lifetimes
> >> rad uses in a static-address situation are 90-minutes valid,
> >> 45-minutes preferred.
> >
> > In my previous message, I did a really poor job explaining what it was
> > that made me uncomfortable with this situation. Please allow me a
> > second attempt.
> >
> > Let's say that dhcp6leased gets a really long prefix delegation --
> > 1 year, 20 years, it doesn't matter. Let's call that prefix
> >     XXXX:XXXX:XXXX:XXXX::/64
> > Now, in the situation I described above, rad will echo that prefix
> > into the internal network, inside the gateway. All the clients on that
> > side will now have autoconfigured IPv6 addresses in the
> > XXXX:XXXX:XXXX:XXXX::/64 block with really long lifetimes.
> >
> > My concern is what would happen, now, if dhcp6leased were to get a new
> > prefix delegation early. As for how this could happen, maybe the
> > original DHCPv6 reply was malicious? Maybe there was a change to the
> > DHCPv6 server immediately upstream? I'm admittedly hand-waving the
> > specifics here, because my concern is only that it's a possibility.
> >
> > So, let's say that dhcp6leased gets another really long prefix
> > delegation -- again, some number of years, this time for prefix
> >     YYYY:YYYY:YYYY:YYYY::/64
> > Once again, rad will echo that prefix into the internal network. So,
> > all the hosts on the internal network will now have autoconfigured
> > addresses in the YYYY:YYYY:YYYY:YYYY::/64 range and the
> > XXXX:XXXX:XXXX:XXXX::/64 range, both with really long lifetimes. It
> > doesn't matter if rad isn't advertising XXXX:XXXX:XXXX:XXXX::/64
> > anymore, because the hosts will remember it for its lifetime.
> >
> > Repeat by giving dhcp6leased a ZZZZ:ZZZZ:ZZZZ:ZZZZ::/64 delegation,
> > a WWWW:WWWW:WWWW:WWWW::/64 delegation, etc., and all the hosts inside
> > the network could have a lot of IPv6 addresses, all with really long
> > lifetimes.
> >
> > While IPv6 is designed to allow hosts to have lots of different
> > addresses, what concerns me is whether there's some risk of resource
> > exhaustion to the hosts inside the network, if rad keeps advertising
> > different prefixes with very long lifetimes.
> >
> > Bounding rad's advertised lifetimes doesn't make this potential
> > problem go away (and there are a lot of question marks after
> > "potential problem", because I'm not sure how much of one it is). But
> > my thinking, in my previous message, was that bounding rad's
> > advertised lifetimes could limit how long any issue persists.
> >
> > As always, thank you very much for your time.
> >
> > Cheers,
> > Ryan
> >
> >> All of that to say, is the current default behaviour of rad(8) what
> >> the default behaviour should be? I've added some example configuration
> >> files, inline below, showing what would happen if we changed that
> >> behaviour.
> >>
> >> [...] 
> >> 
> >> As a thought exercise, here's what would happen if I were to change
> >> the patch slightly, making "configured lifetimes no" the default,
> >> instead of matching rad's current behaviour with
> >> "configured lifetimes yes" being the default.
> >> 
> >> [...]
> >> 
> >> This would change the default behaviour of rad only in situations with
> >> configured lifetimes, such as where a lifetime has been placed onto
> >> the interface by dhcp6leased(8).
> >> 
> >> Here are some example rad.conf(5) configuration files and how they
> >> would behave with this modification:
> >> 
> >> Configuration file 1 - rad would send lifetimes of at most
> >> 5400 seconds (90 minutes) valid, 2700 seconds (45 minutes) preferred.
> >> If the configured lifetimes on the interface are shorter, rad would
> >> send those smaller values:
> >> 
> >>     interface em1
> >> 
> >> Configuration file 2 - rad would send lifetimes of at most 600 seconds
> >> (10 mintues) valid, 300 seconds (5 minutes) preferred. Again, if the
> >> configurated lifetimes are less, rad would send those smaller values:
> >> 
> >>     interface em1 {
> >>         auto prefix {
> >>             valid lifetime 600
> >>             preferred lifetime 300
> >>         }
> >>     }
> >> 
> >> Configuration file 3 - we explicitly tell rad to go ahead and send
> >> whatever lifetimes dhcp6leased places onto em1, even if they're huge:
> >> 
> >>     interface em1 {
> >>         auto prefix {
> >>             configured lifetimes yes
> >>         }
> >>     }
> >> 
> >> (For clarity: the behaviour of rad in -current with the first two
> >> configuration files is to send the unbounded configured lifetimes.
> >> There's no "configured lifetimes" option in -current, so the third
> >> configuration file has no meaning in -current.)
> >> 
> >> Changing the patch like this to bound rad's behaviour by default,
> >> though, would change the behaviour of rad for what I imagine are a lot
> >> of people's current rad.conf files. And, it would be over something
> >> that I can't actually convince myself is a real problem. Thoughts?
> >> 
> >> Again, thank you very much for taking the time to look through this
> >> wall of text I've created, and for sharing any insight.
> >> 
> >> Cheers,
> >> Ryan
> >> 
> >> > Thank you very much for taking the time to consider this patch, and
> >> > for any feedback provided.
> >> > 
> >> > Cheers,
> >> > Ryan
> >> > 
> >> > diff --git usr.sbin/rad/frontend.c usr.sbin/rad/frontend.c
> >> > index 2a519dac0b1..cb955043576 100644
> >> > --- usr.sbin/rad/frontend.c
> >> > +++ usr.sbin/rad/frontend.c
> >> > @@ -89,6 +89,9 @@
> >> >  #define	RA_MAX_SIZE		1500
> >> >  #define	ROUTE_SOCKET_BUF_SIZE	16384
> >> >  
> >> > +#define LT_IS_DECAYING(vltime, pltime) \
> >> > +    ((vltime) != ND6_INFINITE_LIFETIME || (pltime) != ND6_INFINITE_LIFETIME)
> >> > +
> >> >  struct icmp6_ev {
> >> >  	struct event		 ev;
> >> >  	uint8_t			 answer[1500];
> >> > @@ -1122,37 +1125,35 @@ add_new_prefix_to_ra_iface(struct ra_iface *ra_iface, struct in6_addr *addr,
> >> >      uint32_t decaying_vltime, uint32_t decaying_pltime)
> >> >  {
> >> >  	struct ra_prefix_conf	*new_ra_prefix_conf;
> >> > +	struct ra_prefix_conf	*pc;
> >> > +	int			 was_decaying;
> >> >  
> >> >  	if ((new_ra_prefix_conf = find_ra_prefix_conf(&ra_iface->prefixes, addr,
> >> >  	    prefixlen)) != NULL) {
> >> > -		if (decaying_vltime != ND6_INFINITE_LIFETIME ||
> >> > -		    decaying_pltime != ND6_INFINITE_LIFETIME) {
> >> > +		if (new_ra_prefix_conf->decaying_vltime == decaying_vltime &&
> >> > +		    new_ra_prefix_conf->decaying_pltime == decaying_pltime) {
> >> > +			log_debug("ignoring duplicate %s/%d prefix",
> >> > +			    in6_to_str(addr), prefixlen);
> >> > +			return;
> >> > +		}
> >> > +		was_decaying = LT_IS_DECAYING(
> >> > +		    new_ra_prefix_conf->decaying_vltime,
> >> > +		    new_ra_prefix_conf->decaying_pltime);
> >> > +		new_ra_prefix_conf->decaying_vltime = decaying_vltime;
> >> > +		new_ra_prefix_conf->decaying_pltime = decaying_pltime;
> >> > +		if (LT_IS_DECAYING(decaying_vltime, decaying_pltime))
> >> >  			ra_iface->ltime_decaying = 1;
> >> > -			new_ra_prefix_conf->ltime_decaying = 0;
> >> > -			if (decaying_vltime != ND6_INFINITE_LIFETIME) {
> >> > -				new_ra_prefix_conf->vltime = decaying_vltime;
> >> > -				new_ra_prefix_conf->ltime_decaying |=
> >> > -				    VLTIME_DECAYING;
> >> > -			}
> >> > -			if (decaying_pltime != ND6_INFINITE_LIFETIME) {
> >> > -				new_ra_prefix_conf->pltime = decaying_pltime;
> >> > -				new_ra_prefix_conf->ltime_decaying |=
> >> > -				    PLTIME_DECAYING;
> >> > -			}
> >> > -		} else if (new_ra_prefix_conf->ltime_decaying) {
> >> > -			struct ra_prefix_conf *pc;
> >> > -
> >> > -			new_ra_prefix_conf->ltime_decaying = 0;
> >> > +		else if (was_decaying) {
> >> >  			ra_iface->ltime_decaying = 0;
> >> >  			SIMPLEQ_FOREACH(pc, &ra_iface->prefixes, entry) {
> >> > -				if (pc->ltime_decaying) {
> >> > +				if (LT_IS_DECAYING(pc->vltime, pc->pltime)) {
> >> >  					ra_iface->ltime_decaying = 1;
> >> >  					break;
> >> >  				}
> >> >  			}
> >> > -		} else
> >> > -			log_debug("ignoring duplicate %s/%d prefix",
> >> > -			    in6_to_str(addr), prefixlen);
> >> > +		}
> >> > +		log_debug("updating %s/%d prefix lifetimes", in6_to_str(addr),
> >> > +		    prefixlen);
> >> >  		return;
> >> >  	}
> >> >  
> >> > @@ -1164,18 +1165,11 @@ add_new_prefix_to_ra_iface(struct ra_iface *ra_iface, struct in6_addr *addr,
> >> >  	new_ra_prefix_conf->prefixlen = prefixlen;
> >> >  	new_ra_prefix_conf->vltime = ra_prefix_conf->vltime;
> >> >  	new_ra_prefix_conf->pltime = ra_prefix_conf->pltime;
> >> > -	if (decaying_vltime != ND6_INFINITE_LIFETIME ||
> >> > -	    decaying_pltime != ND6_INFINITE_LIFETIME) {
> >> > +	new_ra_prefix_conf->decaying_vltime = decaying_vltime;
> >> > +	new_ra_prefix_conf->decaying_pltime = decaying_pltime;
> >> > +	if (LT_IS_DECAYING(decaying_vltime, decaying_pltime))
> >> >  		ra_iface->ltime_decaying = 1;
> >> > -		if (decaying_vltime != ND6_INFINITE_LIFETIME) {
> >> > -			new_ra_prefix_conf->vltime = decaying_vltime;
> >> > -			new_ra_prefix_conf->ltime_decaying |= VLTIME_DECAYING;
> >> > -		}
> >> > -		if (decaying_pltime != ND6_INFINITE_LIFETIME) {
> >> > -			new_ra_prefix_conf->pltime = decaying_pltime;
> >> > -			new_ra_prefix_conf->ltime_decaying |= PLTIME_DECAYING;
> >> > -		}
> >> > -	}
> >> > +	new_ra_prefix_conf->cfgltimes = ra_prefix_conf->cfgltimes;
> >> >  	new_ra_prefix_conf->aflag = ra_prefix_conf->aflag;
> >> >  	new_ra_prefix_conf->lflag = ra_prefix_conf->lflag;
> >> >  	SIMPLEQ_INSERT_TAIL(&ra_iface->prefixes, new_ra_prefix_conf, entry);
> >> > @@ -1294,14 +1288,22 @@ build_packet(struct ra_iface *ra_iface)
> >> >  			ndopt_pi->nd_opt_pi_flags_reserved |=
> >> >  			    ND_OPT_PI_FLAG_AUTO;
> >> >  
> >> > -		if (ra_prefix_conf->ltime_decaying & VLTIME_DECAYING)
> >> > -			vltime = ra_prefix_conf->vltime < t ? 0 :
> >> > -			    ra_prefix_conf->vltime - t;
> >> > +		if (ra_prefix_conf->decaying_vltime != ND6_INFINITE_LIFETIME) {
> >> > +			vltime = ra_prefix_conf->decaying_vltime < t ? 0 :
> >> > +			    ra_prefix_conf->decaying_vltime - t;
> >> > +			if (vltime > ra_prefix_conf->vltime &&
> >> > +			    !ra_prefix_conf->cfgltimes)
> >> > +			    vltime = ra_prefix_conf->vltime;
> >> > +		}
> >> >  		else
> >> >  			vltime = ra_prefix_conf->vltime;
> >> > -		if (ra_prefix_conf->ltime_decaying & PLTIME_DECAYING)
> >> > -			pltime = ra_prefix_conf->pltime < t ? 0 :
> >> > -			    ra_prefix_conf->pltime - t;
> >> > +		if (ra_prefix_conf->decaying_pltime != ND6_INFINITE_LIFETIME) {
> >> > +			pltime = ra_prefix_conf->decaying_pltime < t ? 0 :
> >> > +			    ra_prefix_conf->decaying_pltime - t;
> >> > +			if (pltime > ra_prefix_conf->pltime &&
> >> > +			    !ra_prefix_conf->cfgltimes)
> >> > +			    pltime = ra_prefix_conf->pltime;
> >> > +		}
> >> >  		else
> >> >  			pltime = ra_prefix_conf->pltime;
> >> >  
> >> > diff --git usr.sbin/rad/parse.y usr.sbin/rad/parse.y
> >> > index bf6b6e6b86d..d4d058491dd 100644
> >> > --- usr.sbin/rad/parse.y
> >> > +++ usr.sbin/rad/parse.y
> >> > @@ -122,7 +122,7 @@ typedef struct {
> >> >  %token	CONFIGURATION OTHER LIFETIME REACHABLE TIME RETRANS TIMER
> >> >  %token	AUTO PREFIX VALID PREFERENCE PREFERRED LIFETIME ONLINK AUTONOMOUS
> >> >  %token	ADDRESS_CONFIGURATION DNS NAMESERVER SEARCH MTU NAT64 HIGH MEDIUM LOW
> >> > -%token	SOURCE LINK_LAYER
> >> > +%token	SOURCE LINK_LAYER CONFIGURED LIFETIMES
> >> >  
> >> >  %token	<v.string>	STRING
> >> >  %token	<v.number>	NUMBER
> >> > @@ -368,6 +368,9 @@ ra_prefixoptsl	: VALID LIFETIME NUMBER {
> >> >  		| PREFERRED LIFETIME NUMBER {
> >> >  			ra_prefix_conf->pltime = $3;
> >> >  		}
> >> > +		| CONFIGURED LIFETIMES yesno {
> >> > +			ra_prefix_conf->cfgltimes = $3;
> >> > +		}
> >> >  		| ONLINK yesno {
> >> >  			ra_prefix_conf->lflag = $2;
> >> >  		}
> >> > @@ -519,6 +522,7 @@ lookup(char *s)
> >> >  		{"auto",		AUTO},
> >> >  		{"autonomous",		AUTONOMOUS},
> >> >  		{"configuration",	CONFIGURATION},
> >> > +		{"configured",		CONFIGURED},
> >> >  		{"default",		DEFAULT},
> >> >  		{"dns",			DNS},
> >> >  		{"high",		HIGH},
> >> > @@ -526,6 +530,7 @@ lookup(char *s)
> >> >  		{"include",		INCLUDE},
> >> >  		{"interface",		RA_IFACE},
> >> >  		{"lifetime",		LIFETIME},
> >> > +		{"lifetimes",		LIFETIMES},
> >> >  		{"limit",		LIMIT},
> >> >  		{"link-layer",		LINK_LAYER},
> >> >  		{"low",			LOW},
> >> > @@ -1087,6 +1092,7 @@ conf_get_ra_prefix(struct in6_addr *addr, int prefixlen)
> >> >  	prefix->prefixlen = prefixlen;
> >> >  	prefix->vltime = ADV_VALID_LIFETIME;
> >> >  	prefix->pltime = ADV_PREFERRED_LIFETIME;
> >> > +	prefix->cfgltimes = 1;
> >> >  	prefix->lflag = 1;
> >> >  	prefix->aflag = 1;
> >> >  
> >> > diff --git usr.sbin/rad/rad.conf.5 usr.sbin/rad/rad.conf.5
> >> > index bf684759e11..292070c7c60 100644
> >> > --- usr.sbin/rad/rad.conf.5
> >> > +++ usr.sbin/rad/rad.conf.5
> >> > @@ -168,6 +168,20 @@ options are as follows:
> >> >  This prefix can be used to generate IPv6 addresses.
> >> >  The default is
> >> >  .Cm yes .
> >> > +.It Ic configured lifetimes Pq Cm yes Ns | Ns Cm no
> >> > +If the prefix is discovered from a network interface and it has configured
> >> > +valid or preferred lifetimes, use the configured lifetimes instead of those
> >> > +specified in the static
> >> > +.Ic preferred lifetime
> >> > +and
> >> > +.Ic valid lifetime
> >> > +options.
> >> > +Even if set to
> >> > +.Cm no ,
> >> > +static lifetimes will never be used when they are longer than the
> >> > +prefix's configured lifetimes.
> >> > +The default is
> >> > +.Cm yes .
> >> >  .It Ic on-link Pq Cm yes Ns | Ns Cm no
> >> >  This prefix is considered on-link.
> >> >  The default is
> >> > @@ -176,14 +190,10 @@ The default is
> >> >  The preferred lifetime (pltime) in seconds for addresses generated from this
> >> >  prefix.
> >> >  The default is 2700.
> >> > -This option is ignored if the prefix is discovered from a network interface
> >> > -and it has a preferred lifetime configured.
> >> >  .It Ic valid lifetime Ar seconds
> >> >  The valid lifetime (vltime) in seconds for addresses generated from this
> >> >  prefix.
> >> >  The default is 5400.
> >> > -This option is ignored if the prefix is discovered from a network interface
> >> > -and it has a valid lifetime configured.
> >> >  .El
> >> >  .El
> >> >  .El
> >> > diff --git usr.sbin/rad/rad.h usr.sbin/rad/rad.h
> >> > index 7775ac4a0aa..5f7b79fb38b 100644
> >> > --- usr.sbin/rad/rad.h
> >> > +++ usr.sbin/rad/rad.h
> >> > @@ -35,8 +35,6 @@
> >> >  #define	MIN_DELAY_BETWEEN_RAS	3	/* 3 seconds */
> >> >  #define	MAX_SEARCH		1025	/* MAXDNAME in arpa/nameser.h */
> >> >  #define	DEFAULT_RDNS_LIFETIME	3 * MAX_RTR_ADV_INTERVAL
> >> > -#define	PLTIME_DECAYING		1
> >> > -#define	VLTIME_DECAYING		2
> >> >  
> >> >  #define	IMSG_DATA_SIZE(imsg)	((imsg).hdr.len - IMSG_HEADER_SIZE)
> >> >  
> >> > @@ -116,7 +114,9 @@ struct ra_prefix_conf {
> >> >  	int				 prefixlen;	/* prefix length */
> >> >  	uint32_t			 vltime;	/* valid lifetime */
> >> >  	uint32_t			 pltime;	/* preferred lifetime */
> >> > -	int				 ltime_decaying;
> >> > +	uint32_t			 decaying_vltime;
> >> > +	uint32_t			 decaying_pltime;
> >> > +	int				 cfgltimes;	/* config'd lifetimes */
> >> >  	int				 lflag;		/* on-link flag*/
> >> >  	int				 aflag;		/* autonom. addr flag */
> >> >  };
> >
> 
> -- 
> In my defence, I have been left unsupervised.