summaryrefslogtreecommitdiffstats
path: root/PICA_media.c
diff options
context:
space:
mode:
Diffstat (limited to 'PICA_media.c')
-rw-r--r--PICA_media.c640
1 files changed, 640 insertions, 0 deletions
diff --git a/PICA_media.c b/PICA_media.c
new file mode 100644
index 0000000..b0f33cf
--- /dev/null
+++ b/PICA_media.c
@@ -0,0 +1,640 @@
+/*
+ (c) Copyright 2012 - 2026 Anton Sviridenko
+ https://picapica.im
+
+ 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, version 3.
+
+ 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 <https://www.gnu.org/licenses/>.
+*/
+#include "PICA_security.h"
+#include "PICA_client.h"
+#include "PICA_media.h"
+#include "PICA_proto.h"
+#include "PICA_common.h"
+#include <string.h>
+#include <time.h>
+
+#define PICA_DEBUG
+
+#ifdef PICA_DEBUG
+#define PICA_TRACEFUNC fprintf(stderr, "%s()\n", __PRETTY_FUNCTION__);
+#else
+#define PICA_TRACEFUNC
+#endif
+
+static void media_fail(struct PICA_media_channel *m);
+static void media_handshake(struct PICA_media_channel *m);
+
+static int media_socket_open(SOCKET *s, int local_port)
+{
+ int flag = 1;
+ struct sockaddr_in a;
+
+ *s = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
+
+ if (*s == SOCKET_ERROR)
+ return PICA_ERRSOCK;
+
+ setsockopt(*s, SOL_SOCKET, SO_REUSEADDR, (const char*)&flag, sizeof(flag));
+
+ memset(&a, 0, sizeof(a));
+ a.sin_family = AF_INET;
+ a.sin_addr.s_addr = INADDR_ANY;
+ /* local_port 0 asks the system for an unused port, which is what the
+ * connecting side wants - only the accepting side needs a port number
+ * known to the peer in advance.
+ */
+ a.sin_port = htons(local_port);
+
+ if (bind(*s, (const struct sockaddr*)&a, sizeof(a)) == SOCKET_ERROR)
+ {
+ CLOSE(*s);
+ *s = -1;
+ return PICA_ERRSOCK;
+ }
+
+ IOCTLSETNONBLOCKINGSOCKET(*s, 1);
+
+ return PICA_OK;
+}
+
+int PICA_media_listener_new(const struct PICA_acc *acc, int public_port, int local_port,
+ struct PICA_media_listener **ml)
+{
+ struct PICA_media_listener *l;
+ int ret;
+
+ PICA_TRACEFUNC
+
+ if (!acc || !ml || public_port <= 0 || public_port > 65535 || local_port <= 0 || local_port > 65535)
+ return PICA_ERRINVARG;
+
+ l = *ml = (struct PICA_media_listener*)calloc(sizeof(struct PICA_media_listener), 1);
+
+ if (!l)
+ return PICA_ERRNOMEM;
+
+ l->acc = acc;
+ l->public_port = public_port;
+ l->local_port = local_port;
+
+ ret = media_socket_open(&l->sck, local_port);
+
+ if (ret != PICA_OK)
+ {
+ free(l);
+ *ml = 0;
+ return ret;
+ }
+
+ return PICA_OK;
+}
+
+void PICA_media_listener_close(struct PICA_media_listener *ml)
+{
+ PICA_TRACEFUNC
+
+ if (!ml)
+ return;
+
+ if (ml->channel)
+ {
+ ml->channel->listener = NULL;
+ ml->channel->sck = -1;
+ ml->channel = NULL;
+ }
+
+ if (ml->sck >= 0)
+ CLOSE(ml->sck);
+
+ free(ml);
+}
+
+/* The listener's socket gets connect()ed to the peer for the duration of a
+ * call, so that the system drops everything coming from anywhere else. It has
+ * to be given back in its original, unconnected state afterwards; recreating
+ * it is the one way of doing that which behaves the same everywhere.
+ */
+static void media_listener_rearm(struct PICA_media_listener *ml)
+{
+ PICA_TRACEFUNC
+
+ if (ml->sck >= 0)
+ CLOSE(ml->sck);
+
+ if (media_socket_open(&ml->sck, ml->local_port) != PICA_OK)
+ {
+ fprintf(stderr, "failed to reopen the media listening socket on port %i\n", ml->local_port);
+ ml->sck = -1;
+ }
+}
+
+static int media_ssl_new(struct PICA_media_channel *m)
+{
+ BIO *bio;
+
+ m->ssl = SSL_new(m->c2c->acc->dtls_ctx);
+
+ if (!m->ssl)
+ return PICA_ERRSSL;
+
+ bio = BIO_new_dgram(m->sck, BIO_NOCLOSE);
+
+ if (!bio)
+ {
+ SSL_free(m->ssl);
+ m->ssl = NULL;
+ return PICA_ERRSSL;
+ }
+
+ /* The socket is connected, so the system already knows where the
+ * datagrams go; the BIO has to be told as well, otherwise it has no
+ * peer address to hand to sendto().
+ */
+ BIO_ctrl(bio, BIO_CTRL_DGRAM_SET_CONNECTED, 0, (char*)&m->peer_addr);
+
+ SSL_set_bio(m->ssl, bio, bio);
+
+ SSL_set_verify(m->ssl, SSL_VERIFY_PEER, PICA_verify_callback);
+ SSL_set_verify_depth(m->ssl, 1);
+
+ /* OpenSSL performs no path MTU discovery of its own: off Linux it cannot
+ * even read the value the system discovered, and falls back to a 228
+ * octet floor that would fragment every handshake message. Telling it
+ * the MTU explicitly, and stopping it from asking the BIO
+ * (SSL_OP_NO_QUERY_MTU), gives the same behaviour everywhere. It also
+ * keeps OpenSSL from shrinking the value on its own after two handshake
+ * retransmissions, which it never undoes.
+ * This has to happen before the handshake starts: with
+ * SSL_OP_NO_QUERY_MTU set and no MTU, writing a handshake message fails.
+ */
+ SSL_set_options(m->ssl, SSL_OP_NO_QUERY_MTU);
+#if OPENSSL_VERSION_NUMBER >= 0x10100000L
+ DTLS_set_link_mtu(m->ssl, PICA_MEDIA_LINK_MTU);
+#else
+ SSL_set_mtu(m->ssl, PICA_MEDIA_LINK_MTU - PICA_MEDIA_IPUDP_OVERHEAD);
+#endif
+
+ return PICA_OK;
+}
+
+static struct PICA_media_channel *media_channel_new(struct PICA_c2c *c2c)
+{
+ struct PICA_media_channel *m;
+
+ m = (struct PICA_media_channel*)calloc(sizeof(struct PICA_media_channel), 1);
+
+ if (!m)
+ return NULL;
+
+ m->c2c = c2c;
+ m->sck = -1;
+ m->started = time(0);
+ m->last_rx = m->started;
+ m->last_tx = m->started;
+
+ return m;
+}
+
+void PICA_media_start(struct PICA_c2c *c2c)
+{
+ struct PICA_media_channel *m;
+ int ret;
+
+ PICA_TRACEFUNC
+
+ if (c2c->media)
+ return;
+
+ /* Media over UDP is only attempted between clients that already reached
+ * each other directly over TCP: that address is known to work and the
+ * user has already agreed to reveal it by enabling direct connections.
+ */
+ if (c2c->directc2c_state != PICA_DIRECTC2C_STATE_ACTIVE || !c2c->direct)
+ return;
+
+ if (!c2c->acc->dtls_ctx)
+ return;
+
+ m = media_channel_new(c2c);
+
+ if (!m)
+ return;
+
+ if (c2c->direct->is_outgoing == PICA_DIRECTC2C_OUTGOING)
+ {
+ /* We reached the peer over TCP at this address, so we are the one
+ * that can reach it over UDP as well - the peer's announced port
+ * number is used for both.
+ */
+ m->is_dtls_client = 1;
+ m->peer_addr = c2c->direct->addr;
+
+ ret = media_socket_open(&m->sck, 0);
+
+ if (ret != PICA_OK)
+ {
+ free(m);
+ return;
+ }
+
+ m->owns_sck = 1;
+
+ if (connect(m->sck, (struct sockaddr*)&m->peer_addr, sizeof(m->peer_addr)) == SOCKET_ERROR)
+ {
+ CLOSE(m->sck);
+ free(m);
+ return;
+ }
+
+ if (media_ssl_new(m) != PICA_OK)
+ {
+ CLOSE(m->sck);
+ free(m);
+ return;
+ }
+
+ SSL_set_connect_state(m->ssl);
+
+ m->state = PICA_MEDIA_STATE_HANDSHAKE;
+ c2c->media = m;
+
+ media_handshake(m);
+ }
+ else
+ {
+ struct PICA_media_listener *ml;
+
+ if (!c2c->conn->directc2c_listener || !c2c->conn->directc2c_listener->media)
+ {
+ free(m);
+ return;
+ }
+
+ ml = c2c->conn->directc2c_listener->media;
+
+ if (ml->sck < 0 || ml->channel)
+ {
+ free(m);
+ return;
+ }
+
+ /* Nothing to do until the peer sends its first datagram: the source
+ * address it arrives from is the only way to learn which port its
+ * NAT gave it.
+ */
+ m->listener = ml;
+ m->sck = ml->sck;
+ m->owns_sck = 0;
+ m->state = PICA_MEDIA_STATE_LISTENING;
+
+ ml->channel = m;
+ c2c->media = m;
+ }
+}
+
+void PICA_media_close(struct PICA_c2c *c2c)
+{
+ struct PICA_media_channel *m = c2c->media;
+
+ PICA_TRACEFUNC
+
+ if (!m)
+ return;
+
+ c2c->media = NULL;
+
+ if (m->peer_cert)
+ X509_free(m->peer_cert);
+
+ if (m->ssl)
+ SSL_free(m->ssl);
+
+ if (m->owns_sck && m->sck >= 0)
+ CLOSE(m->sck);
+
+ if (m->listener)
+ {
+ m->listener->channel = NULL;
+
+ /* the socket was connected to the peer while the call lasted */
+ if (m->state != PICA_MEDIA_STATE_LISTENING)
+ media_listener_rearm(m->listener);
+ }
+
+ free(m);
+}
+
+static void media_fail(struct PICA_media_channel *m)
+{
+ struct PICA_c2c *c2c = m->c2c;
+
+ PICA_TRACEFUNC
+
+ PICA_media_close(c2c);
+
+ /* the call carries on, its media goes back to the c2c connection */
+ PICA_media_transport_changed(c2c, PICA_CALL_TRANSPORT_C2C, NULL);
+}
+
+int PICA_media_is_active(const struct PICA_c2c *c2c)
+{
+ return c2c->media && c2c->media->state == PICA_MEDIA_STATE_ACTIVE;
+}
+
+unsigned int PICA_media_max_payload(const struct PICA_c2c *c2c)
+{
+ unsigned int mtu;
+
+ if (!PICA_media_is_active(c2c))
+ return PICA_PROTO_C2CMSG_MAXDATASIZE - PICA_PROTO_CALL_PACKET_HDRSIZE;
+
+ mtu = c2c->media->max_payload;
+
+ if (mtu <= 4 + PICA_PROTO_CALL_PACKET_HDRSIZE)
+ return 0;
+
+ /* the message header, its length field and the media packet header all
+ * have to fit into the same datagram
+ */
+ return mtu - 4 - PICA_PROTO_CALL_PACKET_HDRSIZE;
+}
+
+int PICA_media_send(struct PICA_c2c *c2c, const unsigned char *buf, unsigned int len)
+{
+ struct PICA_media_channel *m = c2c->media;
+ int ret;
+
+ if (!PICA_media_is_active(c2c))
+ return PICA_ERRINVARG;
+
+ ret = SSL_write(m->ssl, buf, len);
+
+ if (ret > 0)
+ {
+ m->last_tx = time(0);
+ return PICA_OK;
+ }
+
+ switch (SSL_get_error(m->ssl, ret))
+ {
+ case SSL_ERROR_WANT_WRITE:
+ case SSL_ERROR_WANT_READ:
+ /* The send buffer is full - the packet is dropped rather than
+ * queued. Delaying live media is worse than losing a frame of it,
+ * and the receiving side is built to cope with losses anyway.
+ */
+ return PICA_OK;
+
+ default:
+ media_fail(m);
+ return PICA_ERRSSL;
+ }
+}
+
+static void media_activate(struct PICA_media_channel *m)
+{
+ int ret;
+
+ PICA_TRACEFUNC
+
+ ret = PICA_verify_peer_cert_common(&m->peer_cert, m->ssl, m->c2c->peer_id);
+
+ if (ret != PICA_OK)
+ {
+ fprintf(stderr, "media connection peer certificate verification failed: %i\n", ret);
+ media_fail(m);
+ return;
+ }
+
+#if OPENSSL_VERSION_NUMBER >= 0x10100000L
+ m->max_payload = (unsigned int)DTLS_get_data_mtu(m->ssl);
+#else
+ /* DTLS record header, explicit nonce and authentication tag of the
+ * ciphersuites Pica Pica uses
+ */
+ m->max_payload = PICA_MEDIA_LINK_MTU - PICA_MEDIA_IPUDP_OVERHEAD - 13 - 8 - 16;
+#endif
+
+ m->state = PICA_MEDIA_STATE_ACTIVE;
+ m->last_rx = time(0);
+
+ PICA_media_transport_changed(m->c2c, PICA_CALL_TRANSPORT_MEDIAC2C, SSL_get_cipher_name(m->ssl));
+}
+
+static void media_handshake(struct PICA_media_channel *m)
+{
+ int ret;
+
+ ret = SSL_do_handshake(m->ssl);
+
+ if (ret == 1)
+ {
+ media_activate(m);
+ return;
+ }
+
+ switch (SSL_get_error(m->ssl, ret))
+ {
+ case SSL_ERROR_WANT_READ:
+ case SSL_ERROR_WANT_WRITE:
+ break;
+
+ default:
+ fprintf(stderr, "media connection DTLS handshake failed\n");
+ media_fail(m);
+ }
+}
+
+/* The first datagram of a call arrives at the listening socket from a source
+ * port only the peer's NAT knows. Once it is seen, the socket is connected to
+ * that address for the rest of the call.
+ */
+static void media_accept(struct PICA_media_channel *m)
+{
+ struct sockaddr_in from;
+#ifdef WIN32
+ int fromlen = sizeof(from);
+#else
+ socklen_t fromlen = sizeof(from);
+#endif
+ /* Large enough for any datagram the peer can send us, so that the peek
+ * never truncates: a truncated peek is reported as an error on Windows.
+ */
+ unsigned char peekbuf[PICA_MEDIA_READBUFSIZE];
+ int ret;
+
+ PICA_TRACEFUNC
+
+ ret = recvfrom(m->sck, (char*)peekbuf, sizeof(peekbuf), MSG_PEEK, (struct sockaddr*)&from, &fromlen);
+
+ if (ret < 0)
+ return;
+
+ /* The peer of the call is already known - it is the client on the other
+ * end of the directc2c connection - so a datagram from any other address
+ * cannot belong to this call. Only the address is checked here, the port
+ * number is whatever the peer's system picked.
+ */
+ if (from.sin_addr.s_addr != m->c2c->direct->addr.sin_addr.s_addr)
+ {
+ /* consume it, otherwise it is peeked again on every pass */
+ recvfrom(m->sck, (char*)peekbuf, sizeof(peekbuf), 0, (struct sockaddr*)&from, &fromlen);
+ fprintf(stderr, "dropped a datagram from an address the call is not with\n");
+ return;
+ }
+
+ m->peer_addr = from;
+
+ if (connect(m->sck, (struct sockaddr*)&m->peer_addr, sizeof(m->peer_addr)) == SOCKET_ERROR)
+ {
+ media_fail(m);
+ return;
+ }
+
+ if (media_ssl_new(m) != PICA_OK)
+ {
+ media_fail(m);
+ return;
+ }
+
+ SSL_set_accept_state(m->ssl);
+
+ m->state = PICA_MEDIA_STATE_HANDSHAKE;
+
+ media_handshake(m);
+}
+
+static void media_read(struct PICA_media_channel *m)
+{
+ int ret;
+
+ /* One SSL_read() per datagram, so this loop runs until the socket has
+ * nothing left for us.
+ */
+ while (1)
+ {
+ ret = SSL_read(m->ssl, m->read_buf, PICA_MEDIA_READBUFSIZE);
+
+ if (ret <= 0)
+ {
+ switch (SSL_get_error(m->ssl, ret))
+ {
+ case SSL_ERROR_WANT_READ:
+ case SSL_ERROR_WANT_WRITE:
+ return;
+
+ default:
+ fprintf(stderr, "media connection read failed\n");
+ media_fail(m);
+ return;
+ }
+ }
+
+ m->last_rx = time(0);
+ m->read_pos = (unsigned int)ret;
+
+ /* A datagram carries exactly one message. Anything that does not
+ * parse is dropped together with the rest of the datagram - unlike
+ * on a c2c connection, bad data here says nothing about the peer,
+ * anyone able to reach the port can send it.
+ */
+ PICA_media_process_messages(m->read_buf, &m->read_pos, m->c2c);
+
+ m->read_pos = 0;
+ }
+}
+
+void PICA_media_fdset(struct PICA_c2n *c2n, fd_set *rfds, int *nfds)
+{
+ struct PICA_c2c *c2c = c2n->chan_list_head;
+
+ while (c2c)
+ {
+ if (c2c->media && c2c->media->sck >= 0)
+ {
+ FD_SET(c2c->media->sck, rfds);
+
+ if (c2c->media->sck > *nfds)
+ *nfds = c2c->media->sck;
+ }
+
+ c2c = c2c->next;
+ }
+}
+
+void PICA_media_process(struct PICA_c2n *c2n, fd_set *rfds)
+{
+ struct PICA_c2c *chan = c2n->chan_list_head;
+ time_t now = time(0);
+
+ while (chan)
+ {
+ struct PICA_c2c *c2c = chan;
+ struct PICA_media_channel *m = c2c->media;
+
+ chan = chan->next;
+
+ if (!m)
+ continue;
+
+ if (m->state != PICA_MEDIA_STATE_ACTIVE
+ && (now - m->started) > PICA_MEDIA_HANDSHAKE_TIMEOUT)
+ {
+ fprintf(stderr, "media connection was not established in time\n");
+ media_fail(m);
+ continue;
+ }
+
+ /* Anything below can close the media connection, which frees m -
+ * hence the checks through c2c, which stays valid.
+ */
+ switch (m->state)
+ {
+ case PICA_MEDIA_STATE_LISTENING:
+ if (m->sck >= 0 && FD_ISSET(m->sck, rfds))
+ media_accept(m);
+ break;
+
+ case PICA_MEDIA_STATE_HANDSHAKE:
+ /* Lost handshake messages are retransmitted by OpenSSL, but
+ * only when it is told that its timer has expired.
+ */
+ DTLSv1_handle_timeout(m->ssl);
+
+ if (m->sck >= 0 && FD_ISSET(m->sck, rfds))
+ media_handshake(m);
+ break;
+
+ case PICA_MEDIA_STATE_ACTIVE:
+ if (m->sck >= 0 && FD_ISSET(m->sck, rfds))
+ media_read(m);
+
+ if (!PICA_media_is_active(c2c))
+ break;
+
+ if ((now - m->last_rx) > PICA_MEDIA_SILENCE_TIMEOUT)
+ {
+ fprintf(stderr, "media connection went silent\n");
+ media_fail(m);
+ break;
+ }
+
+ if ((now - m->last_tx) >= PICA_MEDIA_KEEPALIVE_INTERVAL)
+ PICA_media_send_ping(c2c, 0);
+
+ break;
+
+ default:
+ break;
+ }
+ }
+}