diff options
Diffstat (limited to 'main/tcptls.c')
-rw-r--r-- | main/tcptls.c | 536 |
1 files changed, 456 insertions, 80 deletions
diff --git a/main/tcptls.c b/main/tcptls.c index 3a8e412b5..076f94bae 100644 --- a/main/tcptls.c +++ b/main/tcptls.c @@ -50,102 +50,483 @@ ASTERISK_FILE_VERSION(__FILE__, "$Revision$") #include "asterisk/astobj2.h" #include "asterisk/pbx.h" -/*! \brief - * replacement read/write functions for SSL support. - * We use wrappers rather than SSL_read/SSL_write directly so - * we can put in some debugging. - */ +/*! ao2 object used for the FILE stream fopencookie()/funopen() cookie. */ +struct ast_tcptls_stream { + /*! SSL state if not NULL */ + SSL *ssl; + /*! + * \brief Start time from when an I/O sequence must complete + * by struct ast_tcptls_stream.timeout. + * + * \note If struct ast_tcptls_stream.start.tv_sec is zero then + * start time is the current I/O request. + */ + struct timeval start; + /*! + * \brief The socket returned by accept(). + * + * \note Set to -1 if the stream is closed. + */ + int fd; + /*! + * \brief Timeout in ms relative to struct ast_tcptls_stream.start + * to wait for an event on struct ast_tcptls_stream.fd. + * + * \note Set to -1 to disable timeout. + * \note The socket needs to be set to non-blocking for the timeout + * feature to work correctly. + */ + int timeout; +}; -#ifdef DO_SSL -static HOOK_T ssl_read(void *cookie, char *buf, LEN_T len) +void ast_tcptls_stream_set_timeout_disable(struct ast_tcptls_stream *stream) { - int i = SSL_read(cookie, buf, len-1); -#if 0 - if (i >= 0) { - buf[i] = '\0'; - } - ast_verb(0, "ssl read size %d returns %d <%s>\n", (int)len, i, buf); -#endif - return i; + ast_assert(stream != NULL); + + stream->timeout = -1; } -static HOOK_T ssl_write(void *cookie, const char *buf, LEN_T len) +void ast_tcptls_stream_set_timeout_inactivity(struct ast_tcptls_stream *stream, int timeout) { -#if 0 - char *s = ast_alloca(len+1); + ast_assert(stream != NULL); - strncpy(s, buf, len); - s[len] = '\0'; - ast_verb(0, "ssl write size %d <%s>\n", (int)len, s); -#endif - return SSL_write(cookie, buf, len); + stream->start.tv_sec = 0; + stream->timeout = timeout; } -static int ssl_close(void *cookie) +void ast_tcptls_stream_set_timeout_sequence(struct ast_tcptls_stream *stream, struct timeval start, int timeout) { - int cookie_fd = SSL_get_fd(cookie); - int ret; + ast_assert(stream != NULL); - if (cookie_fd > -1) { - /* - * According to the TLS standard, it is acceptable for an application to only send its shutdown - * alert and then close the underlying connection without waiting for the peer's response (this - * way resources can be saved, as the process can already terminate or serve another connection). - */ - if ((ret = SSL_shutdown(cookie)) < 0) { - ast_log(LOG_ERROR, "SSL_shutdown() failed: %d\n", SSL_get_error(cookie, ret)); + stream->start = start; + stream->timeout = timeout; +} + +/*! + * \internal + * \brief fopencookie()/funopen() stream read function. + * + * \param cookie Stream control data. + * \param buf Where to put read data. + * \param size Size of the buffer. + * + * \retval number of bytes put into buf. + * \retval 0 on end of file. + * \retval -1 on error. + */ +static HOOK_T tcptls_stream_read(void *cookie, char *buf, LEN_T size) +{ + struct ast_tcptls_stream *stream = cookie; + struct timeval start; + int ms; + int res; + + if (!size) { + /* You asked for no data you got no data. */ + return 0; + } + + if (!stream || stream->fd == -1) { + errno = EBADF; + return -1; + } + + if (stream->start.tv_sec) { + start = stream->start; + } else { + start = ast_tvnow(); + } + +#if defined(DO_SSL) + if (stream->ssl) { + for (;;) { + res = SSL_read(stream->ssl, buf, size); + if (0 < res) { + /* We read some payload data. */ + return res; + } + switch (SSL_get_error(stream->ssl, res)) { + case SSL_ERROR_ZERO_RETURN: + /* Report EOF for a shutdown */ + ast_debug(1, "TLS clean shutdown alert reading data\n"); + return 0; + case SSL_ERROR_WANT_READ: + while ((ms = ast_remaining_ms(start, stream->timeout))) { + res = ast_wait_for_input(stream->fd, ms); + if (0 < res) { + /* Socket is ready to be read. */ + break; + } + if (res < 0) { + if (errno == EINTR || errno == EAGAIN) { + /* Try again. */ + continue; + } + ast_debug(1, "TLS socket error waiting for read data: %s\n", + strerror(errno)); + return -1; + } + } + break; + case SSL_ERROR_WANT_WRITE: + while ((ms = ast_remaining_ms(start, stream->timeout))) { + res = ast_wait_for_output(stream->fd, ms); + if (0 < res) { + /* Socket is ready to be written. */ + break; + } + if (res < 0) { + if (errno == EINTR || errno == EAGAIN) { + /* Try again. */ + continue; + } + ast_debug(1, "TLS socket error waiting for write space: %s\n", + strerror(errno)); + return -1; + } + } + break; + default: + /* Report EOF for an undecoded SSL or transport error. */ + ast_debug(1, "TLS transport or SSL error reading data\n"); + return 0; + } + if (!ms) { + /* Report EOF for a timeout */ + ast_debug(1, "TLS timeout reading data\n"); + return 0; + } + } + } +#endif /* defined(DO_SSL) */ + + for (;;) { + res = read(stream->fd, buf, size); + if (0 <= res) { + return res; } + if (errno != EINTR && errno != EAGAIN) { + /* Not a retryable error. */ + ast_debug(1, "TCP socket error reading data: %s\n", + strerror(errno)); + return -1; + } + ms = ast_remaining_ms(start, stream->timeout); + if (!ms) { + /* Report EOF for a timeout */ + ast_debug(1, "TCP timeout reading data\n"); + return 0; + } + ast_wait_for_input(stream->fd, ms); + } +} + +/*! + * \internal + * \brief fopencookie()/funopen() stream write function. + * + * \param cookie Stream control data. + * \param buf Where to get data to write. + * \param size Size of the buffer. + * + * \retval number of bytes written from buf. + * \retval -1 on error. + */ +static HOOK_T tcptls_stream_write(void *cookie, const char *buf, LEN_T size) +{ + struct ast_tcptls_stream *stream = cookie; + struct timeval start; + int ms; + int res; + int written; + int remaining; + + if (!size) { + /* You asked to write no data you wrote no data. */ + return 0; + } + + if (!stream || stream->fd == -1) { + errno = EBADF; + return -1; + } + + if (stream->start.tv_sec) { + start = stream->start; + } else { + start = ast_tvnow(); + } - if (!((SSL*)cookie)->server) { - /* For client threads, ensure that the error stack is cleared */ - ERR_remove_state(0); +#if defined(DO_SSL) + if (stream->ssl) { + written = 0; + remaining = size; + for (;;) { + res = SSL_write(stream->ssl, buf + written, remaining); + if (res == remaining) { + /* Everything was written. */ + return size; + } + if (0 < res) { + /* Successfully wrote part of the buffer. Try to write the rest. */ + written += res; + remaining -= res; + continue; + } + switch (SSL_get_error(stream->ssl, res)) { + case SSL_ERROR_ZERO_RETURN: + ast_debug(1, "TLS clean shutdown alert writing data\n"); + if (written) { + /* Report partial write. */ + return written; + } + errno = EBADF; + return -1; + case SSL_ERROR_WANT_READ: + ms = ast_remaining_ms(start, stream->timeout); + if (!ms) { + /* Report partial write. */ + ast_debug(1, "TLS timeout writing data (want read)\n"); + return written; + } + ast_wait_for_input(stream->fd, ms); + break; + case SSL_ERROR_WANT_WRITE: + ms = ast_remaining_ms(start, stream->timeout); + if (!ms) { + /* Report partial write. */ + ast_debug(1, "TLS timeout writing data (want write)\n"); + return written; + } + ast_wait_for_output(stream->fd, ms); + break; + default: + /* Undecoded SSL or transport error. */ + ast_debug(1, "TLS transport or SSL error writing data\n"); + if (written) { + /* Report partial write. */ + return written; + } + errno = EBADF; + return -1; + } } + } +#endif /* defined(DO_SSL) */ - SSL_free(cookie); - /* adding shutdown(2) here has no added benefit */ - if (close(cookie_fd)) { + written = 0; + remaining = size; + for (;;) { + res = write(stream->fd, buf + written, remaining); + if (res == remaining) { + /* Yay everything was written. */ + return size; + } + if (0 < res) { + /* Successfully wrote part of the buffer. Try to write the rest. */ + written += res; + remaining -= res; + continue; + } + if (errno != EINTR && errno != EAGAIN) { + /* Not a retryable error. */ + ast_debug(1, "TCP socket error writing: %s\n", strerror(errno)); + if (written) { + return written; + } + return -1; + } + ms = ast_remaining_ms(start, stream->timeout); + if (!ms) { + /* Report partial write. */ + ast_debug(1, "TCP timeout writing data\n"); + return written; + } + ast_wait_for_output(stream->fd, ms); + } +} + +/*! + * \internal + * \brief fopencookie()/funopen() stream close function. + * + * \param cookie Stream control data. + * + * \retval 0 on success. + * \retval -1 on error. + */ +static int tcptls_stream_close(void *cookie) +{ + struct ast_tcptls_stream *stream = cookie; + + if (!stream) { + errno = EBADF; + return -1; + } + + if (stream->fd != -1) { +#if defined(DO_SSL) + if (stream->ssl) { + int res; + + /* + * According to the TLS standard, it is acceptable for an + * application to only send its shutdown alert and then + * close the underlying connection without waiting for + * the peer's response (this way resources can be saved, + * as the process can already terminate or serve another + * connection). + */ + res = SSL_shutdown(stream->ssl); + if (res < 0) { + ast_log(LOG_ERROR, "SSL_shutdown() failed: %d\n", + SSL_get_error(stream->ssl, res)); + } + + if (!stream->ssl->server) { + /* For client threads, ensure that the error stack is cleared */ + ERR_remove_state(0); + } + + SSL_free(stream->ssl); + stream->ssl = NULL; + } +#endif /* defined(DO_SSL) */ + + /* + * Issuing shutdown() is necessary here to avoid a race + * condition where the last data written may not appear + * in the TCP stream. See ASTERISK-23548 + */ + shutdown(stream->fd, SHUT_RDWR); + if (close(stream->fd)) { ast_log(LOG_ERROR, "close() failed: %s\n", strerror(errno)); } + stream->fd = -1; } + ao2_t_ref(stream, -1, "Closed tcptls stream cookie"); + return 0; } -#endif /* DO_SSL */ + +/*! + * \internal + * \brief fopencookie()/funopen() stream destructor function. + * + * \param cookie Stream control data. + * + * \return Nothing + */ +static void tcptls_stream_dtor(void *cookie) +{ + struct ast_tcptls_stream *stream = cookie; + + ast_assert(stream->fd == -1); +} + +/*! + * \internal + * \brief fopencookie()/funopen() stream allocation function. + * + * \retval stream_cookie on success. + * \retval NULL on error. + */ +static struct ast_tcptls_stream *tcptls_stream_alloc(void) +{ + struct ast_tcptls_stream *stream; + + stream = ao2_alloc_options(sizeof(*stream), tcptls_stream_dtor, + AO2_ALLOC_OPT_LOCK_NOLOCK); + if (stream) { + stream->fd = -1; + stream->timeout = -1; + } + return stream; +} + +/*! + * \internal + * \brief Open a custom FILE stream for tcptls. + * + * \param stream Stream cookie control data. + * \param ssl SSL state if not NULL. + * \param fd Socket file descriptor. + * \param timeout ms to wait for an event on fd. -1 if timeout disabled. + * + * \retval fp on success. + * \retval NULL on error. + */ +static FILE *tcptls_stream_fopen(struct ast_tcptls_stream *stream, SSL *ssl, int fd, int timeout) +{ + FILE *fp; + +#if defined(HAVE_FOPENCOOKIE) /* the glibc/linux interface */ + static const cookie_io_functions_t cookie_funcs = { + tcptls_stream_read, + tcptls_stream_write, + NULL, + tcptls_stream_close + }; +#endif /* defined(HAVE_FOPENCOOKIE) */ + + if (fd == -1) { + /* Socket not open. */ + return NULL; + } + + stream->ssl = ssl; + stream->fd = fd; + stream->timeout = timeout; + ao2_t_ref(stream, +1, "Opening tcptls stream cookie"); + +#if defined(HAVE_FUNOPEN) /* the BSD interface */ + fp = funopen(stream, tcptls_stream_read, tcptls_stream_write, NULL, + tcptls_stream_close); +#elif defined(HAVE_FOPENCOOKIE) /* the glibc/linux interface */ + fp = fopencookie(stream, "w+", cookie_funcs); +#else + /* could add other methods here */ + ast_debug(2, "No stream FILE methods attempted!\n"); + fp = NULL; +#endif + + if (!fp) { + stream->fd = -1; + ao2_t_ref(stream, -1, "Failed to open tcptls stream cookie"); + } + return fp; +} HOOK_T ast_tcptls_server_read(struct ast_tcptls_session_instance *tcptls_session, void *buf, size_t count) { - if (tcptls_session->fd == -1) { - ast_log(LOG_ERROR, "server_read called with an fd of -1\n"); + if (!tcptls_session->stream_cookie || tcptls_session->stream_cookie->fd == -1) { + ast_log(LOG_ERROR, "TCP/TLS read called on invalid stream.\n"); errno = EIO; return -1; } -#ifdef DO_SSL - if (tcptls_session->ssl) { - return ssl_read(tcptls_session->ssl, buf, count); - } -#endif - return read(tcptls_session->fd, buf, count); + return tcptls_stream_read(tcptls_session->stream_cookie, buf, count); } HOOK_T ast_tcptls_server_write(struct ast_tcptls_session_instance *tcptls_session, const void *buf, size_t count) { - if (tcptls_session->fd == -1) { - ast_log(LOG_ERROR, "server_write called with an fd of -1\n"); + if (!tcptls_session->stream_cookie || tcptls_session->stream_cookie->fd == -1) { + ast_log(LOG_ERROR, "TCP/TLS write called on invalid stream.\n"); errno = EIO; return -1; } -#ifdef DO_SSL - if (tcptls_session->ssl) { - return ssl_write(tcptls_session->ssl, buf, count); - } -#endif - return write(tcptls_session->fd, buf, count); + return tcptls_stream_write(tcptls_session->stream_cookie, buf, count); } static void session_instance_destructor(void *obj) { struct ast_tcptls_session_instance *i = obj; + + if (i->stream_cookie) { + ao2_t_ref(i->stream_cookie, -1, "Destroying tcptls session instance"); + i->stream_cookie = NULL; + } ast_free(i->overflow_buf); } @@ -177,12 +558,21 @@ static void *handle_tcptls_connection(void *data) return NULL; } + tcptls_session->stream_cookie = tcptls_stream_alloc(); + if (!tcptls_session->stream_cookie) { + ast_tcptls_close_session_file(tcptls_session); + ao2_ref(tcptls_session, -1); + return NULL; + } + /* * open a FILE * as appropriate. */ if (!tcptls_session->parent->tls_cfg) { - if ((tcptls_session->f = fdopen(tcptls_session->fd, "w+"))) { - if(setvbuf(tcptls_session->f, NULL, _IONBF, 0)) { + tcptls_session->f = tcptls_stream_fopen(tcptls_session->stream_cookie, NULL, + tcptls_session->fd, -1); + if (tcptls_session->f) { + if (setvbuf(tcptls_session->f, NULL, _IONBF, 0)) { ast_tcptls_close_session_file(tcptls_session); } } @@ -192,19 +582,8 @@ static void *handle_tcptls_connection(void *data) SSL_set_fd(tcptls_session->ssl, tcptls_session->fd); if ((ret = ssl_setup(tcptls_session->ssl)) <= 0) { ast_log(LOG_ERROR, "Problem setting up ssl connection: %s\n", ERR_error_string(ERR_get_error(), err)); - } else { -#if defined(HAVE_FUNOPEN) /* the BSD interface */ - tcptls_session->f = funopen(tcptls_session->ssl, ssl_read, ssl_write, NULL, ssl_close); - -#elif defined(HAVE_FOPENCOOKIE) /* the glibc/linux interface */ - static const cookie_io_functions_t cookie_funcs = { - ssl_read, ssl_write, NULL, ssl_close - }; - tcptls_session->f = fopencookie(tcptls_session->ssl, "w+", cookie_funcs); -#else - /* could add other methods here */ - ast_debug(2, "no tcptls_session->f methods attempted!\n"); -#endif + } else if ((tcptls_session->f = tcptls_stream_fopen(tcptls_session->stream_cookie, + tcptls_session->ssl, tcptls_session->fd, -1))) { if ((tcptls_session->client && !ast_test_flag(&tcptls_session->parent->tls_cfg->flags, AST_SSL_DONT_VERIFY_SERVER)) || (!tcptls_session->client && ast_test_flag(&tcptls_session->parent->tls_cfg->flags, AST_SSL_VERIFY_CLIENT))) { X509 *peer; @@ -625,21 +1004,18 @@ error: void ast_tcptls_close_session_file(struct ast_tcptls_session_instance *tcptls_session) { if (tcptls_session->f) { - /* - * Issuing shutdown() is necessary here to avoid a race - * condition where the last data written may not appear - * in the TCP stream. See ASTERISK-23548 - */ fflush(tcptls_session->f); - if (tcptls_session->fd != -1) { - shutdown(tcptls_session->fd, SHUT_RDWR); - } if (fclose(tcptls_session->f)) { ast_log(LOG_ERROR, "fclose() failed: %s\n", strerror(errno)); } tcptls_session->f = NULL; tcptls_session->fd = -1; } else if (tcptls_session->fd != -1) { + /* + * Issuing shutdown() is necessary here to avoid a race + * condition where the last data written may not appear + * in the TCP stream. See ASTERISK-23548 + */ shutdown(tcptls_session->fd, SHUT_RDWR); if (close(tcptls_session->fd)) { ast_log(LOG_ERROR, "close() failed: %s\n", strerror(errno)); |