forked from cellular-infrastructure/osmocom-analog
584 lines
16 KiB
C
584 lines
16 KiB
C
/* Osmo-CC: Socket handling
|
|
*
|
|
* (C) 2016 by Andreas Eversberg <jolly@eversberg.eu>
|
|
* All Rights Reserved
|
|
*
|
|
* 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 3 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, see <http://www.gnu.org/licenses/>.
|
|
*/
|
|
|
|
#include <stdio.h>
|
|
#include <stdint.h>
|
|
#include <stdlib.h>
|
|
#include <unistd.h>
|
|
#include <fcntl.h>
|
|
#include <string.h>
|
|
#include <errno.h>
|
|
#include <sys/socket.h>
|
|
#include <netdb.h>
|
|
#include "../libdebug/debug.h"
|
|
#include "../libtimer/timer.h"
|
|
#include "message.h"
|
|
#include "cause.h"
|
|
#include "socket.h"
|
|
|
|
static const char version_string[] = OSMO_CC_VERSION;
|
|
|
|
static int _getaddrinfo(const char *host, uint16_t port, struct addrinfo **result)
|
|
{
|
|
char portstr[8];
|
|
struct addrinfo hints;
|
|
int rc;
|
|
|
|
sprintf(portstr, "%d", port);
|
|
|
|
/* bind socket */
|
|
memset(&hints, 0, sizeof(struct addrinfo));
|
|
hints.ai_family = AF_UNSPEC;
|
|
hints.ai_socktype = SOCK_STREAM;
|
|
hints.ai_flags = AI_PASSIVE;
|
|
hints.ai_protocol = 0;
|
|
hints.ai_canonname = NULL;
|
|
hints.ai_addr = NULL;
|
|
hints.ai_next = NULL;
|
|
|
|
rc = getaddrinfo(host, portstr, &hints, result);
|
|
if (rc < 0) {
|
|
PDEBUG(DCC, DEBUG_ERROR, "Failed to create socket for host '%s', port '%d': %s.\n", host, port, gai_strerror(rc));
|
|
return rc;
|
|
}
|
|
return rc;
|
|
}
|
|
|
|
/* send a reject message toward CC process.
|
|
* the CC process will change the reject message to a release message when not in INIT_IN state
|
|
*/
|
|
static void rej_msg(osmo_cc_socket_t *os, uint32_t callref, uint8_t socket_cause, uint8_t isdn_cause, uint16_t sip_cause)
|
|
{
|
|
osmo_cc_msg_t *msg;
|
|
|
|
/* create message */
|
|
msg = osmo_cc_new_msg(OSMO_CC_MSG_REJ_REQ);
|
|
if (!msg)
|
|
abort();
|
|
|
|
/* add cause */
|
|
osmo_cc_add_ie_cause(msg, os->location, isdn_cause, sip_cause, socket_cause);
|
|
osmo_cc_convert_cause_msg(msg);
|
|
|
|
/* message down */
|
|
os->recv_msg_cb(os->priv, callref, msg);
|
|
}
|
|
|
|
void tx_keepalive_timeout(struct timer *timer)
|
|
{
|
|
osmo_cc_conn_t *conn = (osmo_cc_conn_t *)timer->priv;
|
|
osmo_cc_msg_t *msg;
|
|
|
|
/* send keepalive message */
|
|
msg = osmo_cc_new_msg(OSMO_CC_MSG_DUMMY_REQ);
|
|
osmo_cc_msg_list_enqueue(&conn->os->write_list, msg, conn->callref);
|
|
timer_start(&conn->tx_keepalive_timer, OSMO_CC_SOCKET_TX_KEEPALIVE);
|
|
}
|
|
|
|
static void close_conn(osmo_cc_conn_t *conn, uint8_t socket_cause);
|
|
|
|
void rx_keepalive_timeout(struct timer *timer)
|
|
{
|
|
osmo_cc_conn_t *conn = (osmo_cc_conn_t *)timer->priv;
|
|
|
|
PDEBUG(DCC, DEBUG_ERROR, "OsmoCC-Socket failed due to timeout.\n");
|
|
close_conn(conn, OSMO_CC_SOCKET_CAUSE_TIMEOUT);
|
|
}
|
|
|
|
/* create socket process and bind socket */
|
|
int osmo_cc_open_socket(osmo_cc_socket_t *os, const char *host, uint16_t port, void *priv, void (*recv_msg_cb)(void *priv, uint32_t callref, osmo_cc_msg_t *msg), uint8_t location)
|
|
{
|
|
int try = 0, auto_port = 0;
|
|
struct addrinfo *result, *rp;
|
|
int rc, sock, flags;
|
|
|
|
memset(os, 0, sizeof(*os));
|
|
|
|
try_again:
|
|
/* check for given port, if NULL, autoselect port */
|
|
if (!port || auto_port) {
|
|
port = OSMO_CC_DEFAULT_PORT + try;
|
|
try++;
|
|
auto_port = 1;
|
|
}
|
|
|
|
PDEBUG(DCC, DEBUG_DEBUG, "Create socket for host %s port %d.\n", host, port);
|
|
|
|
rc = _getaddrinfo(host, port, &result);
|
|
if (rc < 0)
|
|
return rc;
|
|
for (rp = result; rp; rp = rp->ai_next) {
|
|
int on = 1;
|
|
sock = socket(rp->ai_family, rp->ai_socktype, rp->ai_protocol);
|
|
if (sock < 0)
|
|
continue;
|
|
setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, (unsigned char *)&on, sizeof(on));
|
|
rc = bind(sock, rp->ai_addr, rp->ai_addrlen);
|
|
if (rc == 0)
|
|
break;
|
|
close(sock);
|
|
}
|
|
freeaddrinfo(result);
|
|
if (rp == NULL) {
|
|
if (auto_port && port < OSMO_CC_DEFAULT_PORT_MAX) {
|
|
PDEBUG(DCC, DEBUG_DEBUG, "Failed to bind host %s port %d, trying again.\n", host, port);
|
|
goto try_again;
|
|
}
|
|
PDEBUG(DCC, DEBUG_ERROR, "Failed to bind given host %s port %d.\n", host, port);
|
|
return -EIO;
|
|
}
|
|
|
|
/* listen to socket */
|
|
rc = listen(sock, 10);
|
|
if (rc < 0) {
|
|
PDEBUG(DCC, DEBUG_ERROR, "Failed to listen on socket.\n");
|
|
return rc;
|
|
}
|
|
|
|
/* set nonblocking io */
|
|
flags = fcntl(sock, F_GETFL);
|
|
flags |= O_NONBLOCK;
|
|
fcntl(sock, F_SETFL, flags);
|
|
|
|
os->socket = sock;
|
|
os->recv_msg_cb = recv_msg_cb;
|
|
os->priv = priv;
|
|
os->location = location;
|
|
|
|
return port;
|
|
}
|
|
|
|
/* create a connection */
|
|
static osmo_cc_conn_t *open_conn(osmo_cc_socket_t *os, int sock, uint32_t callref, int read_setup)
|
|
{
|
|
osmo_cc_conn_t *conn, **connp;
|
|
|
|
/* create connection */
|
|
conn = calloc(1, sizeof(*conn));
|
|
if (!conn) {
|
|
PDEBUG(DCC, DEBUG_ERROR, "No mem!\n");
|
|
abort();
|
|
}
|
|
conn->os = os;
|
|
conn->socket = sock;
|
|
conn->read_version = 1;
|
|
conn->write_version = 1;
|
|
conn->read_setup = read_setup;
|
|
if (callref)
|
|
conn->callref = callref;
|
|
else
|
|
conn->callref = osmo_cc_new_callref();
|
|
|
|
timer_init(&conn->tx_keepalive_timer, tx_keepalive_timeout, conn);
|
|
timer_init(&conn->rx_keepalive_timer, rx_keepalive_timeout, conn);
|
|
timer_start(&conn->tx_keepalive_timer, OSMO_CC_SOCKET_TX_KEEPALIVE);
|
|
timer_start(&conn->rx_keepalive_timer, OSMO_CC_SOCKET_RX_KEEPALIVE);
|
|
|
|
PDEBUG(DCC, DEBUG_DEBUG, "New socket connection (callref %d).\n", conn->callref);
|
|
|
|
/* attach to list */
|
|
connp = &os->conn_list;
|
|
while (*connp)
|
|
connp = &((*connp)->next);
|
|
*connp = conn;
|
|
|
|
return conn;
|
|
}
|
|
|
|
/* remove a connection */
|
|
static void close_conn(osmo_cc_conn_t *conn, uint8_t socket_cause)
|
|
{
|
|
osmo_cc_conn_t **connp;
|
|
osmo_cc_msg_list_t *ml;
|
|
|
|
/* detach connection first, to prevent a destruction during message handling (double free) */
|
|
connp = &conn->os->conn_list;
|
|
while (*connp != conn)
|
|
connp = &((*connp)->next);
|
|
*connp = conn->next;
|
|
/* send reject message, if socket_cause is set */
|
|
if (socket_cause && !conn->read_setup) {
|
|
/* receive a release or reject (depending on state), but only if we sent a setup */
|
|
rej_msg(conn->os, conn->callref, socket_cause, 0, 0);
|
|
}
|
|
|
|
PDEBUG(DCC, DEBUG_DEBUG, "Destroy socket connection (callref %d).\n", conn->callref);
|
|
|
|
/* close socket */
|
|
if (conn->socket)
|
|
close(conn->socket);
|
|
/* free partly received message */
|
|
if (conn->read_msg)
|
|
osmo_cc_free_msg(conn->read_msg);
|
|
/* free send queue */
|
|
while ((ml = conn->write_list)) {
|
|
osmo_cc_free_msg(ml->msg);
|
|
conn->write_list = ml->next;
|
|
free(ml);
|
|
}
|
|
/* free timers */
|
|
timer_exit(&conn->tx_keepalive_timer);
|
|
timer_exit(&conn->rx_keepalive_timer);
|
|
/* free connection (already detached above) */
|
|
free(conn);
|
|
}
|
|
|
|
/* close socket and remove */
|
|
void osmo_cc_close_socket(osmo_cc_socket_t *os)
|
|
{
|
|
osmo_cc_msg_list_t *ml;
|
|
|
|
PDEBUG(DCC, DEBUG_DEBUG, "Destroy socket.\n");
|
|
|
|
/* free all connections */
|
|
while (os->conn_list)
|
|
close_conn(os->conn_list, 0);
|
|
/* close socket */
|
|
if (os->socket > 0) {
|
|
close(os->socket);
|
|
os->socket = 0;
|
|
}
|
|
/* free send queue */
|
|
while ((ml = os->write_list)) {
|
|
osmo_cc_free_msg(ml->msg);
|
|
os->write_list = ml->next;
|
|
free(ml);
|
|
}
|
|
}
|
|
|
|
/* send message to send_queue of sock instance */
|
|
int osmo_cc_sock_send_msg(osmo_cc_socket_t *os, uint32_t callref, osmo_cc_msg_t *msg, const char *host, uint16_t port)
|
|
{
|
|
osmo_cc_msg_list_t *ml;
|
|
|
|
/* turn _IND into _REQ and _CNF into _RSP */
|
|
msg->type &= ~1;
|
|
|
|
/* create list entry */
|
|
ml = osmo_cc_msg_list_enqueue(&os->write_list, msg, callref);
|
|
if (host)
|
|
strncpy(ml->host, host, sizeof(ml->host) - 1);
|
|
ml->port = port;
|
|
|
|
return 0;
|
|
}
|
|
|
|
/* receive message
|
|
* return 1 if work was done.
|
|
*/
|
|
static int receive_conn(osmo_cc_conn_t *conn)
|
|
{
|
|
uint8_t socket_cause = OSMO_CC_SOCKET_CAUSE_BROKEN_PIPE;
|
|
int rc;
|
|
osmo_cc_msg_t *msg;
|
|
uint8_t msg_type;
|
|
int len;
|
|
int work = 0;
|
|
|
|
/* get version from remote */
|
|
if (conn->read_version) {
|
|
rc = recv(conn->socket, conn->read_version_string + conn->read_version_pos, strlen(version_string) - conn->read_version_pos, 0);
|
|
if (rc < 0 && errno == EAGAIN)
|
|
return work;
|
|
work = 1;
|
|
if (rc <= 0) {
|
|
goto close;
|
|
}
|
|
conn->read_version_pos += rc;
|
|
if (conn->read_version_pos == strlen(version_string)) {
|
|
conn->read_version = 0;
|
|
if (!!memcmp(conn->read_version_string, version_string, strlen(version_string) - 1)) {
|
|
PDEBUG(DCC, DEBUG_NOTICE, "Remote does not seem to be an Osmo-CC socket, rejecting!\n");
|
|
socket_cause = OSMO_CC_SOCKET_CAUSE_FAILED;
|
|
goto close;
|
|
}
|
|
if (conn->read_version_string[strlen(version_string) - 1] != version_string[strlen(version_string) - 1]) {
|
|
PDEBUG(DCC, DEBUG_NOTICE, "Remote Osmo-CC socket has wrong version (local=%s, remote=%s), rejecting!\n", version_string, conn->read_version_string);
|
|
socket_cause = OSMO_CC_SOCKET_CAUSE_VERSION_MISMATCH;
|
|
goto close;
|
|
}
|
|
} else
|
|
return work;
|
|
}
|
|
|
|
try_next_message:
|
|
/* read message header from remote */
|
|
if (!conn->read_msg) {
|
|
rc = recv(conn->socket, ((uint8_t *)&conn->read_hdr) + conn->read_pos, sizeof(conn->read_hdr) - conn->read_pos, 0);
|
|
if (rc < 0 && errno == EAGAIN)
|
|
return work;
|
|
work = 1;
|
|
if (rc <= 0) {
|
|
goto close;
|
|
}
|
|
conn->read_pos += rc;
|
|
if (conn->read_pos == sizeof(conn->read_hdr)) {
|
|
conn->read_msg = osmo_cc_new_msg(conn->read_hdr.type);
|
|
if (!conn->read_msg)
|
|
abort();
|
|
conn->read_msg->length_networkorder = conn->read_hdr.length_networkorder;
|
|
/* prepare for reading message */
|
|
conn->read_pos = 0;
|
|
} else
|
|
return work;
|
|
}
|
|
|
|
/* read message data from remote */
|
|
msg = conn->read_msg;
|
|
len = ntohs(msg->length_networkorder);
|
|
if (len == 0)
|
|
goto empty_message;
|
|
rc = recv(conn->socket, msg->data + conn->read_pos, len - conn->read_pos, 0);
|
|
if (rc < 0 && errno == EAGAIN)
|
|
return work;
|
|
work = 1;
|
|
if (rc <= 0) {
|
|
goto close;
|
|
}
|
|
conn->read_pos += rc;
|
|
if (conn->read_pos == len) {
|
|
empty_message:
|
|
/* start RX keepalive timeer, if not already */
|
|
timer_start(&conn->rx_keepalive_timer, OSMO_CC_SOCKET_RX_KEEPALIVE);
|
|
/* we got our setup message, so we clear the flag */
|
|
conn->read_setup = 0;
|
|
/* prepare for reading header */
|
|
conn->read_pos = 0;
|
|
/* detach message first, because the connection might be destroyed during message handling */
|
|
msg_type = conn->read_msg->type;
|
|
conn->read_msg = NULL;
|
|
/* drop dummy or forward message */
|
|
if (msg_type == OSMO_CC_MSG_DUMMY_REQ)
|
|
osmo_cc_free_msg(msg);
|
|
else
|
|
conn->os->recv_msg_cb(conn->os->priv, conn->callref, msg);
|
|
if (msg_type == OSMO_CC_MSG_REL_REQ || msg_type == OSMO_CC_MSG_REJ_REQ) {
|
|
PDEBUG(DCC, DEBUG_DEBUG, "closing socket because we received a release or reject message.\n");
|
|
close_conn(conn, 0);
|
|
return 1; /* conn removed */
|
|
}
|
|
goto try_next_message;
|
|
}
|
|
return work;
|
|
|
|
close:
|
|
PDEBUG(DCC, DEBUG_ERROR, "OsmoCC-Socket failed, socket cause %d.\n", socket_cause);
|
|
close_conn(conn, socket_cause);
|
|
return work; /* conn removed */
|
|
}
|
|
|
|
/* transmit message
|
|
* return 1 if work was done.
|
|
*/
|
|
static int transmit_conn(osmo_cc_conn_t *conn)
|
|
{
|
|
uint8_t socket_cause = OSMO_CC_SOCKET_CAUSE_BROKEN_PIPE;
|
|
int rc;
|
|
osmo_cc_msg_t *msg;
|
|
int len;
|
|
osmo_cc_msg_list_t *ml;
|
|
int work = 0;
|
|
|
|
/* send socket version to remote */
|
|
if (conn->write_version) {
|
|
rc = write(conn->socket, version_string, strlen(version_string));
|
|
if (rc < 0 && errno == EAGAIN)
|
|
return work;
|
|
work = 1;
|
|
if (rc <= 0) {
|
|
goto close;
|
|
}
|
|
if (rc != strlen(version_string)) {
|
|
PDEBUG(DCC, DEBUG_ERROR, "short write, please fix handling!\n");
|
|
abort();
|
|
}
|
|
conn->write_version = 0;
|
|
}
|
|
|
|
/* send message to remote */
|
|
while (conn->write_list) {
|
|
timer_stop(&conn->tx_keepalive_timer);
|
|
msg = conn->write_list->msg;
|
|
len = sizeof(*msg) + ntohs(msg->length_networkorder);
|
|
rc = write(conn->socket, msg, len);
|
|
if (rc < 0 && errno == EAGAIN)
|
|
return work;
|
|
work = 1;
|
|
if (rc <= 0) {
|
|
goto close;
|
|
}
|
|
if (rc != len) {
|
|
PDEBUG(DCC, DEBUG_ERROR, "short write, please fix handling!\n");
|
|
abort();
|
|
}
|
|
/* close socket after sending release/reject message */
|
|
if (msg->type == OSMO_CC_MSG_REL_REQ || msg->type == OSMO_CC_MSG_REJ_REQ) {
|
|
PDEBUG(DCC, DEBUG_DEBUG, "closing socket because we sent a release or reject message.\n");
|
|
close_conn(conn, 0);
|
|
return work; /* conn removed */
|
|
}
|
|
/* free message after sending */
|
|
ml = conn->write_list;
|
|
conn->write_list = ml->next;
|
|
osmo_cc_free_msg(msg);
|
|
free(ml);
|
|
}
|
|
|
|
/* start TX keepalive timeer, if not already
|
|
* because we stop at every message above, we actually restart the timer here.
|
|
* only if there is no message for the amount of time, the timer fires.
|
|
*/
|
|
if (!timer_running(&conn->tx_keepalive_timer))
|
|
timer_start(&conn->tx_keepalive_timer, OSMO_CC_SOCKET_TX_KEEPALIVE);
|
|
|
|
return work;
|
|
|
|
close:
|
|
PDEBUG(DCC, DEBUG_NOTICE, "OsmoCC-Socket failed.\n");
|
|
close_conn(conn, socket_cause);
|
|
return work; /* conn removed */
|
|
}
|
|
|
|
/* handle all sockets of a socket interface
|
|
* return 1 if work was done.
|
|
*/
|
|
int osmo_cc_handle_socket(osmo_cc_socket_t *os)
|
|
{
|
|
struct sockaddr_storage sa;
|
|
socklen_t slen = sizeof(sa);
|
|
int sock;
|
|
osmo_cc_conn_t *conn;
|
|
osmo_cc_msg_list_t *ml, **mlp;
|
|
int flags;
|
|
struct addrinfo *result, *rp;
|
|
int rc;
|
|
int work = 0;
|
|
|
|
/* handle messages in send queue */
|
|
while ((ml = os->write_list)) {
|
|
work = 1;
|
|
/* detach list entry */
|
|
os->write_list = ml->next;
|
|
ml->next = NULL;
|
|
/* search for socket connection */
|
|
for (conn = os->conn_list; conn; conn=conn->next) {
|
|
if (conn->callref == ml->callref)
|
|
break;
|
|
}
|
|
if (conn) {
|
|
/* attach to list */
|
|
mlp = &conn->write_list;
|
|
while (*mlp)
|
|
mlp = &((*mlp)->next);
|
|
*mlp = ml;
|
|
/* done with message */
|
|
continue;
|
|
}
|
|
|
|
/* reject and release are ignored */
|
|
if (ml->msg->type == OSMO_CC_MSG_REJ_REQ
|
|
|| ml->msg->type == OSMO_CC_MSG_REL_REQ) {
|
|
/* drop message */
|
|
osmo_cc_free_msg(ml->msg);
|
|
free(ml);
|
|
/* done with message */
|
|
continue;
|
|
}
|
|
|
|
/* reject, if this is not a setup message */
|
|
if (ml->msg->type != OSMO_CC_MSG_SETUP_REQ
|
|
&& ml->msg->type != OSMO_CC_MSG_ATTACH_REQ) {
|
|
PDEBUG(DCC, DEBUG_ERROR, "Message with unknown callref.\n");
|
|
rej_msg(os, ml->callref, 0, OSMO_CC_ISDN_CAUSE_INVAL_CALLREF, 0);
|
|
/* drop message */
|
|
osmo_cc_free_msg(ml->msg);
|
|
free(ml);
|
|
/* done with message */
|
|
continue;
|
|
}
|
|
/* connect to remote */
|
|
rc = _getaddrinfo(ml->host, ml->port, &result);
|
|
if (rc < 0) {
|
|
rej_msg(os, ml->callref, OSMO_CC_SOCKET_CAUSE_FAILED, 0, 0);
|
|
/* drop message */
|
|
osmo_cc_free_msg(ml->msg);
|
|
free(ml);
|
|
/* done with message */
|
|
continue;
|
|
}
|
|
for (rp = result; rp; rp = rp->ai_next) {
|
|
sock = socket(rp->ai_family, rp->ai_socktype, rp->ai_protocol);
|
|
if (sock < 0)
|
|
continue;
|
|
/* set nonblocking io */
|
|
flags = fcntl(sock, F_GETFL);
|
|
flags |= O_NONBLOCK;
|
|
fcntl(sock, F_SETFL, flags);
|
|
/* connect */
|
|
rc = connect(sock, rp->ai_addr, rp->ai_addrlen);
|
|
if (rc == 0 || errno == EINPROGRESS)
|
|
break;
|
|
close(sock);
|
|
}
|
|
freeaddrinfo(result);
|
|
if (rp == NULL) {
|
|
PDEBUG(DCC, DEBUG_ERROR, "Failed to connect to given host %s port %d.\n", ml->host, ml->port);
|
|
rej_msg(os, ml->callref, OSMO_CC_SOCKET_CAUSE_FAILED, 0, 0);
|
|
/* drop message */
|
|
osmo_cc_free_msg(ml->msg);
|
|
free(ml);
|
|
/* done with message */
|
|
continue;
|
|
}
|
|
/* create connection */
|
|
conn = open_conn(os, sock, ml->callref, 0);
|
|
/* attach to list */
|
|
conn->write_list = ml;
|
|
/* done with (setup) message */
|
|
}
|
|
|
|
/* handle new socket connection */
|
|
while ((sock = accept(os->socket, (struct sockaddr *)&sa, &slen)) > 0) {
|
|
work = 1;
|
|
/* set nonblocking io */
|
|
flags = fcntl(sock, F_GETFL);
|
|
flags |= O_NONBLOCK;
|
|
fcntl(sock, F_SETFL, flags);
|
|
/* create connection */
|
|
open_conn(os, sock, 0, 1);
|
|
}
|
|
|
|
/* start with list after each read/write, because while handling (the message), one or more connections may be destroyed */
|
|
for (conn = os->conn_list; conn; conn=conn->next) {
|
|
/* check for rx */
|
|
work = receive_conn(conn);
|
|
/* if "change" is set, connection list might have changed, so we restart processing the list */
|
|
if (work)
|
|
break;
|
|
/* check for tx */
|
|
work = transmit_conn(conn);
|
|
/* if "change" is set, connection list might have changed, so we restart processing the list */
|
|
if (work)
|
|
break;
|
|
}
|
|
|
|
return work;
|
|
}
|
|
|