tls.c 16.6 KB
Newer Older
1 2 3
/*****************************************************************************
 * tls.c
 *****************************************************************************
4
 * Copyright © 2004-2016 Rémi Denis-Courmont
Rémi Denis-Courmont's avatar
Rémi Denis-Courmont committed
5
 * $Id$
6
 *
Jean-Baptiste Kempf's avatar
LGPL  
Jean-Baptiste Kempf committed
7 8 9
 * This program is free software; you can redistribute it and/or modify it
 * under the terms of the GNU Lesser General Public License as published by
 * the Free Software Foundation; either version 2.1 of the License, or
10 11 12 13
 * (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
Jean-Baptiste Kempf's avatar
LGPL  
Jean-Baptiste Kempf committed
14 15
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU Lesser General Public License for more details.
16
 *
Jean-Baptiste Kempf's avatar
LGPL  
Jean-Baptiste Kempf committed
17 18 19
 * You should have received a copy of the GNU Lesser General Public License
 * along with this program; if not, write to the Free Software Foundation,
 * Inc., 51 Franklin Street, Fifth Floor, Boston MA 02110-1301, USA.
20 21
 *****************************************************************************/

Rémi Denis-Courmont's avatar
Rémi Denis-Courmont committed
22 23
/**
 * @file
24 25 26
 * Transport Layer Socket abstraction.
 *
 * This file implements the Transport Layer Socket (vlc_tls) abstraction.
Rémi Denis-Courmont's avatar
Rémi Denis-Courmont committed
27 28
 */

29 30 31 32
#ifdef HAVE_CONFIG_H
# include "config.h"
#endif

33 34 35 36
#ifdef HAVE_POLL
# include <poll.h>
#endif
#include <assert.h>
37 38 39
#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
Rémi Denis-Courmont's avatar
Rémi Denis-Courmont committed
40
#include <string.h>
41 42 43
#ifdef HAVE_SYS_UIO_H
# include <sys/uio.h>
#endif
44 45 46 47 48 49
#ifdef HAVE_NETINET_TCP_H
# include <netinet/tcp.h>
#endif
#ifndef SOL_TCP
# define SOL_TCP IPPROTO_TCP
#endif
50

51
#include <vlc_common.h>
52
#include "libvlc.h"
53

Clément Stenac's avatar
Clément Stenac committed
54
#include <vlc_tls.h>
55
#include <vlc_modules.h>
56
#include <vlc_interrupt.h>
57

58 59
/*** TLS credentials ***/

60 61 62 63 64 65 66 67 68 69
static int tls_server_load(void *func, va_list ap)
{
    int (*activate) (vlc_tls_creds_t *, const char *, const char *) = func;
    vlc_tls_creds_t *crd = va_arg (ap, vlc_tls_creds_t *);
    const char *cert = va_arg (ap, const char *);
    const char *key = va_arg (ap, const char *);

    return activate (crd, cert, key);
}

70 71 72 73 74 75 76 77
static int tls_client_load(void *func, va_list ap)
{
    int (*activate) (vlc_tls_creds_t *) = func;
    vlc_tls_creds_t *crd = va_arg (ap, vlc_tls_creds_t *);

    return activate (crd);
}

78 79 80 81 82 83 84 85
static void tls_unload(void *func, va_list ap)
{
    void (*deactivate) (vlc_tls_creds_t *) = func;
    vlc_tls_creds_t *crd = va_arg (ap, vlc_tls_creds_t *);

    deactivate (crd);
}

86 87 88
vlc_tls_creds_t *
vlc_tls_ServerCreate (vlc_object_t *obj, const char *cert_path,
                      const char *key_path)
89
{
90 91
    vlc_tls_creds_t *srv = vlc_custom_create (obj, sizeof (*srv),
                                              "tls server");
92
    if (unlikely(srv == NULL))
93
        return NULL;
94

95 96
    if (key_path == NULL)
        key_path = cert_path;
97

98 99
    srv->module = vlc_module_load (srv, "tls server", NULL, false,
                                   tls_server_load, srv, cert_path, key_path);
100
    if (srv->module == NULL)
101 102
    {
        msg_Err (srv, "TLS server plugin not available");
103
        vlc_object_release (srv);
104 105 106 107
        return NULL;
    }

    return srv;
108 109
}

110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127
vlc_tls_creds_t *vlc_tls_ClientCreate (vlc_object_t *obj)
{
    vlc_tls_creds_t *crd = vlc_custom_create (obj, sizeof (*crd),
                                              "tls client");
    if (unlikely(crd == NULL))
        return NULL;

    crd->module = vlc_module_load (crd, "tls client", NULL, false,
                                   tls_client_load, crd);
    if (crd->module == NULL)
    {
        msg_Err (crd, "TLS client plugin not available");
        vlc_object_release (crd);
        return NULL;
    }

    return crd;
}
128

129
void vlc_tls_Delete (vlc_tls_creds_t *crd)
130
{
131
    if (crd == NULL)
132
        return;
133

134
    vlc_module_unload(crd, crd->module, tls_unload, crd);
135
    vlc_object_release (crd);
136 137
}

138

139 140
/*** TLS  session ***/

141 142
static vlc_tls_t *vlc_tls_SessionCreate(vlc_tls_creds_t *crd,
                                        vlc_tls_t *sock,
143 144
                                        const char *host,
                                        const char *const *alpn)
145
{
146
    vlc_tls_t *session;
147
    int canc = vlc_savecancel();
148
    session = crd->open(crd, sock, host, alpn);
149
    vlc_restorecancel(canc);
150 151
    if (session != NULL)
        session->p = sock;
152
    return session;
153 154
}

155
void vlc_tls_SessionDelete (vlc_tls_t *session)
156
{
157 158 159
    int canc = vlc_savecancel();
    session->close(session);
    vlc_restorecancel(canc);
160 161
}

162 163 164 165 166 167 168
static void cleanup_tls(void *data)
{
    vlc_tls_t *session = data;

    vlc_tls_SessionDelete (session);
}

169 170 171 172
#undef vlc_tls_ClientSessionCreate
vlc_tls_t *vlc_tls_ClientSessionCreate(vlc_tls_creds_t *crd, vlc_tls_t *sock,
                                       const char *host, const char *service,
                                       const char *const *alpn, char **alp)
173
{
174
    int val;
175

176
    vlc_tls_t *session = vlc_tls_SessionCreate(crd, sock, host, alpn);
177
    if (session == NULL)
178
        return NULL;
179

180
    int canc = vlc_savecancel();
181 182 183 184
    mtime_t deadline = mdate ();
    deadline += var_InheritInteger (crd, "ipv4-timeout") * 1000;

    struct pollfd ufd[1];
185
    ufd[0].fd = vlc_tls_GetFD(sock);
186

187
    vlc_cleanup_push (cleanup_tls, session);
188
    while ((val = crd->handshake(crd, session, host, service, alp)) != 0)
189
    {
190
        if (val < 0 || vlc_killed() )
191
        {
192 193
            if (val < 0)
                msg_Err(crd, "TLS session handshake error");
194 195 196 197
error:
            vlc_tls_SessionDelete (session);
            session = NULL;
            break;
198 199
        }

200 201 202 203 204 205 206
        mtime_t now = mdate ();
        if (now > deadline)
           now = deadline;

        assert (val <= 2);
        ufd[0] .events = (val == 1) ? POLLIN : POLLOUT;

207
        vlc_restorecancel(canc);
208
        val = vlc_poll_i11e(ufd, 1, (deadline - now) / 1000);
209 210
        canc = vlc_savecancel();
        if (val == 0)
211
        {
212
            msg_Err(crd, "TLS session handshake timeout");
213
            goto error;
214 215
        }
    }
216 217
    vlc_cleanup_pop();
    vlc_restorecancel(canc);
218
    return session;
219
}
220

221 222
vlc_tls_t *vlc_tls_ServerSessionCreate(vlc_tls_creds_t *crd,
                                       vlc_tls_t *sock,
223 224
                                       const char *const *alpn)
{
225
    return vlc_tls_SessionCreate(crd, sock, NULL, alpn);
226 227
}

228
ssize_t vlc_tls_Read(vlc_tls_t *session, void *buf, size_t len, bool waitall)
229
{
230
    struct pollfd ufd;
231
    struct iovec iov;
232

233
    ufd.fd = vlc_tls_GetFD(session);
234
    ufd.events = POLLIN;
235 236
    iov.iov_base = buf;
    iov.iov_len = len;
237 238 239

    for (size_t rcvd = 0;;)
    {
240
        if (vlc_killed())
241 242 243 244 245
        {
            errno = EINTR;
            return -1;
        }

246
        ssize_t val = session->readv(session, &iov, 1);
247 248 249 250
        if (val > 0)
        {
            if (!waitall)
                return val;
251 252
            iov.iov_base = (char *)iov.iov_base + val;
            iov.iov_len -= val;
253 254
            rcvd += val;
        }
255
        if (iov.iov_len == 0 || val == 0)
256
            return rcvd;
Thomas Guillem's avatar
Thomas Guillem committed
257 258 259 260 261 262 263
        if (val == -1)
        {
            if (vlc_killed())
                return -1;
            if (errno != EINTR && errno != EAGAIN)
                return rcvd ? (ssize_t)rcvd : -1;
        }
264

265
        vlc_poll_i11e(&ufd, 1, -1);
266 267 268
    }
}

269
ssize_t vlc_tls_Write(vlc_tls_t *session, const void *buf, size_t len)
270
{
271
    struct pollfd ufd;
272
    struct iovec iov;
273

274
    ufd.fd = vlc_tls_GetFD(session);
275
    ufd.events = POLLOUT;
276 277
    iov.iov_base = (void *)buf;
    iov.iov_len = len;
278 279 280

    for (size_t sent = 0;;)
    {
281
        if (vlc_killed())
282 283 284 285 286
        {
            errno = EINTR;
            return -1;
        }

287
        ssize_t val = session->writev(session, &iov, 1);
288 289
        if (val > 0)
        {
290 291
            iov.iov_base = ((char *)iov.iov_base) + val;
            iov.iov_len -= val;
292 293
            sent += val;
        }
294
        if (iov.iov_len == 0 || val == 0)
295
            return sent;
Thomas Guillem's avatar
Thomas Guillem committed
296 297 298 299 300 301 302
        if (val == -1)
        {
            if (vlc_killed())
                return -1;
            if (errno != EINTR && errno != EAGAIN)
                return sent ? (ssize_t)sent : -1;
        }
303

304
        vlc_poll_i11e(&ufd, 1, -1);
305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337
    }
}

char *vlc_tls_GetLine(vlc_tls_t *session)
{
    char *line = NULL;
    size_t linelen = 0, linesize = 0;

    do
    {
        if (linelen == linesize)
        {
            linesize += 1024;

            char *newline = realloc(line, linesize);
            if (unlikely(newline == NULL))
                goto error;
            line = newline;
        }

        if (vlc_tls_Read(session, line + linelen, 1, false) <= 0)
            goto error;
    }
    while (line[linelen++] != '\n');

    if (linelen >= 2 && line[linelen - 2] == '\r')
        line[linelen - 2] = '\0';
    return line;

error:
    free(line);
    return NULL;
}
338

339 340 341 342
typedef struct vlc_tls_socket
{
    struct vlc_tls tls;
    int fd;
Rémi Denis-Courmont's avatar
Rémi Denis-Courmont committed
343 344
    socklen_t peerlen;
    struct sockaddr peer[];
345 346
} vlc_tls_socket_t;

347
static int vlc_tls_SocketGetFD(vlc_tls_t *tls)
348
{
349 350 351
    vlc_tls_socket_t *sock = (struct vlc_tls_socket *)tls;

    return sock->fd;
352 353
}

354 355
static ssize_t vlc_tls_SocketRead(vlc_tls_t *tls, struct iovec *iov,
                                  unsigned count)
356
{
357 358 359 360 361
    struct msghdr msg =
    {
        .msg_iov = iov,
        .msg_iovlen = count,
    };
362 363

    return recvmsg(vlc_tls_SocketGetFD(tls), &msg, 0);
364 365
}

366 367
static ssize_t vlc_tls_SocketWrite(vlc_tls_t *tls, const struct iovec *iov,
                                   unsigned count)
368
{
369 370 371 372 373
    const struct msghdr msg =
    {
        .msg_iov = (struct iovec *)iov,
        .msg_iovlen = count,
    };
374 375

    return sendmsg(vlc_tls_SocketGetFD(tls), &msg, MSG_NOSIGNAL);
376 377
}

378
static int vlc_tls_SocketShutdown(vlc_tls_t *tls, bool duplex)
379
{
380
    return shutdown(vlc_tls_SocketGetFD(tls), duplex ? SHUT_RDWR : SHUT_WR);
381 382
}

383
static void vlc_tls_SocketClose(vlc_tls_t *tls)
384
{
385 386
    net_Close(vlc_tls_SocketGetFD(tls));
    free(tls);
387 388
}

Rémi Denis-Courmont's avatar
Rémi Denis-Courmont committed
389 390 391
static vlc_tls_t *vlc_tls_SocketAlloc(int fd,
                                      const struct sockaddr *restrict peer,
                                      socklen_t peerlen)
392
{
Rémi Denis-Courmont's avatar
Rémi Denis-Courmont committed
393
    vlc_tls_socket_t *sock = malloc(sizeof (*sock) + peerlen);
394
    if (unlikely(sock == NULL))
395 396
        return NULL;

397 398 399 400 401 402 403 404
    vlc_tls_t *tls = &sock->tls;

    tls->get_fd = vlc_tls_SocketGetFD;
    tls->readv = vlc_tls_SocketRead;
    tls->writev = vlc_tls_SocketWrite;
    tls->shutdown = vlc_tls_SocketShutdown;
    tls->close = vlc_tls_SocketClose;
    tls->p = NULL;
Rémi Denis-Courmont's avatar
Rémi Denis-Courmont committed
405

406
    sock->fd = fd;
Rémi Denis-Courmont's avatar
Rémi Denis-Courmont committed
407 408 409
    sock->peerlen = peerlen;
    if (peerlen > 0)
        memcpy(sock->peer, peer, peerlen);
410
    return tls;
411
}
412

Rémi Denis-Courmont's avatar
Rémi Denis-Courmont committed
413 414 415 416 417
vlc_tls_t *vlc_tls_SocketOpen(int fd)
{
    return vlc_tls_SocketAlloc(fd, NULL, 0);
}

418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443
int vlc_tls_SocketPair(int family, int protocol, vlc_tls_t *pair[2])
{
    int fds[2];

    if (vlc_socketpair(family, SOCK_STREAM, protocol, fds, true))
        return -1;

    for (size_t i = 0; i < 2; i++)
    {
        setsockopt(fds[i], SOL_SOCKET, SO_REUSEADDR,
                   &(int){ 1 }, sizeof (int));

        pair[i] = vlc_tls_SocketAlloc(fds[i], NULL, 0);
        if (unlikely(pair[i] == NULL))
        {
            net_Close(fds[i]);
            if (i)
                vlc_tls_SessionDelete(pair[0]);
            else
                net_Close(fds[1]);
            return -1;
        }
    }
    return 0;
}

Rémi Denis-Courmont's avatar
Rémi Denis-Courmont committed
444 445 446 447
/**
 * Allocates an unconnected transport layer socket.
 */
static vlc_tls_t *vlc_tls_SocketAddrInfo(const struct addrinfo *restrict info)
448 449 450 451 452 453 454 455
{
    int fd = vlc_socket(info->ai_family, info->ai_socktype, info->ai_protocol,
                        true /* nonblocking */);
    if (fd == -1)
        return NULL;

    setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &(int){ 1 }, sizeof (int));

456 457 458
    if (info->ai_socktype == SOCK_STREAM && info->ai_protocol == IPPROTO_TCP)
        setsockopt(fd, SOL_TCP, TCP_NODELAY, &(int){ 1 }, sizeof (int));

Rémi Denis-Courmont's avatar
Rémi Denis-Courmont committed
459 460 461 462 463
    vlc_tls_t *sk = vlc_tls_SocketAlloc(fd, info->ai_addr, info->ai_addrlen);
    if (unlikely(sk == NULL))
        net_Close(fd);
    return sk;
}
464

Rémi Denis-Courmont's avatar
Rémi Denis-Courmont committed
465 466 467 468 469 470 471
/**
 * Waits for pending transport layer socket connection.
 */
static int vlc_tls_WaitConnect(vlc_tls_t *tls)
{
    const int fd = vlc_tls_GetFD(tls);
    struct pollfd ufd;
472

Rémi Denis-Courmont's avatar
Rémi Denis-Courmont committed
473 474
    ufd.fd = fd;
    ufd.events = POLLOUT;
475

Rémi Denis-Courmont's avatar
Rémi Denis-Courmont committed
476 477 478
    do
    {
        if (vlc_killed())
479
        {
Rémi Denis-Courmont's avatar
Rémi Denis-Courmont committed
480 481
            errno = EINTR;
            return -1;
482
        }
Rémi Denis-Courmont's avatar
Rémi Denis-Courmont committed
483 484
    }
    while (vlc_poll_i11e(&ufd, 1, -1) <= 0);
485

Rémi Denis-Courmont's avatar
Rémi Denis-Courmont committed
486 487
    int val;
    socklen_t len = sizeof (val);
488

Rémi Denis-Courmont's avatar
Rémi Denis-Courmont committed
489 490 491 492 493 494 495
    if (getsockopt(fd, SOL_SOCKET, SO_ERROR, &val, &len))
        return -1;

    if (val != 0)
    {
        errno = val;
        return -1;
496
    }
Rémi Denis-Courmont's avatar
Rémi Denis-Courmont committed
497 498
    return 0;
}
499

Rémi Denis-Courmont's avatar
Rémi Denis-Courmont committed
500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517
/**
 * Connects a transport layer socket.
 */
static ssize_t vlc_tls_Connect(vlc_tls_t *tls)
{
    const vlc_tls_socket_t *sock = (vlc_tls_socket_t *)tls;

    if (connect(sock->fd, sock->peer, sock->peerlen) == 0)
        return 0;
#ifndef _WIN32
    if (errno != EINPROGRESS)
        return -1;
#else
    if (WSAGetLastError() != WSAEWOULDBLOCK)
        return -1;
#endif
    return vlc_tls_WaitConnect(tls);
}
518

Rémi Denis-Courmont's avatar
Rémi Denis-Courmont committed
519 520 521 522
/* Callback for combined connection establishment and initial send */
static ssize_t vlc_tls_ConnectWrite(vlc_tls_t *tls,
                                    const struct iovec *iov,unsigned count)
{
523 524 525 526 527 528 529 530 531 532 533
#ifdef MSG_FASTOPEN
    vlc_tls_socket_t *sock = (vlc_tls_socket_t *)tls;
    const struct msghdr msg =
    {
        .msg_name = sock->peer,
        .msg_namelen = sock->peerlen,
        .msg_iov = (struct iovec *)iov,
        .msg_iovlen = count,
    };
    ssize_t ret;

534 535 536
    /* Next time, write directly. Do not retry to connect. */
    tls->writev = vlc_tls_SocketWrite;

537 538 539 540 541 542 543
    ret = sendmsg(vlc_tls_SocketGetFD(tls), &msg, MSG_NOSIGNAL|MSG_FASTOPEN);
    if (ret >= 0)
    {   /* Fast open in progress */
        return ret;
    }

    if (errno == EINPROGRESS)
544 545 546 547 548
    {
        if (vlc_tls_WaitConnect(tls))
            return -1;
    }
    else
549 550 551
    if (errno != EOPNOTSUPP)
        return -1;
    /* Fast open not supported or disabled... fallback to normal mode */
552 553
#else
    tls->writev = vlc_tls_SocketWrite;
554 555
#endif

Rémi Denis-Courmont's avatar
Rémi Denis-Courmont committed
556 557
    if (vlc_tls_Connect(tls))
        return -1;
558

Rémi Denis-Courmont's avatar
Rémi Denis-Courmont committed
559 560 561
    return vlc_tls_SocketWrite(tls, iov, count);
}

562 563
vlc_tls_t *vlc_tls_SocketOpenAddrInfo(const struct addrinfo *restrict info,
                                      bool defer_connect)
Rémi Denis-Courmont's avatar
Rémi Denis-Courmont committed
564
{
565 566
    vlc_tls_t *sock = vlc_tls_SocketAddrInfo(info);
    if (sock == NULL)
Rémi Denis-Courmont's avatar
Rémi Denis-Courmont committed
567 568
        return NULL;

569 570 571 572 573 574
    if (defer_connect)
    {   /* The socket is not connected yet.
         * The connection will be triggered on the first send. */
        sock->writev = vlc_tls_ConnectWrite;
    }
    else
575
    {
576 577 578 579 580
        if (vlc_tls_Connect(sock))
        {
            vlc_tls_SessionDelete(sock);
            sock = NULL;
        }
581
    }
582
    return sock;
583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606
}

vlc_tls_t *vlc_tls_SocketOpenTCP(vlc_object_t *obj, const char *name,
                                 unsigned port)
{
    struct addrinfo hints =
    {
        .ai_socktype = SOCK_STREAM,
        .ai_protocol = IPPROTO_TCP,
    }, *res;

    assert(name != NULL);
    msg_Dbg(obj, "resolving %s ...", name);

    int val = vlc_getaddrinfo_i11e(name, port, &hints, &res);
    if (val != 0)
    {   /* TODO: C locale for gai_strerror() */
        msg_Err(obj, "cannot resolve %s port %u: %s", name, port,
                gai_strerror(val));
        return NULL;
    }

    msg_Dbg(obj, "connecting to %s port %u ...", name, port);

607 608 609
    /* TODO: implement RFC6555 */
    for (const struct addrinfo *p = res; p != NULL; p = p->ai_next)
    {
610
        vlc_tls_t *tls = vlc_tls_SocketOpenAddrInfo(p, false);
611 612 613 614 615 616 617 618 619 620
        if (tls == NULL)
        {
            msg_Err(obj, "connection error: %s", vlc_strerror_c(errno));
            continue;
        }

        freeaddrinfo(res);
        return tls;
    }

621
    freeaddrinfo(res);
622
    return NULL;
623
}
624 625 626 627 628

vlc_tls_t *vlc_tls_SocketOpenTLS(vlc_tls_creds_t *creds, const char *name,
                                 unsigned port, const char *service,
                                 const char *const *alpn, char **alp)
{
Rémi Denis-Courmont's avatar
Rémi Denis-Courmont committed
629 630 631 632 633 634 635 636 637 638 639 640 641
    struct addrinfo hints =
    {
        .ai_socktype = SOCK_STREAM,
        .ai_protocol = IPPROTO_TCP,
    }, *res;

    msg_Dbg(creds, "resolving %s ...", name);

    int val = vlc_getaddrinfo_i11e(name, port, &hints, &res);
    if (val != 0)
    {   /* TODO: C locale for gai_strerror() */
        msg_Err(creds, "cannot resolve %s port %u: %s", name, port,
                gai_strerror(val));
642
        return NULL;
Rémi Denis-Courmont's avatar
Rémi Denis-Courmont committed
643
    }
644

Rémi Denis-Courmont's avatar
Rémi Denis-Courmont committed
645 646
    for (const struct addrinfo *p = res; p != NULL; p = p->ai_next)
    {
647
        vlc_tls_t *tcp = vlc_tls_SocketOpenAddrInfo(p, true);
Rémi Denis-Courmont's avatar
Rémi Denis-Courmont committed
648 649 650 651 652 653 654 655 656 657 658 659 660 661 662
        if (tcp == NULL)
        {
            msg_Err(creds, "socket error: %s", vlc_strerror_c(errno));
            continue;
        }

        vlc_tls_t *tls = vlc_tls_ClientSessionCreate(creds, tcp, name, service,
                                                     alpn, alp);
        if (tls != NULL)
        {   /* Success! */
            freeaddrinfo(res);
            return tls;
        }

        msg_Err(creds, "connection error: %s", vlc_strerror_c(errno));
663
        vlc_tls_SessionDelete(tcp);
Rémi Denis-Courmont's avatar
Rémi Denis-Courmont committed
664 665 666 667 668
    }

    /* Failure! */
    freeaddrinfo(res);
    return NULL;
669
}