/* $OpenBSD: dpd.c,v 1.20 2017/12/05 20:31:45 jca Exp $ */ /* * Copyright (c) 2004 Håkan Olsson. All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * 1. Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ #include #include #include #include "conf.h" #include "dpd.h" #include "exchange.h" #include "hash.h" #include "ipsec.h" #include "isakmp_fld.h" #include "log.h" #include "message.h" #include "pf_key_v2.h" #include "sa.h" #include "timer.h" #include "transport.h" #include "util.h" /* From RFC 3706. */ #define DPD_MAJOR 0x01 #define DPD_MINOR 0x00 #define DPD_SEQNO_SZ 4 static const u_int8_t dpd_vendor_id[] = { 0xAF, 0xCA, 0xD7, 0x13, 0x68, 0xA1, 0xF1, /* RFC 3706 */ 0xC9, 0x6B, 0x86, 0x96, 0xFC, 0x77, 0x57, DPD_MAJOR, DPD_MINOR }; #define DPD_RETRANS_MAX 5 /* max number of retries. */ #define DPD_RETRANS_WAIT 5 /* seconds between retries. */ /* DPD Timer State */ enum dpd_tstate { DPD_TIMER_NORMAL, DPD_TIMER_CHECK }; static void dpd_check_event(void *); static void dpd_event(void *); static u_int32_t dpd_timer_interval(u_int32_t); static void dpd_timer_reset(struct sa *, u_int32_t, enum dpd_tstate); /* Add the DPD VENDOR ID payload. */ int dpd_add_vendor_payload(struct message *msg) { u_int8_t *buf; size_t buflen = sizeof dpd_vendor_id + ISAKMP_GEN_SZ; buf = malloc(buflen); if (!buf) { log_error("dpd_add_vendor_payload: malloc(%lu) failed", (unsigned long)buflen); return -1; } SET_ISAKMP_GEN_LENGTH(buf, buflen); memcpy(buf + ISAKMP_VENDOR_ID_OFF, dpd_vendor_id, sizeof dpd_vendor_id); if (message_add_payload(msg, ISAKMP_PAYLOAD_VENDOR, buf, buflen, 1)) { free(buf); return -1; } return 0; } /* * Check an incoming message for DPD capability markers. */ void dpd_check_vendor_payload(struct message *msg, struct payload *p) { u_int8_t *pbuf = p->p; size_t vlen; /* Already checked? */ if (msg->exchange->flags & EXCHANGE_FLAG_DPD_CAP_PEER) { /* Just mark it as handled and return. */ p->flags |= PL_MARK; return; } vlen = GET_ISAKMP_GEN_LENGTH(pbuf) - ISAKMP_GEN_SZ; if (vlen != sizeof dpd_vendor_id) { LOG_DBG((LOG_EXCHANGE, 90, "dpd_check_vendor_payload: bad size %lu != %lu", (unsigned long)vlen, (unsigned long)sizeof dpd_vendor_id)); return; } if (memcmp(dpd_vendor_id, pbuf + ISAKMP_GEN_SZ, vlen) == 0) { /* This peer is DPD capable. */ if (msg->isakmp_sa) { msg->exchange->flags |= EXCHANGE_FLAG_DPD_CAP_PEER; LOG_DBG((LOG_EXCHANGE, 10, "dpd_check_vendor_payload: " "DPD capable peer detected")); } p->flags |= PL_MARK; } } /* * Arm the DPD timer */ void dpd_start(struct sa *isakmp_sa) { if (dpd_timer_interval(0) != 0) { LOG_DBG((LOG_EXCHANGE, 10, "dpd_enable: enabling")); isakmp_sa->flags |= SA_FLAG_DPD; dpd_timer_reset(isakmp_sa, 0, DPD_TIMER_NORMAL); } } /* * All incoming DPD Notify messages enter here. Message has been validated. */ void dpd_handle_notify(struct message *msg, struct payload *p) { struct sa *isakmp_sa = msg->isakmp_sa; u_int16_t notify = GET_ISAKMP_NOTIFY_MSG_TYPE(p->p); u_int32_t p_seq; /* Extract the sequence number. */ memcpy(&p_seq, p->p + ISAKMP_NOTIFY_SPI_OFF + ISAKMP_HDR_COOKIES_LEN, sizeof p_seq); p_seq = ntohl(p_seq); LOG_DBG((LOG_MESSAGE, 40, "dpd_handle_notify: got %s seq %u", constant_name(isakmp_notify_cst, notify), p_seq)); switch (notify) { case ISAKMP_NOTIFY_STATUS_DPD_R_U_THERE: /* The other peer wants to know we're alive. */ if (p_seq < isakmp_sa->dpd_rseq || (p_seq == isakmp_sa->dpd_rseq && ++isakmp_sa->dpd_rdupcount >= DPD_RETRANS_MAX)) { log_print("dpd_handle_notify: bad R_U_THERE seqno " "%u <= %u", p_seq, isakmp_sa->dpd_rseq); return; } if (isakmp_sa->dpd_rseq != p_seq) { isakmp_sa->dpd_rdupcount = 0; isakmp_sa->dpd_rseq = p_seq; } message_send_dpd_notify(isakmp_sa, ISAKMP_NOTIFY_STATUS_DPD_R_U_THERE_ACK, p_seq); break; case ISAKMP_NOTIFY_STATUS_DPD_R_U_THERE_ACK: /* This should be a response to a R_U_THERE we've sent. */ if (isakmp_sa->dpd_seq != p_seq) { log_print("dpd_handle_notify: got bad ACK seqno %u, " "expected %u", p_seq, isakmp_sa->dpd_seq); /* XXX Give up? Retry? */ return; } break; default: break; } /* Mark handled. */ p->flags |= PL_MARK; /* The other peer is alive, so we can safely wait a while longer. */ if (isakmp_sa->flags & SA_FLAG_DPD) dpd_timer_reset(isakmp_sa, 0, DPD_TIMER_NORMAL); } /* Calculate the time until next DPD exchange. */ static u_int32_t dpd_timer_interval(u_int32_t offset) { int32_t v = 0; #ifdef notyet v = ...; /* XXX Per-peer specified DPD intervals? */ #endif if (!v) v = conf_get_num("General", "DPD-check-interval", 0); if (v < 1) return 0; /* DPD-Check-Interval < 1 means disable DPD */ v -= offset; return v < 1 ? 1 : v; } static void dpd_timer_reset(struct sa *sa, u_int32_t time_passed, enum dpd_tstate mode) { struct timespec ts; if (sa->dpd_event) timer_remove_event(sa->dpd_event); clock_gettime(CLOCK_MONOTONIC, &ts); switch (mode) { case DPD_TIMER_NORMAL: sa->dpd_failcount = 0; ts.tv_sec += dpd_timer_interval(time_passed); sa->dpd_event = timer_add_event("dpd_event", dpd_event, sa, &ts); break; case DPD_TIMER_CHECK: ts.tv_sec += DPD_RETRANS_WAIT; sa->dpd_event = timer_add_event("dpd_check_event", dpd_check_event, sa, &ts); break; default: break; } if (!sa->dpd_event) log_print("dpd_timer_reset: timer_add_event failed"); } /* Helper function for dpd_exchange_finalization(). */ static int dpd_find_sa(struct sa *sa, void *v_sa) { struct sa *isakmp_sa = v_sa; if (!isakmp_sa->id_i || !isakmp_sa->id_r) return 0; return (sa->phase == 2 && (sa->flags & SA_FLAG_READY) && memcmp(sa->id_i, isakmp_sa->id_i, sa->id_i_len) == 0 && memcmp(sa->id_r, isakmp_sa->id_r, sa->id_r_len) == 0); } struct dpd_args { struct sa *isakmp_sa; u_int32_t interval; }; /* Helper function for dpd_event(). */ static int dpd_check_time(struct sa *sa, void *v_arg) { struct dpd_args *args = v_arg; struct sockaddr *dst; struct proto *proto; struct sa_kinfo *ksa; struct timespec ts; if (sa->phase == 1 || (args->isakmp_sa->flags & SA_FLAG_DPD) == 0 || dpd_find_sa(sa, args->isakmp_sa) == 0) return 0; proto = TAILQ_FIRST(&sa->protos); if (!proto || !proto->data) return 0; sa->transport->vtbl->get_src(sa->transport, &dst); clock_gettime(CLOCK_MONOTONIC, &ts); ksa = pf_key_v2_get_kernel_sa(proto->spi[1], proto->spi_sz[1], proto->proto, dst); if (!ksa || !ksa->last_used) return 0; LOG_DBG((LOG_MESSAGE, 80, "dpd_check_time: " "SA %p last use %u second(s) ago", sa, (u_int32_t)(ts.tv_sec - ksa->last_used))); if ((u_int32_t)(ts.tv_sec - ksa->last_used) < args->interval) { args->interval = (u_int32_t)(ts.tv_sec - ksa->last_used); return 1; } return 0; } /* Called by the timer. */ static void dpd_event(void *v_sa) { struct sa *isakmp_sa = v_sa; struct dpd_args args; struct sockaddr *dst; char *addr; isakmp_sa->dpd_event = 0; /* Check if there's been any incoming SA activity since last time. */ args.isakmp_sa = isakmp_sa; args.interval = dpd_timer_interval(0); if (sa_find(dpd_check_time, &args)) { if (args.interval > dpd_timer_interval(0)) args.interval = 0; dpd_timer_reset(isakmp_sa, args.interval, DPD_TIMER_NORMAL); return; } /* No activity seen, do a DPD exchange. */ if (isakmp_sa->dpd_seq == 0) { /* * RFC 3706: first seq# should be random, with MSB zero, * otherwise we just increment it. */ arc4random_buf((u_int8_t *)&isakmp_sa->dpd_seq, sizeof isakmp_sa->dpd_seq); isakmp_sa->dpd_seq &= 0x7FFF; } else isakmp_sa->dpd_seq++; isakmp_sa->transport->vtbl->get_dst(isakmp_sa->transport, &dst); if (sockaddr2text(dst, &addr, 0) == -1) addr = 0; LOG_DBG((LOG_MESSAGE, 30, "dpd_event: sending R_U_THERE to %s seq %u", addr ? addr : "", isakmp_sa->dpd_seq)); free(addr); message_send_dpd_notify(isakmp_sa, ISAKMP_NOTIFY_STATUS_DPD_R_U_THERE, isakmp_sa->dpd_seq); /* And set the short timer. */ dpd_timer_reset(isakmp_sa, 0, DPD_TIMER_CHECK); } /* * Called by the timer. If this function is called, it means we did not * received any R_U_THERE_ACK confirmation from the other peer. */ static void dpd_check_event(void *v_sa) { struct sa *isakmp_sa = v_sa; struct sa *sa; isakmp_sa->dpd_event = 0; if (++isakmp_sa->dpd_failcount < DPD_RETRANS_MAX) { LOG_DBG((LOG_MESSAGE, 10, "dpd_check_event: " "peer not responding, retry %u of %u", isakmp_sa->dpd_failcount, DPD_RETRANS_MAX)); message_send_dpd_notify(isakmp_sa, ISAKMP_NOTIFY_STATUS_DPD_R_U_THERE, isakmp_sa->dpd_seq); dpd_timer_reset(isakmp_sa, 0, DPD_TIMER_CHECK); return; } /* * Peer is considered dead. Delete all SAs created under isakmp_sa. */ LOG_DBG((LOG_MESSAGE, 10, "dpd_check_event: peer is dead, " "deleting all SAs connected to SA %p", isakmp_sa)); while ((sa = sa_find(dpd_find_sa, isakmp_sa)) != 0) { LOG_DBG((LOG_MESSAGE, 30, "dpd_check_event: deleting SA %p", sa)); sa_delete(sa, 0); } LOG_DBG((LOG_MESSAGE, 30, "dpd_check_event: deleting ISAKMP SA %p", isakmp_sa)); sa_delete(isakmp_sa, 0); }