From: Stefan Sperling Subject: iked SA_INIT cookies To: tech@openbsd.org Date: Mon, 23 Mar 2026 15:45:24 +0100 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 STRING %token NUMBER %type 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",