root/sbin/isakmpd/dpd.c
/*      $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 <sys/types.h>
#include <stdlib.h>
#include <string.h>

#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 : "<unknown>", 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);
}