/*
 * Copyright (c) 2020 VMware, Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at:
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

#include <config.h>

#include <errno.h>
#include "conntrack-private.h"
#include "conntrack-tp.h"
#include "ct-dpif.h"
#include "openvswitch/vlog.h"

VLOG_DEFINE_THIS_MODULE(conntrack_tp);
static struct vlog_rate_limit rl = VLOG_RATE_LIMIT_INIT(1, 5);

static const char *ct_timeout_str[] = {
#define CT_TIMEOUT(NAME) #NAME,
    CT_TIMEOUTS
#undef CT_TIMEOUT
};

/* Default timeout policy in seconds. */
static unsigned int ct_dpif_netdev_tp_def[] = {
    [CT_DPIF_TP_ATTR_TCP_SYN_SENT] = 30,
    [CT_DPIF_TP_ATTR_TCP_SYN_RECV] = 30,
    [CT_DPIF_TP_ATTR_TCP_ESTABLISHED] = 24 * 60 * 60,
    [CT_DPIF_TP_ATTR_TCP_FIN_WAIT] = 15 * 60,
    [CT_DPIF_TP_ATTR_TCP_TIME_WAIT] = 45,
    [CT_DPIF_TP_ATTR_TCP_CLOSE] = 30,
    [CT_DPIF_TP_ATTR_UDP_FIRST] = 60,
    [CT_DPIF_TP_ATTR_UDP_SINGLE] = 60,
    [CT_DPIF_TP_ATTR_UDP_MULTIPLE] = 30,
    [CT_DPIF_TP_ATTR_ICMP_FIRST] = 60,
    [CT_DPIF_TP_ATTR_ICMP_REPLY] = 30,
};

static struct timeout_policy *
timeout_policy_lookup_protected(struct conntrack *ct, int32_t tp_id)
    OVS_REQUIRES(ct->ct_lock)
{
    struct timeout_policy *tp;
    uint32_t hash;

    hash = hash_int(tp_id, ct->hash_basis);
    CMAP_FOR_EACH_WITH_HASH_PROTECTED (tp, node, hash,
                                       &ct->timeout_policies) {
        if (tp->policy.id == tp_id) {
            return tp;
        }
    }
    return NULL;
}

static struct timeout_policy *
timeout_policy_lookup(struct conntrack *ct, int32_t tp_id)
{
    struct timeout_policy *tp;
    uint32_t hash;

    hash = hash_int(tp_id, ct->hash_basis);
    CMAP_FOR_EACH_WITH_HASH (tp, node, hash, &ct->timeout_policies) {
        if (tp->policy.id == tp_id) {
            return tp;
        }
    }
    return NULL;
}

struct timeout_policy *
timeout_policy_get(struct conntrack *ct, int32_t tp_id)
{
    return timeout_policy_lookup(ct, tp_id);
}

static void
update_existing_tp(struct timeout_policy *tp_dst,
                   const struct timeout_policy *tp_src)
{
    struct ct_dpif_timeout_policy *dst;
    const struct ct_dpif_timeout_policy *src;
    int i;

    dst = &tp_dst->policy;
    src = &tp_src->policy;

    /* Set the value and present bit to dst if present
     * bit in src is set.
     */
    for (i = 0; i < ARRAY_SIZE(dst->attrs); i++) {
        if (src->present & (1 << i)) {
            dst->attrs[i] = src->attrs[i];
            dst->present |= (1 << i);
        }
    }
}

static void
init_default_tp(struct timeout_policy *tp, uint32_t tp_id)
{
    tp->policy.id = tp_id;
    /* Initialize the timeout value to default, but not
     * setting the present bit.
     */
    tp->policy.present = 0;
    memcpy(tp->policy.attrs, ct_dpif_netdev_tp_def,
           sizeof tp->policy.attrs);
}

static void
timeout_policy_create(struct conntrack *ct,
                      struct timeout_policy *new_tp)
    OVS_REQUIRES(ct->ct_lock)
{
    uint32_t tp_id = new_tp->policy.id;
    struct timeout_policy *tp;
    uint32_t hash;

    tp = xzalloc(sizeof *tp);
    init_default_tp(tp, tp_id);
    update_existing_tp(tp, new_tp);
    hash = hash_int(tp_id, ct->hash_basis);
    cmap_insert(&ct->timeout_policies, &tp->node, hash);
}

static void
timeout_policy_clean(struct conntrack *ct, struct timeout_policy *tp)
    OVS_REQUIRES(ct->ct_lock)
{
    uint32_t hash = hash_int(tp->policy.id, ct->hash_basis);
    cmap_remove(&ct->timeout_policies, &tp->node, hash);
    ovsrcu_postpone(free, tp);
}

static int
timeout_policy_delete__(struct conntrack *ct, uint32_t tp_id,
                        bool warn_on_error)
    OVS_REQUIRES(ct->ct_lock)
{
    struct timeout_policy *tp;
    int err = 0;

    tp = timeout_policy_lookup_protected(ct, tp_id);
    if (tp) {
        timeout_policy_clean(ct, tp);
    } else if (warn_on_error) {
        VLOG_WARN_RL(&rl, "Failed to delete a non-existent timeout "
                     "policy: id=%d", tp_id);
        err = ENOENT;
    }
    return err;
}

int
timeout_policy_delete(struct conntrack *ct, uint32_t tp_id)
{
    int err;

    conntrack_lock(ct);
    err = timeout_policy_delete__(ct, tp_id, true);
    conntrack_unlock(ct);
    return err;
}

void
timeout_policy_init(struct conntrack *ct)
    OVS_REQUIRES(ct->ct_lock)
{
    struct timeout_policy tp;

    cmap_init(&ct->timeout_policies);

    /* Create default timeout policy. */
    memset(&tp, 0, sizeof tp);
    tp.policy.id = DEFAULT_TP_ID;
    timeout_policy_create(ct, &tp);
}

int
timeout_policy_update(struct conntrack *ct,
                      struct timeout_policy *new_tp)
{
    uint32_t tp_id = new_tp->policy.id;
    int err = 0;

    conntrack_lock(ct);
    timeout_policy_delete__(ct, tp_id, false);
    timeout_policy_create(ct, new_tp);
    conntrack_unlock(ct);
    return err;
}

static enum ct_dpif_tp_attr
tm_to_ct_dpif_tp(enum ct_timeout tm)
{
    switch (tm) {
    case CT_TM_TCP_FIRST_PACKET:
        return CT_DPIF_TP_ATTR_TCP_SYN_SENT;
    case CT_TM_TCP_OPENING:
        return CT_DPIF_TP_ATTR_TCP_SYN_RECV;
    case CT_TM_TCP_ESTABLISHED:
        return CT_DPIF_TP_ATTR_TCP_ESTABLISHED;
    case CT_TM_TCP_CLOSING:
        return CT_DPIF_TP_ATTR_TCP_FIN_WAIT;
    case CT_TM_TCP_FIN_WAIT:
        return CT_DPIF_TP_ATTR_TCP_TIME_WAIT;
    case CT_TM_TCP_CLOSED:
        return CT_DPIF_TP_ATTR_TCP_CLOSE;
    case CT_TM_OTHER_FIRST:
        return CT_DPIF_TP_ATTR_UDP_FIRST;
    case CT_TM_OTHER_BIDIR:
        return CT_DPIF_TP_ATTR_UDP_MULTIPLE;
    case CT_TM_OTHER_MULTIPLE:
        return CT_DPIF_TP_ATTR_UDP_SINGLE;
    case CT_TM_ICMP_FIRST:
        return CT_DPIF_TP_ATTR_ICMP_FIRST;
    case CT_TM_ICMP_REPLY:
        return CT_DPIF_TP_ATTR_ICMP_REPLY;
    case N_CT_TM:
    default:
        OVS_NOT_REACHED();
        break;
    }
    OVS_NOT_REACHED();
    return CT_DPIF_TP_ATTR_MAX;
}

static void
conn_schedule_expiration(struct conn *conn, enum ct_timeout tm, long long now,
                         uint32_t tp_value)
{
    atomic_store_relaxed(&conn->expiration, now + tp_value * 1000);
    conn->exp.tm = tm;
    ignore(atomic_flag_test_and_set(&conn->exp.reschedule));
}

static uint32_t
get_tp_id_val(struct conntrack *ct, uint32_t tp_id, enum ct_timeout tm)
{
    struct timeout_policy *tp;
    uint32_t val;

    tp = timeout_policy_lookup(ct, tp_id);
    if (tp) {
        val = tp->policy.attrs[tm_to_ct_dpif_tp(tm)];
    } else {
        val = ct_dpif_netdev_tp_def[tm_to_ct_dpif_tp(tm)];
    }
    return val;
}

/* The conn entry lock must be held on entry and exit. */
void
conn_update_expiration(struct conntrack *ct, struct conn *conn,
                       enum ct_timeout tm, long long now)
    OVS_REQUIRES(conn->lock)
{
    uint32_t tp_value;

    tp_value = get_tp_id_val(ct, conn->tp_id, tm);

    VLOG_DBG_RL(&rl, "Update timeout %s zone=%u with policy id=%d "
                "val=%u sec.",
                ct_timeout_str[tm], conn->key.zone, conn->tp_id, tp_value);

    conn_schedule_expiration(conn, tm, now, tp_value);
}

static void
conn_expire_init(struct conn *conn)
{
    atomic_flag_clear(&conn->exp.reschedule);
    ovs_refcount_init(&conn->exp.refcount);
}

void
conn_init_expiration(struct conntrack *ct, struct conn *conn,
                     enum ct_timeout tm, long long now)
{
    struct timeout_policy *tp;
    uint32_t val;

    tp = timeout_policy_lookup(ct, conn->tp_id);
    if (tp) {
        val = tp->policy.attrs[tm_to_ct_dpif_tp(tm)];
    } else {
        val = ct_dpif_netdev_tp_def[tm_to_ct_dpif_tp(tm)];
    }

    VLOG_DBG_RL(&rl, "Init timeout %s zone=%u with policy id=%d val=%u sec.",
                ct_timeout_str[tm], conn->key.zone, conn->tp_id, val);

    conn_expire_init(conn);
    conn_schedule_expiration(conn, tm, now, val);
}
