Index | Thread | Search

From:
Stefan Sperling <stsp@stsp.name>
Subject:
iked SA_INIT cookies
To:
tech@openbsd.org
Date:
Mon, 23 Mar 2026 15:45:24 +0100

Download raw body.

Thread
  • Stefan Sperling:

    iked SA_INIT cookies

This patch adds support for sending cookies during SA establishment,
as described in RFC 7296 section 2.6 "IKE SA SPIs and Cookies".

iked can already reflect cookies sent by other implementations but
currently lacks support for generating such cookies itself. This patch
makes iked send cookies to new peers based on a configurable threshold
of half-open SAs (checked once per second).

The goal of this mechanism is to avoid allocating resources during a
flood of SA_INIT requests sent from forged source IPs. If a peer does
not send back the non-forgable cookie we sent then we know that
bi-directional communication is impossible and we can drop SA INIT
requests from spoofed peers without allocating a persistent SA data
structure, which would need 300 seconds to time out before being freed.

ok?

M  sbin/iked/config.c     |    4+  0-
M  sbin/iked/iked.conf.5  |   24+  0-
M  sbin/iked/iked.h       |   18+  0-
M  sbin/iked/ikev2.c      |  248+  1-
M  sbin/iked/ikev2_pld.c  |   38+  0-
M  sbin/iked/parse.y      |   21+  1-

6 files changed, 353 insertions(+), 2 deletions(-)

commit - caeb49275dc38b7e4986806f540114b2665e2d27
commit + d96c024ff9c0b71e80fa2974999ca060b2e05c26
blob - def970e05a0e7fb6cda29388b044b93e6ae976f4
blob + 143216dc315d60443e130274b19ec8dcd338f545
--- sbin/iked/config.c
+++ sbin/iked/config.c
@@ -989,6 +989,10 @@ config_getstatic(struct iked *env, struct imsg *imsg)
 	log_debug("%s: nattport %u", __func__, env->sc_nattport);
 	log_debug("%s: %sstickyaddress", __func__,
 	    env->sc_stickyaddress ? "" : "no ");
+	log_debug("%s: sa_cookies_threshold %u", __func__,
+	    env->sc_cookies_threshold);
+	log_debug("%s: sa_cookies_renewal %u", __func__,
+	    env->sc_cookies_renewal);
 
 	ikev2_reset_alive_timer(env);
 
blob - d7f1c75bcfaa34cb00cb37d6062d519236b7c7b3
blob + ef091c04acc4817fba40ee48cc2d8f53ba503122
--- sbin/iked/iked.conf.5
+++ sbin/iked/iked.conf.5
@@ -180,6 +180,30 @@ is established, it replaces the old SA.
 Don't limit the number of IKE SAs per
 .Ic dstid .
 This is the default.
+.It Ic set sa_cookies_threshold Ar number
+Set the amount of half-open SAs beyond which
+.Xr iked 8
+will start requiring use of cookies during the initial handshake with
+a new peer, which mitigates some denial-of-service scenarios.
+The default value is 30 half-open SAs.
+A zero value disables use of cookies.
+.Pp
+When a peer requests establishment of a new SA,
+.Xr iked 8
+can either allocate state for the new SA immediately or respond with a
+non-forgable cookie.
+When cookies are used, the peer must retry SA establishment and the retried
+request must include a copy of the cookie sent by
+.Xr iked 8 .
+If the peer fails to provide the cookie then establishment of the SA will fail.
+This ensures that bi-directional communication with the peer is indeed possible
+before committing any resources to communication with the peer.
+.It Ic set sa_cookies_renewal Ar interval
+Set the frequency at which the secret used to calculate SA establishment
+cookies is renewed.
+Cookies sent before renewal will no longer be accepted, forcing peers
+to retry pending SA establishment requests, possibly with a fresh cookie.
+The default interval is every 5 seconds.
 .It Ic set fragmentation
 Enable IKEv2 Message Fragmentation (RFC 7383) support.
 This allows IKEv2 to operate in environments that might block IP fragments.
blob - 367ea76047ae3d7ec009bd36cab0e3bbeb9ec36a
blob + 1ec34a613a1680f68fa74213b7b6c4e564ab1e2d
--- sbin/iked/iked.h
+++ sbin/iked/iked.h
@@ -866,8 +866,16 @@ struct iked_static {
 	in_port_t		 st_nattport;
 	int			 st_stickyaddress; /* addr per DSTID  */
 	int			 st_vendorid;
+	int			 st_cookies_threshold;
+	int			 st_cookies_renewal;
 };
 
+enum iked_cookie_status {
+	IKED_COOKIE_BAD,	/* bad cookie received */
+	IKED_COOKIE_ERROR,	/* error occurred while processing cookie */
+	IKED_COOKIE_VALID,	/* valid cookie received */
+};
+
 struct iked {
 	char				 sc_conffile[PATH_MAX];
 
@@ -886,6 +894,8 @@ struct iked {
 #define sc_nattport		sc_static.st_nattport
 #define sc_stickyaddress	sc_static.st_stickyaddress
 #define sc_vendorid		sc_static.st_vendorid
+#define sc_cookies_threshold	sc_static.st_cookies_threshold
+#define sc_cookies_renewal	sc_static.st_cookies_renewal
 
 	struct iked_policies		 sc_policies;
 	struct iked_policy		*sc_defaultcon;
@@ -904,6 +914,14 @@ struct iked {
 	struct iked_raddaes		 sc_raddaes;
 	struct iked_radclients		 sc_raddaeclients;
 
+	uint8_t				 sc_require_cookies;
+	unsigned char			 sc_cookies_secret[32];
+	struct timespec			 sc_cookies_secret_renew;
+	struct iked_timer		 sc_cookies_timer;
+#define IKED_COOKIES_TIMER		1	/* seconds */
+#define IKED_COOKIES_SECRET_MAX_AGE	5	/* seconds */
+#define IKED_COOKIES_THRESHOLD		30	/* number of half-open SAs */
+
 	struct iked_stats		 sc_stats;
 
 	void				*sc_priv;	/* per-process */
blob - 4ed537624692de0489ebf9c2a0bea378e164b8b2
blob + 61043dcd321ce15857f52539ad37a3b70b109d16
--- sbin/iked/ikev2.c
+++ sbin/iked/ikev2.c
@@ -90,6 +90,14 @@ int	 ikev2_record_dstid(struct iked *, struct iked_sa 
 void	 ikev2_enable_timer(struct iked *, struct iked_sa *);
 void	 ikev2_disable_timer(struct iked *, struct iked_sa *);
 
+struct ibuf *ikedv2_generate_cookie(struct iked *, struct iked_message *,
+	    struct iked_sa *);
+
+void	ikev2_cookies_trigger(struct iked *, void *);
+enum iked_cookie_status	ikedv2_verify_cookie(struct iked *,
+	    struct ike_header *, struct iked_message *);
+void	 ikedv2_send_cookie(struct iked *, struct ike_header *,
+	    struct iked_message *);
 void	 ikev2_resp_recv(struct iked *, struct iked_message *,
 	    struct ike_header *);
 int	 ikev2_resp_ike_sa_init(struct iked *, struct iked_message *);
@@ -209,6 +217,8 @@ ikev2(struct privsep *ps, struct privsep_proc *p)
 void
 ikev2_run(struct privsep *ps, struct privsep_proc *p, void *arg)
 {
+	struct iked		*env = iked_env;
+
 	/*
 	 * pledge in the ikev2 process:
 	 * stdio - for malloc and basic I/O including events.
@@ -219,6 +229,11 @@ ikev2_run(struct privsep *ps, struct privsep_proc *p, 
 	p->p_shutdown = ikev2_shutdown;
 	if (pledge("stdio inet recvfd", NULL) == -1)
 		fatal("pledge");
+	
+	arc4random_buf(env->sc_cookies_secret, sizeof(env->sc_cookies_secret));
+	clock_gettime(CLOCK_MONOTONIC, &env->sc_cookies_secret_renew);
+
+	timer_set(env, &env->sc_cookies_timer, ikev2_cookies_trigger, NULL);
 }
 
 void
@@ -226,6 +241,7 @@ ikev2_shutdown(void)
 {
 	struct iked		*env = iked_env;
 
+	timer_del(env, &env->sc_cookies_timer);
 	ibuf_free(env->sc_certreq);
 	env->sc_certreq = NULL;
 	config_doreset(env, RESET_ALL);
@@ -311,7 +327,13 @@ ikev2_dispatch_parent(int fd, struct privsep_proc *p, 
 	case IMSG_COMPILE:
 		return (config_getcompile(env));
 	case IMSG_CTL_STATIC:
-		return (config_getstatic(env, imsg));
+		if (config_getstatic(env, imsg))
+			return (-1);
+		if (env->sc_cookies_threshold > 0) {
+			timer_add(env, &env->sc_cookies_timer,
+			    IKED_COOKIES_TIMER);
+		}
+		return (0);
 	default:
 		break;
 	}
@@ -2932,7 +2954,201 @@ ikev2_resp_informational(struct iked *env, struct iked
 	return (ret);
 }
 
+struct ibuf *
+ikedv2_generate_cookie(struct iked *env, struct iked_message *msg,
+    struct iked_sa *sa)
+{
+	struct ibuf	*cookie;
+	SHA256_CTX	 ctx;
+	unsigned char	 digest[SHA256_DIGEST_LENGTH];
+
+	SHA256_Init(&ctx);
+
+	if (msg->msg_nonce == NULL) {
+		log_debug("%s: malformed SA_INIT payload: no NONCE given",
+		    __func__);
+		return NULL;
+		
+	}
+	cookie = ibuf_open(SHA256_DIGEST_LENGTH);
+	if (cookie == NULL)
+		return NULL;
+
+	/* COOKIE = HASH(secret | Ni | IPi | SPIi) */
+	SHA256_Update(&ctx, env->sc_cookies_secret,
+	    sizeof(env->sc_cookies_secret));
+	SHA256_Update(&ctx, ibuf_data(msg->msg_nonce),
+	    ibuf_size(msg->msg_nonce));
+	SHA256_Update(&ctx, &msg->msg_peer, msg->msg_peerlen);
+	SHA256_Update(&ctx, &sa->sa_hdr.sh_ispi,
+	    sizeof(sa->sa_hdr.sh_ispi));
+	SHA256_Final(digest, &ctx);
+
+	if (ibuf_add(cookie, &digest, sizeof(digest)) != 0) {
+		ibuf_free(cookie);
+		return NULL;
+	}
+
+	return cookie;
+}
+
 void
+ikedv2_send_cookie(struct iked *env, struct ike_header *msg_hdr,
+    struct iked_message *msg)
+{
+	struct iked_message		 resp;
+	struct iked_sa			*sa;
+	struct ike_header		*hdr;
+	struct ikev2_payload		*pld;
+	struct ikev2_notify		*n;
+	struct ibuf			*buf;
+	struct ibuf			*cookie;
+	size_t				 len;
+	
+	/* Temporary SA used only for sending the cookie. */
+	sa = config_new_sa(env, 0);
+	if (sa == NULL) {
+		log_debug("%s: failed to allocate IKE SA", __func__);
+		return;
+	}
+	sa->sa_hdr.sh_ispi = betoh64(msg_hdr->ike_ispi);
+	sa->sa_hdr.sh_rspi = betoh64(msg_hdr->ike_rspi);
+	sa->sa_fd = msg->msg_fd;
+
+	cookie = ikedv2_generate_cookie(env, msg, sa);
+	if (cookie == NULL)
+		goto done;
+
+	if ((buf = ikev2_msg_init(env, &resp,
+	    &msg->msg_peer, msg->msg_peerlen,
+	    &msg->msg_local, msg->msg_locallen, 1)) == NULL)
+		goto done;
+
+	msg->msg_fd = sa->sa_fd;
+
+	if ((hdr = ikev2_add_header(buf, sa, 0, IKEV2_PAYLOAD_NOTIFY,
+	    IKEV2_EXCHANGE_IKE_SA_INIT, 0)) == NULL)
+		goto done;
+
+	if ((pld = ikev2_add_payload(buf)) == NULL)
+		goto done;
+	if ((n = ibuf_reserve(buf, sizeof(*n))) == NULL)
+		goto done;
+	n->n_protoid = IKEV2_SAPROTO_NONE;
+	n->n_spisize = 0;
+	n->n_type = htobe16(IKEV2_N_COOKIE);
+	if (ikev2_add_buf(buf, cookie) == -1)
+		goto done;
+	len = sizeof(*n) + ibuf_size(cookie);
+
+	log_debug("%s: added cookie, len %zu", __func__,
+	    ibuf_size(cookie));
+	print_hexbuf(cookie);
+
+	if (ikev2_next_payload(pld, len, IKEV2_PAYLOAD_NONE) == -1)
+		goto done;
+
+	if (ikev2_set_header(hdr, ibuf_size(buf) - sizeof(*hdr)) == -1)
+		goto done;
+
+	(void)ikev2_pld_parse(env, hdr, &resp, 0);
+	resp.msg_fd = msg->msg_fd;
+	ikev2_msg_send(env, &resp);
+done:
+	ikev2_msg_cleanup(env, &resp);
+	ibuf_free(cookie);
+	config_free_sa(env, sa);
+}
+
+void
+ikev2_cookies_trigger(struct iked *env, void *arg)
+{
+	struct timespec	 now, secret_age;
+	struct iked_sa	*sa;
+	unsigned int	 half_open_count = 0;
+
+	RB_FOREACH(sa, iked_sas, &env->sc_sas) {
+		if (sa->sa_state >= IKEV2_STATE_ESTABLISHED)
+			continue;
+
+		half_open_count++;
+		if (half_open_count > IKED_COOKIES_THRESHOLD)
+			break;
+	}
+	if (half_open_count > IKED_COOKIES_THRESHOLD) {
+		if (env->sc_require_cookies == 0)
+			log_debug("%s: enabling SA_INIT cookies", __func__);
+		env->sc_require_cookies = 1;
+	} else {
+		if (env->sc_require_cookies)
+			log_debug("%s: disabling SA_INIT cookies", __func__);
+		env->sc_require_cookies = 0;
+	}
+
+	if (env->sc_require_cookies) {
+		clock_gettime(CLOCK_MONOTONIC, &now);
+		timespecsub(&now, &env->sc_cookies_secret_renew, &secret_age);
+		if (secret_age.tv_sec >= env->sc_cookies_renewal) {
+			arc4random_buf(env->sc_cookies_secret,
+			    sizeof(env->sc_cookies_secret));
+			env->sc_cookies_secret_renew = now;
+		}
+	}
+
+	timer_add(env, &env->sc_cookies_timer, IKED_COOKIES_TIMER);
+}
+
+enum iked_cookie_status
+ikedv2_verify_cookie(struct iked *env, struct ike_header *msg_hdr,
+    struct iked_message *msg)
+{
+	struct iked_sa		*sa;
+	struct ibuf		*cookie;
+	int			 ret = IKED_COOKIE_ERROR;
+
+	if (ibuf_length(msg->msg_cookie) != SHA256_DIGEST_LENGTH) {
+		log_debug("%s: bad SA_INIT cookie", __func__);
+		return IKED_COOKIE_BAD;
+	}
+	
+	/* Temporary SA used only for verifying the cookie. */
+	sa = config_new_sa(env, 0);
+	if (sa == NULL) {
+		log_debug("%s: failed to allocate IKE SA", __func__);
+		return IKED_COOKIE_ERROR;
+	}
+
+	sa->sa_hdr.sh_ispi = betoh64(msg_hdr->ike_ispi);
+	sa->sa_hdr.sh_rspi = betoh64(msg_hdr->ike_rspi);
+
+	cookie = ikedv2_generate_cookie(env, msg, sa);
+	if (cookie == NULL) {
+		config_free_sa(env, sa);
+		return IKED_COOKIE_ERROR;
+	}
+
+	log_debug("%s: received SA_INIT cookie, len %zu", __func__,
+	    ibuf_size(msg->msg_cookie));
+	print_hexbuf(msg->msg_cookie);
+
+	log_debug("%s: local SA_INIT cookie, len %zu", __func__,
+	    ibuf_size(cookie));
+	print_hexbuf(cookie);
+
+	if (ibuf_length(msg->msg_cookie) != ibuf_size(cookie) ||
+	    memcmp(ibuf_data(cookie), ibuf_data(msg->msg_cookie),
+	    ibuf_size(cookie)) != 0) {
+		ret = IKED_COOKIE_BAD;
+		log_debug("%s: bad SA_INIT cookie", __func__);
+	} else
+		ret = IKED_COOKIE_VALID;
+
+	config_free_sa(env, sa);
+	ibuf_free(cookie);
+	return ret;
+}
+
+void
 ikev2_resp_recv(struct iked *env, struct iked_message *msg,
     struct ike_header *hdr)
 {
@@ -2944,6 +3160,37 @@ ikev2_resp_recv(struct iked *env, struct iked_message 
 			log_debug("%s: SA already exists", __func__);
 			return;
 		}
+		if (env->sc_require_cookies) {
+			/* Cookies need the NONCE value. */
+			if (ikev2_pld_parse_quick(env, hdr, msg,
+			    msg->msg_offset) != 0) {
+				log_debug("%s: failed to parse message",
+				    __func__);
+				return;
+			}
+			if (msg->msg_nonce == NULL) {
+				log_debug("%s: no NONCE given", __func__);
+				return;
+			}
+
+			if (msg->msg_cookie == NULL) {
+				ikedv2_send_cookie(env, hdr, msg);
+				return;
+			}
+
+			switch (ikedv2_verify_cookie(env, hdr, msg)) {
+			case IKED_COOKIE_BAD:
+				ikedv2_send_cookie(env, hdr, msg);
+				return;
+			case IKED_COOKIE_ERROR:
+				return;
+			case IKED_COOKIE_VALID:
+				/* Message will be parsed again below. */
+				ibuf_free(msg->msg_nonce);
+				msg->msg_nonce = NULL;
+				break;
+			}
+		}
 		if ((msg->msg_sa = sa_new(env,
 		    betoh64(hdr->ike_ispi), betoh64(hdr->ike_rspi),
 		    0, msg->msg_policy)) == NULL) {
blob - ac8635e79fc41d63beb678d1345d4c1edbcb12c2
blob + 864068ce847e290a4339e90315d6e8d4950e2683
--- sbin/iked/ikev2_pld.c
+++ sbin/iked/ikev2_pld.c
@@ -2126,11 +2126,13 @@ ikev2_pld_parse_quick(struct iked *env, struct ike_hea
 {
 	struct ikev2_payload	 pld;
 	struct ikev2_frag_payload frag;
+	struct ikev2_notify	 n;
 	uint8_t			*msgbuf = ibuf_data(msg->msg_data);
 	uint8_t			*buf;
 	size_t			 len, total, left;
 	size_t			 length;
 	unsigned int		 payload;
+	uint16_t		 type;
 
 	log_debug("%s: header ispi %s rspi %s"
 	    " nextpayload %s version 0x%02x exchange %s flags 0x%02x"
@@ -2185,6 +2187,42 @@ ikev2_pld_parse_quick(struct iked *env, struct ike_hea
 			memcpy(&frag, buf, sizeof(frag));
 			msg->msg_frag_num = betoh16(frag.frag_num);
 			break;
+		case IKEV2_PAYLOAD_NONCE:
+			len = left;
+			buf = msgbuf + offset;
+			print_hex(buf, 0, len);
+			if (len == 0)
+				return (-1);
+			if ((msg->msg_nonce = ibuf_new(buf, len)) == NULL) {
+				log_debug("%s: failed to get peer nonce",
+				    __func__);
+				return (-1);
+			}
+			break;
+		case IKEV2_PAYLOAD_NOTIFY:
+			if (ikev2_validate_notify(msg, offset, left, &n))
+				return (-1);
+			type = betoh16(n.n_type);
+			if (type != IKEV2_N_COOKIE)
+				break;
+			len = left - sizeof(n);
+			if (len < IKED_COOKIE_MIN || len > IKED_COOKIE_MAX) {
+				log_debug("%s: ignoring malformed cookie"
+				    " notification: %zu", __func__, left);
+				return (-1);
+			}
+			log_debug("%s: received cookie, len %zu", __func__,
+			    len);
+			buf = msgbuf + offset + sizeof(n);
+			print_hex(buf, 0, len);
+
+			ibuf_free(msg->msg_cookie);
+			if ((msg->msg_cookie = ibuf_new(buf, len)) == NULL) {
+				log_debug("%s: failed to get peer cookie",
+				    __func__);
+				return (-1);
+			}
+			break;
 		}
 
 		payload = pld.pld_nextpayload;
blob - baa27827647e4c1f1e445c713809b6ca3cbd18d3
blob + a29ec37715af009bab3b6e2bc3805319513e6e99
--- sbin/iked/parse.y
+++ sbin/iked/parse.y
@@ -112,6 +112,8 @@ static long		 ocsp_maxage = -1;
 static int		 cert_partial_chain = 0;
 static struct iked_radopts
 			 radauth, radacct;
+static int		 cookies_threshold = IKED_COOKIES_THRESHOLD;
+static int		 cookies_renewal = IKED_COOKIES_SECRET_MAX_AGE;
 
 struct iked_transform ikev2_default_ike_transforms[] = {
 	{ IKEV2_XFORMTYPE_ENCR, IKEV2_XFORMENCR_AES_CBC, 256 },
@@ -457,7 +459,7 @@ typedef struct {
 %token	CERTPARTIALCHAIN
 %token	REQUEST IFACE
 %token	RADIUS ACCOUNTING SERVER SECRET MAX_TRIES MAX_FAILOVERS
-%token	CLIENT DAE LISTEN ON NATT
+%token	CLIENT DAE LISTEN ON NATT SA_COOKIES_THRESHOLD SA_COOKIES_RENEWAL
 %token	<v.string>		STRING
 %token	<v.number>		NUMBER
 %type	<v.string>		string
@@ -554,6 +556,20 @@ set		: SET ACTIVE	{ passive = 0; }
 			}
 			dpd_interval = $3;
 		}
+		| SET SA_COOKIES_THRESHOLD NUMBER {
+			if ($3 < 0) {
+				yyerror("threshold outside range");
+				YYERROR;
+			}
+			cookies_threshold = $3;
+		}
+		| SET SA_COOKIES_RENEWAL NUMBER {
+			if ($3 < 1 || $3 > 60) {
+				yyerror("number outside range (1-60)");
+				YYERROR;
+			}
+			cookies_renewal = $3;
+		}
 		;
 
 user		: USER STRING STRING		{
@@ -1626,6 +1642,8 @@ lookup(char *s)
 		{ "rdomain",		RDOMAIN },
 		{ "request",		REQUEST },
 		{ "sa",			SA },
+		{ "sa_cookies_renewal",	SA_COOKIES_RENEWAL },
+		{ "sa_cookies_threshold",SA_COOKIES_THRESHOLD },
 		{ "secret",		SECRET },
 		{ "server",		SERVER },
 		{ "set",		SET },
@@ -2055,6 +2073,8 @@ parse_config(const char *filename, struct iked *x_env)
 	env->sc_vendorid = vendorid;
 	env->sc_radauth = radauth;
 	env->sc_radacct = radacct;
+	env->sc_cookies_threshold = cookies_threshold;
+	env->sc_cookies_renewal = cookies_renewal;
 
 	if (!rules)
 		log_warnx("%s: no valid configuration rules found",