Thursday, October 29, 2020

Kamailio and mobile TCP endpoints. Unregistering

The modern world is mobile. You may like it or not. But it is. So, with all these technologies that are about saving your battery life, all application reachability goes to vendor-lock push servers.
 
But the problem emerges in the following. SIP client registers on registrar (Kamailio) with TCP. Yes, you can do SIP keepalives here, but why to have em if we have a built-in mechanism of keepalive in TCP itself? Plus, additional packets over the network will drain the battery faster. And iOS, when putting the app to the background, just cut the network of app off. Literally killing it.
 
But we somehow need to know which actual state of the endpoint is. And here Kamailio is to help us with tcpops module.
 
The idea is quite simple. On each query to location table, we will clean-up it from "dead" TCP connections. So, after save(). And the only actual state of endpoints would be taken in the branch and calling process. Yes, it will give extra initial INVITE on already 'dead' connection, but appears to be, it sometimes cleaning just arrived REGISTER's so it's acceptable.


# Save info for TCP connections for unregister on close/timeout/error
loadmodule "htable.so"

modparam("htable", "htable", "tcpconn=>size=15;autoexpire=7200;")

...

# Handle SIP registrations
route[REGISTRAR] {

   ...

    save("location");
    route(TRACK_TCP_STATE);

    $var(processed_subscriber) = $fu;

    route(TCP_REGISISTER_CLEANUP);

   ...

}

 

route[TRACK_TCP_STATE] {
    if (proto == UDP) {
        return;
    }
    xlog("[TRACK_TCP_STATE] Saving state for future disconect track of $fu\n");

    $sht(tcpconn=>$conid) = $fu;


    tcp_enable_closed_event();
}


# Make sure you set up $var(processed_subscriber) before calling this route

route[TCP_REGISISTER_CLEANUP] {

    if ($var(processed_subscriber) == $null) {
        return;
    }

    xlog("[TCP_REGISISTER_CLEANUP] Processing subscriber $var(processed_subscriber)\n");

    # Getting registered endpoints for this AoR
    if (!reg_fetch_contacts("location", "$var(processed_subscriber)", "subscriber")) {
        $var(processed_subscriber) = $null;
        xlog("[TCP_REGISISTER_CLEANUP] No registered contacts for $var(processed_subscriber)\n");
        return;
    }

    $var(i) = 0;

    # Loop through registered endpoints
    while ($var(i) < $(ulc(subscriber=>count))) {

        $var(stored_subscriber_conid) = $(ulc(subscriber=>conid)[$var(i)]);

        # Make sure proto is TCP
        if ($var(stored_subscriber_conid) != $null) {

            # Check if entry is still active TCP connection. Unregister otherwise.
            if ($var(stored_subscriber_conid) == -1 || !tcp_conid_alive("$var(stored_subscriber_conid)")) {

                $var(stored_subscriber_ruid) = $(ulc(subscriber=>ruid)[$var(i)]);
                $var(stored_subscriber_address) = $(ulc(subscriber=>addr)[$var(i)]);

                xlog("[TCP_REGISISTER_CLEANUP]: Unregistering entry $var(i)/$var(stored_subscriber_conid) -> $var(stored_subscriber_address)\n");

                if (!unregister("location", "$var(processed_subscriber)", "$var(stored_subscriber_ruid)")) {
                    xlog("[TCP_REGISISTER_CLEANUP]: Unregistering entry $var(i)/$var(stored_subscriber_conid) -> $var(stored_subscriber_address) FAILED!\n");
                }
            }
        }
        $var(i) = $var(i) + 1;
    }
    $var(processed_subscriber) = $null;
}


event_route[tcp:closed] {
    xlog("[TCP:CLOSED] $proto connection closed conid=$conid\n");

    $var(processed_subscriber) = $sht(tcpconn=>$conid);

    if ($var(processed_subscriber) != $null) {
        route(TCP_REGISISTER_CLEANUP);
        $sht(tcpconn=>$conid) = $null;
    }
}

event_route[tcp:timeout] {
    xlog("[TCP:TIMEOUT] $proto connection timeout conid=$conid\n");

    $var(processed_subscriber) = $sht(tcpconn=>$conid);

    if ($var(processed_subscriber) != $null) {
        route(TCP_REGISISTER_CLEANUP);
        $sht(tcpconn=>$conid) = $null;
    }
}

event_route[tcp:reset] {
    xlog("[TCP:RESET] $proto reset closed conid=$conid\n");

    $var(processed_subscriber) = $sht(tcpconn=>$conid);

    if ($var(processed_subscriber) != $null) {
        route(TCP_REGISISTER_CLEANUP);
        $sht(tcpconn=>$conid) = $null;
    }
}


Simple and efficient solution. Yes, some of "dead" endpoints would be present in table during expiration time, but I'm ok with that.