From 81f2204ceaec3bc99442344c87e2872747c2100a Mon Sep 17 00:00:00 2001 From: Rusty Russell Date: Sun, 17 Mar 2013 15:18:10 +1030 Subject: [PATCH] net: add server support. Creating a server for IPv4 and IPv6 has similar issues to clients, with some novel twists. Slightly different arguments need to be given to getaddrinfo(), but worse, some platforms (Linux without /proc/sys/net/ipv6/bindv6only set) automatically bind IPv6 sockets to IPv4 ports as well. Thus we need a function which can bind (and listen) to one or two fds. Signed-off-by: Rusty Russell --- ccan/net/_info | 6 +- ccan/net/net.c | 88 ++++++++++++++++ ccan/net/net.h | 51 ++++++++++ ccan/net/test/run-bind.c | 212 +++++++++++++++++++++++++++++++++++++++ 4 files changed, 355 insertions(+), 2 deletions(-) create mode 100644 ccan/net/test/run-bind.c diff --git a/ccan/net/_info b/ccan/net/_info index a9c57206..9ce0e150 100644 --- a/ccan/net/_info +++ b/ccan/net/_info @@ -2,9 +2,11 @@ #include "config.h" /** - * net - simple IPv4/IPv6 client library + * net - simple IPv4/IPv6 socket library * - * This code makes it simple to support IPv4 and IPv6 without speed penalty. + * This code makes it simple to support IPv4 and IPv6 without speed penalty + * in clients by using non-blocking simultaneous connect, and using + * a convenience function to create both IPv4 and IPv6 sockets for servers. * * License: MIT * diff --git a/ccan/net/net.c b/ccan/net/net.c index 30f6560e..eef28e2f 100644 --- a/ccan/net/net.c +++ b/ccan/net/net.c @@ -150,3 +150,91 @@ out: errno = saved_errno; return sockfd; } + +struct addrinfo *net_server_lookup(const char *service, + int family, + int socktype) +{ + struct addrinfo *res, hints; + + memset(&hints, 0, sizeof(hints)); + hints.ai_family = family; + hints.ai_socktype = socktype; + hints.ai_flags = AI_PASSIVE; + hints.ai_protocol = 0; + + if (getaddrinfo(NULL, service, &hints, &res) != 0) + return NULL; + + return res; +} + +static bool should_listen(const struct addrinfo *addrinfo) +{ +#ifdef SOCK_SEQPACKET + if (addrinfo->ai_socktype == SOCK_SEQPACKET) + return true; +#endif + return (addrinfo->ai_socktype == SOCK_STREAM); +} + +static int make_listen_fd(const struct addrinfo *addrinfo) +{ + int saved_errno, fd, on = 1; + + fd = socket(addrinfo->ai_family, addrinfo->ai_socktype, + addrinfo->ai_protocol); + if (fd < 0) + return -1; + + setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on)); + if (bind(fd, addrinfo->ai_addr, addrinfo->ai_addrlen) != 0) + goto fail; + + if (should_listen(addrinfo) && listen(fd, 5) != 0) + goto fail; + return fd; + +fail: + saved_errno = errno; + close(fd); + errno = saved_errno; + return -1; +} + +int net_bind(const struct addrinfo *addrinfo, int fds[2]) +{ + const struct addrinfo *ipv6, *ipv4; + unsigned int num; + + if (addrinfo->ai_family == AF_INET) + ipv4 = addrinfo; + else if (addrinfo->ai_family == AF_INET6) + ipv6 = addrinfo; + + if (addrinfo->ai_next) { + if (addrinfo->ai_next->ai_family == AF_INET) + ipv4 = addrinfo->ai_next; + else if (addrinfo->ai_next->ai_family == AF_INET6) + ipv6 = addrinfo->ai_next; + } + + num = 0; + /* Take IPv6 first, since it might bind to IPv4 port too. */ + if (ipv6) { + if ((fds[num] = make_listen_fd(ipv6)) >= 0) + num++; + else + ipv6 = NULL; + } + if (ipv4) { + if ((fds[num] = make_listen_fd(ipv4)) >= 0) + num++; + else + ipv4 = NULL; + } + if (num == 0) + return -1; + + return num; +} diff --git a/ccan/net/net.h b/ccan/net/net.h index a336281e..9ad6de56 100644 --- a/ccan/net/net.h +++ b/ccan/net/net.h @@ -47,4 +47,55 @@ struct addrinfo *net_client_lookup(const char *hostname, * freeaddrinfo(addr); */ int net_connect(const struct addrinfo *addrinfo); + +/** + * net_server_lookup - look up a service name to bind to. + * @service: the service to look up + * @family: Usually AF_UNSPEC, otherwise AF_INET or AF_INET6. + * @socktype: SOCK_DGRAM or SOCK_STREAM. + * + * This will do a synchronous lookup of a given name, returning a linked list + * of results, or NULL on error. You should use freeaddrinfo() to free it. + * + * Example: + * #include + * #include + * #include + * #include + * #include + * ... + * struct addrinfo *addr; + * + * // Get address(es) to bind for our service. + * addr = net_server_lookup("8888", AF_UNSPEC, SOCK_STREAM); + * if (!addr) + * errx(1, "Failed to look up 8888 to bind to"); + */ +struct addrinfo *net_server_lookup(const char *service, + int family, + int socktype); + +/** + * net_bind - create listening socket(s) + * @addrinfo: the address(es) to bind to. + * @fds: array of two fds. + * + * This will create one (or if necessary) two sockets, mark them + * SO_REUSEADDR, bind them to the given address(es), and make them + * listen() (if the socket type is SOCK_STREAM or SOCK_SEQPACKET). + * + * Returns -1 (and sets errno) on error, or 1 or 2 depending on how many + * @fds are valid. + * + * Example: + * int fds[2], i, num_fds; + * + * num_fds = net_bind(addr, fds); + * if (num_fds < 0) + * err(1, "Failed to listen on port 8888"); + * + * for (i = 0; i < num_fds; i++) + * printf(" Got fd %u/%u: %i\n", i, num_fds, fds[i]); + */ +int net_bind(const struct addrinfo *addrinfo, int fds[2]); #endif /* CCAN_NET_H */ diff --git a/ccan/net/test/run-bind.c b/ccan/net/test/run-bind.c new file mode 100644 index 00000000..9fb2081d --- /dev/null +++ b/ccan/net/test/run-bind.c @@ -0,0 +1,212 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +static int ipv6_only; + +#ifdef IPV6_V6ONLY +static int my_setsockopt(int sockfd, int level, int optname, + const void *optval, socklen_t optlen) +{ + int ret; + setsockopt(sockfd, IPPROTO_IPV6, IPV6_V6ONLY, &ipv6_only, + sizeof(ipv6_only)); + ret = setsockopt(sockfd, level, optname, optval, optlen); + return ret; +} +#define setsockopt my_setsockopt +#endif + +#include + +#define TEST_PORT "65001" + +static void do_connect(int family, int type) +{ + int fd, ret; + struct addrinfo *addr; + char buf[8]; + + /* Just in case... */ + alarm(5); + addr = net_client_lookup(NULL, TEST_PORT, family, type); + fd = net_connect(addr); + if (fd < 0) + err(1, "Failed net_connect"); + freeaddrinfo(addr); + + ret = write(fd, "Yay!", strlen("Yay!")); + if (ret != strlen("Yay!")) + err(1, "Write returned %i", ret); + ret = read(fd, buf, sizeof(buf)); + if (ret != 5) + err(1, "Read returned %i", ret); + if (memcmp(buf, "metoo", ret) != 0) + err(1, "Read returned '%.*s'", ret, buf); + close(fd); +} + +static int wait_for_readable(int fds[2], int num_fds) +{ + int i, max_fd = -1; + fd_set set; + + FD_ZERO(&set); + for (i = 0; i < num_fds; i++) { + if (fds[i] > max_fd) + max_fd = fds[i]; + FD_SET(fds[i], &set); + } + + select(max_fd+1, &set, NULL, NULL, NULL); + for (i = 0; i < num_fds; i++) { + if (FD_ISSET(fds[i], &set)) + return i; + } + return num_fds+1; +} + +int main(void) +{ + struct addrinfo *addr; + int fds[2], num_fds, i, fd, status, ret; + char buf[20]; + union { + struct sockaddr addr; + struct sockaddr_in ipv4; + struct sockaddr_in6 ipv6; + } remote_addr; + socklen_t addlen = sizeof(remote_addr); + + plan_tests(35); + + /* Simple TCP test. */ + addr = net_server_lookup(TEST_PORT, AF_UNSPEC, SOCK_STREAM); + ok1(addr); + num_fds = net_bind(addr, fds); + ok1(num_fds == 1 || num_fds == 2); + + if (!fork()) { + for (i = 0; i < num_fds; i++) + close(fds[i]); + do_connect(AF_UNSPEC, SOCK_STREAM); + exit(0); + } + + i = wait_for_readable(fds, num_fds); + ok1(i < num_fds); + fd = accept(fds[i], NULL, NULL); + ok1(fd >= 0); + + ret = read(fd, buf, strlen("Yay!")); + ok1(ret == strlen("Yay!")); + ok1(memcmp(buf, "Yay!", ret) == 0); + ret = write(fd, "metoo", strlen("metoo")); + ok1(ret == strlen("metoo")); + ok1(wait(&status) != -1); + ok1(WIFEXITED(status)); + ok1(WEXITSTATUS(status) == 0); + close(fd); + for (i = 0; i < num_fds; i++) + close(fds[i]); + + /* Simple UDP test. */ + addr = net_server_lookup(TEST_PORT, AF_UNSPEC, SOCK_DGRAM); + ok1(addr); + num_fds = net_bind(addr, fds); + ok1(num_fds == 1 || num_fds == 2); + + if (!fork()) { + for (i = 0; i < num_fds; i++) + close(fds[i]); + do_connect(AF_UNSPEC, SOCK_DGRAM); + exit(0); + } + + i = wait_for_readable(fds, num_fds); + ok1(i < num_fds); + fd = fds[i]; + + ret = recvfrom(fd, buf, strlen("Yay!"), 0, + (void *)&remote_addr, &addlen); + ok1(ret == strlen("Yay!")); + ok1(memcmp(buf, "Yay!", ret) == 0); + ret = sendto(fd, "metoo", strlen("metoo"), 0, + (void *)&remote_addr, addlen); + ok1(ret == strlen("metoo")); + ok1(wait(&status) >= 0); + ok1(WIFEXITED(status)); + ok1(WEXITSTATUS(status) == 0); + close(fd); + for (i = 0; i < num_fds; i++) + close(fds[i]); + +/* This seems like a Linux-only extension */ +#ifdef IPV6_V6ONLY + /* Try to force separate sockets for IPv4/IPv6, if we can. */ + if (addr->ai_next) + ipv6_only = true; +#endif + + if (ipv6_only) { + int j; + + addr = net_server_lookup(TEST_PORT, AF_UNSPEC, SOCK_STREAM); + ok1(addr); + num_fds = net_bind(addr, fds); + ok1(num_fds == 2); + freeaddrinfo(addr); + + if (!fork()) { + for (i = 0; i < num_fds; i++) + close(fds[i]); + do_connect(AF_INET, SOCK_STREAM); + do_connect(AF_INET6, SOCK_STREAM); + exit(0); + } + + i = wait_for_readable(fds, num_fds); + ok1(i < num_fds); + fd = accept(fds[i], NULL, NULL); + ok1(fd >= 0); + + ret = read(fd, buf, strlen("Yay!")); + ok1(ret == strlen("Yay!")); + ok1(memcmp(buf, "Yay!", ret) == 0); + ret = write(fd, "metoo", strlen("metoo")); + ok1(ret == strlen("metoo")); + close(fd); + + j = wait_for_readable(fds, num_fds); + ok1(j < num_fds); + ok1(j != i); + fd = accept(fds[j], NULL, NULL); + ok1(fd >= 0); + + ret = read(fd, buf, strlen("Yay!")); + ok1(ret == strlen("Yay!")); + ok1(memcmp(buf, "Yay!", ret) == 0); + ret = write(fd, "metoo", strlen("metoo")); + ok1(ret == strlen("metoo")); + + ok1(wait(&status) >= 0); + ok1(WIFEXITED(status)); + ok1(WEXITSTATUS(status) == 0); + close(fd); + } else + skip(16, "No support for IPv6-only binding"); + + for (i = 0; i < num_fds; i++) + close(fds[i]); + + /* This exits depending on whether all tests passed */ + return exit_status(); +} -- 2.39.2