#include <err.h>
#include <port.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netinet/sctp.h>
#include <arpa/inet.h>
#include <sys/sysmacros.h>
#include <stdbool.h>
#include <unistd.h>
#include <string.h>
#include <errno.h>
#include <sys/debug.h>
#define TTL_IPPROTO_EXP 0xfd
static hrtime_t tt_sock_to = MSEC2NSEC(100);
static const uint32_t tt_msg = 0x7777;
typedef enum {
TTL_SENDRECV,
TTL_NOCONNECT,
TTL_NODATA
} ttl_pass_t;
typedef struct {
const char *tt_desc;
int tt_domain;
int tt_type;
int tt_ttl;
int tt_proto;
int tt_minttl;
ttl_pass_t tt_pass;
} ttl_test_t;
static const ttl_test_t ttl_tests[] = {
{
.tt_desc = "IPv4 TCP TTL/MIN: unset/0",
.tt_domain = PF_INET,
.tt_type = SOCK_STREAM,
.tt_ttl = 0,
.tt_minttl = 0,
.tt_pass = TTL_SENDRECV
}, {
.tt_desc = "IPv4 TCP TTL/MIN: 200/100",
.tt_domain = PF_INET,
.tt_type = SOCK_STREAM,
.tt_ttl = 200,
.tt_minttl = 100,
.tt_pass = TTL_SENDRECV
}, {
.tt_desc = "IPv4 TCP TTL/MIN: 255/255",
.tt_domain = PF_INET,
.tt_type = SOCK_STREAM,
.tt_ttl = 255,
.tt_minttl = 255,
.tt_pass = TTL_SENDRECV
}, {
.tt_desc = "IPv4 TCP TTL/MIN: 23/169",
.tt_domain = PF_INET,
.tt_type = SOCK_STREAM,
.tt_ttl = 23,
.tt_minttl = 169,
.tt_pass = TTL_NOCONNECT
}, {
.tt_desc = "IPv4 TCP TTL/MIN: 254/255",
.tt_domain = PF_INET,
.tt_type = SOCK_STREAM,
.tt_ttl = 254,
.tt_minttl = 255,
.tt_pass = TTL_NOCONNECT
}, {
.tt_desc = "IPv4 UDP TTL/MIN: unset/0",
.tt_domain = PF_INET,
.tt_type = SOCK_DGRAM,
.tt_ttl = 0,
.tt_minttl = 0,
.tt_pass = TTL_SENDRECV
}, {
.tt_desc = "IPv4 UDP TTL/MIN: 200/100",
.tt_domain = PF_INET,
.tt_type = SOCK_DGRAM,
.tt_ttl = 200,
.tt_minttl = 100,
.tt_pass = TTL_SENDRECV
}, {
.tt_desc = "IPv4 UDP TTL/MIN: 255/255",
.tt_domain = PF_INET,
.tt_type = SOCK_DGRAM,
.tt_ttl = 255,
.tt_minttl = 255,
.tt_pass = TTL_SENDRECV
}, {
.tt_desc = "IPv4 UDP TTL/MIN: 23/169",
.tt_domain = PF_INET,
.tt_type = SOCK_DGRAM,
.tt_ttl = 23,
.tt_minttl = 169,
.tt_pass = TTL_NODATA
}, {
.tt_desc = "IPv4 UDP TTL/MIN: 254/255",
.tt_domain = PF_INET,
.tt_type = SOCK_DGRAM,
.tt_ttl = 254,
.tt_minttl = 255,
.tt_pass = TTL_NODATA
}, {
.tt_desc = "IPv4 SCTP TTL/MIN: unset/0",
.tt_domain = PF_INET,
.tt_type = SOCK_STREAM,
.tt_proto = IPPROTO_SCTP,
.tt_ttl = 0,
.tt_minttl = 0,
.tt_pass = TTL_SENDRECV
}, {
.tt_desc = "IPv4 SCTP TTL/MIN: 200/100",
.tt_domain = PF_INET,
.tt_type = SOCK_STREAM,
.tt_proto = IPPROTO_SCTP,
.tt_ttl = 200,
.tt_minttl = 100,
.tt_pass = TTL_SENDRECV
}, {
.tt_desc = "IPv4 SCTP TTL/MIN: 255/255",
.tt_domain = PF_INET,
.tt_type = SOCK_STREAM,
.tt_proto = IPPROTO_SCTP,
.tt_ttl = 255,
.tt_minttl = 255,
.tt_pass = TTL_SENDRECV
}, {
.tt_desc = "IPv4 SCTP TTL/MIN: 23/169",
.tt_domain = PF_INET,
.tt_type = SOCK_STREAM,
.tt_proto = IPPROTO_SCTP,
.tt_ttl = 23,
.tt_minttl = 169,
.tt_pass = TTL_NOCONNECT
}, {
.tt_desc = "IPv4 SCTP TTL/MIN: 254/255",
.tt_domain = PF_INET,
.tt_type = SOCK_STREAM,
.tt_proto = IPPROTO_SCTP,
.tt_ttl = 254,
.tt_minttl = 255,
.tt_pass = TTL_NOCONNECT
}, {
.tt_desc = "IPv4 RAW (0xfd) TTL/MIN: unset/0",
.tt_domain = PF_INET,
.tt_type = SOCK_RAW,
.tt_proto = TTL_IPPROTO_EXP,
.tt_ttl = 0,
.tt_minttl = 0,
.tt_pass = TTL_SENDRECV
}, {
.tt_desc = "IPv4 RAW (0xfd) TTL/MIN: 200/100",
.tt_domain = PF_INET,
.tt_type = SOCK_RAW,
.tt_proto = TTL_IPPROTO_EXP,
.tt_ttl = 200,
.tt_minttl = 100,
.tt_pass = TTL_SENDRECV
}, {
.tt_desc = "IPv4 RAW (0xfd) TTL/MIN: 255/255",
.tt_domain = PF_INET,
.tt_type = SOCK_RAW,
.tt_proto = TTL_IPPROTO_EXP,
.tt_ttl = 255,
.tt_minttl = 255,
.tt_pass = TTL_SENDRECV
}, {
.tt_desc = "IPv4 RAW (0xfd) TTL/MIN: 23/169",
.tt_domain = PF_INET,
.tt_type = SOCK_RAW,
.tt_proto = TTL_IPPROTO_EXP,
.tt_ttl = 23,
.tt_minttl = 169,
.tt_pass = TTL_NODATA
}, {
.tt_desc = "IPv4 RAW (0xfd) TTL/MIN: 254/255",
.tt_domain = PF_INET,
.tt_type = SOCK_RAW,
.tt_proto = TTL_IPPROTO_EXP,
.tt_ttl = 254,
.tt_minttl = 255,
.tt_pass = TTL_NODATA
}, {
.tt_desc = "IPv6 TCP TTL/MIN: unset/0",
.tt_domain = PF_INET6,
.tt_type = SOCK_STREAM,
.tt_ttl = 0,
.tt_minttl = 0,
.tt_pass = TTL_SENDRECV
}, {
.tt_desc = "IPv6 TCP TTL/MIN: 200/100",
.tt_domain = PF_INET6,
.tt_type = SOCK_STREAM,
.tt_ttl = 200,
.tt_minttl = 100,
.tt_pass = TTL_SENDRECV
}, {
.tt_desc = "IPv6 TCP TTL/MIN: 255/255",
.tt_domain = PF_INET6,
.tt_type = SOCK_STREAM,
.tt_ttl = 255,
.tt_minttl = 255,
.tt_pass = TTL_SENDRECV
}, {
.tt_desc = "IPv6 CTP TTL/MIN: 23/169",
.tt_domain = PF_INET6,
.tt_type = SOCK_STREAM,
.tt_ttl = 23,
.tt_minttl = 169,
.tt_pass = TTL_NOCONNECT
}, {
.tt_desc = "IPv6 CTP TTL/MIN: 254/255",
.tt_domain = PF_INET6,
.tt_type = SOCK_STREAM,
.tt_ttl = 254,
.tt_minttl = 255,
.tt_pass = TTL_NOCONNECT
}, {
.tt_desc = "IPv6 UDP TTL/MIN: unset/0",
.tt_domain = PF_INET6,
.tt_type = SOCK_DGRAM,
.tt_ttl = 0,
.tt_minttl = 0,
.tt_pass = TTL_SENDRECV
}, {
.tt_desc = "IPv6 UDP TTL/MIN: 200/100",
.tt_domain = PF_INET6,
.tt_type = SOCK_DGRAM,
.tt_ttl = 200,
.tt_minttl = 100,
.tt_pass = TTL_SENDRECV
}, {
.tt_desc = "IPv6 UDP TTL/MIN: 255/255",
.tt_domain = PF_INET6,
.tt_type = SOCK_DGRAM,
.tt_ttl = 255,
.tt_minttl = 255,
.tt_pass = TTL_SENDRECV
}, {
.tt_desc = "IPv6 UDP TTL/MIN: 23/169",
.tt_domain = PF_INET6,
.tt_type = SOCK_DGRAM,
.tt_ttl = 23,
.tt_minttl = 169,
.tt_pass = TTL_NODATA
}, {
.tt_desc = "IPv6 UDP TTL/MIN: 254/255",
.tt_domain = PF_INET6,
.tt_type = SOCK_DGRAM,
.tt_ttl = 254,
.tt_minttl = 255,
.tt_pass = TTL_NODATA
}, {
.tt_desc = "IPv6 SCTP TTL/MIN: unset/0",
.tt_domain = PF_INET6,
.tt_type = SOCK_STREAM,
.tt_proto = IPPROTO_SCTP,
.tt_ttl = 0,
.tt_minttl = 0,
.tt_pass = TTL_SENDRECV
}, {
.tt_desc = "IPv6 SCTP TTL/MIN: 200/100",
.tt_domain = PF_INET6,
.tt_type = SOCK_STREAM,
.tt_proto = IPPROTO_SCTP,
.tt_ttl = 200,
.tt_minttl = 100,
.tt_pass = TTL_SENDRECV
}, {
.tt_desc = "IPv6 SCTP TTL/MIN: 255/255",
.tt_domain = PF_INET6,
.tt_type = SOCK_STREAM,
.tt_proto = IPPROTO_SCTP,
.tt_ttl = 255,
.tt_minttl = 255,
.tt_pass = TTL_SENDRECV
}, {
.tt_desc = "IPv6 SCTP TTL/MIN: 23/169",
.tt_domain = PF_INET6,
.tt_type = SOCK_STREAM,
.tt_proto = IPPROTO_SCTP,
.tt_ttl = 23,
.tt_minttl = 169,
.tt_pass = TTL_NOCONNECT
}, {
.tt_desc = "IPv6 SCTP TTL/MIN: 254/255",
.tt_domain = PF_INET6,
.tt_type = SOCK_STREAM,
.tt_proto = IPPROTO_SCTP,
.tt_ttl = 254,
.tt_minttl = 255,
.tt_pass = TTL_NOCONNECT
}, {
.tt_desc = "IPv6 RAW (0xfd) TTL/MIN: unset/0",
.tt_domain = PF_INET6,
.tt_type = SOCK_RAW,
.tt_proto = TTL_IPPROTO_EXP,
.tt_ttl = 0,
.tt_minttl = 0,
.tt_pass = TTL_SENDRECV
}, {
.tt_desc = "IPv6 RAW (0xfd) TTL/MIN: 200/100",
.tt_domain = PF_INET6,
.tt_type = SOCK_RAW,
.tt_proto = TTL_IPPROTO_EXP,
.tt_ttl = 200,
.tt_minttl = 100,
.tt_pass = TTL_SENDRECV
}, {
.tt_desc = "IPv6 RAW (0xfd) TTL/MIN: 255/255",
.tt_domain = PF_INET6,
.tt_type = SOCK_RAW,
.tt_proto = TTL_IPPROTO_EXP,
.tt_ttl = 255,
.tt_minttl = 255,
.tt_pass = TTL_SENDRECV
}, {
.tt_desc = "IPv6 RAW (0xfd) TTL/MIN: 23/169",
.tt_domain = PF_INET6,
.tt_type = SOCK_RAW,
.tt_proto = TTL_IPPROTO_EXP,
.tt_ttl = 23,
.tt_minttl = 169,
.tt_pass = TTL_NODATA
}, {
.tt_desc = "IPv6 RAW (0xfd) TTL/MIN: 254/255",
.tt_domain = PF_INET6,
.tt_type = SOCK_RAW,
.tt_proto = TTL_IPPROTO_EXP,
.tt_ttl = 254,
.tt_minttl = 255,
.tt_pass = TTL_NODATA
}
};
static bool
ttl_bind_dest(const ttl_test_t *test, int sock, struct sockaddr_storage *dst)
{
socklen_t len;
struct sockaddr_storage addr;
(void) memset(&addr, 0, sizeof (struct sockaddr_storage));
if (test->tt_domain == PF_INET) {
struct sockaddr_in *in = (struct sockaddr_in *)&addr;
in->sin_family = AF_INET;
in->sin_port = htons(0);
if (inet_pton(AF_INET, "127.0.0.1", &in->sin_addr) != 1) {
warnx("TEST FAILED: %s: failed to convert 127.0.0.1 "
"to an IPv4 address", test->tt_desc);
return (false);
}
len = sizeof (struct sockaddr_in);
} else {
struct sockaddr_in6 *in6 = (struct sockaddr_in6 *)&addr;
in6->sin6_family = AF_INET6;
in6->sin6_port = htons(0);
if (inet_pton(AF_INET6, "::1", &in6->sin6_addr) != 1) {
warnx("TEST FAILED: %s: failed to convert ::1 "
"to an IPv6 address", test->tt_desc);
return (false);
}
len = sizeof (struct sockaddr_in6);
}
if (bind(sock, (struct sockaddr *)&addr, len) != 0) {
warn("TEST FAILED: %s: failed to bind listen socket",
test->tt_desc);
return (false);
}
len = sizeof (struct sockaddr_storage);
if (getsockname(sock, (struct sockaddr *)dst, &len) != 0) {
warn("TEST FAILED: %s: failed to retrieve socket address ",
test->tt_desc);
return (false);
}
return (true);
}
static bool
ttl_connect(const ttl_test_t *test, int port, int src, int dst, int *cfd,
const struct sockaddr *addr)
{
struct timespec to = { .tv_nsec = tt_sock_to };
int namelen = test->tt_domain == PF_INET ? sizeof (struct sockaddr_in) :
sizeof (struct sockaddr_in6);
int conn;
port_event_t pe;
if (listen(dst, 5) != 0) {
warn("TEST FAILED: %s: failed to listen", test->tt_desc);
return (false);
}
if (connect(src, addr, namelen) != 0 && errno != EINPROGRESS) {
warn("TEST FAILED: %s: failed to connect", test->tt_desc);
return (false);
}
if (port_associate(port, PORT_SOURCE_FD, src, POLLOUT, NULL) != 0) {
err(EXIT_FAILURE, "INTERNAL TEST FAILURE: %s: could not port "
"associate to watch connect", test->tt_desc);
}
if (port_get(port, &pe, &to) != 0) {
if (test->tt_pass == TTL_NOCONNECT) {
(void) printf("TEST PASSED: %s: correctly failed to "
"connect\n", test->tt_desc);
return (true);
} else {
warn("TEST FAILED: %s: timed out waiting to connect",
test->tt_desc);
return (false);
}
}
if ((pe.portev_events & POLLOUT) == 0) {
warnx("TEST FAILED: %s: connect port event doesn't contain "
"POLLOUT, found 0x%x", test->tt_desc, pe.portev_events);
return (false);
}
if (port_associate(port, PORT_SOURCE_FD, dst, POLLIN, NULL) != 0) {
err(EXIT_FAILURE, "INTERNAL TEST FAILURE: %s: could not port "
"associate to watch accept", test->tt_desc);
}
if (port_get(port, &pe, &to) != 0) {
warn("TEST FAILED: %s: timed out waiting to accept",
test->tt_desc);
return (false);
}
if ((pe.portev_events & POLLIN) == 0) {
warnx("TEST FAILED: %s: accept port event doesn't contain "
"POLLIN, found 0x%x", test->tt_desc, pe.portev_events);
return (false);
}
conn = accept4(dst, NULL, NULL, SOCK_NONBLOCK);
if (conn < 0) {
warn("TEST FAILED: %s: failed to get client connection",
test->tt_desc);
return (false);
}
if (test->tt_pass != TTL_SENDRECV) {
warnx("TEST FAILED: %s: expected connect to fail, but passed",
test->tt_desc);
return (false);
}
*cfd = conn;
return (true);
}
static bool
ttl_check_ancil(const ttl_test_t *test, const struct msghdr *msg)
{
int level, ttlopt;
if (test->tt_domain == PF_INET) {
level = IPPROTO_IP;
ttlopt = IP_RECVTTL;
} else {
level = IPPROTO_IPV6;
ttlopt = IPV6_HOPLIMIT;
}
if (msg->msg_controllen != CMSG_SPACE(sizeof (int))) {
warnx("TEST FAILED: %s: expected %u bytes of ancillary "
"data, found %u", test->tt_desc, CMSG_SPACE(sizeof (int)),
msg->msg_controllen);
return (false);
}
for (const struct cmsghdr *cmsg = CMSG_FIRSTHDR(msg); cmsg != NULL;
cmsg = CMSG_NXTHDR(msg, cmsg)) {
int val;
if (cmsg->cmsg_level != level || cmsg->cmsg_type != ttlopt)
continue;
(void) memcpy(&val, CMSG_DATA(cmsg), sizeof (int));
if (test->tt_ttl != 0 && val != test->tt_ttl) {
warnx("TEST FAILED: %s: TTL/HLIM mismatch: expected "
"0x%x, found 0x%x", test->tt_desc, test->tt_ttl,
val);
return (false);
}
(void) printf("TEST PASSED: %s: TTL/HLIM is correct\n",
test->tt_desc);
return (true);
}
warnx("TEST FAILED: %s: failed to find TTL/HLIM in ancillary options",
test->tt_desc);
return (false);
}
static bool
ttl_sendrecv(const ttl_test_t *test, int port, int src, int dst,
struct sockaddr *addr)
{
struct timespec to = { .tv_nsec = tt_sock_to };
int namelen = test->tt_domain == PF_INET ? sizeof (struct sockaddr_in) :
sizeof (struct sockaddr_in6);
uint8_t ancil[CMSG_SPACE(sizeof (int)) * 2];
port_event_t pe;
struct msghdr msg;
uint32_t data;
struct iovec iov;
ssize_t sret;
if (sendto(src, &tt_msg, sizeof (tt_msg), MSG_NOSIGNAL, addr,
namelen) != sizeof (tt_msg)) {
warn("TEST FAILED: %s: failed to write message to socket",
test->tt_desc);
}
if (port_associate(port, PORT_SOURCE_FD, dst, POLLIN, NULL) != 0) {
err(EXIT_FAILURE, "INTERNAL TEST FAILURE: %s: could not port "
"associate to watch recv", test->tt_desc);
}
if (port_get(port, &pe, &to) != 0) {
if (test->tt_pass == TTL_NODATA) {
(void) printf("TEST PASSED: %s: timed out waiting "
"for data\n", test->tt_desc);
return (true);
} else {
warn("TEST FAILED: %s: timed out waiting to recv",
test->tt_desc);
return (false);
}
}
if ((pe.portev_events & POLLIN) == 0) {
warnx("TEST FAILED: %s: receive port event doesn't contain "
"POLLIN, found 0x%x", test->tt_desc, pe.portev_events);
return (false);
}
(void) memset(&msg, 0, sizeof (msg));
iov.iov_base = (void *)&data;
iov.iov_len = sizeof (data);
msg.msg_iov = &iov;
msg.msg_iovlen = 1;
msg.msg_control = ancil;
msg.msg_controllen = sizeof (ancil);
sret = recvmsg(dst, &msg, MSG_DONTWAIT);
if (sret != (ssize_t)sizeof (data)) {
warnx("TEST FAILED: %s: failed to receive data: %zx",
test->tt_desc, sret);
return (false);
}
if (test->tt_pass != TTL_SENDRECV) {
warnx("TEST FAILED: %s: found data, despite expecting not to",
test->tt_desc);
return (false);
}
if (data != tt_msg && test->tt_type != SOCK_RAW) {
warnx("TEST FAILED: %s: data mismatch: expected 0x%x, found "
"0x%x", test->tt_desc, tt_msg, data);
return (false);
}
if (test->tt_type == SOCK_DGRAM && !ttl_check_ancil(test, &msg)) {
return (false);
}
(void) printf("TEST PASSED: %s: Successfully received data\n",
test->tt_desc);
return (true);
}
static bool
ttl_test_one(const ttl_test_t *test)
{
int src = -1, dst = -1, cfd = -1, port = -1, tdst;
int level, ttlopt, minttlopt, recvopt, en = 1;
bool ret = true;
struct sockaddr_storage dst_addr;
if ((port = port_create()) < 0) {
err(EXIT_FAILURE, "TEST FAILED: failed to create event port");
}
src = socket(test->tt_domain, test->tt_type | SOCK_NONBLOCK,
test->tt_proto);
if (src < 0) {
warn("TEST FAILED: %s: failed to create source socket",
test->tt_desc);
ret = false;
goto cleanup;
}
dst = socket(test->tt_domain, test->tt_type | SOCK_NONBLOCK,
test->tt_proto);
if (dst < 0) {
warn("TEST FAILED: %s: failed to create destination socket",
test->tt_desc);
ret = false;
goto cleanup;
}
if (!ttl_bind_dest(test, dst, &dst_addr)) {
ret = false;
goto cleanup;
}
if (test->tt_domain == PF_INET) {
level = IPPROTO_IP;
ttlopt = IP_TTL;
minttlopt = IP_MINTTL;
recvopt = IP_RECVTTL;
} else {
level = IPPROTO_IPV6;
ttlopt = IPV6_UNICAST_HOPS;
minttlopt = IPV6_MINHOPCOUNT;
recvopt = IPV6_RECVHOPLIMIT;
}
if (test->tt_ttl > 0 && setsockopt(src, level, ttlopt, &test->tt_ttl,
sizeof (int)) != 0) {
warn("TEST FAILED: %s: failed to set TTL/HLIM to %d",
test->tt_desc, test->tt_ttl);
ret = false;
goto cleanup;
}
if (setsockopt(dst, level, minttlopt, &test->tt_minttl,
sizeof (int)) != 0) {
warn("TEST FAILED: %s: failed to set min TTL/HLIM to %d",
test->tt_desc, test->tt_minttl);
ret = false;
goto cleanup;
}
if (test->tt_type == SOCK_DGRAM && setsockopt(dst, level, recvopt, &en,
sizeof (int)) != 0) {
warn("TEST FAILED: %s failed to enable receiving the TTL",
test->tt_desc);
ret = false;
goto cleanup;
}
if (test->tt_type != SOCK_DGRAM && test->tt_type != SOCK_RAW) {
if (!ttl_connect(test, port, src, dst, &cfd,
(struct sockaddr *)&dst_addr)) {
ret = false;
goto cleanup;
}
if (test->tt_pass != TTL_SENDRECV) {
goto cleanup;
}
tdst = cfd;
} else {
tdst = dst;
}
if (!ttl_sendrecv(test, port, src, tdst,
(struct sockaddr *)&dst_addr)) {
ret = false;
goto cleanup;
}
cleanup:
if (port > -1)
(void) close(port);
if (src > -1)
(void) close(src);
if (dst > -1)
(void) close(dst);
if (cfd > -1)
(void) close(cfd);
return (ret);
}
int
main(void)
{
int ret = EXIT_SUCCESS;
for (size_t i = 0; i < ARRAY_SIZE(ttl_tests); i++) {
if (!ttl_test_one(&ttl_tests[i])) {
ret = EXIT_FAILURE;
}
}
if (ret == EXIT_SUCCESS) {
(void) printf("All tests passed successfully\n");
}
return (ret);
}