/*
 *   stunnel       TLS offloading and load-balancing proxy
 *   Copyright (C) 1998-2026 Michal Trojnara <Michal.Trojnara@stunnel.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, see <http://www.gnu.org/licenses>.
 *
 *   Linking stunnel statically or dynamically with other modules is making
 *   a combined work based on stunnel. Thus, the terms and conditions of
 *   the GNU General Public License cover the whole combination.
 *
 *   In addition, as a special exception, the copyright holder of stunnel
 *   gives you permission to combine stunnel with free software programs or
 *   libraries that are released under the GNU LGPL and with code included
 *   in the standard release of OpenSSL under the OpenSSL License (or
 *   modified versions of such code, with unchanged license). You may copy
 *   and distribute such a system following the terms of the GNU GPL for
 *   stunnel and the licenses of the other code concerned.
 *
 *   Note that people who make modified versions of stunnel are not obligated
 *   to grant this special exception for their modified versions; it is their
 *   choice whether to do so. The GNU General Public License gives permission
 *   to release a modified version without this exception; this exception
 *   also makes it possible to release a modified version which carries
 *   forward this exception.
 */

#if defined(_WIN32) || defined(_WIN32_WCE)
/* bypass automatic index bound checks in the FD_SET() macro */
#define FD_SETSIZE 1000000
#endif

#include "prototypes.h"

#define FDS_INITIAL_CAPACITY 4
/* #define DEBUG_UCONTEXT */

NOEXPORT void s_poll_realloc(s_poll_set *, unsigned);
#ifndef USE_UCONTEXT
NOEXPORT void check_terminate(s_poll_set *);
#endif

/**************************************** s_poll functions */

#ifdef USE_POLL

s_poll_set *s_poll_alloc(void) {
    /* it needs to be filled with zeros */
    return str_alloc(sizeof(s_poll_set));
}

void s_poll_free(s_poll_set *fds) {
    if(fds) {
        str_free(fds->ufds);
        str_free(fds);
    }
}

void s_poll_init(s_poll_set *fds, int main_thread) {
    fds->nfds=0;
    s_poll_realloc(fds, FDS_INITIAL_CAPACITY);
    fds->main_thread=main_thread;
#ifdef USE_TERMINATE_PIPE
    s_poll_add(fds, main_thread ? signal_pipe[0] : terminate_pipe[0], 1, 0);
#else
    if(main_thread)
        s_poll_add(fds, signal_pipe[0], 1, 0);
#endif
}

void s_poll_add(s_poll_set *fds, SOCKET fd, int rd, int wr) {
    unsigned i;

    for(i=0; i<fds->nfds && fds->ufds[i].fd!=fd; i++)
        ;
    if(i==fds->nfds) { /* not found */
        if(i==fds->capacity)
            s_poll_realloc(fds, i+1);
        fds->ufds[i].fd=fd;
        fds->ufds[i].events=0;
        fds->nfds++;
    }
    if(rd) {
        fds->ufds[i].events|=POLLIN;
#ifdef POLLRDHUP
        fds->ufds[i].events|=POLLRDHUP;
#endif
    }
    if(wr)
        fds->ufds[i].events|=POLLOUT;
}

void s_poll_remove(s_poll_set *fds, SOCKET fd) {
    unsigned i;

    for(i=0; i<fds->nfds && fds->ufds[i].fd!=fd; i++)
        ;
    if(i<fds->nfds) { /* found */
        memmove(fds->ufds+i, fds->ufds+i+1,
            (fds->nfds-i-1)*sizeof(struct pollfd));
        fds->nfds--;
    }
}

int s_poll_canread(s_poll_set *fds, SOCKET fd) {
    unsigned i;

    for(i=0; i<fds->nfds; i++)
        if(fds->ufds[i].fd==fd)
            return fds->ufds[i].revents&(POLLIN|POLLERR);
    return 0; /* not listed in fds */
}

int s_poll_canwrite(s_poll_set *fds, SOCKET fd) {
    unsigned i;

    for(i=0; i<fds->nfds; i++)
        if(fds->ufds[i].fd==fd)
            return fds->ufds[i].revents&(POLLOUT|POLLERR);
    return 0; /* not listed in fds */
}

/* best doc: http://lxr.free-electrons.com/source/net/ipv4/tcp.c#L456 */

int s_poll_hup(s_poll_set *fds, SOCKET fd) {
    unsigned i;

    for(i=0; i<fds->nfds; i++)
        if(fds->ufds[i].fd==fd)
            return fds->ufds[i].revents&POLLHUP; /* read and write closed */
    return 0; /* not listed in fds */
}

int s_poll_rdhup(s_poll_set *fds, SOCKET fd) {
    unsigned i;

    for(i=0; i<fds->nfds; i++)
        if(fds->ufds[i].fd==fd)
#ifdef POLLRDHUP
            return fds->ufds[i].revents&POLLRDHUP; /* read closed */
#else
            return fds->ufds[i].revents&POLLHUP; /* read and write closed */
#endif
    return 0; /* not listed in fds */
}

int s_poll_err(s_poll_set *fds, SOCKET fd) {
    unsigned i;

    for(i=0; i<fds->nfds; i++)
        if(fds->ufds[i].fd==fd)
            return fds->ufds[i].revents&POLLERR;
    return 0; /* not listed in fds */
}

NOEXPORT void s_poll_realloc(s_poll_set *fds, unsigned capacity) {
    fds->capacity=capacity;
    fds->ufds=str_realloc(fds->ufds, capacity*sizeof(struct pollfd));
}

void s_poll_dump(s_poll_set *fds, int level) {
    unsigned i;

    for(i=0; i<fds->nfds; i++)
        s_log(level, "FD=%ld events=0x%X revents=0x%X",
            (long)fds->ufds[i].fd, fds->ufds[i].events, fds->ufds[i].revents);
}

#ifdef USE_UCONTEXT

/* move ready contexts from waiting queue to ready queue */
NOEXPORT void scan_waiting_queue(void) {
    int retval;
    CONTEXT *context, *prev;
    int min_timeout;
    unsigned nfds, i;
    time_t now;
    static unsigned max_nfds=0;
    static struct pollfd *ufds=NULL;

    time(&now);
    /* count file descriptors */
    min_timeout=-1; /* infinity */
    nfds=0;
    for(context=waiting_head; context; context=context->next) {
        nfds+=context->fds->nfds;
        if(context->finish>=0) /* finite time */
            if(min_timeout<0 || min_timeout>context->finish-now)
                min_timeout=
                    (int)(context->finish-now<0 ? 0 : context->finish-now);
    }
    /* setup ufds structure */
    if(nfds>max_nfds) { /* need to allocate more memory */
        ufds=str_realloc(ufds, nfds*sizeof(struct pollfd));
        max_nfds=nfds;
    }
    nfds=0;
    for(context=waiting_head; context; context=context->next)
        for(i=0; i<context->fds->nfds; i++) {
            ufds[nfds].fd=context->fds->ufds[i].fd;
            ufds[nfds].events=context->fds->ufds[i].events;
            nfds++;
        }

#ifdef DEBUG_UCONTEXT
    s_log(LOG_DEBUG, "Waiting %d second(s) for %d file descriptor(s)",
        min_timeout, nfds);
#endif
    do { /* skip "Interrupted system call" errors */
        retval=poll(ufds, nfds, min_timeout<0 ? -1 : 1000*min_timeout);
    } while(retval<0 && get_last_socket_error()==S_EINTR);
    time(&now);
    /* process the returned data */
    nfds=0;
    prev=NULL; /* previous element of the waiting queue */
    context=waiting_head;
    while(context) {
        context->ready=0;
        /* count ready file descriptors in each context */
        for(i=0; i<context->fds->nfds; i++) {
            context->fds->ufds[i].revents=ufds[nfds].revents;
#ifdef DEBUG_UCONTEXT
            s_log(LOG_DEBUG, "CONTEXT %ld, FD=%ld,%s%s ->%s%s%s%s%s",
                context->id, (long)ufds[nfds].fd,
                (ufds[nfds].events & POLLIN) ? " IN" : "",
                (ufds[nfds].events & POLLOUT) ? " OUT" : "",
                (ufds[nfds].revents & POLLIN) ? " IN" : "",
                (ufds[nfds].revents & POLLOUT) ? " OUT" : "",
                (ufds[nfds].revents & POLLERR) ? " ERR" : "",
                (ufds[nfds].revents & POLLHUP) ? " HUP" : "",
                (ufds[nfds].revents & POLLNVAL) ? " NVAL" : "");
#endif
            if(ufds[nfds].revents)
                context->ready++;
            nfds++;
        }
        if(context->ready || (context->finish>=0 && context->finish<=now)) {
            /* remove context from the waiting queue */
            if(prev)
                prev->next=context->next;
            else
                waiting_head=context->next;
            if(!context->next) /* same as context==waiting_tail */
                waiting_tail=prev;

            /* append context context to the ready queue */
            context->next=NULL;
            if(ready_tail)
                ready_tail->next=context;
            ready_tail=context;
            if(!ready_head)
                ready_head=context;
        } else { /* leave the context context in the waiting queue */
            prev=context;
        }
        context=prev ? prev->next : waiting_head;
    }
}

int s_poll_wait(s_poll_set *fds, int sec, int msec) {
    CONTEXT *context; /* current context */
    static CONTEXT *to_free=NULL; /* delayed memory deallocation */

    /* FIXME: msec parameter is currently ignored with UCONTEXT threads */
    (void)msec; /* squash the unused parameter warning */

    /* remove the current context from ready queue */
    context=ready_head;
    ready_head=ready_head->next;
    if(!ready_head) /* the queue is empty */
        ready_tail=NULL;
    /* it is safe to s_log() after new ready_head is set */

    /* it is illegal to deallocate the stack of the current context */
    if(to_free) { /* a delayed deallocation is scheduled */
#ifdef DEBUG_UCONTEXT
        s_log(LOG_DEBUG, "Releasing context %ld", to_free->id);
#endif
        str_free(to_free->stack);
        str_free(to_free);
        to_free=NULL;
    }

    /* manage the current thread */
    if(fds) { /* something to wait for -> swap the context */
        context->fds=fds; /* set file descriptors to wait for */
        context->finish=sec<0 ? -1 : time(NULL)+sec;

        /* append the current context to the waiting queue */
        context->next=NULL;
        if(waiting_tail)
            waiting_tail->next=context;
        waiting_tail=context;
        if(!waiting_head)
            waiting_head=context;
    } else { /* nothing to wait for -> drop the context */
        to_free=context; /* schedule for delayed deallocation */
    }

    while(!ready_head) /* wait until there is a thread to switch to */
        scan_waiting_queue();

    /* switch threads */
    if(fds) { /* swap the current context */
        if(context->id!=ready_head->id) {
#ifdef DEBUG_UCONTEXT
            s_log(LOG_DEBUG, "Context swap: %ld -> %ld",
                context->id, ready_head->id);
#endif
            swapcontext(&context->context, &ready_head->context);
#ifdef DEBUG_UCONTEXT
            s_log(LOG_DEBUG, "Current context: %ld", ready_head->id);
#endif
        }
        return ready_head->ready;
    } else { /* drop the current context */
#ifdef DEBUG_UCONTEXT
        s_log(LOG_DEBUG, "Context set: %ld (dropped) -> %ld",
            context->id, ready_head->id);
#endif
        setcontext(&ready_head->context);
        ioerror("setcontext"); /* should not ever happen */
        return 0;
    }
}

#else /* USE_UCONTEXT */

int s_poll_wait(s_poll_set *fds, int sec, int msec) {
    int retval;

    if(!fds)
        fatal("NULL fds is only allowed with UCONTEXT threads");
    do { /* skip "Interrupted system call" errors */
        retval=poll(fds->ufds, fds->nfds, sec<0 ? -1 : 1000*sec+msec);
    } while(retval<0 && get_last_socket_error()==S_EINTR);
    if(retval>0)
        check_terminate(fds);
    return retval;
}

#endif /* USE_UCONTEXT */

#else /* select */

s_poll_set *s_poll_alloc(void) {
    /* it needs to be filled with zeros */
    return str_alloc(sizeof(s_poll_set));
}

void s_poll_free(s_poll_set *fds) {
    if(fds) {
        str_free(fds->irfds);
        str_free(fds->orfds);
        str_free(fds->iwfds);
        str_free(fds->owfds);
#ifndef USE_WIN32
        str_free(fds->ixfds);
        str_free(fds->oxfds);
#endif
        str_free(fds);
    }
}

void s_poll_init(s_poll_set *fds, int main_thread) {
    s_poll_realloc(fds, FDS_INITIAL_CAPACITY);
    FD_ZERO(fds->irfds);
    FD_ZERO(fds->iwfds);
#ifndef USE_WIN32
    FD_ZERO(fds->ixfds);
#endif
    fds->max=0; /* no file descriptors */
    fds->main_thread=main_thread;
#ifdef USE_TERMINATE_PIPE
    s_poll_add(fds, main_thread ? signal_pipe[0] : terminate_pipe[0], 1, 0);
#else
    if(main_thread)
        s_poll_add(fds, signal_pipe[0], 1, 0);
#endif
}

void s_poll_add(s_poll_set *fds, SOCKET fd, int rd, int wr) {
#ifdef USE_WIN32
    unsigned max_count=fds->irfds->fd_count > fds->iwfds->fd_count ?
        fds->irfds->fd_count : fds->iwfds->fd_count;

    if(max_count>=fds->capacity)
        s_poll_realloc(fds, max_count+1);
#endif
    if(rd)
        FD_SET(fd, fds->irfds);
    if(wr)
        FD_SET(fd, fds->iwfds);
#ifndef USE_WIN32
    /* expect errors (and the Spanish Inquisition) except for WIN32,
     * which signals tons of non-error events on exceptfds */
    FD_SET(fd, fds->ixfds);
#endif
    if(fd>fds->max)
        fds->max=fd;
}

void s_poll_remove(s_poll_set *fds, SOCKET fd) {
    FD_CLR(fd, fds->irfds);
    FD_CLR(fd, fds->iwfds);
#ifndef USE_WIN32
    FD_CLR(fd, fds->ixfds);
#endif
}

int s_poll_canread(s_poll_set *fds, SOCKET fd) {
#ifdef USE_WIN32
    return FD_ISSET(fd, fds->orfds);
#else
    return FD_ISSET(fd, fds->orfds) || FD_ISSET(fd, fds->oxfds);
#endif
}

int s_poll_canwrite(s_poll_set *fds, SOCKET fd) {
#ifdef USE_WIN32
    return FD_ISSET(fd, fds->owfds);
#else
    return FD_ISSET(fd, fds->owfds) || FD_ISSET(fd, fds->oxfds);
#endif
}

int s_poll_hup(s_poll_set *fds, SOCKET fd) {
    (void)fds; /* squash the unused parameter warning */
    (void)fd; /* squash the unused parameter warning */
    return 0; /* FIXME: how to detect the HUP condition with select()? */
}

int s_poll_rdhup(s_poll_set *fds, SOCKET fd) {
    (void)fds; /* squash the unused parameter warning */
    (void)fd; /* squash the unused parameter warning */
    return 0; /* FIXME: how to detect the RDHUP condition with select()? */
}

int s_poll_err(s_poll_set *fds, SOCKET fd) {
#ifdef USE_WIN32
    (void)fds; /* squash the unused parameter warning */
    (void)fd; /* squash the unused parameter warning */
    return 0;
#else
    return FD_ISSET(fd, fds->oxfds);
#endif
}

#ifdef USE_WIN32
#define FD_SIZE(fds) (offsetof(fd_set, fd_array)+(fds)->capacity*sizeof(SOCKET))
#else
#define FD_SIZE(fds) (sizeof(fd_set))
#endif

int s_poll_wait(s_poll_set *fds, int sec, int msec) {
    int retval;
    struct timeval tv, *tv_ptr;

    if(!fds)
        fatal("NULL fds is only allowed with UCONTEXT threads");
    do { /* skip "Interrupted system call" errors */
        memcpy(fds->orfds, fds->irfds, FD_SIZE(fds));
        memcpy(fds->owfds, fds->iwfds, FD_SIZE(fds));
#ifndef USE_WIN32
        memcpy(fds->oxfds, fds->ixfds, FD_SIZE(fds));
#endif
        if(sec<0) { /* infinite timeout */
            tv_ptr=NULL;
        } else {
            tv.tv_sec=sec;
            tv.tv_usec=1000*msec;
            tv_ptr=&tv;
        }
#ifdef USE_WIN32
        retval=select((int)fds->max+1,
            fds->orfds, fds->owfds, NULL, tv_ptr);
#else
        retval=select((int)fds->max+1,
            fds->orfds, fds->owfds, fds->oxfds, tv_ptr);
#endif
    } while(retval<0 && get_last_socket_error()==S_EINTR);
    if(retval>0)
        check_terminate(fds);
    return retval;
}

NOEXPORT void s_poll_realloc(s_poll_set *fds, unsigned capacity) {
#ifdef USE_WIN32
    fds->capacity=capacity;
#else
    (void)capacity; /* squash the unused parameter warning */
#endif
    fds->irfds=str_realloc(fds->irfds, FD_SIZE(fds));
    fds->orfds=str_realloc(fds->orfds, FD_SIZE(fds));
    fds->iwfds=str_realloc(fds->iwfds, FD_SIZE(fds));
    fds->owfds=str_realloc(fds->owfds, FD_SIZE(fds));
#ifndef USE_WIN32
    fds->ixfds=str_realloc(fds->ixfds, FD_SIZE(fds));
    fds->oxfds=str_realloc(fds->oxfds, FD_SIZE(fds));
#endif
}

void s_poll_dump(s_poll_set *fds, int level) {
    SOCKET fd;
    int ir, or, iw, ow;
#ifndef USE_WIN32
    int ix, ox;
#endif

    for(fd=0; fd<fds->max+1; fd++) {
        ir=FD_ISSET(fd, fds->irfds);
        iw=FD_ISSET(fd, fds->iwfds);
#ifndef USE_WIN32
        ix=FD_ISSET(fd, fds->ixfds);
#endif
        or=FD_ISSET(fd, fds->orfds);
        ow=FD_ISSET(fd, fds->owfds);
#ifndef USE_WIN32
        ox=FD_ISSET(fd, fds->oxfds);
        if(ir || iw || ix || or || ow || ox)
            s_log(level, "FD=%ld ifds=%c%c%c ofds=%c%c%c", (long)fd,
                ir?'r':'-', iw?'w':'-', ix?'x':'-',
                or?'r':'-', ow?'w':'-', ox?'x':'-');
#else
        if(ir || iw || or || ow)
            s_log(level, "FD=%ld ifds=%c%c ofds=%c%c", (long)fd,
                ir?'r':'-', iw?'w':'-', or?'r':'-', ow?'w':'-');
#endif
    }
}

#endif /* USE_POLL */

void s_poll_sleep(int sec, int msec) {
#ifdef USE_WIN32
    Sleep(1000*(DWORD)sec+(DWORD)msec);
#else
    s_poll_set *fds=s_poll_alloc();
    s_poll_init(fds, 0);
    s_poll_wait(fds, sec, msec);
    s_poll_free(fds);
#endif
}

#ifndef USE_UCONTEXT
NOEXPORT void check_terminate(s_poll_set *fds) {
#ifdef USE_TERMINATE_PIPE
    if(!fds->main_thread && s_poll_canread(fds, terminate_pipe[0])) {
#ifdef USE_PTHREAD
        pthread_exit(NULL);
#endif /* USE_PTHREAD */
#if defined(USE_WIN32) || defined(USE_OS2)
#if defined(_WIN32_WCE)
        /* FIXME */
#else /* !_WIN32_WCE */
        _endthreadex(0);
#endif /* _WIN32_WCE */
#endif /* USE_WIN32 || USE_OS2 */
#ifdef USE_UCONTEXT
        /* currently unused */
        s_poll_wait(NULL, 0, 0); /* wait on poll() */
#endif /* USE_UCONTEXT */
#ifdef USE_FORK
        exit(0);
#endif /* USE_FORK */
    }
#else /* USE_TERMINATE_PIPE */
    (void)fds; /* squash the unused parameter warning */
#endif /* USE_TERMINATE_PIPE */
}
#endif

/**************************************** fd management */

int socket_options_set(SERVICE_OPTIONS *service, SOCKET s, int type) {
    SOCK_OPT *ptr;
    static const char *type_str[3]={"accept", "local", "remote"};
    socklen_t opt_size;
    int retval=0; /* no error found */

    s_log(LOG_DEBUG, "Setting %s socket options (FD=%ld)",
        type_str[type], (long)s);
    for(ptr=service->sock_opts; ptr->opt_str; ptr++) {
        OPT_UNION **opt_val=service->sock_type==SOCK_DGRAM ?
            ptr->opt_val_udp : ptr->opt_val_tcp;
        if(!opt_val[type])
            continue; /* default */
        switch(ptr->opt_type) {
        case TYPE_LINGER:
            opt_size=sizeof(struct linger);
            break;
        case TYPE_TIMEVAL:
            opt_size=sizeof(struct timeval);
            break;
        case TYPE_STRING:
            opt_size=(socklen_t)strlen(opt_val[type]->c_val)+1;
            break;
        default:
            opt_size=sizeof(int);
        }
        if(setsockopt(s, ptr->opt_level, ptr->opt_name,
                (void *)opt_val[type], opt_size)) {
            if(get_last_socket_error()==S_EOPNOTSUPP) {
                /* most likely stdin/stdout or AF_UNIX socket */
                s_log(LOG_DEBUG,
                    "Option %s not supported on %s socket",
                    ptr->opt_str, type_str[type]);
            } else {
                sockerror(ptr->opt_str);
                retval=-1; /* failed to set this option */
            }
        }
        else {
            s_log(LOG_DEBUG, "Option %s set on %s socket",
                ptr->opt_str, type_str[type]);
        }
    }
    return retval; /* returns 0 when all options succeeded */
}

int get_socket_error(const SOCKET fd) {
    int err;
    socklen_t optlen=sizeof err;

    if(getsockopt(fd, SOL_SOCKET, SO_ERROR, (void *)&err, &optlen))
        err=get_last_socket_error(); /* failed -> ask why */
    return err==S_ENOTSOCK ? 0 : err;
}

/**************************************** simulate blocking I/O */

int s_connect(CLI *c, SOCKADDR_UNION *addr, socklen_t addrlen, int timeout) {
    int error;
    char *dst;

    dst=s_ntop(addr, addrlen);
    s_log(LOG_INFO, "s_connect: connecting %s", dst);

    if(!connect(c->fd, &addr->sa, addrlen)) {
        s_log(LOG_INFO, "s_connect: connected %s", dst);
        str_free(dst);
        return 0; /* no error -> success (on some OSes over the loopback) */
    }
    error=get_last_socket_error();
    if(error!=S_EINPROGRESS && error!=S_EWOULDBLOCK) {
        s_log(LOG_ERR, "s_connect: connect %s: %s (%d)",
            dst, s_strerror(error), error);
        str_free(dst);
        return -1;
    }

    s_log(LOG_DEBUG, "s_connect: s_poll_wait %s: waiting %d seconds",
        dst, timeout);
    s_poll_init(c->fds, 0);
    s_poll_add(c->fds, c->fd, 1, 1);
    s_poll_dump(c->fds, LOG_DEBUG);
    switch(s_poll_wait(c->fds, timeout, 0)) {
    case -1:
        error=get_last_socket_error();
        s_log(LOG_ERR, "s_connect: s_poll_wait %s: %s (%d)",
            dst, s_strerror(error), error);
        str_free(dst);
        return -1;
    case 0:
        s_log(LOG_ERR, "s_connect: s_poll_wait %s:"
            " TIMEOUTconnect exceeded", dst);
        str_free(dst);
        return -1;
    default:
        error=get_socket_error(c->fd);
        if(error) {
            s_log(LOG_ERR, "s_connect: connect %s: %s (%d)",
               dst, s_strerror(error), error);
            str_free(dst);
            return -1;
        }
        if(s_poll_canwrite(c->fds, c->fd)) {
            s_log(LOG_NOTICE, "s_connect: connected %s", dst);
            str_free(dst);
            return 0; /* success */
        }
        s_log(LOG_ERR, "s_connect: s_poll_wait %s: internal error",
            dst);
        str_free(dst);
        return -1;
    }
}

void s_write(CLI *c, SOCKET fd, const void *buf, size_t len) {
        /* simulate a blocking write */
    const uint8_t *ptr=(const uint8_t *)buf;

    while(len>0) {
        ssize_t num;

        s_poll_init(c->fds, 0);
        s_poll_add(c->fds, fd, 0, 1); /* write */
        switch(s_poll_wait(c->fds, c->opt->timeout_busy, 0)) {
        case -1:
            sockerror("s_write: s_poll_wait");
            throw_exception(c, 1); /* error */
        case 0:
            s_log(LOG_INFO, "s_write: s_poll_wait:"
                " TIMEOUTbusy exceeded: sending reset");
            throw_exception(c, 1); /* timeout */
        case 1:
            break; /* OK */
        default:
            s_log(LOG_ERR, "s_write: s_poll_wait: unknown result");
            throw_exception(c, 1); /* error */
        }

        num=writesocket(fd, (const void *)ptr, len);
        if(num>=0) {
            ptr+=(size_t)num;
            len-=(size_t)num;
        } else { /* error */
            if(!socket_needs_retry(c, "s_write: writesocket"))
                throw_exception(c, 1);
        }
    }
}

size_t s_read_eof(CLI *c, SOCKET fd, void *ptr, size_t len) {
        /* simulate a blocking read */
        /* return a value < len on EOF */
    size_t total=0;

    while(len>0) {
        ssize_t num;

        s_poll_init(c->fds, 0);
        s_poll_add(c->fds, fd, 1, 0); /* read */
        switch(s_poll_wait(c->fds, c->opt->timeout_busy, 0)) {
        case -1:
            sockerror("s_read_eof: s_poll_wait");
            throw_exception(c, 1); /* error */
        case 0:
            s_log(LOG_INFO, "s_read_eof: s_poll_wait:"
                " TIMEOUTbusy exceeded: sending reset");
            throw_exception(c, 1); /* timeout */
        case 1:
            break; /* OK */
        default:
            s_log(LOG_ERR, "s_read_eof: s_poll_wait: unknown result");
            throw_exception(c, 1); /* error */
        }

        num=readsocket(fd, (char *)ptr+total, len);
        if(num>0) {
            total+=(size_t)num;
            len-=(size_t)num;
        } else if(num==0) { /* EOF */
            s_log(LOG_DEBUG, "s_read_eof: EOF");
            break; /* EOF */
        } else { /* error */
            if(!socket_needs_retry(c, "s_read_eof: readsocket"))
                break; /* EOF */
        }
    }
    return total;
}

void s_read(CLI *c, SOCKET fd, void *ptr, size_t len) {
        /* simulate a blocking read */
        /* throw an exception on EOF */
    size_t received=s_read_eof(c, fd, ptr, len);
    if(received!=len) {
        s_log(LOG_ERR, "s_read: Received %llu out of requested %llu byte(s)",
            (unsigned long long)received, (unsigned long long)len);
        throw_exception(c, 1);
    }
}

void fd_putline(CLI *c, SOCKET fd, const char *line) {
    char *tmpline;
    const char crlf[]="\r\n";
    size_t len;

    tmpline=str_printf("%s%s", line, crlf);
    len=strlen(tmpline);
    s_write(c, fd, tmpline, len);
    str_free(tmpline);
    s_log(LOG_DEBUG, " -> %s", line);
}

char *fd_getline(CLI *c, SOCKET fd) {
    char *line;
    size_t ptr=0, allocated=32;

    line=str_alloc(allocated);
    for(;;) {
        if(ptr>65536) { /* >64KB --> DoS protection */
            s_log(LOG_ERR, "fd_getline: Line too long");
            str_free(line);
            throw_exception(c, 1);
        }
        if(allocated<ptr+1) {
            allocated*=2;
            line=str_realloc(line, allocated);
        }
        s_read(c, fd, line+ptr, 1);
        if(line[ptr]=='\r')
            continue;
        if(line[ptr]=='\n')
            break;
        if(line[ptr]=='\0')
            break;
        ++ptr;
    }
    line[ptr]='\0';
    s_log(LOG_DEBUG, " <- %s", line);
    return line;
}

void fd_printf(CLI *c, SOCKET fd, const char *format, ...) {
    va_list ap;
    char *line;

    va_start(ap, format);
    line=str_vprintf(format, ap);
    va_end(ap);
    if(!line) {
        s_log(LOG_ERR, "fd_printf: str_vprintf failed");
        throw_exception(c, 1);
    }
    fd_putline(c, fd, line);
    str_free(line);
}

void s_ssl_write(CLI *c, const void *buf, int len) {
        /* simulate a blocking SSL_write */
    const uint8_t *ptr=(const uint8_t *)buf;

    while(len>0) {
        int num, err;

        s_poll_init(c->fds, 0);
        s_poll_add(c->fds, c->ssl_wfd->fd, 0, 1); /* write */
        switch(s_poll_wait(c->fds, c->opt->timeout_busy, 0)) {
        case -1:
            sockerror("s_ssl_write: s_poll_wait");
            throw_exception(c, 1); /* error */
        case 0:
            s_log(LOG_INFO, "s_ssl_write: s_poll_wait:"
                " TIMEOUTbusy exceeded: sending reset");
            throw_exception(c, 1); /* timeout */
        case 1:
            break; /* OK */
        default:
            s_log(LOG_ERR, "s_ssl_write: s_poll_wait: unknown result");
            throw_exception(c, 1); /* error */
        }

        num=SSL_write(c->ssl, (const void *)ptr, len);
        err=SSL_get_error(c->ssl, num);
        if(err==SSL_ERROR_NONE) {
            ptr+=num;
            len-=num;
        } else if(err==SSL_ERROR_WANT_WRITE) {
            s_log(LOG_DEBUG, "s_ssl_write: SSL_ERROR_WANT_WRITE: Retrying");
        } else if(err==SSL_ERROR_SSL) {
            ssl_error(c, "s_ssl_write: SSL_write");
            throw_exception(c, 1);
        } else if(err==SSL_ERROR_SYSCALL) {
            if(!socket_needs_retry(c, "s_ssl_write: SSL_write")) {
                SSL_set_shutdown(c->ssl,
                    SSL_SENT_SHUTDOWN|SSL_RECEIVED_SHUTDOWN);
                break; /* EOF */
            }
        } else {
            s_log(LOG_ERR, "s_ssl_write: Unhandled error %d", err);
            throw_exception(c, 1);
        }
    }
}

size_t s_ssl_read_eof(CLI *c, void *ptr, int len) {
        /* simulate a blocking SSL_read */
        /* return a value < len on EOF */
    size_t total=0;

    while(len>0) {
        int num, err;

        if(!SSL_pending(c->ssl)) {
            s_poll_init(c->fds, 0);
            s_poll_add(c->fds, c->ssl_rfd->fd, 1, 0); /* read */
            switch(s_poll_wait(c->fds, c->opt->timeout_busy, 0)) {
            case -1:
                sockerror("s_ssl_read_eof: s_poll_wait");
                throw_exception(c, 1); /* error */
            case 0:
                s_log(LOG_INFO, "s_ssl_read_eof: s_poll_wait:"
                    " TIMEOUTbusy exceeded: sending reset");
                throw_exception(c, 1); /* timeout */
            case 1:
                break; /* OK */
            default:
                s_log(LOG_ERR, "s_ssl_read_eof: s_poll_wait: unknown result");
                throw_exception(c, 1); /* error */
            }
        }

        num=SSL_read(c->ssl, (char *)ptr+total, len);
        err=SSL_get_error(c->ssl, num);
        if(err==SSL_ERROR_NONE) {
            total+=(size_t)num;
            len-=num;
        } else if(err==SSL_ERROR_ZERO_RETURN) {
            s_log(LOG_DEBUG, "s_ssl_read_eof: close_notify");
            break; /* EOF */
        } else if(err==SSL_ERROR_WANT_READ) {
            s_log(LOG_DEBUG, "s_ssl_read_eof: SSL_ERROR_WANT_READ: Retrying");
        } else if(err==SSL_ERROR_SSL) {
#ifdef SSL_R_UNEXPECTED_EOF_WHILE_READING
            /* OpenSSL 3.0 changed the method of reporting socket EOF */
            if(ERR_GET_REASON(ERR_peek_error())==
                    SSL_R_UNEXPECTED_EOF_WHILE_READING) {
                /* EOF -> buggy (e.g. Microsoft) peer:
                 * TLS socket closed without close_notify alert */
                s_log(LOG_DEBUG, "s_ssl_read_eof: TLS socket closed");
                SSL_set_shutdown(c->ssl,
                    SSL_SENT_SHUTDOWN|SSL_RECEIVED_SHUTDOWN);
                break; /* EOF */
            }
#endif /* SSL_R_UNEXPECTED_EOF_WHILE_READING */
            ssl_error(c, "s_ssl_read_eof: SSL_read");
            throw_exception(c, 1);
        } else if(err==SSL_ERROR_SYSCALL) {
            if(!socket_needs_retry(c, "s_ssl_read_eof: SSL_read")) {
                SSL_set_shutdown(c->ssl,
                    SSL_SENT_SHUTDOWN|SSL_RECEIVED_SHUTDOWN);
                break; /* EOF */
            }
        } else {
            s_log(LOG_ERR, "s_ssl_read_eof: Unhandled error %d", err);
            throw_exception(c, 1);
        }
    }
    return total;
}

void s_ssl_read(CLI *c, void *ptr, int len) {
        /* simulate a blocking SSL_read */
        /* throw an exception on EOF */
    size_t received=s_ssl_read_eof(c, ptr, len);
    if(received!=(size_t)len) {
        s_log(LOG_ERR, "s_ssl_read: Received %llu out of requested %d byte(s)",
            (unsigned long long)received, len);
        throw_exception(c, 1);
    }
}

char *ssl_getstring(CLI *c) { /* get null-terminated string */
    char *line;
    size_t ptr=0, allocated=32;

    line=str_alloc(allocated);
    for(;;) {
        if(ptr>65536) { /* >64KB --> DoS protection */
            s_log(LOG_ERR, "ssl_getstring: Line too long");
            str_free(line);
            throw_exception(c, 1);
        }
        if(allocated<ptr+1) {
            allocated*=2;
            line=str_realloc(line, allocated);
        }
        s_ssl_read(c, line+ptr, 1);
        if(line[ptr]=='\0')
            break;
        ++ptr;
    }
    return line;
}

char *ssl_getline(CLI *c) { /* get newline-terminated string */
    char *line;
    size_t ptr=0, allocated=32;

    line=str_alloc(allocated);
    for(;;) {
        if(ptr>65536) { /* >64KB --> DoS protection */
            s_log(LOG_ERR, "ssl_getline: Line too long");
            str_free(line);
            throw_exception(c, 1);
        }
        if(allocated<ptr+1) {
            allocated*=2;
            line=str_realloc(line, allocated);
        }
        s_ssl_read(c, line+ptr, 1);
        if(line[ptr]=='\r')
            continue;
        if(line[ptr]=='\n')
            break;
        if(line[ptr]=='\0')
            break;
        ++ptr;
    }
    line[ptr]='\0';
    s_log(LOG_DEBUG, " <- %s", line);
    return line;
}

void ssl_putline(CLI *c, const char *line) { /* put newline-terminated string */
    char *tmpline;
    const char crlf[]="\r\n";
    size_t len;

    tmpline=str_printf("%s%s", line, crlf);
    len=strlen(tmpline);
    if(len>INT_MAX) { /* paranoia */
        s_log(LOG_ERR, "ssl_putline: Line too long");
        str_free(tmpline);
        throw_exception(c, 1);
    }
    s_ssl_write(c, tmpline, (int)len);
    str_free(tmpline);
    s_log(LOG_DEBUG, " -> %s", line);
}

void ssl_printf(CLI *c, const char *format, ...) {
    va_list ap;
    char *line;

    va_start(ap, format);
    line=str_vprintf(format, ap);
    va_end(ap);
    if(!line) {
        s_log(LOG_ERR, "ssl_printf: str_vprintf failed");
        throw_exception(c, 1);
    }
    ssl_putline(c, line);
    str_free(line);
}

/**************************************** network helpers */

#define INET_SOCKET_PAIR

int make_sockets(SOCKET fd[2], int sock_type) { /* make a pair of connected sockets */
#ifdef INET_SOCKET_PAIR
    struct sockaddr_in addr;
    socklen_t addrlen;
    SOCKET s; /* temporary socket awaiting for connection */

    /* create two *blocking* sockets first */
    s=s_socket(AF_INET, sock_type, 0, 0, "make_sockets: s_socket#1");
    if(s==INVALID_SOCKET)
        return 1;
    fd[1]=s_socket(AF_INET, sock_type, 0, 0, "make_sockets: s_socket#2");
    if(fd[1]==INVALID_SOCKET) {
        closesocket(s);
        return 1;
    }

    addrlen=sizeof addr;
    memset(&addr, 0, sizeof addr);
    addr.sin_family=AF_INET;
    addr.sin_addr.s_addr=htonl(INADDR_LOOPBACK);
    addr.sin_port=htons(0); /* dynamic port allocation */
    if(bind(s, (struct sockaddr *)&addr, addrlen))
        log_error(LOG_DEBUG, get_last_socket_error(), "make_sockets: bind#1");
    if(bind(fd[1], (struct sockaddr *)&addr, addrlen))
        log_error(LOG_DEBUG, get_last_socket_error(), "make_sockets: bind#2");

    /* TCP needs listen/accept; UDP just cross-connects */
    if(sock_type==SOCK_STREAM) {
        if(listen(s, 1)) {
            sockerror("make_sockets: listen");
            closesocket(s);
            closesocket(fd[1]);
            return 1;
        }
    }

    if(getsockname(s, (struct sockaddr *)&addr, &addrlen)) {
        sockerror("make_sockets: getsockname");
        closesocket(s);
        closesocket(fd[1]);
        return 1;
    }
    if(connect(fd[1], (struct sockaddr *)&addr, addrlen)) {
        sockerror("make_sockets: connect");
        closesocket(s);
        closesocket(fd[1]);
        return 1;
    }

    if(sock_type==SOCK_STREAM) {
        fd[0]=s_accept(s, (struct sockaddr *)&addr, &addrlen, 1,
            "make_sockets: s_accept");
        if(fd[0]==INVALID_SOCKET) {
            closesocket(s);
            closesocket(fd[1]);
            return 1;
        }
        closesocket(s); /* don't care about the result */
    } else { /* UDP: connect s back to fd[1] for bidirectional pair */
        addrlen=sizeof addr;
        if(getsockname(fd[1], (struct sockaddr *)&addr, &addrlen)) {
            sockerror("make_sockets: getsockname#2");
            closesocket(s);
            closesocket(fd[1]);
            return 1;
        }
        if(connect(s, (struct sockaddr *)&addr, addrlen)) {
            sockerror("make_sockets: connect#2");
            closesocket(s);
            closesocket(fd[1]);
            return 1;
        }
        fd[0]=s;
    }
    set_nonblock(fd[0], 1);
    set_nonblock(fd[1], 1);
#else
    if(sock_type==SOCK_STREAM) {
        if(s_socketpair(AF_UNIX, SOCK_STREAM, 0, fd, 1,
                "make_sockets: socketpair"))
            return 1;
    } else {
        /* prefer SOCK_SEQPACKET for reliable datagram delivery */
#ifdef SOCK_SEQPACKET
        if(!s_socketpair(AF_UNIX, SOCK_SEQPACKET, 0, fd, 1,
                "make_sockets: socketpair(SOCK_SEQPACKET)"))
            return 0;
#endif
        if(s_socketpair(AF_UNIX, SOCK_DGRAM, 0, fd, 1,
                "make_sockets: socketpair(SOCK_DGRAM)"))
            return 1;
    }
#endif
    return 0;
}

/* returns 0 on success, and -1 on error */
int original_dst(const SOCKET fd, SOCKADDR_UNION *addr) {
    socklen_t addrlen;

    memset(addr, 0, sizeof(SOCKADDR_UNION));
    addrlen=sizeof(SOCKADDR_UNION);
#ifdef SO_ORIGINAL_DST
#ifdef USE_IPV6
    if(!getsockopt(fd, SOL_IPV6, SO_ORIGINAL_DST, (char *)&addr->sa, &addrlen))
        return 0; /* succeeded */
#endif /* USE_IPV6 */
    if(!getsockopt(fd, SOL_IP, SO_ORIGINAL_DST, (char *)&addr->sa, &addrlen))
        return 0; /* succeeded */
    sockerror("getsockopt SO_ORIGINAL_DST");
#else /* SO_ORIGINAL_DST */
    if(!getsockname(fd, &addr->sa, &addrlen))
        return 0; /* succeeded */
    sockerror("getsockname");
#endif /* SO_ORIGINAL_DST */
    return -1; /* failed */
}

    /* returns 0 on close and 1 on non-critical errors */
int socket_needs_retry(CLI *c, const char *text) {
    switch(get_last_socket_error()) {
        /* http://tangentsoft.net/wskfaq/articles/bsd-compatibility.html */
    case 0: /* close on read, or close on write on WIN32 */
        /* fall through */
#ifndef USE_WIN32
    case EPIPE: /* close on write on Unix */
        /* fall through */
#endif
    case S_ECONNABORTED:
        s_log(LOG_INFO, "%s: Socket is closed", text);
        return 0;
    case S_EINTR:
        s_log(LOG_DEBUG, "%s: Interrupted by a signal: retrying", text);
        return 1;
    case S_EWOULDBLOCK:
        s_log(LOG_NOTICE, "%s: Would block: retrying", text);
        s_poll_sleep(1, 0); /* Microsoft bug KB177346 */
        return 1;
#if S_EAGAIN!=S_EWOULDBLOCK
    case S_EAGAIN:
        s_log(LOG_DEBUG,
            "%s: Temporary lack of resources: retrying", text);
        return 1;
#endif
#ifdef USE_WIN32
    case S_ECONNRESET:
        /* dying "exec" processes on Win32 cause reset instead of close */
        if(c->opt->exec_name) {
            s_log(LOG_INFO, "%s: Socket is closed (exec)", text);
            return 0;
        }
#endif
        /* fall through */
    default:
        sockerror(text);
        throw_exception(c, 1);
        return -1; /* some C compilers require a return value */
    }
}

#ifdef USE_DTLS
#if OPENSSL_VERSION_NUMBER>=0x10100000L
/* copy an OpenSSL datagram peer address to the socket representation */
int bio_addr_to_sockaddr(const BIO_ADDR *src, SOCKADDR_UNION *dst) {
    u_short family=(u_short)BIO_ADDR_family(src);
    size_t len;

    memset(dst, 0, sizeof *dst);
    switch(family) {
        case AF_INET:
            dst->in.sin_family=AF_INET;
            dst->in.sin_port=BIO_ADDR_rawport(src);
            len=sizeof dst->in.sin_addr;
            if(!BIO_ADDR_rawaddress(src, &dst->in.sin_addr, &len) ||
                    len!=sizeof dst->in.sin_addr)
                return 1;
            return 0;
#ifdef USE_IPV6
        case AF_INET6:
            dst->in6.sin6_family=AF_INET6;
            dst->in6.sin6_port=BIO_ADDR_rawport(src);
            len=sizeof dst->in6.sin6_addr;
            if(!BIO_ADDR_rawaddress(src, &dst->in6.sin6_addr, &len) ||
                    len!=sizeof dst->in6.sin6_addr)
                return 1;
            return 0;
#endif
        default:
            s_log(LOG_ERR, "DTLS: unsupported peer address family: %d",
                family);
            return 1;
    }
}
#endif /* OpenSSL version >= 1.1.0 */

/* validate DTLS cookie and pre-accept the ClientHello */
int dtls_listen(CLI *c, SOCKET fd) {
    SSL *new_ssl;
    BIO *bio;
#if OPENSSL_VERSION_NUMBER>=0x10100000L
    BIO_ADDR *peer;
    int dtls_ret;
#else /* OPENSSL_VERSION_NUMBER>=0x10100000L */
    SOCKADDR_UNION peer;
    long dtls_ret;
#endif /* OPENSSL_VERSION_NUMBER>=0x10100000L */
    char buf[BUFFSIZE];
    ssize_t len;
    int err;

    c->ssl=NULL;
#if OPENSSL_VERSION_NUMBER>=0x10100000L
    c->udp_preload_bio=NULL;
#endif
    c->peer_addr_len=sizeof c->peer_addr;
    /* The cookie callback needs the peer address.  Peek first, then let
     * DTLSv1_listen() consume the same datagram from the socket BIO.
     * WinSock returns WSAEMSGSIZE if the buffer is shorter than the
     * UDP datagram, even with MSG_PEEK. */
#ifdef MSG_TRUNC
    len=recvfrom(fd, buf, sizeof buf, MSG_PEEK|MSG_TRUNC,
        &c->peer_addr.sa, &c->peer_addr_len);
#else
    len=recvfrom(fd, buf, sizeof buf, MSG_PEEK,
        &c->peer_addr.sa, &c->peer_addr_len);
#endif
    if(len<0) {
        switch(get_last_socket_error()) {
            case S_EINTR:
            case S_EWOULDBLOCK:
                break;
#ifdef S_EMSGSIZE
            case S_EMSGSIZE:
                c->peer_addr_len=sizeof c->peer_addr;
                (void)recvfrom(fd, buf, sizeof buf, 0,
                    &c->peer_addr.sa, &c->peer_addr_len);
                s_log(LOG_WARNING,
                    "DTLS: oversized datagram discarded");
                break;
#endif
            default:
                sockerror("DTLSv1_listen: recvfrom");
        }
        return 0;
    }
#ifdef MSG_TRUNC
    if(len>BUFFSIZE) {
#else
    if(len==BUFFSIZE) {
#endif
        c->peer_addr_len=sizeof c->peer_addr;
#ifdef MSG_TRUNC
        (void)recvfrom(fd, buf, sizeof buf, MSG_TRUNC,
            &c->peer_addr.sa, &c->peer_addr_len);
#else
        (void)recvfrom(fd, buf, sizeof buf, 0,
            &c->peer_addr.sa, &c->peer_addr_len);
#endif
        s_log(LOG_WARNING,
            "DTLS: datagram larger than BUFFSIZE"
            " (%d bytes); discarding", BUFFSIZE);
        return 0;
    }

    new_ssl=SSL_new(c->opt->ctx);
    if(!new_ssl) {
        ssl_error(NULL, "DTLSv1_listen: SSL_new");
        return 0;
    }
    if(!SSL_set_ex_data(new_ssl, index_ssl_cli, c)) {
        ssl_error(NULL, "DTLSv1_listen: SSL_set_ex_data");
        SSL_free(new_ssl);
        return 0;
    }
    bio=BIO_new_dgram((int)fd, BIO_NOCLOSE);
    if(!bio) {
        ssl_error(NULL, "DTLSv1_listen: BIO_new_dgram");
        SSL_free(new_ssl);
        return 0;
    }
    SSL_set_bio(new_ssl, bio, bio);
    SSL_set_accept_state(new_ssl);

#if OPENSSL_VERSION_NUMBER>=0x10100000L
    peer=BIO_ADDR_new();
    if(!peer) {
        s_log(LOG_ERR, "DTLSv1_listen: BIO_ADDR_new() failed");
        SSL_free(new_ssl);
        return 0;
    }
    dtls_ret=DTLSv1_listen(new_ssl, peer);
    if(dtls_ret>0 && bio_addr_to_sockaddr(peer, &c->peer_addr)) {
        s_log(LOG_ERR, "DTLSv1_listen: invalid peer address");
        dtls_ret=0;
    }
    BIO_ADDR_free(peer);
#else /* OPENSSL_VERSION_NUMBER>=0x10100000L */
    memset(&peer, 0, sizeof peer);
    dtls_ret=DTLSv1_listen(new_ssl, &peer.sa);
    if(dtls_ret>0)
        memcpy(&c->peer_addr, &peer, sizeof peer);
#endif /* OPENSSL_VERSION_NUMBER>=0x10100000L */

    if(dtls_ret>0) {
        c->peer_addr_len=addr_len(&c->peer_addr);
        s_log(LOG_DEBUG, "DTLS: ClientHello accepted on listen socket");
        c->ssl=new_ssl;
        return 1;
    }
    if(dtls_ret<0) {
        err=SSL_get_error(new_ssl, (int)dtls_ret);
        if(err!=SSL_ERROR_WANT_READ && err!=SSL_ERROR_WANT_WRITE)
            ssl_error(NULL, "DTLSv1_listen");
        else
            ERR_clear_error();
    } else {
        s_log(LOG_DEBUG,
            "DTLS: HelloVerifyRequest sent or ClientHello discarded");
        ERR_clear_error();
    }
    SSL_free(new_ssl);
    return 0;
}

/* Drain same-peer datagrams from the listen socket.  Called after
 * the first datagram was consumed (by dtls_listen() in server mode
 * or accept_udp_peer() in client mode) to prevent duplicate sessions
 * for the same 4-tuple.  Must be synchronous in the main thread:
 * the accept loop polls the listen fd; leftover datagrams would be
 * consumed as bogus new clients.
 *
 * Server mode: the first ClientHello fragment is already in the SSL
 * handshake state.  Drained fragments are DTLS handshake data queued
 * in udp_preload_bio for consumption by set_preload_bio().
 * Client mode: the first plaintext datagram is in c->sock_ptr.
 * Drained datagrams are plaintext payload queued in udp_preload_bio
 * for consumption by transfer_udp().
 *
 * When USE_DTLS_DGRAM_BIO is available (OpenSSL >= 3.2): datagrams
 * are queued in c->udp_preload_bio (dgram BIO preserves boundaries).
 * On older OpenSSL: extra datagrams are dropped (the first fragment
 * is preserved).  Cap at 32 to bound main-thread blocking.
 *
 * Limitation: interleaved bursts from multiple peers (A, B, A) can
 * leave later same-peer datagrams on the listener and create a
 * duplicate session.  This race is accepted as unlikely in normal
 * DTLS traffic, where same-peer retransmits tend to be adjacent. */
int drain_udp_datagrams(CLI *c, SOCKET fd) {
    SOCKADDR_UNION peer;
    socklen_t peer_len;
    char buf[BUFFSIZE];
    ssize_t len, max_len;
    unsigned count=0, dropped=0;

    max_len=c->opt->option.client ? SSL3_RT_MAX_PLAIN_LENGTH : BUFFSIZE;
    for(;;) {
        if(count>=32) {
            s_log(LOG_WARNING,
                "UDP drain: limit reached, %u datagrams",
                count);
            break;
        }
        peer_len=sizeof peer;
#ifdef MSG_TRUNC
        len=recvfrom(fd, buf, sizeof buf, MSG_PEEK|MSG_TRUNC,
            &peer.sa, &peer_len);
#else
        len=recvfrom(fd, buf, sizeof buf, MSG_PEEK, &peer.sa, &peer_len);
#endif
        if(len<0) {
            switch(get_last_socket_error()) {
                case S_EINTR:
                    continue;
                case S_EWOULDBLOCK:
#if S_EAGAIN!=S_EWOULDBLOCK
                case S_EAGAIN:
#endif
                    break;
#ifdef S_EMSGSIZE
                case S_EMSGSIZE:
                    peer_len=sizeof peer;
                    len=recvfrom(fd, buf, sizeof buf, 0,
                        &peer.sa, &peer_len);
                    if(len<0 && get_last_socket_error()!=S_EMSGSIZE) {
                        sockerror("UDP drain: recvfrom oversized");
                        goto fail;
                    }
                    ++count;
                    ++dropped;
                    continue;
#endif
                default:
                    sockerror("UDP drain: recvfrom(MSG_PEEK)");
                    goto fail;
            }
            break;
        }
        if(peer_len!=c->peer_addr_len ||
                memcmp(&peer.sa, &c->peer_addr.sa, (size_t)peer_len))
            break;

#ifdef MSG_TRUNC
        if(len>max_len) {
#else
        if(len==BUFFSIZE || len>max_len) {
#endif
            peer_len=sizeof peer;
#ifdef MSG_TRUNC
            len=recvfrom(fd, buf, sizeof buf, MSG_TRUNC,
                &peer.sa, &peer_len);
#else
            len=recvfrom(fd, buf, sizeof buf, 0, &peer.sa, &peer_len);
#endif
            if(len<0) {
#ifdef S_EMSGSIZE
                if(get_last_socket_error()!=S_EMSGSIZE) {
                    sockerror("UDP drain: recvfrom oversized");
                    goto fail;
                }
#else
                sockerror("UDP drain: recvfrom oversized");
                goto fail;
#endif
            } else if(peer_len!=c->peer_addr_len ||
                    memcmp(&peer.sa, &c->peer_addr.sa, (size_t)peer_len)) {
                break;
            }
            ++count;
            ++dropped;
            continue;
        }

        peer_len=sizeof peer;
        len=recvfrom(fd, buf, sizeof buf, 0, &peer.sa, &peer_len);
        if(len<0) {
            switch(get_last_socket_error()) {
                case S_EINTR:
                    continue;
                case S_EWOULDBLOCK:
#if S_EAGAIN!=S_EWOULDBLOCK
                case S_EAGAIN:
#endif
                    break;
#ifdef S_EMSGSIZE
                case S_EMSGSIZE:
                    ++count;
                    ++dropped;
                    continue;
#endif
                default:
                    sockerror("UDP drain: recvfrom");
                    goto fail;
            }
            break;
        }
        if(peer_len!=c->peer_addr_len ||
                memcmp(&peer.sa, &c->peer_addr.sa, (size_t)peer_len))
            break;
        ++count;
#ifdef USE_DTLS_DGRAM_BIO
        if(len) {
            if(!c->udp_preload_bio) {
                c->udp_preload_bio=BIO_new(BIO_s_dgram_mem());
                if(!c->udp_preload_bio) {
                    ssl_error(NULL, "UDP drain: BIO_new");
                    goto fail;
                }
            }
            if(BIO_write(c->udp_preload_bio, buf, (int)len)!=(int)len) {
                ssl_error(NULL, "UDP drain: BIO_write");
                goto fail;
            }
        }
#else
        (void)len; /* squash the unused variable warning */
#endif /* USE_DTLS_DGRAM_BIO */
    }
#ifdef USE_DTLS_DGRAM_BIO
    if(count>dropped)
        s_log(LOG_DEBUG, "UDP drain: queued %u datagram(s)", count-dropped);
#else
    if(count>dropped)
        s_log(LOG_WARNING,
            "UDP drain: dropped %u datagram(s) from same peer"
            " (reduced functionality on this OpenSSL version)", count-dropped);
#endif /* USE_DTLS_DGRAM_BIO */
    if(dropped)
        s_log(LOG_WARNING,
            "UDP drain: discarded %u oversized datagram(s)", dropped);
    return 0;

fail:
    BIO_free(c->udp_preload_bio);
    c->udp_preload_bio=NULL;
    return 1;
}

/* pseudo-blocking DTLS accept with cookie exchange */
void dtls_accept(CLI *c) {
#if OPENSSL_VERSION_NUMBER>=0x10100000L
    BIO_ADDR *peer;
    int dtls_ret;
#else /* OPENSSL_VERSION_NUMBER>=0x10100000L */
    SOCKADDR_UNION peer;
    long dtls_ret;
#endif /* OPENSSL_VERSION_NUMBER>=0x10100000L */

#if OPENSSL_VERSION_NUMBER>=0x10100000L
    peer=BIO_ADDR_new();
    if(!peer) {
        s_log(LOG_ERR, "DTLS: BIO_ADDR_new() failed");
        throw_exception(c, 1);
    }
    s_log(LOG_DEBUG, "DTLS: waiting for ClientHello");
    while(1) {
        int ret;

        dtls_ret=DTLSv1_listen(c->ssl, peer);
        if(dtls_ret>0)
            break;
        if(dtls_ret<0) {
            BIO_ADDR_free(peer);
            ssl_error(c, "DTLSv1_listen");
            throw_exception(c, 1);
        }

        s_poll_init(c->fds, 0);
        s_poll_add(c->fds, c->ssl_rfd->fd, 1, 0);
        ret=s_poll_wait(c->fds, c->opt->timeout_busy, 0);
        if(ret<0) {
            sockerror("DTLSv1_listen: s_poll_wait");
            throw_exception(c, 1);
        }
        if(ret==0) {
            s_log(LOG_INFO, "DTLSv1_listen: TIMEOUTbusy exceeded");
            throw_exception(c, 1);
        }
    }
    BIO_ADDR_free(peer);
    s_log(LOG_DEBUG, "DTLS: ClientHello accepted");
#else /* OPENSSL_VERSION_NUMBER>=0x10100000L */
    memset(&peer, 0, sizeof peer);
    s_log(LOG_DEBUG, "DTLS: waiting for ClientHello");
    /* Call DTLSv1_listen() once to start the cookie exchange.
     * Do NOT retry it in a loop: DTLSv1_listen() calls SSL_clear()
     * which destroys the DTLS handshake state (HelloVerifyRequest
     * already sent, waiting for cookie-containing ClientHello).
     * If it returns <=0, fall through to the SSL_accept() loop
     * below which continues the handshake. */
    dtls_ret=DTLSv1_listen(c->ssl, &peer.sa);
    if(dtls_ret>0)
        s_log(LOG_DEBUG, "DTLS: ClientHello accepted");
#endif /* OPENSSL_VERSION_NUMBER>=0x10100000L */
}

#endif /* USE_DTLS */

/* end of network.c */
