/* 
 * Copyright (C) 2008-2011 Teluu Inc. (http://www.teluu.com)
 * Copyright (C) 2003-2008 Benny Prijono <benny@prijono.org>
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA 
 */
#include <pjnath/nat_detect.h>
#include <pjnath/errno.h>
#include <pj/assert.h>
#include <pj/ioqueue.h>
#include <pj/log.h>
#include <pj/os.h>
#include <pj/pool.h>
#include <pj/rand.h>
#include <pj/string.h>
#include <pj/timer.h>
#include <pj/compat/socket.h>


static const char *nat_type_names[] =
{
    "Unknown",
    "ErrUnknown",
    "Open",
    "Blocked",
    "Symmetric UDP",
    "Full Cone",
    "Symmetric",
    "Restricted",
    "Port Restricted"
};


#define CHANGE_IP_FLAG          4
#define CHANGE_PORT_FLAG        2
#define CHANGE_IP_PORT_FLAG     (CHANGE_IP_FLAG | CHANGE_PORT_FLAG)
#define TEST_INTERVAL           50

enum test_type
{
    ST_TEST_1,
    ST_TEST_2,
    ST_TEST_3,
    ST_TEST_1B,
    ST_MAX
};

static const char *test_names[] =
{
    "Test I: Binding request",
    "Test II: Binding request with change address and port request",
    "Test III: Binding request with change port request",
    "Test IB: Binding request to alternate address"
};

enum timer_type
{
    TIMER_TEST      = 1,
    TIMER_DESTROY   = 2
};

typedef struct nat_detect_session
{
    pj_pool_t               *pool;
    pj_grp_lock_t           *grp_lock;

    pj_timer_heap_t         *timer_heap;
    pj_timer_entry           timer;
    unsigned                 timer_executed;

    void                    *user_data;
    pj_stun_nat_detect_cb   *cb;
    pj_sock_t                sock;
    pj_sockaddr              local_addr;
    pj_ioqueue_key_t        *key;
    pj_sockaddr              server;
    pj_sockaddr             *cur_server;
    pj_sockaddr              cur_addr;
    pj_stun_session         *stun_sess;

    pj_ioqueue_op_key_t      read_op, write_op;
    pj_uint8_t               rx_pkt[PJ_STUN_MAX_PKT_LEN];
    pj_ssize_t               rx_pkt_len;
    pj_sockaddr              src_addr;
    int                      src_addr_len;

    struct result
    {
        pj_bool_t       executed;
        pj_bool_t       complete;
        pj_status_t     status;
        pj_sockaddr     ma;
        pj_sockaddr     ca;
        pj_stun_tx_data *tdata;
    } result[ST_MAX];

} nat_detect_session;


static void on_read_complete(pj_ioqueue_key_t *key, 
                             pj_ioqueue_op_key_t *op_key, 
                             pj_ssize_t bytes_read);
static void on_request_complete(pj_stun_session *sess,
                                pj_status_t status,
                                void *token,
                                pj_stun_tx_data *tdata,
                                const pj_stun_msg *response,
                                const pj_sockaddr_t *src_addr,
                                unsigned src_addr_len);
static pj_status_t on_send_msg(pj_stun_session *sess,
                               void *token,
                               const void *pkt,
                               pj_size_t pkt_size,
                               const pj_sockaddr_t *dst_addr,
                               unsigned addr_len);

static pj_status_t send_test(nat_detect_session *sess,
                             enum test_type test_id,
                             const pj_sockaddr *alt_addr,
                             pj_uint32_t change_flag);
static void on_sess_timer(pj_timer_heap_t *th,
                             pj_timer_entry *te);
static void sess_destroy(nat_detect_session *sess);
static void sess_on_destroy(void *member);

/*
 * Get the NAT name from the specified NAT type.
 */
PJ_DEF(const char*) pj_stun_get_nat_name(pj_stun_nat_type type)
{
    PJ_ASSERT_RETURN(type >= 0 && type <= PJ_STUN_NAT_TYPE_PORT_RESTRICTED,
                     "*Invalid*");

    return nat_type_names[type];
}

static int test_executed(nat_detect_session *sess)
{
    unsigned i, count;
    for (i=0, count=0; i<PJ_ARRAY_SIZE(sess->result); ++i) {
        if (sess->result[i].executed)
            ++count;
    }
    return count;
}

static int test_completed(nat_detect_session *sess)
{
    unsigned i, count;
    for (i=0, count=0; i<PJ_ARRAY_SIZE(sess->result); ++i) {
        if (sess->result[i].complete)
            ++count;
    }
    return count;
}

static pj_status_t get_local_interface(const pj_sockaddr *server,
                                       pj_sockaddr *local_addr)
{
    pj_sock_t sock;
    pj_sockaddr tmp, local;
    int addr_len;
    pj_status_t status;

    status = pj_sock_socket(server->addr.sa_family,
                            pj_SOCK_DGRAM() | pj_SOCK_CLOEXEC(),
                            0, &sock);
    if (status != PJ_SUCCESS)
        return status;

    addr_len = pj_sockaddr_get_len(server);
    pj_sockaddr_init(server->addr.sa_family, &local, NULL, 0);
    status = pj_sock_bind(sock, &local, addr_len);
    if (status != PJ_SUCCESS) {
        pj_sock_close(sock);
        return status;
    }

    status = pj_sock_connect(sock, server, addr_len);
    if (status != PJ_SUCCESS) {
        pj_sock_close(sock);
        return status;
    }

    status = pj_sock_getsockname(sock, &tmp, &addr_len);
    if (status != PJ_SUCCESS) {
        pj_sock_close(sock);
        return status;
    }

    pj_sockaddr_cp(local_addr, &tmp);
    
    pj_sock_close(sock);
    return PJ_SUCCESS;
}


PJ_DEF(pj_status_t) pj_stun_detect_nat_type(const pj_sockaddr_in *server,
                                            pj_stun_config *stun_cfg,
                                            void *user_data,
                                            pj_stun_nat_detect_cb *cb)
{
    pj_sockaddr srv;

    PJ_ASSERT_RETURN(server, PJ_EINVAL);

    pj_sockaddr_cp(&srv, server);

    return pj_stun_detect_nat_type2(&srv, stun_cfg, user_data, cb);
}

PJ_DEF(pj_status_t) pj_stun_detect_nat_type2(const pj_sockaddr *server,
                                             pj_stun_config *stun_cfg,
                                             void *user_data,
                                             pj_stun_nat_detect_cb *cb)
{
    pj_pool_t *pool;
    nat_detect_session *sess;
    pj_stun_session_cb sess_cb;
    pj_ioqueue_callback ioqueue_cb;
    int af, addr_len;
    char addr[PJ_INET6_ADDRSTRLEN];
    pj_status_t status;

    PJ_ASSERT_RETURN(server && stun_cfg, PJ_EINVAL);
    PJ_ASSERT_RETURN(stun_cfg->pf && stun_cfg->ioqueue && stun_cfg->timer_heap,
                     PJ_EINVAL);

    /*
     * Init NAT detection session.
     */
    pool = pj_pool_create(stun_cfg->pf, "natck%p", PJNATH_POOL_LEN_NATCK, 
                          PJNATH_POOL_INC_NATCK, NULL);
    if (!pool)
        return PJ_ENOMEM;

    sess = PJ_POOL_ZALLOC_T(pool, nat_detect_session);
    sess->pool = pool;
    sess->user_data = user_data;
    sess->cb = cb;

    status = pj_grp_lock_create(pool, NULL, &sess->grp_lock);
    if (status != PJ_SUCCESS) {
        /* Group lock not created yet, just destroy pool and return */
        pj_pool_release(pool);
        return status;
    }

    pj_grp_lock_add_ref(sess->grp_lock);
    pj_grp_lock_add_handler(sess->grp_lock, pool, sess, &sess_on_destroy);

    pj_sockaddr_cp(&sess->server, server);

    /*
     * Init timer to self-destroy.
     */
    sess->timer_heap = stun_cfg->timer_heap;
    sess->timer.cb = &on_sess_timer;
    sess->timer.user_data = sess;


    /*
     * Initialize socket.
     */
    af = server->addr.sa_family;
    status = pj_sock_socket(af, pj_SOCK_DGRAM() | pj_SOCK_CLOEXEC(), 0, &sess->sock);
    if (status != PJ_SUCCESS)
        goto on_error;

    /*
     * Bind to any.
     */
    addr_len = pj_sockaddr_get_len(server);
    pj_sockaddr_init(server->addr.sa_family, &sess->local_addr, NULL, 0);
    status = pj_sock_bind(sess->sock, &sess->local_addr, addr_len);
    if (status != PJ_SUCCESS)
        goto on_error;

    /*
     * Get local/bound address.
     */
    status = pj_sock_getsockname(sess->sock, &sess->local_addr, &addr_len);
    if (status != PJ_SUCCESS)
        goto on_error;

    /*
     * Find out which interface is used to send to the server.
     */
    status = get_local_interface(server, &sess->local_addr);
    if (status != PJ_SUCCESS)
        goto on_error;

    PJ_LOG(5,(sess->pool->obj_name, "Local address is %s:%d",
              pj_sockaddr_print(&sess->local_addr, addr, sizeof(addr), 2),
              pj_sockaddr_get_port(&sess->local_addr)));

    PJ_LOG(5,(sess->pool->obj_name, "Server set to %s:%d",
              pj_sockaddr_print(server, addr, sizeof(addr), 2),
              pj_sockaddr_get_port(server)));

    /*
     * Register socket to ioqueue to receive asynchronous input
     * notification.
     */
    pj_bzero(&ioqueue_cb, sizeof(ioqueue_cb));
    ioqueue_cb.on_read_complete = &on_read_complete;

    status = pj_ioqueue_register_sock2(sess->pool, stun_cfg->ioqueue, 
                                       sess->sock, sess->grp_lock, sess,
                                       &ioqueue_cb, &sess->key);
    if (status != PJ_SUCCESS)
        goto on_error;

    /*
     * Create STUN session.
     */
    pj_bzero(&sess_cb, sizeof(sess_cb));
    sess_cb.on_request_complete = &on_request_complete;
    sess_cb.on_send_msg = &on_send_msg;
    status = pj_stun_session_create(stun_cfg, pool->obj_name, &sess_cb,
                                    PJ_FALSE, sess->grp_lock, &sess->stun_sess);
    if (status != PJ_SUCCESS)
        goto on_error;

    pj_stun_session_set_user_data(sess->stun_sess, sess);

    /*
     * Kick-off ioqueue reading.
     */
    pj_ioqueue_op_key_init(&sess->read_op, sizeof(sess->read_op));
    pj_ioqueue_op_key_init(&sess->write_op, sizeof(sess->write_op));
    on_read_complete(sess->key, &sess->read_op, 0);

    /*
     * Start TEST_1
     */
    sess->timer.id = TIMER_TEST;
    on_sess_timer(stun_cfg->timer_heap, &sess->timer);

    return PJ_SUCCESS;

on_error:
    sess_destroy(sess);
    return status;
}


static void sess_destroy(nat_detect_session *sess)
{
    if (sess->stun_sess) { 
        pj_stun_session_destroy(sess->stun_sess);
        sess->stun_sess = NULL;
    }

    if (sess->key) {
        pj_ioqueue_unregister(sess->key);
        sess->key = NULL;
        sess->sock = PJ_INVALID_SOCKET;
    } else if (sess->sock != PJ_INVALID_SOCKET) {
        pj_sock_close(sess->sock);
        sess->sock = PJ_INVALID_SOCKET;
    }

    if (sess->grp_lock) {
        pj_grp_lock_dec_ref(sess->grp_lock);
    }
}

static void sess_on_destroy(void *member)
{
    nat_detect_session *sess = (nat_detect_session*)member;
    if (sess->pool) {
        pj_pool_release(sess->pool);
    }
}

static void end_session(nat_detect_session *sess,
                        pj_status_t status,
                        pj_stun_nat_type nat_type)
{
    pj_stun_nat_detect_result result;
    char errmsg[PJ_ERR_MSG_SIZE];
    pj_time_val delay;

    if (sess->timer.id != 0) {
        pj_timer_heap_cancel(sess->timer_heap, &sess->timer);
        sess->timer.id = 0;
    }

    pj_bzero(&result, sizeof(result));
    errmsg[0] = '\0';
    result.status_text = errmsg;

    result.status = status;
    pj_strerror(status, errmsg, sizeof(errmsg));
    result.nat_type = nat_type;
    result.nat_type_name = nat_type_names[result.nat_type];

    if (sess->cb)
        (*sess->cb)(sess->user_data, &result);

    delay.sec = 0;
    delay.msec = 0;

    pj_timer_heap_schedule_w_grp_lock(sess->timer_heap, &sess->timer, &delay,
                                      TIMER_DESTROY, sess->grp_lock);
}


/*
 * Callback upon receiving packet from network.
 */
static void on_read_complete(pj_ioqueue_key_t *key, 
                             pj_ioqueue_op_key_t *op_key, 
                             pj_ssize_t bytes_read)
{
    nat_detect_session *sess;
    pj_status_t status;

    sess = (nat_detect_session *) pj_ioqueue_get_user_data(key);
    pj_assert(sess != NULL);

    pj_grp_lock_acquire(sess->grp_lock);

    /* Ignore packet when STUN session has been destroyed */
    if (!sess->stun_sess)
        goto on_return;

    if (bytes_read < 0) {
        if (-bytes_read != PJ_STATUS_FROM_OS(OSERR_EWOULDBLOCK) &&
            -bytes_read != PJ_STATUS_FROM_OS(OSERR_EINPROGRESS) && 
            -bytes_read != PJ_STATUS_FROM_OS(OSERR_ECONNRESET)) 
        {
            /* Permanent error */
            end_session(sess, (pj_status_t)-bytes_read, 
                        PJ_STUN_NAT_TYPE_ERR_UNKNOWN);
            goto on_return;
        }

    } else if (bytes_read > 0) {
        pj_stun_session_on_rx_pkt(sess->stun_sess, sess->rx_pkt, bytes_read,
                                  PJ_STUN_IS_DATAGRAM|PJ_STUN_CHECK_PACKET, 
                                  NULL, NULL, 
                                  &sess->src_addr, sess->src_addr_len);
    }


    sess->rx_pkt_len = sizeof(sess->rx_pkt);
    sess->src_addr_len = sizeof(sess->src_addr);
    status = pj_ioqueue_recvfrom(key, op_key, sess->rx_pkt, &sess->rx_pkt_len,
                                 PJ_IOQUEUE_ALWAYS_ASYNC, 
                                 &sess->src_addr, &sess->src_addr_len);

    if (status != PJ_EPENDING) {
        pj_assert(status != PJ_SUCCESS);
        end_session(sess, status, PJ_STUN_NAT_TYPE_ERR_UNKNOWN);
    }

on_return:
    pj_grp_lock_release(sess->grp_lock);
}


/*
 * Callback to send outgoing packet from STUN session.
 */
static pj_status_t on_send_msg(pj_stun_session *stun_sess,
                               void *token,
                               const void *pkt,
                               pj_size_t pkt_size,
                               const pj_sockaddr_t *dst_addr,
                               unsigned addr_len)
{
    nat_detect_session *sess;
    pj_ssize_t pkt_len;
    pj_status_t status;

    PJ_UNUSED_ARG(token);

    sess = (nat_detect_session*) pj_stun_session_get_user_data(stun_sess);

    pkt_len = pkt_size;
    status = pj_ioqueue_sendto(sess->key, &sess->write_op, pkt, &pkt_len, 0,
                               dst_addr, addr_len);

    return status;

}

/*
 * Callback upon request completion.
 */
static void on_request_complete(pj_stun_session *stun_sess,
                                pj_status_t status,
                                void *token,
                                pj_stun_tx_data *tdata,
                                const pj_stun_msg *response,
                                const pj_sockaddr_t *src_addr,
                                unsigned src_addr_len)
{
    nat_detect_session *sess;
    pj_stun_sockaddr_attr *mattr = NULL;
    pj_stun_changed_addr_attr *ca = NULL;
    pj_uint32_t *tsx_id;
    int cmp;
    unsigned test_id;

    PJ_UNUSED_ARG(token);
    PJ_UNUSED_ARG(tdata);
    PJ_UNUSED_ARG(src_addr);
    PJ_UNUSED_ARG(src_addr_len);

    sess = (nat_detect_session*) pj_stun_session_get_user_data(stun_sess);

    pj_grp_lock_acquire(sess->grp_lock);

    /* Find errors in the response */
    if (status == PJ_SUCCESS) {

        /* Check error message */
        if (PJ_STUN_IS_ERROR_RESPONSE(response->hdr.type)) {
            pj_stun_errcode_attr *eattr;
            int err_code;

            eattr = (pj_stun_errcode_attr*)
                    pj_stun_msg_find_attr(response, PJ_STUN_ATTR_ERROR_CODE, 0);

            if (eattr != NULL && eattr->err_code)
                err_code = eattr->err_code;
            else
                err_code = PJ_STUN_SC_SERVER_ERROR;

            status = PJ_STATUS_FROM_STUN_CODE(err_code);


        } else {

            /* Get MAPPED-ADDRESS or XOR-MAPPED-ADDRESS */
            mattr = (pj_stun_sockaddr_attr*)
                    pj_stun_msg_find_attr(response, PJ_STUN_ATTR_XOR_MAPPED_ADDR, 0);
            if (mattr == NULL) {
                mattr = (pj_stun_sockaddr_attr*)
                        pj_stun_msg_find_attr(response, PJ_STUN_ATTR_MAPPED_ADDR, 0);
            }

            if (mattr == NULL) {
                status = PJNATH_ESTUNNOMAPPEDADDR;
            }

            /* Get CHANGED-ADDRESS attribute */
            ca = (pj_stun_changed_addr_attr*)
                 pj_stun_msg_find_attr(response, PJ_STUN_ATTR_CHANGED_ADDR, 0);

            if (ca == NULL) {
                status = PJ_STATUS_FROM_STUN_CODE(PJ_STUN_SC_SERVER_ERROR);
            }

        }
    }

    /* Save the result */
    tsx_id = (pj_uint32_t*) tdata->msg->hdr.tsx_id;
    test_id = tsx_id[2];

    if (test_id >= ST_MAX) {
        PJ_LOG(4,(sess->pool->obj_name, "Invalid transaction ID %u in response",
                  test_id));
        end_session(sess, PJ_STATUS_FROM_STUN_CODE(PJ_STUN_SC_SERVER_ERROR),
                    PJ_STUN_NAT_TYPE_ERR_UNKNOWN);
        goto on_return;
    }

    PJ_PERROR(5,(sess->pool->obj_name, status, "Completed %s",
                 test_names[test_id]));

    sess->result[test_id].complete = PJ_TRUE;
    sess->result[test_id].status = status;
    if (status == PJ_SUCCESS) {
        pj_sockaddr_cp(&sess->result[test_id].ma, &mattr->sockaddr);
        pj_sockaddr_cp(&sess->result[test_id].ca, &ca->sockaddr);
    }

    /* Send Test 1B only when Test 2 completes. Must not send Test 1B
     * before Test 2 completes to avoid creating mapping on the NAT.
     */
    if (!sess->result[ST_TEST_1B].executed && 
        sess->result[ST_TEST_2].complete &&
        sess->result[ST_TEST_2].status != PJ_SUCCESS &&
        sess->result[ST_TEST_1].complete &&
        sess->result[ST_TEST_1].status == PJ_SUCCESS) 
    {
        cmp = pj_sockaddr_cmp(&sess->local_addr, &sess->result[ST_TEST_1].ma);
        if (cmp != 0)
            send_test(sess, ST_TEST_1B, &sess->result[ST_TEST_1].ca, 0);
    }

    if (test_completed(sess)<3 || test_completed(sess)!=test_executed(sess))
        goto on_return;

    /* Handle the test result according to RFC 3489 page 22:


                        +--------+
                        |  Test  |
                        |   1    |
                        +--------+
                             |
                             |
                             V
                            /\              /\
                         N /  \ Y          /  \ Y             +--------+
          UDP     <-------/Resp\--------->/ IP \------------->|  Test  |
          Blocked         \ ?  /          \Same/              |   2    |
                           \  /            \? /               +--------+
                            \/              \/                    |
                                             | N                  |
                                             |                    V
                                             V                    /\
                                         +--------+  Sym.      N /  \
                                         |  Test  |  UDP    <---/Resp\
                                         |   2    |  Firewall   \ ?  /
                                         +--------+              \  /
                                             |                    \/
                                             V                     |Y
                  /\                         /\                    |
   Symmetric  N  /  \       +--------+   N  /  \                   V
      NAT  <--- / IP \<-----|  Test  |<--- /Resp\               Open
                \Same/      |   1B   |     \ ?  /               Internet
                 \? /       +--------+      \  /
                  \/                         \/
                  |                           |Y
                  |                           |
                  |                           V
                  |                           Full
                  |                           Cone
                  V              /\
              +--------+        /  \ Y
              |  Test  |------>/Resp\---->Restricted
              |   3    |       \ ?  /
              +--------+        \  /
                                 \/
                                  |N
                                  |       Port
                                  +------>Restricted

                 Figure 2: Flow for type discovery process
     */

    switch (sess->result[ST_TEST_1].status) {
    case PJNATH_ESTUNTIMEDOUT:
        /*
         * Test 1 has timed-out. Conclude with NAT_TYPE_BLOCKED. 
         */
        end_session(sess, PJ_SUCCESS, PJ_STUN_NAT_TYPE_BLOCKED);
        break;
    case PJ_SUCCESS:
        /*
         * Test 1 is successful. Further tests are needed to detect
         * NAT type. Compare the MAPPED-ADDRESS with the local address.
         */
        cmp = pj_sockaddr_cmp(&sess->local_addr, &sess->result[ST_TEST_1].ma);
        if (cmp==0) {
            /*
             * MAPPED-ADDRESS and local address is equal. Need one more
             * test to determine NAT type.
             */
            switch (sess->result[ST_TEST_2].status) {
            case PJ_SUCCESS:
                /*
                 * Test 2 is also successful. We're in the open.
                 */
                end_session(sess, PJ_SUCCESS, PJ_STUN_NAT_TYPE_OPEN);
                break;
            case PJNATH_ESTUNTIMEDOUT:
                /*
                 * Test 2 has timed out. We're behind somekind of UDP
                 * firewall.
                 */
                end_session(sess, PJ_SUCCESS, PJ_STUN_NAT_TYPE_SYMMETRIC_UDP);
                break;
            default:
                /*
                 * We've got other error with Test 2.
                 */
                end_session(sess, sess->result[ST_TEST_2].status, 
                            PJ_STUN_NAT_TYPE_ERR_UNKNOWN);
                break;
            }
        } else {
            /*
             * MAPPED-ADDRESS is different than local address.
             * We're behind NAT.
             */
            switch (sess->result[ST_TEST_2].status) {
            case PJ_SUCCESS:
                /*
                 * Test 2 is successful. We're behind a full-cone NAT.
                 */
                end_session(sess, PJ_SUCCESS, PJ_STUN_NAT_TYPE_FULL_CONE);
                break;
            case PJNATH_ESTUNTIMEDOUT:
                /*
                 * Test 2 has timed-out Check result of test 1B..
                 */
                switch (sess->result[ST_TEST_1B].status) {
                case PJ_SUCCESS:
                    /*
                     * Compare the MAPPED-ADDRESS of test 1B with the
                     * MAPPED-ADDRESS returned in test 1..
                     */
                    cmp = pj_sockaddr_cmp(&sess->result[ST_TEST_1].ma,
                                          &sess->result[ST_TEST_1B].ma);
                    if (cmp != 0) {
                        /*
                         * MAPPED-ADDRESS is different, we're behind a
                         * symmetric NAT.
                         */
                        end_session(sess, PJ_SUCCESS,
                                    PJ_STUN_NAT_TYPE_SYMMETRIC);
                    } else {
                        /*
                         * MAPPED-ADDRESS is equal. We're behind a restricted
                         * or port-restricted NAT, depending on the result of
                         * test 3.
                         */
                        switch (sess->result[ST_TEST_3].status) {
                        case PJ_SUCCESS:
                            /*
                             * Test 3 is successful, we're behind a restricted
                             * NAT.
                             */
                            end_session(sess, PJ_SUCCESS,
                                        PJ_STUN_NAT_TYPE_RESTRICTED);
                            break;
                        case PJNATH_ESTUNTIMEDOUT:
                            /*
                             * Test 3 failed, we're behind a port restricted
                             * NAT.
                             */
                            end_session(sess, PJ_SUCCESS,
                                        PJ_STUN_NAT_TYPE_PORT_RESTRICTED);
                            break;
                        default:
                            /*
                             * Got other error with test 3.
                             */
                            end_session(sess, sess->result[ST_TEST_3].status,
                                        PJ_STUN_NAT_TYPE_ERR_UNKNOWN);
                            break;
                        }
                    }
                    break;
                case PJNATH_ESTUNTIMEDOUT:
                    /*
                     * Strangely test 1B has failed. Maybe connectivity was
                     * lost? Or perhaps port 3489 (the usual port number in
                     * CHANGED-ADDRESS) is blocked?
                     */
                    switch (sess->result[ST_TEST_3].status) {
                    case PJ_SUCCESS:
                        /* Although test 1B failed, test 3 was successful.
                         * It could be that port 3489 is blocked, while the
                         * NAT itself looks to be a Restricted one.
                         */
                        end_session(sess, PJ_SUCCESS, 
                                    PJ_STUN_NAT_TYPE_RESTRICTED);
                        break;
                    default:
                        /* Can't distinguish between Symmetric and Port
                         * Restricted, so set the type to Unknown
                         */
                        end_session(sess, PJ_SUCCESS, 
                                    PJ_STUN_NAT_TYPE_ERR_UNKNOWN);
                        break;
                    }
                    break;
                default:
                    /*
                     * Got other error with test 1B.
                     */
                    end_session(sess, sess->result[ST_TEST_1B].status,
                                PJ_STUN_NAT_TYPE_ERR_UNKNOWN);
                    break;
                }
                break;
            default:
                /*
                 * We've got other error with Test 2.
                 */
                end_session(sess, sess->result[ST_TEST_2].status, 
                            PJ_STUN_NAT_TYPE_ERR_UNKNOWN);
                break;
            }
        }
        break;
    default:
        /*
         * We've got other error with Test 1.
         */
        end_session(sess, sess->result[ST_TEST_1].status, 
                    PJ_STUN_NAT_TYPE_ERR_UNKNOWN);
        break;
    }

on_return:
    pj_grp_lock_release(sess->grp_lock);
}


/* Perform test */
static pj_status_t send_test(nat_detect_session *sess,
                             enum test_type test_id,
                             const pj_sockaddr *alt_addr,
                             pj_uint32_t change_flag)
{
    pj_uint32_t magic, tsx_id[3];
    char addr[PJ_INET6_ADDRSTRLEN];
    pj_status_t status;

    sess->result[test_id].executed = PJ_TRUE;

    /* Randomize tsx id */
    do {
        magic = pj_rand();
    } while (magic == PJ_STUN_MAGIC);

    tsx_id[0] = pj_rand();
    tsx_id[1] = pj_rand();
    tsx_id[2] = test_id;

    /* Create BIND request */
    status = pj_stun_session_create_req(sess->stun_sess, 
                                        PJ_STUN_BINDING_REQUEST, magic,
                                        (pj_uint8_t*)tsx_id, 
                                        &sess->result[test_id].tdata);
    if (status != PJ_SUCCESS)
        goto on_error;

    /* Add CHANGE-REQUEST attribute */
    status = pj_stun_msg_add_uint_attr(sess->pool, 
                                       sess->result[test_id].tdata->msg,
                                       PJ_STUN_ATTR_CHANGE_REQUEST,
                                       change_flag);
    if (status != PJ_SUCCESS)
        goto on_error;

    /* Configure alternate address, synthesize it if necessary */
    if (alt_addr) {
        status = pj_sockaddr_synthesize(sess->server.addr.sa_family,
                                        &sess->cur_addr, alt_addr);
        if (status != PJ_SUCCESS)
            goto on_error;

        sess->cur_server = &sess->cur_addr;
    } else {
        sess->cur_server = &sess->server;
    }

    PJ_LOG(5,(sess->pool->obj_name, 
              "Performing %s to %s:%d", 
              test_names[test_id],
              pj_sockaddr_print(sess->cur_server, addr, sizeof(addr), 2),
              pj_sockaddr_get_port(sess->cur_server)));

    /* Send the request */
    status = pj_stun_session_send_msg(sess->stun_sess, NULL, PJ_TRUE,
                                      PJ_TRUE, sess->cur_server, 
                                      pj_sockaddr_get_len(sess->cur_server),
                                      sess->result[test_id].tdata);
    if (status != PJ_SUCCESS)
        goto on_error;

    return PJ_SUCCESS;

on_error:
    sess->result[test_id].complete = PJ_TRUE;
    sess->result[test_id].status = status;

    return status;
}


/* Timer callback */
static void on_sess_timer(pj_timer_heap_t *th,
                             pj_timer_entry *te)
{
    nat_detect_session *sess;

    sess = (nat_detect_session*) te->user_data;

    if (te->id == TIMER_DESTROY) {
        pj_grp_lock_acquire(sess->grp_lock);
        pj_ioqueue_unregister(sess->key);
        sess->key = NULL;
        sess->sock = PJ_INVALID_SOCKET;
        te->id = 0;
        pj_grp_lock_release(sess->grp_lock);

        sess_destroy(sess);

    } else if (te->id == TIMER_TEST) {

        pj_bool_t next_timer;

        pj_grp_lock_acquire(sess->grp_lock);

        next_timer = PJ_FALSE;

        if (sess->timer_executed == 0) {
            send_test(sess, ST_TEST_1, NULL, 0);
            next_timer = PJ_TRUE;
        } else if (sess->timer_executed == 1) {
            send_test(sess, ST_TEST_2, NULL, CHANGE_IP_PORT_FLAG);
            next_timer = PJ_TRUE;
        } else if (sess->timer_executed == 2) {
            send_test(sess, ST_TEST_3, NULL, CHANGE_PORT_FLAG);
        } else {
            pj_assert(!"Shouldn't have timer at this state");
        }

        ++sess->timer_executed;

        if (next_timer) {
            pj_time_val delay = {0, TEST_INTERVAL};
            pj_timer_heap_schedule_w_grp_lock(th, te, &delay,
                                              TIMER_TEST, sess->grp_lock);
        } else {
            te->id = 0;
        }

        pj_grp_lock_release(sess->grp_lock);

    } else {
        pj_assert(!"Invalid timer ID");
    }
}

