| /* |
| * Copyright (C) 2010-2022 Willy Tarreau <w@1wt.eu> |
| * |
| * Permission is hereby granted, free of charge, to any person obtaining |
| * a copy of this software and associated documentation files (the |
| * "Software"), to deal in the Software without restriction, including |
| * without limitation the rights to use, copy, modify, merge, publish, |
| * distribute, sublicense, and/or sell copies of the Software, and to |
| * permit persons to whom the Software is furnished to do so, subject to |
| * the following conditions: |
| * |
| * The above copyright notice and this permission notice shall be |
| * included in all copies or substantial portions of the Software. |
| * |
| * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, |
| * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES |
| * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND |
| * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT |
| * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, |
| * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING |
| * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR |
| * OTHER DEALINGS IN THE SOFTWARE. |
| */ |
| |
| #include <stdio.h> |
| #include <stdlib.h> |
| #include <unistd.h> |
| #include <string.h> |
| #include <ctype.h> |
| #include <sys/time.h> |
| #include <sys/types.h> |
| #include <sys/socket.h> |
| #include <netinet/tcp.h> |
| #include <netinet/in.h> |
| #include <arpa/inet.h> |
| #include <netdb.h> |
| #include <fcntl.h> |
| #include <errno.h> |
| #include <getopt.h> |
| #include <signal.h> |
| #include <stdarg.h> |
| #include <sys/stat.h> |
| #include <time.h> |
| #include <limits.h> |
| #include <poll.h> |
| #include <stdio.h> |
| #include <stdlib.h> |
| #include <stdarg.h> |
| |
| #define MAXCONN 1 |
| |
| const int zero = 0; |
| const int one = 1; |
| |
| struct conn { |
| struct sockaddr_storage cli_addr; |
| int fd_bck; |
| }; |
| |
| struct errmsg { |
| char *msg; |
| int size; |
| int len; |
| }; |
| |
| struct sockaddr_storage frt_addr; // listen address |
| struct sockaddr_storage srv_addr; // server address |
| |
| #define MAXPKTSIZE 16384 |
| #define MAXREORDER 20 |
| char trash[MAXPKTSIZE]; |
| |
| /* history buffer, to resend random packets */ |
| struct { |
| char buf[MAXPKTSIZE]; |
| size_t len; |
| } history[MAXREORDER]; |
| int history_idx = 0; |
| unsigned int rand_rate = 0; |
| unsigned int corr_rate = 0; |
| unsigned int corr_span = 1; |
| unsigned int corr_base = 0; |
| |
| struct conn conns[MAXCONN]; // sole connection for now |
| int fd_frt; |
| |
| int nbfd = 0; |
| int nbconn = MAXCONN; |
| |
| |
| /* display the message and exit with the code */ |
| __attribute__((noreturn)) void die(int code, const char *format, ...) |
| { |
| va_list args; |
| |
| va_start(args, format); |
| vfprintf(stderr, format, args); |
| va_end(args); |
| exit(code); |
| } |
| |
| /* Xorshift RNG */ |
| unsigned int prng_state = ~0U/3; // half bits set, but any seed will fit |
| static inline unsigned int prng(unsigned int range) |
| { |
| unsigned int x = prng_state; |
| |
| x ^= x << 13; |
| x ^= x >> 17; |
| x ^= x << 5; |
| prng_state = x; |
| return ((unsigned long long)x * (range - 1) + x) >> 32; |
| } |
| |
| /* converts str in the form [<ipv4>|<ipv6>|<hostname>]:port to struct sockaddr_storage. |
| * Returns < 0 with err set in case of error. |
| */ |
| int addr_to_ss(char *str, struct sockaddr_storage *ss, struct errmsg *err) |
| { |
| char *port_str; |
| int port; |
| |
| /* look for the addr/port delimiter, it's the last colon. */ |
| if ((port_str = strrchr(str, ':')) == NULL) |
| port_str = str; |
| else |
| *port_str++ = 0; |
| |
| port = atoi(port_str); |
| if (port <= 0 || port > 65535) { |
| err->len = snprintf(err->msg, err->size, "Missing/invalid port number: '%s'\n", port_str); |
| return -1; |
| } |
| *port_str = 0; // present an empty address if none was set |
| |
| memset(ss, 0, sizeof(*ss)); |
| |
| if (strrchr(str, ':') != NULL) { |
| /* IPv6 address contains ':' */ |
| ss->ss_family = AF_INET6; |
| ((struct sockaddr_in6 *)ss)->sin6_port = htons(port); |
| |
| if (!inet_pton(ss->ss_family, str, &((struct sockaddr_in6 *)ss)->sin6_addr)) { |
| err->len = snprintf(err->msg, err->size, "Invalid IPv6 server address: '%s'", str); |
| return -1; |
| } |
| } |
| else { |
| ss->ss_family = AF_INET; |
| ((struct sockaddr_in *)ss)->sin_port = htons(port); |
| |
| if (*str == '*' || *str == '\0') { /* INADDR_ANY */ |
| ((struct sockaddr_in *)ss)->sin_addr.s_addr = INADDR_ANY; |
| return 0; |
| } |
| |
| if (!inet_pton(ss->ss_family, str, &((struct sockaddr_in *)ss)->sin_addr)) { |
| struct hostent *he = gethostbyname(str); |
| |
| if (he == NULL) { |
| err->len = snprintf(err->msg, err->size, "Invalid IPv4 server name: '%s'", str); |
| return -1; |
| } |
| ((struct sockaddr_in *)ss)->sin_addr = *(struct in_addr *) *(he->h_addr_list); |
| } |
| } |
| return 0; |
| } |
| |
| /* returns <0 with err in case of error or the front FD */ |
| int create_udp_listener(struct sockaddr_storage *addr, struct errmsg *err) |
| { |
| int fd; |
| |
| if ((fd = socket(addr->ss_family, SOCK_DGRAM, 0)) == -1) { |
| err->len = snprintf(err->msg, err->size, "socket(): '%s'", strerror(errno)); |
| goto fail; |
| } |
| |
| if (fcntl(fd, F_SETFL, O_NONBLOCK) == -1) { |
| err->len = snprintf(err->msg, err->size, "fcntl(O_NONBLOCK): '%s'", strerror(errno)); |
| goto fail; |
| } |
| |
| if (setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, (char *) &one, sizeof(one)) == -1) { |
| err->len = snprintf(err->msg, err->size, "setsockopt(SO_REUSEADDR): '%s'", strerror(errno)); |
| goto fail; |
| } |
| |
| #ifdef SO_REUSEPORT |
| if (setsockopt(fd, SOL_SOCKET, SO_REUSEPORT, (char *) &one, sizeof(one)) == -1) { |
| err->len = snprintf(err->msg, err->size, "setsockopt(SO_REUSEPORT): '%s'", strerror(errno)); |
| goto fail; |
| } |
| #endif |
| if (bind(fd, (struct sockaddr *)addr, addr->ss_family == AF_INET6 ? |
| sizeof(struct sockaddr_in6) : sizeof(struct sockaddr_in)) == -1) { |
| err->len = snprintf(err->msg, err->size, "bind(): '%s'", strerror(errno)); |
| goto fail; |
| } |
| |
| /* the socket is ready */ |
| return fd; |
| |
| fail: |
| if (fd > -1) |
| close(fd); |
| fd = -1; |
| return fd; |
| } |
| |
| /* recompute pollfds using frt_fd and scanning nbconn connections. |
| * Returns the number of FDs in the set. |
| */ |
| int update_pfd(struct pollfd *pfd, int frt_fd, struct conn *conns, int nbconn) |
| { |
| int nbfd = 0; |
| int i; |
| |
| pfd[nbfd].fd = frt_fd; |
| pfd[nbfd].events = POLLIN; |
| nbfd++; |
| |
| for (i = 0; i < nbconn; i++) { |
| if (conns[i].fd_bck < 0) |
| continue; |
| pfd[nbfd].fd = conns[i].fd_bck; |
| pfd[nbfd].events = POLLIN; |
| nbfd++; |
| } |
| return nbfd; |
| } |
| |
| /* searches a connection using fd <fd> as back connection, returns it if found |
| * otherwise NULL. |
| */ |
| struct conn *conn_bck_lookup(struct conn *conns, int nbconn, int fd) |
| { |
| int i; |
| |
| for (i = 0; i < nbconn; i++) { |
| if (conns[i].fd_bck < 0) |
| continue; |
| if (conns[i].fd_bck == fd) |
| return &conns[i]; |
| } |
| return NULL; |
| } |
| |
| /* Try to establish a connection to <sa>. Return the fd or -1 in case of error */ |
| int add_connection(struct sockaddr_storage *ss) |
| { |
| int fd; |
| |
| fd = socket(ss->ss_family, SOCK_DGRAM, 0); |
| if (fd < 0) |
| goto fail; |
| |
| if (fcntl(fd, F_SETFL, O_NONBLOCK) == -1) |
| goto fail; |
| |
| if (setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &one, sizeof(one)) == -1) |
| goto fail; |
| |
| if (connect(fd, (struct sockaddr *)ss, ss->ss_family == AF_INET6 ? |
| sizeof(struct sockaddr_in6) : sizeof(struct sockaddr_in)) == -1) { |
| if (errno != EINPROGRESS) |
| goto fail; |
| } |
| |
| return fd; |
| fail: |
| if (fd > -1) |
| close(fd); |
| return -1; |
| } |
| |
| /* Corrupt <buf> buffer with <buflen> as length if required */ |
| static void pktbuf_apply_corruption(char *buf, size_t buflen) |
| { |
| if (corr_rate > 0 && prng(100) < corr_rate) { |
| unsigned int rnd = prng(corr_span * 256); // pos and value |
| unsigned int pos = corr_base + (rnd >> 8); |
| |
| if (pos < buflen) |
| buf[pos] ^= rnd; |
| } |
| } |
| |
| /* Handle a read operation on an front FD. Will either reuse the existing |
| * connection if the source is found, or will allocate a new one, possibly |
| * replacing the oldest one. Returns <0 on error or the number of bytes |
| * transmitted. |
| */ |
| int handle_frt(int fd, struct pollfd *pfd, struct conn *conns, int nbconn) |
| { |
| struct sockaddr_storage addr; |
| socklen_t addrlen; |
| struct conn *conn; |
| char *pktbuf = trash; |
| int ret; |
| int i; |
| |
| if (rand_rate > 0) { |
| /* keep a copy of this packet */ |
| history_idx++; |
| if (history_idx >= MAXREORDER) |
| history_idx = 0; |
| pktbuf = history[history_idx].buf; |
| } |
| |
| addrlen = sizeof(addr); |
| ret = recvfrom(fd, pktbuf, MAXPKTSIZE, MSG_DONTWAIT | MSG_NOSIGNAL, |
| (struct sockaddr *)&addr, &addrlen); |
| |
| if (rand_rate > 0) { |
| history[history_idx].len = ret; // note: we may store -1/EAGAIN |
| if (prng(100) < rand_rate) { |
| /* return a random buffer or nothing */ |
| int idx = prng(MAXREORDER + 1) - 1; |
| if (idx < 0) { |
| /* pretend we didn't receive anything */ |
| return 0; |
| } |
| pktbuf = history[idx].buf; |
| ret = history[idx].len; |
| if (ret < 0) |
| errno = EAGAIN; |
| } |
| } |
| |
| if (ret == 0) |
| return 0; |
| |
| if (ret < 0) |
| return errno == EAGAIN ? 0 : -1; |
| |
| pktbuf_apply_corruption(pktbuf, ret); |
| |
| conn = NULL; |
| for (i = 0; i < nbconn; i++) { |
| if (addr.ss_family != conns[i].cli_addr.ss_family) |
| continue; |
| if (memcmp(&conns[i].cli_addr, &addr, |
| (addr.ss_family == AF_INET6) ? |
| sizeof(struct sockaddr_in6) : |
| sizeof(struct sockaddr_in)) != 0) |
| continue; |
| conn = &conns[i]; |
| break; |
| } |
| |
| if (!conn) { |
| /* address not found, create a new conn or replace the oldest |
| * one. For now we support a single one. |
| */ |
| conn = &conns[0]; |
| |
| memcpy(&conn->cli_addr, &addr, |
| (addr.ss_family == AF_INET6) ? |
| sizeof(struct sockaddr_in6) : |
| sizeof(struct sockaddr_in)); |
| |
| if (conn->fd_bck < 0) { |
| /* try to create a new connection */ |
| conn->fd_bck = add_connection(&srv_addr); |
| nbfd = update_pfd(pfd, fd, conns, nbconn); // FIXME: MAXCONN instead ? |
| } |
| } |
| |
| if (conn->fd_bck < 0) |
| return 0; |
| |
| ret = send(conn->fd_bck, pktbuf, ret, MSG_DONTWAIT | MSG_NOSIGNAL); |
| return ret; |
| } |
| |
| /* Handle a read operation on an FD. Close and return 0 when the read returns zero or an error */ |
| int handle_bck(int fd, struct pollfd *pfd, struct conn *conns, int nbconn) |
| { |
| struct sockaddr_storage addr; |
| socklen_t addrlen; |
| struct conn *conn; |
| char *pktbuf = trash; |
| int ret; |
| |
| if (rand_rate > 0) { |
| /* keep a copy of this packet */ |
| history_idx++; |
| if (history_idx >= MAXREORDER) |
| history_idx = 0; |
| pktbuf = history[history_idx].buf; |
| } |
| |
| ret = recvfrom(fd, pktbuf, MAXPKTSIZE, MSG_DONTWAIT | MSG_NOSIGNAL, |
| (struct sockaddr *)&addr, &addrlen); |
| |
| if (rand_rate > 0) { |
| history[history_idx].len = ret; // note: we may store -1/EAGAIN |
| if (prng(100) < rand_rate) { |
| /* return a random buffer or nothing */ |
| int idx = prng(MAXREORDER + 1) - 1; |
| if (idx < 0) { |
| /* pretend we didn't receive anything */ |
| return 0; |
| } |
| pktbuf = history[idx].buf; |
| ret = history[idx].len; |
| if (ret < 0) |
| errno = EAGAIN; |
| } |
| } |
| |
| if (ret == 0) |
| return 0; |
| |
| if (ret < 0) |
| return errno == EAGAIN ? 0 : -1; |
| |
| pktbuf_apply_corruption(pktbuf, ret); |
| |
| conn = conn_bck_lookup(conns, nbconn, fd); |
| if (!conn) |
| return 0; |
| |
| ret = sendto(fd_frt, pktbuf, ret, MSG_DONTWAIT | MSG_NOSIGNAL, |
| (struct sockaddr *)&conn->cli_addr, |
| conn->cli_addr.ss_family == AF_INET6 ? |
| sizeof(struct sockaddr_in6) : sizeof(struct sockaddr_in)); |
| return ret; |
| } |
| |
| /* print the usage message for program named <name> and exit with status <status> */ |
| void usage(int status, const char *name) |
| { |
| if (strchr(name, '/')) |
| name = strrchr(name, '/') + 1; |
| die(status, |
| "Usage: %s [-h] [options] [<laddr>:]<lport> [<saddr>:]<sport>\n" |
| "Options:\n" |
| " -h display this help\n" |
| " -r rate reorder/duplicate/lose around <rate>%% of packets\n" |
| " -s seed force initial random seed (currently %#x)\n" |
| " -c rate corrupt around <rate>%% of packets\n" |
| " -o ofs start offset of corrupted area (def: 0)\n" |
| " -w width width of the corrupted area (def: 1)\n" |
| "", name, prng_state); |
| } |
| |
| int main(int argc, char **argv) |
| { |
| struct errmsg err; |
| struct pollfd *pfd; |
| int opt; |
| int i; |
| |
| err.len = 0; |
| err.size = 100; |
| err.msg = malloc(err.size); |
| |
| while ((opt = getopt(argc, argv, "hr:s:c:o:w:")) != -1) { |
| switch (opt) { |
| case 'r': // rand_rate% |
| rand_rate = atoi(optarg); |
| break; |
| case 's': // seed |
| prng_state = atol(optarg); |
| break; |
| case 'c': // corruption rate |
| corr_rate = atol(optarg); |
| break; |
| case 'o': // corruption offset |
| corr_base = atol(optarg); |
| break; |
| case 'w': // corruption width |
| corr_span = atol(optarg); |
| break; |
| default: // help, anything else |
| usage(0, argv[0]); |
| } |
| } |
| |
| if (argc - optind < 2) |
| usage(1, argv[0]); |
| |
| if (addr_to_ss(argv[optind], &frt_addr, &err) < 0) |
| die(1, "parsing listen address: %s\n", err.msg); |
| |
| if (addr_to_ss(argv[optind+1], &srv_addr, &err) < 0) |
| die(1, "parsing server address: %s\n", err.msg); |
| |
| pfd = calloc(sizeof(struct pollfd), MAXCONN + 1); |
| if (!pfd) |
| die(1, "out of memory\n"); |
| |
| fd_frt = create_udp_listener(&frt_addr, &err); |
| if (fd_frt < 0) |
| die(1, "binding listener: %s\n", err.msg); |
| |
| |
| for (i = 0; i < MAXCONN; i++) |
| conns[i].fd_bck = -1; |
| |
| nbfd = update_pfd(pfd, fd_frt, conns, MAXCONN); |
| |
| while (1) { |
| /* listen for incoming packets */ |
| int ret, i; |
| |
| ret = poll(pfd, nbfd, 1000); |
| if (ret <= 0) |
| continue; |
| |
| for (i = 0; ret; i++) { |
| if (!pfd[i].revents) |
| continue; |
| ret--; |
| |
| if (pfd[i].fd == fd_frt) { |
| handle_frt(pfd[i].fd, pfd, conns, nbconn); |
| continue; |
| } |
| |
| handle_bck(pfd[i].fd, pfd, conns, nbconn); |
| } |
| } |
| } |