/*-
* Copyright (c) 2017 Maksym Sobolyev <sobomax@FreeBSD.org>
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
* 1. Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
* OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
* HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
* LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
* OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
* SUCH DAMAGE.
*/
/*
* The test that setups two processes A and B and make A sending
* B UDP packet(s) and B send it back. The time of sending is recorded
* in the payload and time of the arrival is either determined by
* reading clock after recv() completes or using kernel-supplied
* via recvmsg(). End-to-end time t(A->B->A) is then calculated
* and compared against time for both t(A->B) + t(B->A) to make
* sure it makes sense.
*/
#include <sys/cdefs.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/wait.h>
#include <sys/time.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <err.h>
#include <poll.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <strings.h>
#include <time.h>
#include <unistd.h>
#define NPKTS 1000
#define PKT_SIZE 128
/* Timeout to receive pong on the side A, 100ms */
#define SRECV_TIMEOUT (1 * 100)
/*
* Timeout to receive ping on the side B. 4x as large as on the side A,
* so that in the case of packet loss the side A will have a chance to
* realize that and send few more before B bails out.
*/
#define RRECV_TIMEOUT (SRECV_TIMEOUT * 4)
#define MIN_NRECV ((NPKTS * 99) / 100) /* 99% */
//#define SIMULATE_PLOSS
struct trip_ts {
struct timespec sent;
struct timespec recvd;
};
struct test_pkt {
int pnum;
struct trip_ts tss[2];
int lost;
unsigned char data[PKT_SIZE];
};
struct test_ctx {
const char *name;
int fds[2];
struct pollfd pfds[2];
union {
struct sockaddr_in v4;
struct sockaddr_in6 v6;
} sin[2];
struct test_pkt test_pkts[NPKTS];
int nsent;
int nrecvd;
clockid_t clock;
int use_recvmsg;
int ts_type;
};
struct rtt {
struct timespec a2b;
struct timespec b2a;
struct timespec e2e;
struct timespec a2b_b2a;
};
#define SEC(x) ((x)->tv_sec)
#define NSEC(x) ((x)->tv_nsec)
#define NSEC_MAX 1000000000L
#define NSEC_IN_USEC 1000L
#define timeval2timespec(tv, ts) \
do { \
SEC(ts) = (tv)->tv_sec; \
NSEC(ts) = (tv)->tv_usec * NSEC_IN_USEC; \
} while (0);
static const struct timespec zero_ts;
/* 0.01s, should be more than enough for the loopback communication */
static const struct timespec max_ts = {.tv_nsec = (NSEC_MAX / 100)};
enum ts_types {TT_TIMESTAMP = -2, TT_BINTIME = -1,
TT_REALTIME_MICRO = SO_TS_REALTIME_MICRO, TT_TS_BINTIME = SO_TS_BINTIME,
TT_REALTIME = SO_TS_REALTIME, TT_MONOTONIC = SO_TS_MONOTONIC};
static clockid_t
get_clock_type(struct test_ctx *tcp)
{
switch (tcp->ts_type) {
case TT_TIMESTAMP:
case TT_BINTIME:
case TT_REALTIME_MICRO:
case TT_TS_BINTIME:
case TT_REALTIME:
return (CLOCK_REALTIME);
case TT_MONOTONIC:
return (CLOCK_MONOTONIC);
}
abort();
}
static int
get_scm_type(struct test_ctx *tcp)
{
switch (tcp->ts_type) {
case TT_TIMESTAMP:
case TT_REALTIME_MICRO:
return (SCM_TIMESTAMP);
case TT_BINTIME:
case TT_TS_BINTIME:
return (SCM_BINTIME);
case TT_REALTIME:
return (SCM_REALTIME);
case TT_MONOTONIC:
return (SCM_MONOTONIC);
}
abort();
}
static size_t
get_scm_size(struct test_ctx *tcp)
{
switch (tcp->ts_type) {
case TT_TIMESTAMP:
case TT_REALTIME_MICRO:
return (sizeof(struct timeval));
case TT_BINTIME:
case TT_TS_BINTIME:
return (sizeof(struct bintime));
case TT_REALTIME:
case TT_MONOTONIC:
return (sizeof(struct timespec));
}
abort();
}
static void
setup_ts_sockopt(struct test_ctx *tcp, int fd)
{
int rval, oname1, oname2, sval1, sval2;
oname1 = SO_TIMESTAMP;
oname2 = -1;
sval2 = -1;
switch (tcp->ts_type) {
case TT_REALTIME_MICRO:
case TT_TS_BINTIME:
case TT_REALTIME:
case TT_MONOTONIC:
oname2 = SO_TS_CLOCK;
sval2 = tcp->ts_type;
break;
case TT_TIMESTAMP:
break;
case TT_BINTIME:
oname1 = SO_BINTIME;
break;
default:
abort();
}
sval1 = 1;
rval = setsockopt(fd, SOL_SOCKET, oname1, &sval1,
sizeof(sval1));
if (rval != 0) {
err(1, "%s: setup_udp: setsockopt(%d, %d, 1)", tcp->name,
fd, oname1);
}
if (oname2 == -1)
return;
rval = setsockopt(fd, SOL_SOCKET, oname2, &sval2,
sizeof(sval2));
if (rval != 0) {
err(1, "%s: setup_udp: setsockopt(%d, %d, %d)",
tcp->name, fd, oname2, sval2);
}
}
static void
setup_udp(struct test_ctx *tcp)
{
int i;
socklen_t sin_len, af_len;
af_len = sizeof(tcp->sin[0].v4);
for (i = 0; i < 2; i++) {
tcp->sin[i].v4.sin_len = af_len;
tcp->sin[i].v4.sin_family = AF_INET;
tcp->sin[i].v4.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
tcp->fds[i] = socket(PF_INET, SOCK_DGRAM, 0);
if (tcp->fds[i] < 0)
err(1, "%s: setup_udp: socket", tcp->name);
if (bind(tcp->fds[i], (struct sockaddr *)&tcp->sin[i], af_len) < 0)
err(1, "%s: setup_udp: bind(%s, %d)", tcp->name,
inet_ntoa(tcp->sin[i].v4.sin_addr), 0);
sin_len = af_len;
if (getsockname(tcp->fds[i], (struct sockaddr *)&tcp->sin[i], &sin_len) < 0)
err(1, "%s: setup_udp: getsockname(%d)", tcp->name, tcp->fds[i]);
if (tcp->use_recvmsg != 0) {
setup_ts_sockopt(tcp, tcp->fds[i]);
}
tcp->pfds[i].fd = tcp->fds[i];
tcp->pfds[i].events = POLLIN;
}
if (connect(tcp->fds[0], (struct sockaddr *)&tcp->sin[1], af_len) < 0)
err(1, "%s: setup_udp: connect(%s, %d)", tcp->name,
inet_ntoa(tcp->sin[1].v4.sin_addr), ntohs(tcp->sin[1].v4.sin_port));
if (connect(tcp->fds[1], (struct sockaddr *)&tcp->sin[0], af_len) < 0)
err(1, "%s: setup_udp: connect(%s, %d)", tcp->name,
inet_ntoa(tcp->sin[0].v4.sin_addr), ntohs(tcp->sin[0].v4.sin_port));
}
static char *
inet_ntoa6(const void *sin6_addr)
{
static char straddr[INET6_ADDRSTRLEN];
inet_ntop(AF_INET6, sin6_addr, straddr, sizeof(straddr));
return (straddr);
}
static void
setup_udp6(struct test_ctx *tcp)
{
int i;
socklen_t sin_len, af_len;
af_len = sizeof(tcp->sin[0].v6);
for (i = 0; i < 2; i++) {
tcp->sin[i].v6.sin6_len = af_len;
tcp->sin[i].v6.sin6_family = AF_INET6;
tcp->sin[i].v6.sin6_addr = in6addr_loopback;
tcp->fds[i] = socket(PF_INET6, SOCK_DGRAM, 0);
if (tcp->fds[i] < 0)
err(1, "%s: setup_udp: socket", tcp->name);
if (bind(tcp->fds[i], (struct sockaddr *)&tcp->sin[i], af_len) < 0)
err(1, "%s: setup_udp: bind(%s, %d)", tcp->name,
inet_ntoa6(&tcp->sin[i].v6.sin6_addr), 0);
sin_len = af_len;
if (getsockname(tcp->fds[i], (struct sockaddr *)&tcp->sin[i], &sin_len) < 0)
err(1, "%s: setup_udp: getsockname(%d)", tcp->name, tcp->fds[i]);
if (tcp->use_recvmsg != 0) {
setup_ts_sockopt(tcp, tcp->fds[i]);
}
tcp->pfds[i].fd = tcp->fds[i];
tcp->pfds[i].events = POLLIN;
}
if (connect(tcp->fds[0], (struct sockaddr *)&tcp->sin[1], af_len) < 0)
err(1, "%s: setup_udp: connect(%s, %d)", tcp->name,
inet_ntoa6(&tcp->sin[1].v6.sin6_addr),
ntohs(tcp->sin[1].v6.sin6_port));
if (connect(tcp->fds[1], (struct sockaddr *)&tcp->sin[0], af_len) < 0)
err(1, "%s: setup_udp: connect(%s, %d)", tcp->name,
inet_ntoa6(&tcp->sin[0].v6.sin6_addr),
ntohs(tcp->sin[0].v6.sin6_port));
}
static void
teardown_udp(struct test_ctx *tcp)
{
close(tcp->fds[0]);
close(tcp->fds[1]);
}
static void
send_pkt(struct test_ctx *tcp, int pnum, int fdidx, const char *face)
{
ssize_t r;
size_t slen;
slen = sizeof(tcp->test_pkts[pnum]);
clock_gettime(get_clock_type(tcp), &tcp->test_pkts[pnum].tss[fdidx].sent);
r = send(tcp->fds[fdidx], &tcp->test_pkts[pnum], slen, 0);
if (r < 0) {
err(1, "%s: %s: send(%d)", tcp->name, face, tcp->fds[fdidx]);
}
if (r < (ssize_t)slen) {
errx(1, "%s: %s: send(%d): short send", tcp->name, face,
tcp->fds[fdidx]);
}
tcp->nsent += 1;
}
#define PDATA(tcp, i) ((tcp)->test_pkts[(i)].data)
static void
hdr_extract_ts(struct test_ctx *tcp, struct msghdr *mhp, struct timespec *tp)
{
int scm_type;
size_t scm_size;
union {
struct timespec ts;
struct bintime bt;
struct timeval tv;
} tdata;
struct cmsghdr *cmsg;
scm_type = get_scm_type(tcp);
scm_size = get_scm_size(tcp);
for (cmsg = CMSG_FIRSTHDR(mhp); cmsg != NULL;
cmsg = CMSG_NXTHDR(mhp, cmsg)) {
if ((cmsg->cmsg_level == SOL_SOCKET) &&
(cmsg->cmsg_type == scm_type)) {
memcpy(&tdata, CMSG_DATA(cmsg), scm_size);
break;
}
}
if (cmsg == NULL) {
abort();
}
switch (tcp->ts_type) {
case TT_REALTIME:
case TT_MONOTONIC:
*tp = tdata.ts;
break;
case TT_TIMESTAMP:
case TT_REALTIME_MICRO:
timeval2timespec(&tdata.tv, tp);
break;
case TT_BINTIME:
case TT_TS_BINTIME:
bintime2timespec(&tdata.bt, tp);
break;
default:
abort();
}
}
static void
recv_pkt_recvmsg(struct test_ctx *tcp, int fdidx, const char *face, void *buf,
size_t rlen, struct timespec *tp)
{
/* We use a union to make sure hdr is aligned */
union {
struct cmsghdr hdr;
unsigned char buf[CMSG_SPACE(1024)];
} cmsgbuf;
struct msghdr msg;
struct iovec iov;
ssize_t rval;
memset(&msg, '\0', sizeof(msg));
iov.iov_base = buf;
iov.iov_len = rlen;
msg.msg_iov = &iov;
msg.msg_iovlen = 1;
msg.msg_control = cmsgbuf.buf;
msg.msg_controllen = sizeof(cmsgbuf.buf);
rval = recvmsg(tcp->fds[fdidx], &msg, 0);
if (rval < 0) {
err(1, "%s: %s: recvmsg(%d)", tcp->name, face, tcp->fds[fdidx]);
}
if (rval < (ssize_t)rlen) {
errx(1, "%s: %s: recvmsg(%d): short recv", tcp->name, face,
tcp->fds[fdidx]);
}
hdr_extract_ts(tcp, &msg, tp);
}
static void
recv_pkt_recv(struct test_ctx *tcp, int fdidx, const char *face, void *buf,
size_t rlen, struct timespec *tp)
{
ssize_t rval;
rval = recv(tcp->fds[fdidx], buf, rlen, 0);
clock_gettime(get_clock_type(tcp), tp);
if (rval < 0) {
err(1, "%s: %s: recv(%d)", tcp->name, face, tcp->fds[fdidx]);
}
if (rval < (ssize_t)rlen) {
errx(1, "%s: %s: recv(%d): short recv", tcp->name, face,
tcp->fds[fdidx]);
}
}
static int
recv_pkt(struct test_ctx *tcp, int fdidx, const char *face, int tout)
{
int pr;
struct test_pkt recv_buf;
size_t rlen;
pr = poll(&tcp->pfds[fdidx], 1, tout);
if (pr < 0) {
err(1, "%s: %s: poll(%d)", tcp->name, face, tcp->fds[fdidx]);
}
if (pr == 0) {
return (-1);
}
if(tcp->pfds[fdidx].revents != POLLIN) {
errx(1, "%s: %s: poll(%d): unexpected result", tcp->name, face,
tcp->fds[fdidx]);
}
rlen = sizeof(recv_buf);
if (tcp->use_recvmsg == 0) {
recv_pkt_recv(tcp, fdidx, face, &recv_buf, rlen,
&recv_buf.tss[fdidx].recvd);
} else {
recv_pkt_recvmsg(tcp, fdidx, face, &recv_buf, rlen,
&recv_buf.tss[fdidx].recvd);
}
if (recv_buf.pnum < 0 || recv_buf.pnum >= NPKTS ||
memcmp(recv_buf.data, PDATA(tcp, recv_buf.pnum), PKT_SIZE) != 0) {
errx(1, "%s: %s: recv(%d): corrupted data, packet %d", tcp->name,
face, tcp->fds[fdidx], recv_buf.pnum);
}
tcp->nrecvd += 1;
memcpy(tcp->test_pkts[recv_buf.pnum].tss, recv_buf.tss,
sizeof(recv_buf.tss));
tcp->test_pkts[recv_buf.pnum].lost = 0;
return (recv_buf.pnum);
}
static void
test_server(struct test_ctx *tcp)
{
int i, j;
for (i = 0; i < NPKTS; i++) {
send_pkt(tcp, i, 0, __FUNCTION__);
j = recv_pkt(tcp, 0, __FUNCTION__, SRECV_TIMEOUT);
if (j < 0) {
warnx("packet %d is lost", i);
/* timeout */
continue;
}
}
}
static void
test_client(struct test_ctx *tcp)
{
int i, j;
for (i = 0; i < NPKTS; i++) {
j = recv_pkt(tcp, 1, __FUNCTION__, RRECV_TIMEOUT);
if (j < 0) {
/* timeout */
return;
}
#if defined(SIMULATE_PLOSS)
if ((i % 99) == 0) {
warnx("dropping packet %d", i);
continue;
}
#endif
send_pkt(tcp, j, 1, __FUNCTION__);
}
}
static void
calc_rtt(struct test_pkt *tpp, struct rtt *rttp)
{
timespecsub(&tpp->tss[1].recvd, &tpp->tss[0].sent, &rttp->a2b);
timespecsub(&tpp->tss[0].recvd, &tpp->tss[1].sent, &rttp->b2a);
timespecadd(&rttp->a2b, &rttp->b2a, &rttp->a2b_b2a);
timespecsub(&tpp->tss[0].recvd, &tpp->tss[0].sent, &rttp->e2e);
}
static void
test_run(int ts_type, int use_ipv6, int use_recvmsg, const char *name)
{
struct test_ctx test_ctx;
pid_t pid, cpid;
int i, j, status;
printf("Testing %s via %s: ", name, (use_ipv6 == 0) ? "IPv4" : "IPv6");
fflush(stdout);
bzero(&test_ctx, sizeof(test_ctx));
test_ctx.name = name;
test_ctx.use_recvmsg = use_recvmsg;
test_ctx.ts_type = ts_type;
if (use_ipv6 == 0) {
setup_udp(&test_ctx);
} else {
setup_udp6(&test_ctx);
}
for (i = 0; i < NPKTS; i++) {
test_ctx.test_pkts[i].pnum = i;
test_ctx.test_pkts[i].lost = 1;
for (j = 0; j < PKT_SIZE; j++) {
test_ctx.test_pkts[i].data[j] = (unsigned char)random();
}
}
cpid = fork();
if (cpid < 0) {
err(1, "%s: fork()", test_ctx.name);
}
if (cpid == 0) {
test_client(&test_ctx);
exit(0);
}
test_server(&test_ctx);
pid = waitpid(cpid, &status, 0);
if (pid == (pid_t)-1) {
err(1, "%s: waitpid(%d)", test_ctx.name, cpid);
}
if (WIFEXITED(status)) {
if (WEXITSTATUS(status) != EXIT_SUCCESS) {
errx(1, "client exit status is %d",
WEXITSTATUS(status));
}
} else {
if (WIFSIGNALED(status))
errx(1, "abnormal termination of client, signal %d%s",
WTERMSIG(status), WCOREDUMP(status) ?
" (core file generated)" : "");
else
errx(1, "termination of client, unknown status");
}
if (test_ctx.nrecvd < MIN_NRECV) {
errx(1, "packet loss is too high %d received out of %d, min %d",
test_ctx.nrecvd, test_ctx.nsent, MIN_NRECV);
}
for (i = 0; i < NPKTS; i++) {
struct rtt rtt;
if (test_ctx.test_pkts[i].lost != 0) {
continue;
}
calc_rtt(&test_ctx.test_pkts[i], &rtt);
if (!timespeccmp(&rtt.e2e, &rtt.a2b_b2a, >))
errx(1, "end-to-end trip time is too small");
if (!timespeccmp(&rtt.e2e, &max_ts, <))
errx(1, "end-to-end trip time is too large");
if (!timespeccmp(&rtt.a2b, &zero_ts, >))
errx(1, "A2B trip time is not positive");
if (!timespeccmp(&rtt.b2a, &zero_ts, >))
errx(1, "B2A trip time is not positive");
}
teardown_udp(&test_ctx);
}
int
main(void)
{
int i;
srandomdev();
for (i = 0; i < 2; i++) {
test_run(0, i, 0, "send()/recv()");
printf("OK\n");
test_run(TT_TIMESTAMP, i, 1,
"send()/recvmsg(), setsockopt(SO_TIMESTAMP, 1)");
printf("OK\n");
if (i == 0) {
test_run(TT_BINTIME, i, 1,
"send()/recvmsg(), setsockopt(SO_BINTIME, 1)");
printf("OK\n");
}
test_run(TT_REALTIME_MICRO, i, 1,
"send()/recvmsg(), setsockopt(SO_TS_CLOCK, SO_TS_REALTIME_MICRO)");
printf("OK\n");
test_run(TT_TS_BINTIME, i, 1,
"send()/recvmsg(), setsockopt(SO_TS_CLOCK, SO_TS_BINTIME)");
printf("OK\n");
test_run(TT_REALTIME, i, 1,
"send()/recvmsg(), setsockopt(SO_TS_CLOCK, SO_TS_REALTIME)");
printf("OK\n");
test_run(TT_MONOTONIC, i, 1,
"send()/recvmsg(), setsockopt(SO_TS_CLOCK, SO_TS_MONOTONIC)");
printf("OK\n");
}
exit(0);
}