From d6171b8df956fe506044e321ad4de1588f9bd173 Mon Sep 17 00:00:00 2001 From: hiro <23hiro@gmail.com> Date: Tue, 9 Nov 2021 17:50:12 +0000 Subject: [PATCH 01/33] ipv6 udp checksums like ipv4 but with ipv6 pseudoheader this fixes a problem where both source and destination port are supplied on the commandline, this lead to packets not showing up any more due to checksum being different. this was mentioned first on https://github.com/traviscross/mtr/issues/351 --- packet/construct_unix.c | 29 ++++++++++++++++++----------- 1 file changed, 18 insertions(+), 11 deletions(-) diff --git a/packet/construct_unix.c b/packet/construct_unix.c index 644536d8..faca7689 100644 --- a/packet/construct_unix.c +++ b/packet/construct_unix.c @@ -306,18 +306,25 @@ int construct_udp6_packet( set_udp_ports(udp, probe, param); udp->length = htons(udp_size); - if (net_state->platform.ip6_socket_raw) { - /* - Instruct the kernel to put the pseudoheader checksum into the - UDP header, this is only needed when using RAW socket. - */ - int chksum_offset = (char *) &udp->checksum - (char *) udp; - if (setsockopt(udp_socket, IPPROTO_IPV6, - IPV6_CHECKSUM, &chksum_offset, sizeof(int))) { - return -1; - } - } + struct IP6PseudoHeader udph = { + .zero = {0,0,0}, + .protocol = 17, + .len = udp->length + }; + memcpy(udph.saddr, sockaddr_addr_offset(&probe->local_addr), 16); + memcpy(udph.daddr, sockaddr_addr_offset(&probe->remote_addr), 16); + /* get position to write checksum */ + uint16_t *checksum_off = &udp->checksum; + + if (udp->checksum != 0) + { /* checksum is sequence number - correct the payload to match the checksum + checksum_off is udp payload */ + checksum_off = (uint16_t *)&packet_buffer[sizeof(struct UDPHeader)]; + } + *checksum_off = htons(udp4_checksum(&udph, udp, + sizeof(struct IP6PseudoHeader), + udp_size, udp->checksum != 0)); return 0; } From e95eaf47158eb3995dab462b78a8a9c0451447a5 Mon Sep 17 00:00:00 2001 From: Andrew Marshall Date: Wed, 23 Mar 2022 11:36:48 -0400 Subject: [PATCH 02/33] Adjust MIN_PORT to match other implementations Port 33434 is the IANA-reserved port for traceroute. It also seems to be the most common start port. The following implementations, and likely others, use it as their start port: - Apple macOS traceroute - FreeBSD traceroute - GNU inetutils-traceroute - Modern traceroute for Linux - OpenBSD traceroute The benefit to using a standard starting port is that some firewalls may have these ports allowed, whereas other ports may be blocked, so there is a greater probability the probe will succeed. --- packet/probe_unix.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packet/probe_unix.h b/packet/probe_unix.h index f217a66e..f3e8207b 100644 --- a/packet/probe_unix.h +++ b/packet/probe_unix.h @@ -25,7 +25,7 @@ #endif /* The range of local port numbers to use for probes */ -#define MIN_PORT 33000 +#define MIN_PORT 33434 #define MAX_PORT 65535 /* We need to track the transmission and timeouts on Unix systems */ From 5beecb8cc4fa2b65c433ab294ace28696ac3b927 Mon Sep 17 00:00:00 2001 From: Brandon Ewing Date: Fri, 13 May 2022 10:27:55 -0500 Subject: [PATCH 03/33] use addrs for static host ordering in curses Change d2552ca has the side effect of moving hosts around in the multipath view, as the last host to reply to a packet was set as addr. Using addrs[0] istead of addr to draw the first host keeps the ordering deterministic, in order of first reply. --- ui/curses.c | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/ui/curses.c b/ui/curses.c index d01e178c..ce662757 100644 --- a/ui/curses.c +++ b/ui/curses.c @@ -421,8 +421,8 @@ static void mtr_curses_hosts( for (at = net_min(ctl) + ctl->display_offset; at < max; at++) { printw("%2d. ", at + 1); err = net_err(at); - addr = net_addr(at); - mpls = net_mpls(at); + addr = net_addrs(at, 0); + mpls = net_mplss(at, 0); addrcmp_result = addrcmp(addr, &ctl->unspec_addr, ctl->af); @@ -471,7 +471,7 @@ static void mtr_curses_hosts( } /* Multi path */ - for (i = 0; i < MAX_PATH; i++) { + for (i = 1; i < MAX_PATH; i++) { addrs = net_addrs(at, i); mplss = net_mplss(at, i); if (addrcmp(addrs, addr, ctl->af) == 0) From 7b4153a3d2d0ec31a01ad49b812da1590c07a318 Mon Sep 17 00:00:00 2001 From: Brandon Ewing Date: Fri, 13 May 2022 11:18:38 -0500 Subject: [PATCH 04/33] add --max-display-paths option It is not unknown to have datacenters with over 8 ECMP paths between two hops. Expand the maximum number of ECMP hosts stored in addrs to 128, and add a new option to control the number to display, defaulting to the old MAX_PATHS of 8 --- man/mtr.8.in | 6 ++++++ ui/curses.c | 2 +- ui/mtr.c | 8 ++++++++ ui/mtr.h | 3 ++- ui/report.c | 4 ++-- 5 files changed, 19 insertions(+), 4 deletions(-) diff --git a/man/mtr.8.in b/man/mtr.8.in index 80c47902..04ef1f3c 100644 --- a/man/mtr.8.in +++ b/man/mtr.8.in @@ -92,6 +92,9 @@ mtr \- a network diagnostic tool .BI \-U \ MAX\-UNKNOWN\c ] [\c +.BI \-E \ MAX\-DISPLAY\-PATH\c +] +[\c .B \-\-udp\c ] [\c @@ -407,6 +410,9 @@ probe. Default is 30. .B \-U \fINUM\fR, \fB\-\-max-unknown \fINUM Specifies the maximum unknown host. Default is 5. .TP +.B \-E \fINUM\fR, \fB\-\-max-display-path \fINUM +Specifies the maximum number of ECMP paths to display. Default is 8. +.TP .B \-u\fR, \fB\-\-udp Use UDP datagrams instead of ICMP ECHO. .TP diff --git a/ui/curses.c b/ui/curses.c index ce662757..6ecca4d7 100644 --- a/ui/curses.c +++ b/ui/curses.c @@ -471,7 +471,7 @@ static void mtr_curses_hosts( } /* Multi path */ - for (i = 1; i < MAX_PATH; i++) { + for (i = 1; i < ctl->maxDisplayPath; i++) { addrs = net_addrs(at, i); mplss = net_mplss(at, i); if (addrcmp(addrs, addr, ctl->af) == 0) diff --git a/ui/mtr.c b/ui/mtr.c index 2c6fc68d..5675ec52 100644 --- a/ui/mtr.c +++ b/ui/mtr.c @@ -369,6 +369,7 @@ static void parse_arg( {"first-ttl", 1, NULL, 'f'}, /* -f & -m are borrowed from traceroute */ {"max-ttl", 1, NULL, 'm'}, {"max-unknown", 1, NULL, 'U'}, + {"max-display-path", 1, NULL, 'E'}, {"udp", 0, NULL, 'u'}, /* UDP (default is ICMP) */ {"tcp", 0, NULL, 'T'}, /* TCP (default is ICMP) */ #ifdef HAS_SCTP @@ -521,6 +522,12 @@ static void parse_arg( ctl->maxUnknown = 1; } break; + case 'E': + ctl->maxDisplayPath = strtonum_or_err(optarg, "invalid argument", STRTO_INT); + if (ctl->maxDisplayPath > MAX_PATH) { + ctl->maxDisplayPath = MAX_PATH; + } + break; case 'o': /* Check option before passing it on to fld_active. */ if (strlen(optarg) > MAXFLD) { @@ -747,6 +754,7 @@ int main( ctl.fstTTL = 1; ctl.maxTTL = 30; ctl.maxUnknown = 12; + ctl.maxDisplayPath = 8; ctl.probe_timeout = 10 * 1000000; ctl.ipinfo_no = -1; ctl.ipinfo_max = -1; diff --git a/ui/mtr.h b/ui/mtr.h index 8be1da9a..d9a3e0d3 100644 --- a/ui/mtr.h +++ b/ui/mtr.h @@ -64,7 +64,7 @@ typedef int time_t; /* net related definitions */ #define SAVED_PINGS 200 -#define MAX_PATH 8 +#define MAX_PATH 128 #define MaxHost 256 #define MinPort 1024 #define MaxPort 65535 @@ -103,6 +103,7 @@ struct mtr_ctl { int fstTTL; /* initial hub(ttl) to ping byMin */ int maxTTL; /* last hub to ping byMin */ int maxUnknown; /* stop ping threshold */ + int maxDisplayPath; /* maximum number of ECMP paths to display */ int remoteport; /* target port for TCP tracing */ int localport; /* source port for UDP tracing */ int probe_timeout; /* timeout for probe sockets */ diff --git a/ui/report.c b/ui/report.c index a63ed552..93014808 100644 --- a/ui/report.c +++ b/ui/report.c @@ -185,7 +185,7 @@ void report_close( /* This feature shows 'loadbalances' on routes */ /* Print list of all hosts that have responded from ttl = at + 1 away */ - for (z = 0; z < MAX_PATH; z++) { + for (z = 0; z < ctl->maxDisplayPath; z++) { int found = 0; addr2 = net_addrs(at, z); mplss = net_mplss(at, z); @@ -531,7 +531,7 @@ void csv_close( if (ctl->reportwide == 0) continue; - for (z = 0; z < MAX_PATH; z++) { + for (z = 0; z < ctl->maxDisplayPath; z++) { int found = 0; addr2 = net_addrs(at, z); snprint_addr(ctl, name, sizeof(name), addr2); From 7a0320038a64594c138e69b1200c30428bd1c75c Mon Sep 17 00:00:00 2001 From: Robert Vollmer Date: Fri, 5 Aug 2022 09:58:41 +0200 Subject: [PATCH 05/33] Set SO_BINDTODEVICE for -I This is done by traceroute and other tools as well. It requires CAP_NET_RAW, but without it the -I option doesn't seem to have any effect. --- packet/command.c | 5 +++++ packet/construct_unix.c | 22 ++++++++++++++++++++++ packet/probe.h | 3 +++ ui/cmdpipe.c | 21 +++++++++++++++++++++ 4 files changed, 51 insertions(+) diff --git a/packet/command.c b/packet/command.c index b624ea6c..a7088410 100644 --- a/packet/command.c +++ b/packet/command.c @@ -189,6 +189,11 @@ bool decode_probe_argument( param->local_address = value; } + /* Device name to send from */ + if (!strcmp(name, "local-device")) { + param->local_device = value; + } + /* Protocol for the probe */ if (!strcmp(name, "protocol")) { if (!strcmp(value, "icmp")) { diff --git a/packet/construct_unix.c b/packet/construct_unix.c index faca7689..65f0b633 100644 --- a/packet/construct_unix.c +++ b/packet/construct_unix.c @@ -398,6 +398,13 @@ int set_stream_socket_options( } #endif + if (param->local_device) { + if (setsockopt(stream_socket, SOL_SOCKET, + SO_BINDTODEVICE, param->local_device, strlen(param->local_device))) { + return -1; + } + } + return 0; } @@ -614,6 +621,13 @@ int construct_ip4_packet( } #endif + if (param->local_device) { + if (setsockopt(send_socket, SOL_SOCKET, + SO_BINDTODEVICE, param->local_device, strlen(param->local_device))) { + return -1; + } + } + /* Bind src port when not using raw socket to pass in ICMP id, kernel get ICMP id from src_port when using DGRAM socket. @@ -782,6 +796,14 @@ int construct_ip6_packet( } #endif + if (param->local_device) { + if (setsockopt(send_socket, + SOL_SOCKET, SO_BINDTODEVICE, param->local_device, + strlen(param->local_device))) { + return -1; + } + } + return 0; } diff --git a/packet/probe.h b/packet/probe.h index e7b8beea..caf1314b 100644 --- a/packet/probe.h +++ b/packet/probe.h @@ -53,6 +53,9 @@ struct probe_param_t { /* The local address from which to send probes */ const char *local_address; + /* The local device from which to send probes */ + const char *local_device; + /* Protocol for the probe, using the IPPROTO_* defines */ int protocol; diff --git a/ui/cmdpipe.c b/ui/cmdpipe.c index d22b236f..8017cc0f 100644 --- a/ui/cmdpipe.c +++ b/ui/cmdpipe.c @@ -417,6 +417,22 @@ void append_command_argument( strncat(command, argument, remaining_size); } +static +void append_command_string_argument( + char *command, + int buffer_size, + char *name, + char *value) +{ + char argument[COMMAND_BUFFER_SIZE]; + int remaining_size; + + remaining_size = buffer_size - strlen(command) - 1; + + snprintf(argument, buffer_size, " %s %s", name, value); + strncat(command, argument, remaining_size); +} + /* Request a new probe from the "mtr-packet" child process */ void send_probe_command( @@ -466,6 +482,11 @@ void send_probe_command( } #endif + if (ctl->InterfaceName) { + append_command_string_argument(command, COMMAND_BUFFER_SIZE, + "local-device", ctl->InterfaceName); + } + remaining_size = COMMAND_BUFFER_SIZE - strlen(command) - 1; strncat(command, "\n", remaining_size); From a2bd14115cd20b92153e79d1a2962a0fbb74a39f Mon Sep 17 00:00:00 2001 From: Robert Vollmer Date: Wed, 10 Aug 2022 11:15:55 +0200 Subject: [PATCH 06/33] Check if SO_BINDTODEVICE is defined --- packet/construct_unix.c | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/packet/construct_unix.c b/packet/construct_unix.c index 65f0b633..e78228aa 100644 --- a/packet/construct_unix.c +++ b/packet/construct_unix.c @@ -398,12 +398,14 @@ int set_stream_socket_options( } #endif +#ifdef SO_BINDTODEVICE if (param->local_device) { if (setsockopt(stream_socket, SOL_SOCKET, SO_BINDTODEVICE, param->local_device, strlen(param->local_device))) { return -1; } } +#endif return 0; } @@ -621,12 +623,14 @@ int construct_ip4_packet( } #endif +#ifdef SO_BINDTODEVICE if (param->local_device) { if (setsockopt(send_socket, SOL_SOCKET, SO_BINDTODEVICE, param->local_device, strlen(param->local_device))) { return -1; } } +#endif /* Bind src port when not using raw socket to pass in ICMP id, kernel @@ -796,6 +800,7 @@ int construct_ip6_packet( } #endif +#ifdef SO_BINDTODEVICE if (param->local_device) { if (setsockopt(send_socket, SOL_SOCKET, SO_BINDTODEVICE, param->local_device, @@ -803,6 +808,7 @@ int construct_ip6_packet( return -1; } } +#endif return 0; } From 2b9c090d2d5f336450632f2634dfa68372b411d3 Mon Sep 17 00:00:00 2001 From: wenlxie Date: Fri, 19 Aug 2022 13:51:35 +0800 Subject: [PATCH 07/33] Add help info for option -E --- ui/mtr.c | 131 ++++++++++++++++++++++--------------------------------- 1 file changed, 53 insertions(+), 78 deletions(-) diff --git a/ui/mtr.c b/ui/mtr.c index 5675ec52..e4a40410 100644 --- a/ui/mtr.c +++ b/ui/mtr.c @@ -94,84 +94,59 @@ static void __attribute__ ((__noreturn__)) usage(FILE * out) fputs("\nUsage:\n", out); fputs(" mtr [options] hostname\n", out); fputs("\n", out); - fputs(" -F, --filename FILE read hostname(s) from a file\n", - out); - fputs(" -4 use IPv4 only\n", out); -#ifdef ENABLE_IPV6 - fputs(" -6 use IPv6 only\n", out); -#endif - fputs(" -u, --udp use UDP instead of ICMP echo\n", - out); - fputs(" -T, --tcp use TCP instead of ICMP echo\n", - out); - fputs(" -I, --interface NAME use named network interface\n", - out); - fputs - (" -a, --address ADDRESS bind the outgoing socket to ADDRESS\n", - out); - fputs(" -f, --first-ttl NUMBER set what TTL to start\n", out); - fputs(" -m, --max-ttl NUMBER maximum number of hops\n", out); - fputs(" -U, --max-unknown NUMBER maximum unknown host\n", out); - fputs - (" -P, --port PORT target port number for TCP, SCTP, or UDP\n", - out); - fputs(" -L, --localport LOCALPORT source port number for UDP\n", out); - fputs - (" -s, --psize PACKETSIZE set the packet size used for probing\n", - out); - fputs - (" -B, --bitpattern NUMBER set bit pattern to use in payload\n", - out); - fputs(" -i, --interval SECONDS ICMP echo request interval\n", out); - fputs - (" -G, --gracetime SECONDS number of seconds to wait for responses\n", - out); - fputs - (" -Q, --tos NUMBER type of service field in IP header\n", - out); - fputs - (" -e, --mpls display information from ICMP extensions\n", - out); - fputs - (" -Z, --timeout SECONDS seconds to keep probe sockets open\n", - out); -#ifdef SO_MARK - fputs(" -M, --mark MARK mark each sent packet\n", out); -#endif - fputs(" -r, --report output using report mode\n", out); - fputs(" -w, --report-wide output wide report\n", out); - fputs(" -c, --report-cycles COUNT set the number of pings sent\n", - out); -#ifdef HAVE_JANSSON - fputs(" -j, --json output json\n", out); -#endif - fputs(" -x, --xml output xml\n", out); - fputs(" -C, --csv output comma separated values\n", - out); - fputs(" -l, --raw output raw format\n", out); - fputs(" -p, --split split output\n", out); -#ifdef HAVE_CURSES - fputs(" -t, --curses use curses terminal interface\n", - out); -#endif - fputs(" --displaymode MODE select initial display mode\n", - out); -#ifdef HAVE_GTK - fputs(" -g, --gtk use GTK+ xwindow interface\n", out); -#endif - fputs(" -n, --no-dns do not resolve host names\n", out); - fputs(" -b, --show-ips show IP numbers and host names\n", - out); - fputs(" -o, --order FIELDS select output fields\n", out); -#ifdef HAVE_IPINFO - fputs(" -y, --ipinfo NUMBER select IP information in output\n", - out); - fputs(" -z, --aslookup display AS number\n", out); -#endif - fputs(" -h, --help display this help and exit\n", out); - fputs - (" -v, --version output version information and exit\n", - out); + fputs(" -F, --filename FILE read hostname(s) from a file\n", + out); + fputs(" -4 use IPv4 only\n", out); +#ifdef ENABLE_IPV6 + fputs(" -6 use IPv6 only\n", out); +#endif + fputs(" -u, --udp use UDP instead of ICMP echo\n", out); + fputs(" -T, --tcp use TCP instead of ICMP echo\n", out); + fputs(" -I, --interface NAME use named network interface\n", out); + fputs(" -a, --address ADDRESS bind the outgoing socket to ADDRESS\n", out); + fputs(" -f, --first-ttl NUMBER set what TTL to start\n", out); + fputs(" -m, --max-ttl NUMBER maximum number of hops\n", out); + fputs(" -U, --max-unknown NUMBER maximum unknown host\n", out); + fputs(" -E, --max-display-path NUMBER maximum number of ECMP paths to display\n", out); + fputs(" -P, --port PORT arget port number for TCP, SCTP, or UDP\n", out); + fputs(" -L, --localport LOCALPORT source port number for UDP\n", out); + fputs(" -s, --psize PACKETSIZE set the packet size used for probing\n", out); + fputs(" -B, --bitpattern NUMBER set bit pattern to use in payload\n", out); + fputs(" -i, --interval SECONDS ICMP echo request interval\n", out); + fputs(" -G, --gracetime SECONDS number of seconds to wait for responses\n", out); + fputs(" -Q, --tos NUMBER type of service field in IP header\n", out); + fputs(" -e, --mpls display information from ICMP extensions\n", out); + fputs(" -Z, --timeout SECONDS seconds to keep probe sockets open\n", out); +#ifdef SO_MARK + fputs(" -M, --mark MARK mark each sent packet\n", out); +#endif + fputs(" -r, --report output using report mode\n", out); + fputs(" -w, --report-wide output wide report\n", out); + fputs(" -c, --report-cycles COUNT set the number of pings sent\n", out); +#ifdef HAVE_JANSSON + fputs(" -j, --json output json\n", out); +#endif + fputs(" -x, --xml output xml\n", out); + fputs(" -C, --csv output comma separated values\n", out); + fputs(" -l, --raw output raw format\n", out); + fputs(" -p, --split split output\n", out); +#ifdef HAVE_CURSES + fputs(" -t, --curses use curses terminal interface\n", out); +#endif + fputs(" --displaymode MODE select initial display mode\n", out); +#ifdef HAVE_GTK + fputs(" -g, --gtk use GTK+ xwindow interface\n", out); +#endif + fputs(" -n, --no-dns do not resolve host names\n", out); + fputs(" -b, --show-ips show IP numbers and host names\n", out); + fputs(" -o, --order FIELDS select output fields\n", out); +#ifdef HAVE_IPINFO + fputs(" -y, --ipinfo NUMBER select IP information in output\n", + out); + fputs(" -z, --aslookup display AS number\n", out); +#endif + fputs(" -h, --help display this help and exit\n", out); + fputs(" -v, --version output version information and exit\n", out); fputs("\n", out); fputs("See the 'man 8 mtr' for details.\n", out); exit(out == stderr ? EXIT_FAILURE : EXIT_SUCCESS); From d2a29fffb434ef5561e309ee2686d9de1f7f7338 Mon Sep 17 00:00:00 2001 From: Sami Kerola Date: Tue, 6 Sep 2022 14:16:04 +0100 Subject: [PATCH 08/33] ui: make interactive and non-interactive exit code the same Before this change the report gave successful exit value when destination hostname could not be found. $ ./mtr --report nxdomain. ; echo $? ./mtr: Failed to resolve host: nxdomain.: Name or service not known 0 Quickly looking 'git grep ---after-context if.*Interactive' there does not appear to be more than the two instances in main() where exit is called depending on interactive, so this change should cover all these cases. Reported-by: Marek Kroemeke Signed-off-by: Sami Kerola --- ui/mtr.c | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/ui/mtr.c b/ui/mtr.c index e4a40410..5a70f313 100644 --- a/ui/mtr.c +++ b/ui/mtr.c @@ -710,6 +710,7 @@ int main( int argc, char **argv) { + int exit_val = EXIT_SUCCESS; names_t *names_head = NULL; names_t *names_walk; @@ -784,6 +785,7 @@ int main( if (ctl.Interactive) exit(EXIT_FAILURE); else { + exit_val = EXIT_FAILURE; names_walk = names_walk->next; continue; } @@ -794,6 +796,7 @@ int main( if (ctl.Interactive) exit(EXIT_FAILURE); else { + exit_val = EXIT_FAILURE; names_walk = names_walk->next; continue; } @@ -829,5 +832,5 @@ int main( item = NULL; } - return 0; + return exit_val; } From 4f6d0e7c356a1f6be2e72b4f7cc8c2864fab1c75 Mon Sep 17 00:00:00 2001 From: "R.E. Wolff" Date: Fri, 30 Sep 2022 09:42:20 +0200 Subject: [PATCH 09/33] Markus pointed out useless statement. --- ui/mtr.c | 1 - 1 file changed, 1 deletion(-) diff --git a/ui/mtr.c b/ui/mtr.c index 5a70f313..de63ebb9 100644 --- a/ui/mtr.c +++ b/ui/mtr.c @@ -376,7 +376,6 @@ static void parse_arg( /* optional options need two ':', but ignore them now as they are not in use */ } - opt = 0; while (1) { opt = getopt_long(argc, argv, short_options, long_options, NULL); if (opt == -1) From b7a6df88fd75a9524de4bc3cc1348daf0cafaad9 Mon Sep 17 00:00:00 2001 From: lilinjie Date: Tue, 10 Jan 2023 14:20:12 +0800 Subject: [PATCH 10/33] fix typo Signed-off-by: lilinjie --- NEWS | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/NEWS b/NEWS index ddbac03e..f54718e4 100644 --- a/NEWS +++ b/NEWS @@ -354,9 +354,9 @@ V0.88 build-sys: default to ,/configure --enable-silent-rules warnings: do not take abs() when data type is unsigned warnings: mark unused function input variables - warnings: fix couple unsigned vs signed variable comparisions + warnings: fix couple unsigned vs signed variable comparisons warnings: multiply timeval seconds only when the value is small - warnings: fix some missed unsigned vs signed variable comparisions + warnings: fix some missed unsigned vs signed variable comparisons comment: add value range note to initialization cast: do not downgrade to float when double should be used warnings: remove dead code From 5908af4c19188cb17b62f23368b6ef462831a0cb Mon Sep 17 00:00:00 2001 From: Marcus Meissner Date: Tue, 11 Apr 2023 16:05:36 +0200 Subject: [PATCH 11/33] fixed the sizes passed into snprintf --- ui/report.c | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/ui/report.c b/ui/report.c index 93014808..61f2c74b 100644 --- a/ui/report.c +++ b/ui/report.c @@ -140,7 +140,7 @@ void report_close( continue; snprintf(fmt, sizeof(fmt), "%%%ds", data_fields[j].length); - snprintf(buf + len, sizeof(buf), fmt, data_fields[j].title); + snprintf(buf + len, sizeof(buf) - len, fmt, data_fields[j].title); len += data_fields[j].length; } printf("%s\n", buf); @@ -172,10 +172,10 @@ void report_close( /* 1000.0 is a temporary hack for stats usec to ms, impacted net_loss. */ if (strchr(data_fields[j].format, 'f')) { - snprintf(buf + len, sizeof(buf), data_fields[j].format, + snprintf(buf + len, sizeof(buf) - len, data_fields[j].format, data_fields[j].net_xxx(at) / 1000.0); } else { - snprintf(buf + len, sizeof(buf), data_fields[j].format, + snprintf(buf + len, sizeof(buf) - len, data_fields[j].format, data_fields[j].net_xxx(at)); } len += data_fields[j].length; From 3d1f85d0732a17ce561b25bcc9c5e3117497f3df Mon Sep 17 00:00:00 2001 From: eater Date: Tue, 16 May 2023 04:40:24 +0000 Subject: [PATCH 12/33] configure.ac: fix broken cap check --- configure.ac | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/configure.ac b/configure.ac index 3175d56b..286ede60 100644 --- a/configure.ac +++ b/configure.ac @@ -127,9 +127,13 @@ AS_IF([test "x$with_ncurses" = "xyes"], ]) AM_CONDITIONAL([WITH_CURSES], [test "x$with_ncurses" = xyes]) -AC_CHECK_LIB([cap], [cap_set_proc], [have_cap="yes"], - AS_IF([test "$host_os" = linux-gnu], - AC_MSG_WARN([Capabilities support is strongly recommended for increased security. See SECURITY for more information.]))) +have_cap="yes" +AC_CHECK_LIB([cap], [cap_set_proc], [], [ + have_cap="no" + AS_IF([test "$host_os" = linux-gnu], [ + AC_MSG_WARN([Capabilities support is strongly recommended for increased security. See SECURITY for more information.]) + ]) +]) # Enable ipinfo AC_ARG_WITH([ipinfo], From 3a69802cebe7113f9ccd0a342f175e315a46eac1 Mon Sep 17 00:00:00 2001 From: famfo Date: Tue, 16 May 2023 12:41:58 +0200 Subject: [PATCH 13/33] Add option to use custom ipinfo provider --- man/mtr.8.in | 14 ++++++++++++++ ui/asn.c | 6 +++--- ui/mtr.c | 29 ++++++++++++++++++++++++++++- ui/mtr.h | 6 ++++++ 4 files changed, 51 insertions(+), 4 deletions(-) diff --git a/man/mtr.8.in b/man/mtr.8.in index 04ef1f3c..84baac6f 100644 --- a/man/mtr.8.in +++ b/man/mtr.8.in @@ -56,6 +56,12 @@ mtr \- a network diagnostic tool .B \-\-aslookup\c ] [\c +.BI \-\-ipinfo_provider4 \ DOMAIN\c +] +[\c +.BI \-\-ipinfo_provider6 \ DOMAIN\c +] +[\c .BI \-i \ INTERVAL\c ] [\c @@ -350,6 +356,14 @@ Example (columns to the right not shown for clarity): 7. AS1850 www.isnic.is .fi .TP +.B \-\-ipinfo_provider4 \fIDOMAIN +Provider for IPv4 AS lookups. Defaults to origin.asn.cymru.com. +.fi +.TP +.B \-\-ipinfo_provider6 \fIDOMAIN +Provider for IPv6 AS lookups. Defaults to origin6.asn.cymru.com. +.fi +.TP .B \-i \fISECONDS\fR, \fB\-\-interval \fISECONDS Use this option to specify the positive number of seconds between ICMP ECHO requests. The default value for this parameter is one second. The diff --git a/ui/asn.c b/ui/asn.c index 3f424e08..718f4a03 100644 --- a/ui/asn.c +++ b/ui/asn.c @@ -65,7 +65,7 @@ static int iihash = 0; static char fmtinfo[32]; /* items width: ASN, Route, Country, Registry, Allocated */ -static const int iiwidth[] = { 7, 19, 4, 8, 11 }; /* item len + space */ +static const int iiwidth[] = { 12, 19, 4, 8, 11 }; /* item len + space */ typedef char *items_t[ITEMSMAX + 1]; static items_t items_a; /* without hash: items */ @@ -230,7 +230,7 @@ static char *get_ipinfo( if (ctl->af == AF_INET6) { #ifdef ENABLE_IPV6 reverse_host6(addr, key, NAMELEN); - if (snprintf(lookup_key, NAMELEN, "%s.origin6.asn.cymru.com", key) + if (snprintf(lookup_key, NAMELEN, "%s.%s", key, ctl->ipinfo_provider6) >= NAMELEN) return NULL; #else @@ -243,7 +243,7 @@ static char *get_ipinfo( (key, NAMELEN, "%d.%d.%d.%d", buff[3], buff[2], buff[1], buff[0]) >= NAMELEN) return NULL; - if (snprintf(lookup_key, NAMELEN, "%s.origin.asn.cymru.com", key) + if (snprintf(lookup_key, NAMELEN, "%s.%s", key, ctl->ipinfo_provider4) >= NAMELEN) return NULL; } diff --git a/ui/mtr.c b/ui/mtr.c index 5a70f313..7a339f0a 100644 --- a/ui/mtr.c +++ b/ui/mtr.c @@ -144,6 +144,10 @@ static void __attribute__ ((__noreturn__)) usage(FILE * out) fputs(" -y, --ipinfo NUMBER select IP information in output\n", out); fputs(" -z, --aslookup display AS number\n", out); + fputs(" --ipinfo_provider4 providor for IPv4 AS lookups\n", out); +#ifdef ENABLE_IPV6 + fputs(" --ipinfo_provider6 providor for IPv6 AS lookups\n", out); +#endif #endif fputs(" -h, --help display this help and exit\n", out); fputs(" -v, --version output version information and exit\n", out); @@ -294,7 +298,11 @@ static void parse_arg( 3/ update the help message (see usage() function). */ enum { - OPT_DISPLAYMODE = CHAR_MAX + 1 + OPT_DISPLAYMODE = CHAR_MAX + 1, + OPT_IPINFO4 = CHAR_MAX + 2, +#ifdef ENABLE_IPV6 + OPT_IPINFO6 = CHAR_MAX + 3, +#endif /* ifdef ENABLE_IPV6 */ }; static const struct option long_options[] = { /* option name, has argument, NULL, short name */ @@ -331,6 +339,10 @@ static void parse_arg( #ifdef HAVE_IPINFO {"ipinfo", 1, NULL, 'y'}, /* IP info lookup */ {"aslookup", 0, NULL, 'z'}, /* Do AS lookup (--ipinfo 0) */ + {"ipinfo_provider4", 1, NULL, OPT_IPINFO4}, +#ifdef ENABLE_IPV6 + {"ipinfo_provider6", 1, NULL, OPT_IPINFO6}, +#endif #endif {"interval", 1, NULL, 'i'}, @@ -610,6 +622,14 @@ static void parse_arg( case 'z': ctl->ipinfo_no = 0; break; + case OPT_IPINFO4: + ctl->ipinfo_provider4 = optarg; + break; +#ifdef ENABLE_IPV6 + case OPT_IPINFO6: + ctl->ipinfo_provider6 = optarg; + break; +#endif #endif #ifdef SO_MARK case 'M': @@ -734,6 +754,13 @@ int main( ctl.probe_timeout = 10 * 1000000; ctl.ipinfo_no = -1; ctl.ipinfo_max = -1; +#ifdef HAVE_IPINFO + ctl.ipinfo_provider4 = "origin.asn.cymru.com"; +#ifdef ENABLE_IPV6 + ctl.ipinfo_provider6 = "origin6.asn.cymru.com"; +#endif +#endif + xstrncpy(ctl.fld_active, "LS NABWV", 2 * MAXFLD); /* diff --git a/ui/mtr.h b/ui/mtr.h index d9a3e0d3..0adff62d 100644 --- a/ui/mtr.h +++ b/ui/mtr.h @@ -118,6 +118,12 @@ struct mtr_ctl { use_dns:1, show_ips:1, enablempls:1, dns:1, reportwide:1, Interactive:1, DisplayMode:5; +#ifdef HAVE_IPINFO +#ifdef ENABLE_IPV6 + char *ipinfo_provider6; +#endif + char *ipinfo_provider4; +#endif }; /* dynamic field drawing */ From 7ad8ef6951b621912d50c2ecdd1166b78d6e1246 Mon Sep 17 00:00:00 2001 From: "R.E. Wolff" Date: Tue, 16 May 2023 14:03:37 +0200 Subject: [PATCH 14/33] Fixed typo noted by @szczot3k --- ui/mtr.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ui/mtr.c b/ui/mtr.c index afe3cbe3..60f81671 100644 --- a/ui/mtr.c +++ b/ui/mtr.c @@ -144,9 +144,9 @@ static void __attribute__ ((__noreturn__)) usage(FILE * out) fputs(" -y, --ipinfo NUMBER select IP information in output\n", out); fputs(" -z, --aslookup display AS number\n", out); - fputs(" --ipinfo_provider4 providor for IPv4 AS lookups\n", out); + fputs(" --ipinfo_provider4 provider for IPv4 AS lookups\n", out); #ifdef ENABLE_IPV6 - fputs(" --ipinfo_provider6 providor for IPv6 AS lookups\n", out); + fputs(" --ipinfo_provider6 provider for IPv6 AS lookups\n", out); #endif #endif fputs(" -h, --help display this help and exit\n", out); From 91035185dd36fa721db5fee813e020f966403ceb Mon Sep 17 00:00:00 2001 From: Evan Pratten Date: Tue, 30 May 2023 20:22:48 -0400 Subject: [PATCH 15/33] Implement ASN lookups in well-known nat64 prefix --- ui/asn.c | 29 +++++++++++++++++++++++++---- 1 file changed, 25 insertions(+), 4 deletions(-) diff --git a/ui/asn.c b/ui/asn.c index 718f4a03..111c3942 100644 --- a/ui/asn.c +++ b/ui/asn.c @@ -21,6 +21,7 @@ #include #include #include +#include #include #ifdef HAVE_ERROR_H #include @@ -215,6 +216,13 @@ static void reverse_host6( } #endif +#ifdef ENABLE_IPV6 +static bool is_well_known_nat64(struct in6_addr *addr){ + // 64:ff9b:: + return addr->s6_addr[0] == 0x00 && addr->s6_addr[1] == 0x64 && addr->s6_addr[2] == 0xff && addr->s6_addr[3] == 0x9b; +} +#endif + static char *get_ipinfo( struct mtr_ctl *ctl, ip_t * addr) @@ -229,10 +237,23 @@ static char *get_ipinfo( if (ctl->af == AF_INET6) { #ifdef ENABLE_IPV6 - reverse_host6(addr, key, NAMELEN); - if (snprintf(lookup_key, NAMELEN, "%s.%s", key, ctl->ipinfo_provider6) - >= NAMELEN) - return NULL; + if (is_well_known_nat64(addr)) { + // Treats the final 4 bytes as IPv4 address + unsigned char buff[4]; + memcpy(buff, addr->s6_addr + 12, 4); + if (snprintf + (key, NAMELEN, "%d.%d.%d.%d", buff[3], buff[2], buff[1], + buff[0]) >= NAMELEN) + return NULL; + if (snprintf(lookup_key, NAMELEN, "%s.%s", key, ctl->ipinfo_provider4) + >= NAMELEN) + return NULL; + } else { + reverse_host6(addr, key, NAMELEN); + if (snprintf(lookup_key, NAMELEN, "%s.%s", key, ctl->ipinfo_provider6) + >= NAMELEN) + return NULL; + } #else return NULL; #endif From dc076a6c1561d3a8ad9d27cf2aca29fabaa9c20c Mon Sep 17 00:00:00 2001 From: Jian Cheng Date: Thu, 8 Jun 2023 20:28:29 +0800 Subject: [PATCH 16/33] Add error code ETIMEOUT(110) handle logic --- packet/probe_unix.c | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packet/probe_unix.c b/packet/probe_unix.c index f7f393fc..8fd8cf1b 100644 --- a/packet/probe_unix.c +++ b/packet/probe_unix.c @@ -543,6 +543,8 @@ void report_packet_error( printf("%d address-in-use\n", command_token); } else if (errno == EADDRNOTAVAIL) { printf("%d address-not-available\n", command_token); + } else if (errno == ETIMEDOUT) { + printf("%d wait-tcp-respone-timeout\n", command_token); } else { printf("%d unexpected-error errno %d\n", command_token, errno); } From 4fc3361930ca7f820383b74d87578b45101a7d39 Mon Sep 17 00:00:00 2001 From: darless <3382617+darless@users.noreply.github.com> Date: Tue, 11 Jul 2023 11:18:33 -0500 Subject: [PATCH 17/33] Github actions added to perform lint and compile In the test folder, there was lint.sh, but I think flake8 is a better tool. Added a compile job for linux, this runs compilation as defined in the README, runs a sample mtr and runs cmdparse.py test. Need documentation on what other tests to run and whether to include the testing in tox instead of running them individually. Not adding in this PR support for cygwin or freebsd, as that will need investigation on how to run. Minor: - Fixed a typo in configure.ac that used ---- vs -- for a flag option. --- .flake8 | 14 ++++++++++++++ .github/workflows/test.yaml | 35 +++++++++++++++++++++++++++++++++++ .gitignore | 1 + configure.ac | 2 +- test/probe.py | 1 - 5 files changed, 51 insertions(+), 2 deletions(-) create mode 100644 .flake8 create mode 100644 .github/workflows/test.yaml diff --git a/.flake8 b/.flake8 new file mode 100644 index 00000000..ed16194e --- /dev/null +++ b/.flake8 @@ -0,0 +1,14 @@ +[flake8] + +select = + E902, + E999, + S, + F, + +ignore = + F821 + +per-file-ignores = + test/*: S603,S404 + diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml new file mode 100644 index 00000000..c47f1707 --- /dev/null +++ b/.github/workflows/test.yaml @@ -0,0 +1,35 @@ +name: Test compilation +on: [push, pull_request] +jobs: + lint: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-python@v4 + with: + python-version: 3.9 + - name: Install flake8 + run: pip3 install flake8==3.9.2 flake8-bandit==2.1.2 bandit==1.7.2 + - name: Run flake8 + run: python3 -m flake8 . + compile-linux: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-python@v4 + with: + python-version: 3.9 + - uses: egor-tensin/setup-gcc@v1.3 + with: + version: latest + platform: x64 + - name: Bootstrap + run: ./bootstrap.sh + - name: Configure + run: ./configure --without-gtk --without-jansson + - name: Make + run: make -j $(nproc) + - name: Run sample mtr against 1.1.1.1 + run: ./mtr --report --report-cycles 1 -m 1 1.1.1.1 + - name: Run test - cmdparse.py + run: python3 ./test/cmdparse.py \ No newline at end of file diff --git a/.gitignore b/.gitignore index d3d1068e..887cc01b 100644 --- a/.gitignore +++ b/.gitignore @@ -42,3 +42,4 @@ stamp-h1* /test/*.py.trs /mtr-*.tar.gz +*.swp diff --git a/configure.ac b/configure.ac index 286ede60..cc07d817 100644 --- a/configure.ac +++ b/configure.ac @@ -25,7 +25,7 @@ m4_ifndef([PKG_CHECK_MODULES], [m4_defun([PKG_CHECK_MODULES], [AC_MSG_ERROR( [Could not locate the pkg-config autoconf macros. These are usually located in /usr/share/aclocal/pkg.m4. If your macros are in a different location, try setting the environment variable ACLOCAL_OPTS="-I/other/macro/dir" -before running ./bootstrap.sh again, or configure --without-gtk ----without-jansson ])]) +before running ./bootstrap.sh again, or configure --without-gtk --without-jansson ])]) ]) PKG_PROG_PKG_CONFIG diff --git a/test/probe.py b/test/probe.py index df5f4967..30acd453 100755 --- a/test/probe.py +++ b/test/probe.py @@ -263,7 +263,6 @@ def test_parallel_probes(self): required_success = int(loop_count * 0.90) self.assertGreaterEqual(success_count, required_success) - class TestProbeICMPv6(mtrpacket.MtrPacketTest): '''Test sending probes using IP version 6''' From d529dbeefc6dc4b103d395510cd71949cea4f25e Mon Sep 17 00:00:00 2001 From: Alarig Le Lay Date: Mon, 11 Sep 2023 11:48:53 +0200 Subject: [PATCH 18/33] Change UDP and ICMP sockets binding to accept a source IP from the -a CLI option Issue: #232 Signed-off-by: Alarig Le Lay --- packet/construct_unix.c | 176 +++++++++++++++------------------------- packet/probe_unix.c | 55 +++++++------ packet/probe_unix.h | 7 +- 3 files changed, 100 insertions(+), 138 deletions(-) diff --git a/packet/construct_unix.c b/packet/construct_unix.c index e78228aa..e09d7057 100644 --- a/packet/construct_unix.c +++ b/packet/construct_unix.c @@ -71,19 +71,6 @@ uint16_t compute_checksum( return (~sum & 0xffff); } -/* Encode the IP header length field in the order required by the OS. */ -static -uint16_t length_byte_swap( - const struct net_state_t *net_state, - uint16_t length) -{ - if (net_state->platform.ip_length_host_order) { - return length; - } else { - return htons(length); - } -} - /* Construct a combined sockaddr from a source address and source port */ static void construct_addr_port( @@ -95,38 +82,9 @@ void construct_addr_port( *sockaddr_port_offset(addr_with_port) = htons(port); } -/* Construct a header for IP version 4 */ -static -void construct_ip4_header( - const struct net_state_t *net_state, - const struct probe_t *probe, - char *packet_buffer, - int packet_size, - const struct probe_param_t *param) -{ - struct IPHeader *ip; - - ip = (struct IPHeader *) &packet_buffer[0]; - - memset(ip, 0, sizeof(struct IPHeader)); - - ip->version = 0x45; - ip->tos = param->type_of_service; - ip->len = length_byte_swap(net_state, packet_size); - ip->ttl = param->ttl; - ip->protocol = param->protocol; -// ip->id = htons(getpid()); - memcpy(&ip->saddr, - sockaddr_addr_offset(&probe->local_addr), - sockaddr_addr_size(&probe->local_addr)); - memcpy(&ip->daddr, - sockaddr_addr_offset(&probe->remote_addr), - sockaddr_addr_size(&probe->remote_addr)); -} - /* Construct an ICMP header for IPv4 */ static -void construct_icmp4_header( +int construct_icmp4_packet( const struct net_state_t *net_state, struct probe_t *probe, char *packet_buffer, @@ -134,22 +92,17 @@ void construct_icmp4_header( const struct probe_param_t *param) { struct ICMPHeader *icmp; - int icmp_size; - if (net_state->platform.ip4_socket_raw) { - icmp = (struct ICMPHeader *) &packet_buffer[sizeof(struct IPHeader)]; - icmp_size = packet_size - sizeof(struct IPHeader); - } else { - icmp = (struct ICMPHeader *) &packet_buffer[0]; - icmp_size = packet_size; - } + icmp = (struct ICMPHeader *) packet_buffer; memset(icmp, 0, sizeof(struct ICMPHeader)); icmp->type = ICMP_ECHO; icmp->id = htons(getpid()); icmp->sequence = htons(probe->sequence); - icmp->checksum = htons(compute_checksum(icmp, icmp_size)); + icmp->checksum = htons(compute_checksum(icmp, packet_size)); + + return 0; } /* Construct an ICMP header for IPv6 */ @@ -238,7 +191,7 @@ int udp4_checksum(void *pheader, void *udata, int psize, int dsize, with the probe. */ static -void construct_udp4_header( +int construct_udp4_packet( const struct net_state_t *net_state, struct probe_t *probe, char *packet_buffer, @@ -248,13 +201,8 @@ void construct_udp4_header( struct UDPHeader *udp; int udp_size; - if (net_state->platform.ip4_socket_raw) { - udp = (struct UDPHeader *) &packet_buffer[sizeof(struct IPHeader)]; - udp_size = packet_size - sizeof(struct IPHeader); - } else { - udp = (struct UDPHeader *) &packet_buffer[0]; - udp_size = packet_size; - } + udp = (struct UDPHeader *) packet_buffer; + udp_size = packet_size; memset(udp, 0, sizeof(struct UDPHeader)); @@ -283,6 +231,8 @@ void construct_udp4_header( *checksum_off = htons(udp4_checksum(&udph, udp, sizeof(struct UDPPseudoHeader), udp_size, udp->checksum != 0)); + + return 0; } /* Construct a header for UDPv6 probes */ @@ -561,10 +511,10 @@ int construct_ip4_packet( int packet_size, const struct probe_param_t *param) { - int send_socket = net_state->platform.ip4_send_socket; + int send_socket; bool is_stream_protocol = false; - int tos, ttl, socket; - bool bind_send_socket = false; + int tos, ttl; + bool bind_send_socket = true; struct sockaddr_storage current_sockaddr; int current_sockaddr_len; @@ -574,23 +524,34 @@ int construct_ip4_packet( } else if (param->protocol == IPPROTO_SCTP) { is_stream_protocol = true; #endif - } else { + } else if (param->protocol == IPPROTO_ICMP) { if (net_state->platform.ip4_socket_raw) { - construct_ip4_header(net_state, probe, packet_buffer, packet_size, - param); + send_socket = net_state->platform.icmp4_send_socket; + } else { + send_socket = net_state->platform.ip4_txrx_icmp_socket; } - if (param->protocol == IPPROTO_ICMP) { - construct_icmp4_header(net_state, probe, packet_buffer, - packet_size, param); - } else if (param->protocol == IPPROTO_UDP) { - construct_udp4_header(net_state, probe, packet_buffer, - packet_size, param); + + if (construct_icmp4_packet + (net_state, probe, packet_buffer, packet_size, param)) { + return -1; + } + } else if (param->protocol == IPPROTO_UDP) { + if (net_state->platform.ip4_socket_raw) { + send_socket = net_state->platform.udp4_send_socket; } else { - errno = EINVAL; + send_socket = net_state->platform.ip4_txrx_udp_socket; + } + + if (construct_udp4_packet + (net_state, probe, packet_buffer, packet_size, param)) { return -1; } + } else { + errno = EINVAL; + return -1; } + if (is_stream_protocol) { send_socket = open_stream_socket(net_state, param->protocol, probe->sequence, @@ -633,54 +594,51 @@ int construct_ip4_packet( #endif /* - Bind src port when not using raw socket to pass in ICMP id, kernel - get ICMP id from src_port when using DGRAM socket. + Check the current socket address, and if it is the same + as the source address we intend, we will skip the bind. + This is to accommodate Solaris, which, as of Solaris 11.3, + will return an EINVAL error on bind if the socket is already + bound, even if the same address is used. */ - if (!net_state->platform.ip4_socket_raw && - param->protocol == IPPROTO_ICMP && - !param->is_probing_byte_order) { - current_sockaddr_len = sizeof(struct sockaddr_in); - bind_send_socket = true; - socket = net_state->platform.ip4_txrx_icmp_socket; - if (getsockname(socket, (struct sockaddr *) ¤t_sockaddr, - ¤t_sockaddr_len)) { - return -1; - } - struct sockaddr_in *sin_cur = - (struct sockaddr_in *) ¤t_sockaddr; + current_sockaddr_len = sizeof(struct sockaddr_in); + if (getsockname(send_socket, (struct sockaddr *) ¤t_sockaddr, + ¤t_sockaddr_len) == 0) { + struct sockaddr_in *sin_cur = (struct sockaddr_in *) ¤t_sockaddr; - /* avoid double bind */ - if (sin_cur->sin_port) { - bind_send_socket = false; + if (net_state->platform.ip4_socket_raw) { + if (memcmp(¤t_sockaddr, + &probe->local_addr, sizeof(struct sockaddr_in)) == 0) { + bind_send_socket = false; + } + } else { + /* avoid double bind for DGRAM socket */ + if (sin_cur->sin_port) { + bind_send_socket = false; + } } } /* Bind to our local address */ - if (bind_send_socket && bind(socket, (struct sockaddr *)&probe->local_addr, + if (bind_send_socket && bind(send_socket, (struct sockaddr *)&probe->local_addr, sizeof(struct sockaddr_in))) { return -1; } - /* set TOS and TTL for non-raw socket */ - if (!net_state->platform.ip4_socket_raw && !param->is_probing_byte_order) { - if (param->protocol == IPPROTO_ICMP) { - socket = net_state->platform.ip4_txrx_icmp_socket; - } else if (param->protocol == IPPROTO_UDP) { - socket = net_state->platform.ip4_txrx_udp_socket; - } else { - return 0; - } - tos = param->type_of_service; - if (setsockopt(socket, SOL_IP, IP_TOS, &tos, sizeof(int))) { - return -1; - } - ttl = param->ttl; - if (setsockopt(socket, SOL_IP, IP_TTL, - &ttl, sizeof(int)) == -1) { - return -1; - } + /* Set the type of service */ + tos = param->type_of_service; + if (setsockopt(send_socket, SOL_IP, IP_TOS, &tos, sizeof(int))) { + return -1; } + /* Set the time-to-live */ + ttl = param->ttl; + if (setsockopt(send_socket, SOL_IP, IP_TTL, + &ttl, sizeof(int)) == -1) { + return -1; + } + + + return 0; } diff --git a/packet/probe_unix.c b/packet/probe_unix.c index f7f393fc..012ec0cd 100644 --- a/packet/probe_unix.c +++ b/packet/probe_unix.c @@ -87,16 +87,21 @@ int send_packet( } else if (sockaddr->ss_family == AF_INET) { sockaddr_length = sizeof(struct sockaddr_in); - if (net_state->platform.ip4_socket_raw) { - send_socket = net_state->platform.ip4_send_socket; - } else { - if (param->protocol == IPPROTO_ICMP) { - if (param->is_probing_byte_order) { - send_socket = net_state->platform.ip4_tmp_icmp_socket;; - } else { - send_socket = net_state->platform.ip4_txrx_icmp_socket; - } - } else if (param->protocol == IPPROTO_UDP) { + if (param->protocol == IPPROTO_ICMP) { + if (net_state->platform.ip4_socket_raw) { + send_socket = net_state->platform.icmp4_send_socket; + } else { + send_socket = net_state->platform.ip4_txrx_icmp_socket; + } + } else if (param->protocol == IPPROTO_UDP) { + if (net_state->platform.ip4_socket_raw) { + send_socket = net_state->platform.udp4_send_socket; + /* we got a ipv4 udp raw socket + * the remote port is in the payload + * we do not set in the sockaddr + */ + *sockaddr_port_offset(&dst) = 0; + } else { send_socket = net_state->platform.ip4_txrx_udp_socket; if (param->dest_port) { *sockaddr_port_offset(&dst) = htons(param->dest_port); @@ -105,6 +110,7 @@ int send_packet( } } } + } if (send_socket == 0) { @@ -236,26 +242,19 @@ static int open_ip4_sockets_raw( struct net_state_t *net_state) { - int send_socket; + int send_socket_icmp; + int send_socket_udp; int recv_socket; - int trueopt = 1; - send_socket = socket(AF_INET, SOCK_RAW, IPPROTO_RAW); - if (send_socket == -1) { - send_socket = socket(AF_INET, SOCK_RAW, IPPROTO_ICMP); - if (send_socket == -1) { - return -1; - } + send_socket_icmp = socket(AF_INET, SOCK_RAW, IPPROTO_ICMP); + if (send_socket_icmp == -1) { + return -1; } - /* - We will be including the IP header in transmitted packets. - Linux doesn't require this, but BSD derived network stacks do. - */ - if (setsockopt - (send_socket, IPPROTO_IP, IP_HDRINCL, &trueopt, sizeof(int))) { + send_socket_udp = socket(AF_INET, SOCK_RAW, IPPROTO_UDP); + if (send_socket_udp == -1) { + close(send_socket_icmp); - close(send_socket); return -1; } @@ -265,13 +264,15 @@ int open_ip4_sockets_raw( */ recv_socket = socket(AF_INET, SOCK_RAW, IPPROTO_ICMP); if (recv_socket == -1) { - close(send_socket); + close(send_socket_icmp); + close(send_socket_udp); return -1; } net_state->platform.ip4_present = true; net_state->platform.ip4_socket_raw = true; - net_state->platform.ip4_send_socket = send_socket; + net_state->platform.icmp4_send_socket = send_socket_icmp; + net_state->platform.udp4_send_socket = send_socket_udp; net_state->platform.ip4_recv_socket = recv_socket; return 0; diff --git a/packet/probe_unix.h b/packet/probe_unix.h index f3e8207b..2ba2fac6 100644 --- a/packet/probe_unix.h +++ b/packet/probe_unix.h @@ -54,8 +54,11 @@ struct net_state_platform_t { /* true if ipv6 socket is raw socket */ bool ip6_socket_raw; - /* Socket used to send raw IPv4 packets */ - int ip4_send_socket; + /* Send socket for ICMPv6 packets */ + int icmp4_send_socket; + + /* Send socket for UDPv6 packets */ + int udp4_send_socket; /* Socket used to receive IPv4 ICMP replies */ int ip4_recv_socket; From 1b519cde3e1e23b11a2ab94c43b0d10ae5a9e588 Mon Sep 17 00:00:00 2001 From: flu0r1ne Date: Fri, 29 Sep 2023 03:07:54 -0500 Subject: [PATCH 19/33] Fix Capability Management, Retain CAP_NET_ADMIN Modify the capability-dropping logic to specifically retain CAP_NET_ADMIN if it is initially provided, in adherence to least-privilege principles. Details: 1. Update the `drop_excess_capabilities` function to only drop capabilities that are unnecessary, retaining CAP_NET_ADMIN when needed for setting the socket mark. 2. Introduce logic in `set_socket_mark` to temporarily elevate CAP_NET_ADMIN into the effective set for the duration of the packet mark setting operation. --- packet/construct_unix.c | 76 +++++++++++++++++++++++++++++++++++---- packet/packet.c | 80 +++++++++++++++++++++++++++++++++-------- 2 files changed, 134 insertions(+), 22 deletions(-) diff --git a/packet/construct_unix.c b/packet/construct_unix.c index e09d7057..a9d6d91f 100644 --- a/packet/construct_unix.c +++ b/packet/construct_unix.c @@ -32,6 +32,10 @@ #define SOL_IP IPPROTO_IP #endif +#ifdef HAVE_LIBCAP +#include +#endif + /* A source of data for computing a checksum */ struct checksum_source_t { const void *data; @@ -278,6 +282,67 @@ int construct_udp6_packet( return 0; } +#define SET_MARK_N_ADDED_CAPS 1 + +/* Set the socket mark */ +static int set_socket_mark(int socket, unsigned int mark) { + int result = -1; + + // Add CAP_NET_ADMIN to the effective set if libcap is present +#ifdef HAVE_LIBCAP + cap_t cap = NULL; + static cap_value_t cap_add[SET_MARK_N_ADDED_CAPS] = { CAP_NET_ADMIN }; + + // Get the capabilities of the current process + cap = cap_get_proc(); + if (cap == NULL) { + goto cleanup_and_exit; + } + + // Set the required capability flag + if (cap_set_flag(cap, CAP_EFFECTIVE, SET_MARK_N_ADDED_CAPS, cap_add, + CAP_SET)) { + goto cleanup_and_exit; + } + + // Apply the modified capabilities to the current process + if (cap_set_proc(cap)) { + goto cleanup_and_exit; + } +#endif /* ifdef HAVE_LIBPCAP */ + + // Set the socket mark + if (setsockopt(socket, SOL_SOCKET, SO_MARK, &mark, sizeof(mark))) { + goto cleanup_and_exit; + } + + // Drop CAP_NET_ADMIN from the effective set if libcap is present +#ifdef HAVE_LIBCAP + + // Clear the CAP_NET_ADMIN capability flag + if (cap_set_flag(cap, CAP_EFFECTIVE, SET_MARK_N_ADDED_CAPS, cap_add, + CAP_CLEAR)) { + goto cleanup_and_exit; + } + + // Apply the modified capabilities to the current process + if (cap_set_proc(cap)) { + goto cleanup_and_exit; + } +#endif /* ifdef HAVE_LIBPCAP */ + + result = 0; // Success + +cleanup_and_exit: + +#ifdef HAVE_LIBCAP + cap_free(cap); +#endif /* ifdef HAVE_LIBPCAP */ + + return result; +} + + /* Set the socket options for an outgoing stream protocol socket based on the packet parameters. @@ -341,8 +406,7 @@ int set_stream_socket_options( } #ifdef SO_MARK if (param->routing_mark) { - if (setsockopt(stream_socket, SOL_SOCKET, - SO_MARK, ¶m->routing_mark, sizeof(int))) { + if (set_socket_mark(stream_socket, param->routing_mark)) { return -1; } } @@ -360,6 +424,7 @@ int set_stream_socket_options( return 0; } + /* Open a TCP or SCTP socket, respecting the probe paramters as much as we can, and use it as an outgoing probe. @@ -577,8 +642,7 @@ int construct_ip4_packet( */ #ifdef SO_MARK if (param->routing_mark) { - if (setsockopt(send_socket, SOL_SOCKET, - SO_MARK, ¶m->routing_mark, sizeof(int))) { + if (set_socket_mark(send_socket, param->routing_mark)) { return -1; } } @@ -750,9 +814,7 @@ int construct_ip6_packet( } #ifdef SO_MARK if (param->routing_mark) { - if (setsockopt(send_socket, - SOL_SOCKET, SO_MARK, ¶m->routing_mark, - sizeof(int))) { + if (set_socket_mark(send_socket, param->routing_mark)) { return -1; } } diff --git a/packet/packet.c b/packet/packet.c index 3821d91b..ba37d25c 100644 --- a/packet/packet.c +++ b/packet/packet.c @@ -35,15 +35,75 @@ #include "wait.h" +#define N_ENTRIES(array) \ + (sizeof((array)) / sizeof(*(array))) + +#ifdef HAVE_LIBCAP +static +void drop_excess_capabilities() { + cap_value_t cap_permitted[] = { +#ifdef SO_MARK + /* + By default, the root user has all capabilities, which poses a security risk. + Since the socket has already been opened, we only need CAP_NET_ADMIN to set + the fwmark. This capability must remain in the permitted set so that it can + be added to the effective set when needed. + */ + CAP_NET_ADMIN +#endif /* ifdef SOMARK */ + }; + + cap_t current_cap = cap_get_proc(); + cap_t wanted_cap = cap_get_proc(); + + if(!current_cap || !wanted_cap) { + goto pcap_error; + } + + // Clear all capabilities from the 'wanted_cap' set + if(cap_clear(wanted_cap)) { + goto pcap_error; + } + + // Retain only the necessary capabilities defined in 'cap_permitted' in the permitted set. + // This approach ensures the principle of least privilege. + // If the user has dropped capabilities, the code assumes those features will not be needed. + for(unsigned i = 0; i < N_ENTRIES(cap_permitted); i++) { + cap_flag_value_t is_set; + + if(cap_get_flag(current_cap, cap_permitted[i], CAP_PERMITTED, &is_set)) { + goto pcap_error; + } + + if(cap_set_flag(wanted_cap, CAP_PERMITTED, 1, &cap_permitted[i], is_set)) { + goto pcap_error; + } + } + + // Update the process's capabilities to match 'wanted_cap' + if(cap_set_proc(wanted_cap)) { + goto pcap_error; + } + + if(cap_free(current_cap) || cap_free(wanted_cap)) { + goto pcap_error; + } + + return; + +pcap_error: + + cap_free(current_cap); + cap_free(wanted_cap); + error(EXIT_FAILURE, errno, "Failed to drop capabilities"); +} +#endif /* ifdef HAVE_LIBCAP */ + /* Drop SUID privileges. To be used after acquiring raw sockets. */ static int drop_elevated_permissions( void) { -#ifdef HAVE_LIBCAP - cap_t cap; -#endif - /* Drop any suid permissions granted */ if (setgid(getgid()) || setuid(getuid())) { return -1; @@ -55,19 +115,9 @@ int drop_elevated_permissions( /* Drop all process capabilities. - This will revoke anything granted by a commandline 'setcap' */ #ifdef HAVE_LIBCAP - cap = cap_get_proc(); - if (cap == NULL) { - return -1; - } - if (cap_clear(cap)) { - return -1; - } - if (cap_set_proc(cap)) { - return -1; - } + drop_excess_capabilities(); #endif return 0; From 04d5abe3b5d9975a4e74f15ef86eae3b355ae729 Mon Sep 17 00:00:00 2001 From: flu0r1ne Date: Fri, 29 Sep 2023 16:28:19 -0500 Subject: [PATCH 20/33] Fix interface binding by retaining CAP_NET_RAW This commit addresses an issue where mtr would fail with EPERM because setting the SO_BINDTODEVICE socket option requires the CAP_NET_RAW capability. Changes: - Refactor the code to abstract setting privileged socket options. This includes a common interface for setting capabilities depending on the platform (with or without LIBCAP). - Replace direct setsockopt calls with the new abstracted function for setting both SO_MARK and SO_BINDTODEVICE. - Update capability management in `drop_excess_capabilities` to retain CAP_NET_RAW when needed. --- packet/construct_unix.c | 64 ++++++++++++++++++++++++++++++++--------- packet/packet.c | 35 ++++++++++++++++------ packet/utils.h | 14 +++++++++ 3 files changed, 91 insertions(+), 22 deletions(-) create mode 100644 packet/utils.h diff --git a/packet/construct_unix.c b/packet/construct_unix.c index a9d6d91f..8df9997a 100644 --- a/packet/construct_unix.c +++ b/packet/construct_unix.c @@ -18,6 +18,8 @@ #include "construct_unix.h" +#include "utils.h" + #include #include #include @@ -282,25 +284,47 @@ int construct_udp6_packet( return 0; } -#define SET_MARK_N_ADDED_CAPS 1 +/* + This defines a common interface which elevates privileges on + platforms with LIBCAP and acts as a NOOP on platforms without + it. +*/ +#ifdef HAVE_LIBCAP + +typedef cap_value_t mayadd_cap_value_t; +#define MAYADD_CAP_NET_RAW CAP_NET_RAW +#define MAYADD_CAP_NET_ADMIN CAP_NET_ADMIN +#define MAYADD_UNUSED + +#else /* ifdef HAVE_LIBCAP */ + +typedef int mayadd_cap_value_t; +#define MAYADD_CAP_NET_RAW ((mayadd_cap_value_t) 0) +#define MAYADD_CAP_NET_ADMIN ((mayadd_cap_value_t) 0) +#define MAYADD_UNUSED UNUSED + +#endif /* ifdef HAVE_LIBCAP */ + +static +int set_privileged_socket_opt(int socket, int option_name, + void const * option_value, socklen_t option_len, + MAYADD_UNUSED mayadd_cap_value_t required_cap) { -/* Set the socket mark */ -static int set_socket_mark(int socket, unsigned int mark) { int result = -1; // Add CAP_NET_ADMIN to the effective set if libcap is present #ifdef HAVE_LIBCAP - cap_t cap = NULL; - static cap_value_t cap_add[SET_MARK_N_ADDED_CAPS] = { CAP_NET_ADMIN }; + static cap_value_t cap_add[1]; + cap_add[0] = required_cap; // Get the capabilities of the current process - cap = cap_get_proc(); + cap_t cap = cap_get_proc(); if (cap == NULL) { goto cleanup_and_exit; } // Set the required capability flag - if (cap_set_flag(cap, CAP_EFFECTIVE, SET_MARK_N_ADDED_CAPS, cap_add, + if (cap_set_flag(cap, CAP_EFFECTIVE, N_ENTRIES(cap_add), cap_add, CAP_SET)) { goto cleanup_and_exit; } @@ -312,7 +336,7 @@ static int set_socket_mark(int socket, unsigned int mark) { #endif /* ifdef HAVE_LIBPCAP */ // Set the socket mark - if (setsockopt(socket, SOL_SOCKET, SO_MARK, &mark, sizeof(mark))) { + if (setsockopt(socket, SOL_SOCKET, option_name, option_value, option_len)) { goto cleanup_and_exit; } @@ -320,7 +344,7 @@ static int set_socket_mark(int socket, unsigned int mark) { #ifdef HAVE_LIBCAP // Clear the CAP_NET_ADMIN capability flag - if (cap_set_flag(cap, CAP_EFFECTIVE, SET_MARK_N_ADDED_CAPS, cap_add, + if (cap_set_flag(cap, CAP_EFFECTIVE, N_ENTRIES(cap_add), cap_add, CAP_CLEAR)) { goto cleanup_and_exit; } @@ -342,6 +366,22 @@ static int set_socket_mark(int socket, unsigned int mark) { return result; } +/* Set the socket mark */ +#ifdef SO_MARK +static +int set_socket_mark(int socket, unsigned int mark) { + return set_privileged_socket_opt(socket, SO_MARK, &mark, sizeof(mark), + MAYADD_CAP_NET_ADMIN); +} +#endif /* ifdef SO_MARK */ + +#ifdef SO_BINDTODEVICE +static +int set_bind_to_device(int socket, char const * device) { + return set_privileged_socket_opt(socket, SO_BINDTODEVICE, device, + strlen(device), MAYADD_CAP_NET_RAW); +} +#endif /* ifdef SO_BINDTODEVICE */ /* Set the socket options for an outgoing stream protocol socket based on @@ -414,8 +454,7 @@ int set_stream_socket_options( #ifdef SO_BINDTODEVICE if (param->local_device) { - if (setsockopt(stream_socket, SOL_SOCKET, - SO_BINDTODEVICE, param->local_device, strlen(param->local_device))) { + if (set_bind_to_device(stream_socket, param->local_device)) { return -1; } } @@ -650,8 +689,7 @@ int construct_ip4_packet( #ifdef SO_BINDTODEVICE if (param->local_device) { - if (setsockopt(send_socket, SOL_SOCKET, - SO_BINDTODEVICE, param->local_device, strlen(param->local_device))) { + if(set_bind_to_device(send_socket, param->local_device)) { return -1; } } diff --git a/packet/packet.c b/packet/packet.c index ba37d25c..5f3a39b4 100644 --- a/packet/packet.c +++ b/packet/packet.c @@ -33,24 +33,41 @@ #include #endif -#include "wait.h" +#include "utils.h" -#define N_ENTRIES(array) \ - (sizeof((array)) / sizeof(*(array))) +#include "wait.h" #ifdef HAVE_LIBCAP static void drop_excess_capabilities() { - cap_value_t cap_permitted[] = { -#ifdef SO_MARK + /* By default, the root user has all capabilities, which poses a security risk. - Since the socket has already been opened, we only need CAP_NET_ADMIN to set - the fwmark. This capability must remain in the permitted set so that it can - be added to the effective set when needed. + + Some capabilities must be retained in the permitted set so that it can be added + to the effective set when needed. */ - CAP_NET_ADMIN + cap_value_t cap_permitted[] = { +#ifdef SO_MARK + /* + CAP_NET_ADMIN is needed to set the routing mark (SO_MARK) on a socket + */ + CAP_NET_ADMIN, #endif /* ifdef SOMARK */ + +#ifdef SO_BINDTODEVICE + /* + The CAP_NET_RAW capability is necessary for binding to a network device using + the SO_BINDTODEVICE socket option. Although this capability is not needed for + the initial bind operation, it is required when calling setsockopt after data has + been sent. + + Given the current architecture, the socket is re-bound to the device every time + a probe is sent. Therefore, CAP_NET_RAW is required when specifying an interface + using the -I or --interface options. + */ + CAP_NET_RAW, +#endif /* ifdef SO_BINDTODEVICE */ }; cap_t current_cap = cap_get_proc(); diff --git a/packet/utils.h b/packet/utils.h new file mode 100644 index 00000000..6e5ca8c2 --- /dev/null +++ b/packet/utils.h @@ -0,0 +1,14 @@ +#ifndef _UTILS_H +#define _UTILS_H + +// Fend off -Wunused-parameter +#if defined(__GNUC__) +# define UNUSED __attribute__((__unused__)) +#else +# define UNUSED +#endif + +// Number of entries in a fixed-length array +#define N_ENTRIES(x) (sizeof(x) / sizeof(*x)) + +#endif From 42f5dcc46a8aa537175b8ac092af254efb1360cb Mon Sep 17 00:00:00 2001 From: flu0r1ne Date: Mon, 2 Oct 2023 03:53:16 -0500 Subject: [PATCH 21/33] Linux-Only Interface, Marking, and IP Unit Tests This commit introduces three unit tests focused on interface binding, packet marking, and IP source address spoofing/selection. Each of these tests builds upon the original `MtrPacketTest` base class. To evaluate these network-dependent features, the tests utilize an emulated network environment. To enable this, a small network emulation library, `netem`, has been developed specifically for this purpose. `Netem` allows for the creation of arbitrary network configurations for testing and can reliably set up and clean up virtual network environments on Linux systems. The only dependencies are `iproute2` and `libc`, which are generally pre-installed on most Linux hosts. The commit adds three tests that are expected to fail: 1. Interface Binding: Evaluates the capability of `mtr-packet` to bind to a specific interface. 2. Packet Marking: Assesses the ability of `mtr-packet` to apply a Linux networking mark (fwmark). 3. Source Address Selection: Tests `mtr-packet`'s ability to spoof or bind to a specified source address. --- test/linux/netem.py | 667 +++++++++++++++++++++++++++++++++++++++ test/linux/netemtests.py | 173 ++++++++++ 2 files changed, 840 insertions(+) create mode 100644 test/linux/netem.py create mode 100644 test/linux/netemtests.py diff --git a/test/linux/netem.py b/test/linux/netem.py new file mode 100644 index 00000000..cc2b03fd --- /dev/null +++ b/test/linux/netem.py @@ -0,0 +1,667 @@ +''' +MtrNetEm - a small network emulation library + +Description +----------- +This small, self-contained Python library serves as a high-level API for the +creation and management of virtual network topologies in a Linux environment. +By leveraging Linux's networking capabilities, it allows for the dynamic +establishment of virutal network environments, links, and complex route and rule +configurations. It only relies on iproute2, libc, and Python 3.10. The core +architecture revolves around the `Network` base class, from which custom network +topologies can be designed. A typical use-case involves inheriting from this base +class and defining `Host` and `Link` objects as class attributes within the +constructor. This design is essential because the assignment of these objects to +class attributes during the constructor's execution is what adds them to the +underlying network topology. + +Key Features +------------ +When defining a custom network topology, `Host` and `Link` objects must be +explicitly assigned to attributes within the constructor. This allows the base +class to properly register and manage these resources. Upon creation of the +network (when entering the `with` block or calling `create` on a network instance), +the library dynamically generates names for the resources — such as the network namespaces +for hosts and the names for links — enabling unique identification and isolation. + +For example, the `SimpleNetwork` class in the code snippet below demonstrates the +creation of a basic network with two hosts (`host0` and `host1`) linked by a +virtual ethernet pair (`link`). IP addresses are then assigned to the interfaces +on this link for each host. The resource names for `Host` and `Link` objects, +like the network namespace names for hosts and the names for links, are automatically +assigned when the network is instantiated. + +Usage Example +------------- +```python +# Create a simple network with two hosts connected on a link +class SimpleNetwork(Network): + def __init__(self): + super().__init__() # must be called first + self.host0 = Host() + self.host1 = Host() + self.link = Link(self.host0, self.host1) + self.host0.add_address('192.168.10.0/31', self.link) + self.host1.add_address('192.168.10.1/31', self.link) + +# Setup the network topology +with SimpleNetwork() as net: + # Enter host0's network namespace + with net.host0.netns(): + # ping host 1 + subprocess.run(['ping', '192.168.10.1']) +``` + +Debugging +--------- +If the MTR_NETEM_TRACE environmental variable is defined, a trace +of all configuration commands will be written to standard error. +''' + +# Standard library imports +import os +import sys +import subprocess +import platform +import functools +from dataclasses import dataclass +from enum import Enum +from functools import partial +from io import IOBase +from typing import Any, Dict, List, Optional, Tuple, Union, Type, cast + +# Third-party imports +from ctypes import CDLL, get_errno + +########################## +## Network Topology API ## +########################## + +# Enum to represent reverse path filtering options +# See RFC 3704 +class Rpfilter(Enum): + '''Reverse-path filtering kernel options''' + + DISABLED = 0 + STRICT = 1 + LOOSE = 2 + +# Data class to hold interface configuration +@dataclass +class Intf(): + '''Interface configuration''' + + addresses: List[str] # List of IP addresses for this interface + name: Optional[str] = None # The link name is determined at configuration time + rpfilter: Rpfilter = Rpfilter.LOOSE # Reverse path filter setting + +@dataclass +class Route(): + '''Route configuration''' + + prefix: str # Network prefix (CIDR notation) + device: Optional[Intf] = None # Optional output interface + table: Optional[int] = None # Optional routing table ID + + +# Represents a policy routing rule +@dataclass +class Rule(): + '''Policy-routing rule''' + + not_: bool = False # Negate the rule + from_: Optional[str] = None # Optional source address + to: Optional[str] = None # Optional destination address + fwmark: Optional[int] = 0 # Optional firewall mark + table: Optional[int] = None # Routing table ID + +from typing import Callable + +class LifecycleException(Exception): + pass + +class Lifecycle(Enum): + CONFIG = 0 + RUNTIME = 1 + +class NetworkObject(): + '''Base class for all network properties''' + + _parent : 'Network' + + def __init__(self): + self._parent = None + + def _register_parent(self, net : 'Network'): + self._parent = net + +def lifecycle_method(method, lifecycle : Lifecycle): + '''Wraps network object method enforcing it is called at a particular point + in the lifecycle. This is important because configuration is static and cannot be + changed after the network object is created. Some runtime methods reference data + only available at runtime.''' + + @functools.wraps(method) + def _ensure_phase(self: NetworkObject, *method_args, **method_kwargs): + + # self._parent may be None during the configuration phase + if self._parent is not None or lifecycle != Lifecycle.CONFIG: + + current_phase = self._parent._phase + + if current_phase != lifecycle: + raise LifecycleException( + f'{method.__name__} called during an incorrect stage in' + f'the emulation lifecycle: {current_phase}, should be called' + f'during {lifecycle}' + ) + + return method(self, *method_args, **method_kwargs) + + return _ensure_phase + +# Create aliases for config and runtime calls +config_method = partial(lifecycle_method, lifecycle=Lifecycle.CONFIG) +runtime_method = partial(lifecycle_method, lifecycle=Lifecycle.RUNTIME) + +class Link(NetworkObject): + '''A link object represents a virtual ethernet pair that links two hosts''' + + hosts: Tuple[Optional['Host'], Optional['Host']] + + def __init__(self): + '''Initialize a Link object with empty hosts.''' + super().__init__() + self.hosts = (None, None) + + @config_method + def connect(self, host1 : 'Host', host2 : 'Host'): + ''' + Connect two Host objects via this Link. + + Parameters: + host1: First host to connect + host2: Second host to connect + ''' + + self.hosts = (host1, host2) + + for host in self.hosts: + host._register_link(self) + +# Define a Host class to represent virtual host +# This is a network namespace with programmatic configuration +class Host(NetworkObject): + '''A host represent a virtual host and is a member of a Network. + This is essentially a network namespace with additional configuration + including routes, rules, and interfaces.''' + + netns_name: Optional[str] + _intf: Dict[Link, Intf] + _routes: List[Route] + _rules: List[Rule] + ip_forwarding: bool + + def __init__(self, ip_forwarding=False): + '''Initialize a Host object with optional IP forwarding.''' + super().__init__() + self.netns_name = None + self._intf = {} + self._routes = [] + self._rules = [] + self.ip_forwarding = ip_forwarding + + def _register_link(self, link : Link): + ''' + Internal method to register a Link with this Host. + + Parameters: + link: The link to register + ''' + self._intf[link] = Intf(addresses = []) + + @config_method + def add_address(self, address : str, dev : Link): + ''' + Add an IP address to a specific interface associated with a link. + + Parameters: + address (str): The IP address to add. + dev (Link): The Link object representing the interface. + ''' + + self._intf[dev].addresses.append(address) + + @config_method + def config_rpfiler(self, rp: Rpfilter, dev : Link): + ''' + Set the reverse-pass filter for an interface associated with a link. + ''' + self._intf[dev].rpfilter = rp + + @runtime_method + def netns(self) -> 'NetNamespace': + ''' + Retrieve the network namespace associated with this Host. + + Returns: + NetNamespace: The network namespace object. + ''' + return NetNamespace(cast(str, self.netns_name)) + + def intf(self, link : Link) -> Intf: + ''' + Retrieve the interface associated with a specific Link. + + Parameters: + link (Link): The Link object to query for. + + Returns: + Intf: The interface associated with the Link. + ''' + return self._intf[link] + + @config_method + def add_route(self, prefix_or_route: Union[str, Route], **kwargs): + ''' + Add a route to the Host's routing table. + + Parameters: + prefix_or_route (Union[str, Route]): Either a prefix (in CIDR format) or a Route object. + kwargs: Additional optional arguments if prefix_or_route is a string. + ''' + if isinstance(prefix_or_route, str): + + if 'device' in kwargs: + device = kwargs['device'] + device = self._intf[device] if isinstance(device, Link) else device + kwargs['device'] = device + + self.add_route(Route( + prefix=prefix_or_route, + **kwargs + )) + else: + assert len(kwargs) == 0 + self._routes.append(prefix_or_route) + + @config_method + def add_rule(self, rule: Optional[Rule] = None, **kwargs): + ''' + Add a policy-based routing rule to this Host. + + Parameters: + rule (Optional[Rule]): A Rule object, if None, a Rule will be created from kwargs. + kwargs: Additional optional arguments to create a Rule object. + ''' + if not rule: + rule = Rule(**kwargs) + + self._rules.append(rule) + +class Network(): + ''' + Network class that serves as a base class for virtual network + topologies. + ''' + + name: str + _hosts: Dict[str, Host] + _links: Dict[str, Link] + _phase: Lifecycle + + def __init__(self, name: Optional[str] = None): + ''' + Initializes a Network object. + + Args: + name (Optional[str], optional): The name of the network. + Defaults to the class name if not provided. + ''' + self.name = self.__class__.__name__ if name is None else name + self._hosts = {} + self._links = {} + self._phase = Lifecycle.CONFIG + + def register_host(self, name : str, host : Host): + self._hosts[name] = host + + def register_link(self, name : str, link : Link): + self._links[name] = link + + def __setattr__(self, name : str, value : Any): + if hasattr(value, '__class__') \ + and issubclass(value.__class__, NetworkObject): + value._register_parent(self) + + if isinstance(value, Host): + self.register_host(name, value) + elif isinstance(value, Link): + self.register_link(name, value) + + super().__setattr__(name, value) + + def __enter__(self): + self.create() + return self + + def __exit__(self, exc_type, exc_value, traceback): + self.destroy() + + def create(self) -> None: + ''' + Creates the virtual network topology. Creates network namespace and associated + resources. + ''' + + assert self._phase == Lifecycle.CONFIG, \ + "Repeated calls to create() on a single network object" + + for name, host in self._hosts.items(): + host.netns_name = f'{self.name}.{name}' + + for name, link in self._links.items(): + for i, host in enumerate(cast(Tuple[Host, Host], link.hosts)): + host._intf[link].name = f'{name}{i}' + + try: + create_network(self) + self._phase = Lifecycle.RUNTIME + except Exception as e: + destroy_network(self) + raise e + + def destroy(self) -> None: + ''' + Destroys the virtual network topology by removing namespaces and + associated resources + ''' + assert self._phase == Lifecycle.RUNTIME, \ + "Network not setup" + + destroy_network(self) + self._phase = Lifecycle.CONFIG + +def supported() -> Tuple[bool, Optional[str]]: + return _supported() + +__all__ = [ 'Link', 'Rpfilter', 'Intf', 'Route', 'Rule', 'Host', 'Network', 'supported' ] + +######################## +## NETWORK NAMESPACES ## +######################## + +LIB_C_SHARED_OBJ = 'libc.so.6' + +# Define possible namespace clone flags +# Ensure the file descriptor refers to a specific namespace type +class CloneFlags(Enum): + ANY = 0 + NEWCGROUP = 0x02000000 + NEWIPC = 0x08000000 + NEWNET = 0x40000000 + NEWNS = 0x00020000 + NEWPID = 0x20000000 + NEWTIME = 0x00000080 + NEWUSER = 0x10000000 + NEWUTS = 0x04000000 + + +# Error handler for setns syscall +def setns_errhandler(ret : int, _func: Any, args: tuple): + + if ret == -1: + e = get_errno() + raise OSError(e, os.strerror(e)) + + +# Initialize libc and setup error handler for setns +libc = CDLL(LIB_C_SHARED_OBJ) +libc.setns.errcheck = setns_errhandler + + +def setns(file : IOBase, nstype : CloneFlags): + return libc.setns(file.fileno(), nstype.value) + + +# Custom exception for namespace errors +class NamespaceException(Exception): + pass + + +# Class to manage network namespaces with the context manager +# Moves the process into the namespace specified by "name" +class NetNamespace(object): + + def __init__(self, name : str): + self.name = name + self.pid = os.getpid() + self._target_ns = f'/var/run/netns/{name}' + self._current_ns = f'/proc/{self.pid}/ns/net' + self._current_ns_file = None + + def enter(self): + try: + self._current_ns_file = open(self._current_ns) + + with open(self._target_ns) as file: + setns(file, CloneFlags.NEWNET) + except FileNotFoundError: + raise NamespaceException('Failed to open the namespace file. Does the namespace exit?') + except PermissionError: + raise NamespaceException('Failed to open the namespace file. Permission denied.') + + def exit(self): + setns(self._current_ns_file, CloneFlags.NEWNET) + self._current_ns_file.close() + self._current_ns_file = None + + def __enter__(self): + self.enter() + + def __exit__(self, exc_type, exc_value, traceback): + self.exit() + + def __del__(self): + if self._current_ns_file: + self._current_ns_file.close() + +#################### +## IMPLEMENTATION ## +#################### + +# Enable tracing +MTR_NETEM_TRACE = len(os.getenv('MTR_NETEM_TRACE', '')) > 0 + +def run_cmd(*args, **kargs): + ''' + Execute a shell command. + + This function takes the same arguments as subprocess.run and executes the command. + If MTR_NETEM_TRACE is enabled, the command will be traced (i.e., printed + before execution). + ''' + + if MTR_NETEM_TRACE: + cmd = ' '.join(args[0]) + print(cmd, file=sys.stderr) + + subprocess.run(*args, **kargs) + +def rule_spec(rule : Rule) -> List[str]: + ''' + Generate a list of arguments for iproute2 to create or delete a routing + rule. + This is the concatenation of the SELECTOR and ACTION for a rule. + ''' + cmd: List[str] = [] + + if rule.not_: + cmd.append('not') + + if rule.from_: + cmd.extend(['from', rule.from_]) + + if rule.to: + cmd.extend(['to', rule.to]) + + if rule.fwmark: + cmd.extend(['fwmark', str(rule.fwmark)]) + + if rule.table: + cmd.extend(['table', str(rule.table)]) + + return cmd + +def route_spec(route : Route) -> List[str]: + ''' + Generate a list of arguments for iproute2 to create or delete a route. + This is the concatenation of the SELECTOR and ACTION for a route. + ''' + + '''Obtain the concatenation of the SELECTOR and ACTION + of an ip route command, useful for adding or deleting + rules with iproute2''' + + cmd: List[str] = [ route.prefix ] + + if route.device: + cmd.extend(['dev', cast(str, route.device.name)]) + + if route.table: + cmd.extend(['table', str(route.table)]) + + return cmd + +def set_kernel_opt(path : str, value : Union[str, int]): + ''' + Set a kernel option by writing to a sysfs or procfs entry. + ''' + + try: + with open(path, 'w') as file: + file.write(str(value)) + except Exception as e: + raise RuntimeError('Failed to configure kernel option: {str(e)}') + +def set_interface_rpfiler(intf_name : str, rpfilter : Rpfilter): + ''' + Configure the reverse path filter setting for a network interface. + ''' + + set_kernel_opt( + f'/proc/sys/net/ipv4/conf/{intf_name}/rp_filter', rpfilter.value + ) + +def set_ip_forwarding(forward : bool): + '''Enable or disable IP forwarding.''' + + set_kernel_opt('/proc/sys/net/ipv4/ip_forward', int(forward)) + +def create_network(net : Network): + ''' + Create a virtual network. + + This involves several steps: + 1. Creating network namespaces for each host. + 2. Creating virtual ethernet pairs for each link. + 3. Configuring each network interface and moving it to the appropriate namespace. + 4. Setting up routes and rules for each host. + + ''' + + cmd = partial(run_cmd, check=True) + + # Add host namespaces + host : Host + for host in net._hosts.values(): + cmd([ 'ip', 'netns', 'add', host.netns_name ], check=True) + + link : Link + for link in net._links.values(): + + intfs = tuple(host._intf[link] \ + for host in cast(Tuple[Host, Host], link.hosts)) + + # Add a virtual ethernet link + cmd([ + 'ip', 'link', 'add', intfs[0].name, + 'type', 'veth', 'peer', 'name', + intfs[1].name + ]) + + intf : Intf + for host, intf in zip(cast(Tuple[Host, Host], link.hosts), intfs): + intf_name = cast(str, intf.name) + netns_name = cast(str, host.netns_name) + + # Move a end of the link pair into the host's network namespace + cmd(['ip', 'link', 'set', intf_name, 'netns', netns_name]) + + with NetNamespace(netns_name): + # Configure the reverse pass filter + set_interface_rpfiler(intf_name, intf.rpfilter) + + # Add IP addresses to the link + for addr in intf.addresses: + cmd(['ip', 'addr', 'add', addr, 'dev', intf_name]) + + # Activate the interface + cmd(['ip', 'link', 'set', intf.name, 'up']) + + for host in net._hosts.values(): + + with NetNamespace(cast(str, host.netns_name)): + # Configure the host's ip forwarding + set_ip_forwarding(host.ip_forwarding) + + # Add the host's routes + for route in host._routes: + cmd(['ip', 'route', 'add', *route_spec(route)]) + + # Add the host's policy-database rules + for rule in host._rules: + cmd(['ip', 'rule', 'add', *rule_spec(rule)]) + +def destroy_network(net : Network): + ''' + Destroy a virtual network. + + This will remove all network namespaces and associated resources created during network setup. + ''' + + host : Host + for host in net._hosts.values(): + run_cmd([ 'ip', 'netns', 'delete', host.netns_name ]) + +def has_iproute() -> bool: + '''Test if the host has iproute2 ensuring `ip -V` returns 0''' + + try: + ip_result = subprocess.run(['ip', '-V'], capture_output=True) + except: + return False + + return ip_result.returncode == 0 + +def _supported() -> Tuple[bool, Optional[str]]: + '''Test if MtrNetEm is supported''' + + if platform.system() != 'Linux': + return False, 'Tests are only supported on Linux' + + parts = platform.release().split('.') + major, minor = int(parts[0]), int(parts[1]) + + # Linux 3.8 added the 'setns' network namespace flag + if (major < 3) or (major == 3 and minor < 8): + return False, 'Tests are only supported on Linux kernel version >= 3.8' + + if os.getuid() != 0: + return False, 'Network emulation test require root' + + if not has_iproute(): + return False, 'The ip utility must be installed (iproute2)' + + return True, None + diff --git a/test/linux/netemtests.py b/test/linux/netemtests.py new file mode 100644 index 00000000..6d3966ca --- /dev/null +++ b/test/linux/netemtests.py @@ -0,0 +1,173 @@ +''' +Tests requiring network emulation +''' + +import sys +import unittest +import netem +from netem import Host, Link, Network, Rpfilter +from pathlib import Path +from typing import TypeVar, Type + +# Allow imports from the parent directory +# +# This is not a "first-party" test since it is not +# cross-platform. + +dir_path = Path(__file__).resolve().parent +sys.path.append(str(dir_path.parent)) + +import mtrpacket + +NetworkDerivative = TypeVar('NetworkDerivative', bound=Network) + +class MtrEmulatedPacketTest(mtrpacket.MtrPacketTest): + '''Base class for network emulation packet tests. + Ensures that the network is set up before executing any tests, + and tears down the network after all tests have been executed. + ''' + + Net: NetworkDerivative + net: Type[NetworkDerivative] + + @classmethod + def setUpClass(cls): + net = cls.Net() + net.create() + cls.net = net + + super().setUpClass() + + @classmethod + def tearDownClass(cls): + cls.net.destroy() + super().tearDownClass() + + +class DualIntf(Network): + ''' + DualIntf Topology: A network with two links + useful for testing interface, route, or ip + selection. + + HOST0 HOST1 + ┌─────────┐ 172.30.1.0 ┌────────┐ + │ LINKA0 ├─────────────────────────┤ LINKA1 │ + │ │ 172.30.1.1│ │ + │ │ │ │ + │ │ 172.30.2.0 │ │ + │ LINKB0 ├─────────────────────────┤ LINKB1 │ + │ │ 172.30.2.1│ │ + └─────────┘ └────────┘ + ''' + + def __init__(self): + super().__init__() + + host0 = Host() + host1 = Host() + + link_a = Link() + link_a.connect(host0, host1) + + link_b = Link() + link_b.connect(host0, host1) + + # Only respond to inbound traffic from the peer link + host1.config_rpfiler(Rpfilter.STRICT, link_a) + host1.config_rpfiler(Rpfilter.STRICT, link_b) + + host0.add_address('172.30.1.0/31', link_a) + host1.add_address('172.30.1.1/31', link_a) + + host0.add_address('172.30.2.0/31', link_b) + host1.add_address('172.30.2.1/31', link_b) + + host0.add_route('172.30.1.0/31', device=link_a, table=100) + host0.add_rule(fwmark=100, table=100) + + self.host0 = host0 + self.host1 = host1 + + self.link_a = link_a + self.link_b = link_b + +class DualIntfPacketTest(MtrEmulatedPacketTest): + '''Test components that require a reproducible network topology''' + + Net = DualIntf + + def setUp(self): + '''Enter the namespace for host0''' + self.ns = DualIntfPacketTest.net.host0.netns() + self.ns.enter() + + super().setUp() + + def tearDown(self): + '''Exit the namespace for host0''' + self.ns.exit() + + super().tearDown() + + def test_interface_binding(self): + '''Test binding to a specific interface by sending a routable probe to an + interface where the probe is not routable.''' + + # use link 'a' + intf_a_h0 = self.net.host0.intf(self.net.link_a) + + # Expect a reply because 172.30.1.1 is on link 'a' + self.write_command(f'14 send-probe ip-4 172.30.1.1 local-device {intf_a_h0.name} timeout 1') + reply = self.parse_reply() + self.assertEqual(reply.token, 14) + self.assertEqual(reply.command_name, 'reply') + self.assertEqual(reply.argument['ip-4'], '172.30.1.1') + + # Expect no reply because 172.30.2.1 is on link 'b' + self.write_command(f'15 send-probe ip-4 172.30.2.1 local-device {intf_a_h0.name} timeout 1') + reply = self.parse_reply() + self.assertEqual(reply.token, 15) + self.assertEqual(reply.command_name, 'no-reply') + + def test_packet_marking(self): + '''Test if mtr-packet marks outbound packets.''' + + # Probes with mark '100' query a table that can only reach link 'a' + + # A probe destined for 172.30.1.1 on link 'a' should succeed + self.write_command('16 send-probe ip-4 172.30.1.1 mark 100') + reply = self.parse_reply() + self.assertEqual(reply.token, 16) + self.assertEqual(reply.command_name, 'reply') + + # A probe destined for 172.30.2.1 on link 'a' should not succeed + self.write_command('17 send-probe ip-4 172.30.2.1 mark 100') + reply = self.parse_reply() + self.assertEqual(reply.token, 17) + self.assertEqual(reply.command_name, 'no-reply') + + def test_source_address_selection(self): + '''Test manual specification of a source address.''' + + # Send a probe to 172.30.1.1 via 172.30.1.0; host2 should respond + self.write_command('18 send-probe ip-4 172.30.1.1 local-address 172.30.1.0') + reply = self.parse_reply() + self.assertEqual(reply.token, 18) + self.assertEqual(reply.command_name, 'reply') + + # Send a probe to 172.30.2.1 via 172.30.1.0; host1 will not respond + # because rp_filter is enabled and the probe is sent over link 'a' + self.write_command('19 send-probe ip-4 172.30.2.1 local-address 172.30.1.0') + reply = self.parse_reply() + self.assertEqual(reply.token, 19) + self.assertEqual(reply.command_name, 'no-reply') + +if __name__ == '__main__': + supported, err = netem.supported() + + if not supported: + print(err, file=sys.stderr) + sys.exit(1) + + unittest.main() From 74645e05ecb66d876dc7669df6e8f17c95679845 Mon Sep 17 00:00:00 2001 From: flu0r1ne Date: Mon, 2 Oct 2023 18:10:43 -0500 Subject: [PATCH 22/33] Annotate `set_privileged_socket_opt` with UNUSED The `set_privileged_socket_opt` function is currently called only when `SO_MARK` or `SO_BINDTODEVICE` are defined. On systems that do not support either feature, `set_privileged_socket_opt` is expected to be unused. Adding an annotation suppresses compiler warnings. --- packet/construct_unix.c | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/packet/construct_unix.c b/packet/construct_unix.c index 8df9997a..0d3b1f2f 100644 --- a/packet/construct_unix.c +++ b/packet/construct_unix.c @@ -294,21 +294,19 @@ int construct_udp6_packet( typedef cap_value_t mayadd_cap_value_t; #define MAYADD_CAP_NET_RAW CAP_NET_RAW #define MAYADD_CAP_NET_ADMIN CAP_NET_ADMIN -#define MAYADD_UNUSED #else /* ifdef HAVE_LIBCAP */ typedef int mayadd_cap_value_t; #define MAYADD_CAP_NET_RAW ((mayadd_cap_value_t) 0) #define MAYADD_CAP_NET_ADMIN ((mayadd_cap_value_t) 0) -#define MAYADD_UNUSED UNUSED #endif /* ifdef HAVE_LIBCAP */ -static +UNUSED static int set_privileged_socket_opt(int socket, int option_name, void const * option_value, socklen_t option_len, - MAYADD_UNUSED mayadd_cap_value_t required_cap) { + UNUSED mayadd_cap_value_t required_cap) { int result = -1; From 54abd964b7dd93cfdca97b7a4aed6b8367b913a5 Mon Sep 17 00:00:00 2001 From: flu0r1ne Date: Mon, 2 Oct 2023 20:16:28 -0500 Subject: [PATCH 23/33] Drop capabilities when `setsockopt` errors Ensure that `set_privileged_socket_opt` drops capabilities even in the event that `setsockopt` returns an error. --- packet/construct_unix.c | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/packet/construct_unix.c b/packet/construct_unix.c index 0d3b1f2f..95fefbaa 100644 --- a/packet/construct_unix.c +++ b/packet/construct_unix.c @@ -331,12 +331,10 @@ int set_privileged_socket_opt(int socket, int option_name, if (cap_set_proc(cap)) { goto cleanup_and_exit; } -#endif /* ifdef HAVE_LIBPCAP */ +#endif /* ifdef HAVE_LIBCAP */ // Set the socket mark - if (setsockopt(socket, SOL_SOCKET, option_name, option_value, option_len)) { - goto cleanup_and_exit; - } + int set_sock_err = setsockopt(socket, SOL_SOCKET, option_name, option_value, option_len); // Drop CAP_NET_ADMIN from the effective set if libcap is present #ifdef HAVE_LIBCAP @@ -351,15 +349,16 @@ int set_privileged_socket_opt(int socket, int option_name, if (cap_set_proc(cap)) { goto cleanup_and_exit; } -#endif /* ifdef HAVE_LIBPCAP */ - - result = 0; // Success +#endif /* ifdef HAVE_LIBCAP */ -cleanup_and_exit: + if(!set_sock_err) { + result = 0; // Success + } #ifdef HAVE_LIBCAP +cleanup_and_exit: cap_free(cap); -#endif /* ifdef HAVE_LIBPCAP */ +#endif /* ifdef HAVE_LIBCAP */ return result; } From 4264e97e172cf77b1b95d60634fbeadc47aa888e Mon Sep 17 00:00:00 2001 From: flu0r1ne Date: Mon, 2 Oct 2023 20:57:51 -0500 Subject: [PATCH 24/33] Fix flake8 linting - Remove unused imports - Search for ip executables in known locations - Fix f-string - Ignore assert warnings --- test/linux/netem.py | 65 ++++++++++++++++++++++++++++++++++----------- 1 file changed, 49 insertions(+), 16 deletions(-) diff --git a/test/linux/netem.py b/test/linux/netem.py index cc2b03fd..8dc8f520 100644 --- a/test/linux/netem.py +++ b/test/linux/netem.py @@ -68,7 +68,7 @@ def __init__(self): from enum import Enum from functools import partial from io import IOBase -from typing import Any, Dict, List, Optional, Tuple, Union, Type, cast +from typing import Any, Dict, List, Optional, Tuple, Union, cast # Third-party imports from ctypes import CDLL, get_errno @@ -115,8 +115,6 @@ class Rule(): fwmark: Optional[int] = 0 # Optional firewall mark table: Optional[int] = None # Routing table ID -from typing import Callable - class LifecycleException(Exception): pass @@ -282,7 +280,7 @@ def add_route(self, prefix_or_route: Union[str, Route], **kwargs): **kwargs )) else: - assert len(kwargs) == 0 + assert len(kwargs) == 0 # noqa: B101 self._routes.append(prefix_or_route) @config_method @@ -355,7 +353,8 @@ def create(self) -> None: ''' assert self._phase == Lifecycle.CONFIG, \ - "Repeated calls to create() on a single network object" + "Repeated calls to create() on a single network object" # noqa: B101 + for name, host in self._hosts.items(): host.netns_name = f'{self.name}.{name}' @@ -377,7 +376,8 @@ def destroy(self) -> None: associated resources ''' assert self._phase == Lifecycle.RUNTIME, \ - "Network not setup" + "Network not setup" # noqa: B101 + destroy_network(self) self._phase = Lifecycle.CONFIG @@ -473,6 +473,27 @@ def __del__(self): # Enable tracing MTR_NETEM_TRACE = len(os.getenv('MTR_NETEM_TRACE', '')) > 0 + +def find_ip_command() -> Optional[str]: + ''' + Search for the location of the `ip` command in common directories. + ''' + # List of possible locations where the `ip` command might be located + possible_locations = [ + "/usr/bin/ip", + "/sbin/ip", + "/usr/sbin/ip", + "/bin/ip" + ] + + # Loop through the possible locations + for location in possible_locations: + # Check if the file exists and is executable + if os.path.isfile(location) and os.access(location, os.X_OK): + return location + + return None + def run_cmd(*args, **kargs): ''' Execute a shell command. @@ -542,7 +563,7 @@ def set_kernel_opt(path : str, value : Union[str, int]): with open(path, 'w') as file: file.write(str(value)) except Exception as e: - raise RuntimeError('Failed to configure kernel option: {str(e)}') + raise RuntimeError(f'Failed to configure kernel option: {str(e)}') def set_interface_rpfiler(intf_name : str, rpfilter : Rpfilter): ''' @@ -572,10 +593,12 @@ def create_network(net : Network): cmd = partial(run_cmd, check=True) + ip_cmd = cast(str, find_ip_command()) + # Add host namespaces host : Host for host in net._hosts.values(): - cmd([ 'ip', 'netns', 'add', host.netns_name ], check=True) + cmd([ ip_cmd, 'netns', 'add', host.netns_name ], check=True) link : Link for link in net._links.values(): @@ -585,7 +608,7 @@ def create_network(net : Network): # Add a virtual ethernet link cmd([ - 'ip', 'link', 'add', intfs[0].name, + ip_cmd, 'link', 'add', intfs[0].name, 'type', 'veth', 'peer', 'name', intfs[1].name ]) @@ -596,7 +619,7 @@ def create_network(net : Network): netns_name = cast(str, host.netns_name) # Move a end of the link pair into the host's network namespace - cmd(['ip', 'link', 'set', intf_name, 'netns', netns_name]) + cmd([ip_cmd, 'link', 'set', intf_name, 'netns', netns_name]) with NetNamespace(netns_name): # Configure the reverse pass filter @@ -604,10 +627,10 @@ def create_network(net : Network): # Add IP addresses to the link for addr in intf.addresses: - cmd(['ip', 'addr', 'add', addr, 'dev', intf_name]) + cmd([ip_cmd, 'addr', 'add', addr, 'dev', intf_name]) # Activate the interface - cmd(['ip', 'link', 'set', intf.name, 'up']) + cmd([ip_cmd, 'link', 'set', intf.name, 'up']) for host in net._hosts.values(): @@ -617,11 +640,11 @@ def create_network(net : Network): # Add the host's routes for route in host._routes: - cmd(['ip', 'route', 'add', *route_spec(route)]) + cmd([ip_cmd, 'route', 'add', *route_spec(route)]) # Add the host's policy-database rules for rule in host._rules: - cmd(['ip', 'rule', 'add', *rule_spec(rule)]) + cmd([ip_cmd, 'rule', 'add', *rule_spec(rule)]) def destroy_network(net : Network): ''' @@ -630,15 +653,25 @@ def destroy_network(net : Network): This will remove all network namespaces and associated resources created during network setup. ''' + ip_cmd = cast(str, find_ip_command()) + host : Host for host in net._hosts.values(): - run_cmd([ 'ip', 'netns', 'delete', host.netns_name ]) + run_cmd([ip_cmd, 'netns', 'delete', host.netns_name]) + +# /usr/bin/ip +# /sbin/ip def has_iproute() -> bool: '''Test if the host has iproute2 ensuring `ip -V` returns 0''' + ip_cmd = find_ip_command() + + if ip_cmd is None: + return False + try: - ip_result = subprocess.run(['ip', '-V'], capture_output=True) + ip_result = subprocess.run([ip_cmd, '-V'], capture_output=True) except: return False From 6ef2b495ab2f31c3bddf99bef8ca5ffe728e1408 Mon Sep 17 00:00:00 2001 From: flu0r1ne Date: Mon, 2 Oct 2023 21:21:53 -0500 Subject: [PATCH 25/33] Change B101->S101 to reflect flake8 --- test/linux/netem.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test/linux/netem.py b/test/linux/netem.py index 8dc8f520..fb744680 100644 --- a/test/linux/netem.py +++ b/test/linux/netem.py @@ -280,7 +280,7 @@ def add_route(self, prefix_or_route: Union[str, Route], **kwargs): **kwargs )) else: - assert len(kwargs) == 0 # noqa: B101 + assert len(kwargs) == 0 # noqa: S101 self._routes.append(prefix_or_route) @config_method @@ -353,7 +353,7 @@ def create(self) -> None: ''' assert self._phase == Lifecycle.CONFIG, \ - "Repeated calls to create() on a single network object" # noqa: B101 + "Repeated calls to create() on a single network object" # noqa: S101 for name, host in self._hosts.items(): @@ -376,7 +376,7 @@ def destroy(self) -> None: associated resources ''' assert self._phase == Lifecycle.RUNTIME, \ - "Network not setup" # noqa: B101 + "Network not setup" # noqa: S101 destroy_network(self) From 47dd026ce3a9b726d1613586b8923db2c1afd645 Mon Sep 17 00:00:00 2001 From: flu0r1ne Date: Tue, 3 Oct 2023 22:13:00 -0500 Subject: [PATCH 26/33] Use a uint32 for the type of a Linux mark It is typical to store and manipulate Linux packet marks using unsigned values. --- packet/probe.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packet/probe.h b/packet/probe.h index caf1314b..5a5dc3af 100644 --- a/packet/probe.h +++ b/packet/probe.h @@ -69,7 +69,7 @@ struct probe_param_t { int type_of_service; /* The packet "mark" used for mark-based routing on Linux */ - int routing_mark; + uint32_t routing_mark; /* Time to live for the transmitted probe */ int ttl; From 23486b312c0dd9c147799d5f312c0b9ac015eec3 Mon Sep 17 00:00:00 2001 From: flu0r1ne Date: Tue, 3 Oct 2023 22:31:23 -0500 Subject: [PATCH 27/33] Use Packet Marking for IP Address Selection In certain scenarios, the routing policy database may affect packet routing. When selecting an address in `mtr`, assign a packet mark if `SO_MARK` is defined and a mark has been supplied. --- ui/net.c | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/ui/net.c b/ui/net.c index efeb7822..45c33ec8 100644 --- a/ui/net.c +++ b/ui/net.c @@ -687,8 +687,8 @@ static void net_find_interface_address_from_name( host by connecting a UDP socket and checking the address the socket is bound to. */ -static void net_find_local_address( - void) +static +void net_find_local_address(struct mtr_ctl * ctl) { int udp_socket; int addr_length; @@ -700,6 +700,15 @@ static void net_find_local_address( error(EXIT_FAILURE, errno, "udp socket creation failed"); } +#ifdef SO_MARK + /* On Linux, the packet mark can affect the selection of the source address */ + if(ctl->mark) { + if(setsockopt(udp_socket, SOL_SOCKET, SO_MARK, &ctl->mark, sizeof(ctl->mark))) { + error(EXIT_FAILURE, errno, "failed to set the packet mark"); + } + } +#endif + /* We need to set the port to a non-zero value for the connect to succeed. @@ -778,7 +787,7 @@ void net_reopen( &sourcesockaddr_struct, ctl->af, ctl->InterfaceName); inet_ntop(sourcesockaddr->sa_family, sourceaddress, localaddr, sizeof(localaddr)); } else { - net_find_local_address(); + net_find_local_address(ctl); } } From 7d8b704e8d6d7ea40919659a763a64d43c416725 Mon Sep 17 00:00:00 2001 From: flu0r1ne Date: Tue, 3 Oct 2023 22:45:20 -0500 Subject: [PATCH 28/33] Support Hexadecimal Arguments for Packet Marking Packet marks are often specified in hexadecimal format. Update the `strtonum_or_err` function to parse both hexadecimal and decimal values. --- ui/utils.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui/utils.c b/ui/utils.c index edd59055..5e3a701c 100644 --- a/ui/utils.c +++ b/ui/utils.c @@ -80,7 +80,7 @@ int strtonum_or_err( if (str != NULL && *str != '\0') { errno = 0; - num = strtoul(str, &end, 10); + num = strtoul(str, &end, 0); if (errno == 0 && str != end && end != NULL && *end == '\0') { switch (type) { case STRTO_INT: From 9c9bc719776790ccbf495f734fe93d6759f7d05d Mon Sep 17 00:00:00 2001 From: Tony Lewis Hiroaki URAHAMA <50810875+slord399@users.noreply.github.com> Date: Sat, 7 Oct 2023 20:58:59 +0100 Subject: [PATCH 29/33] Add WSL method to Windows Install It would be much simpler and not experience IPv6 related build fail at all. --- README.md | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 9e4ebbb5..1766cae6 100644 --- a/README.md +++ b/README.md @@ -73,9 +73,24 @@ Building on MacOS should not require any special steps. BUILDING FOR WINDOWS === -Building for Windows requires Cygwin. To obtain Cygwin, see +Building for Windows requires Windows Subsystem for Linux (WSL). +To install WSL, see +[How to install Linux on Windows with WSL](https://learn.microsoft.com/en-us/windows/wsl/install). + +After complete initial process, +simple as: + + sudo apt-get -y install mtr + + +This method will be simpler and easier than tranditional method using cygwin. + +If you prefer traditional method. +Obtain Cygwin, see https://cygwin.com/install.html. + Next, re-run cygwin's `setup-x86.exe` (or `setup-x86_64.exe` if you're using 64bit cygwin) with the following arguments, + which will install the packages required for building: setup-x86.exe --package-manager --wait --packages automake,pkg-config,make,gcc-core,libncurses-devel,libjansson-devel @@ -89,6 +104,8 @@ Finally, install the built binaries: make install + + WHERE CAN I GET THE LATEST VERSION OR MORE INFORMATION? === From 2579026dd84015f56eb828936abd6adac59bf82d Mon Sep 17 00:00:00 2001 From: Tony Lewis Hiroaki URAHAMA <50810875+slord399@users.noreply.github.com> Date: Sat, 7 Oct 2023 21:05:19 +0100 Subject: [PATCH 30/33] Add Ubuntu as specific distribution --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 1766cae6..5f056bc5 100644 --- a/README.md +++ b/README.md @@ -74,7 +74,7 @@ BUILDING FOR WINDOWS === Building for Windows requires Windows Subsystem for Linux (WSL). -To install WSL, see +To install WSL with Ubuntu distribution (Default), see [How to install Linux on Windows with WSL](https://learn.microsoft.com/en-us/windows/wsl/install). After complete initial process, From 0845501b347aaf7acd4d340764a31825bed82189 Mon Sep 17 00:00:00 2001 From: Tony Lewis Hiroaki URAHAMA <50810875+slord399@users.noreply.github.com> Date: Sat, 7 Oct 2023 21:20:42 +0100 Subject: [PATCH 31/33] Update section title --- README.md | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 5f056bc5..b3fab52a 100644 --- a/README.md +++ b/README.md @@ -70,10 +70,10 @@ to the "trusted" directory.) Building on MacOS should not require any special steps. -BUILDING FOR WINDOWS +USING MTR ON WINDOWS === -Building for Windows requires Windows Subsystem for Linux (WSL). +Using mtr on Windows requires Windows Subsystem for Linux (WSL). To install WSL with Ubuntu distribution (Default), see [How to install Linux on Windows with WSL](https://learn.microsoft.com/en-us/windows/wsl/install). @@ -83,7 +83,9 @@ simple as: sudo apt-get -y install mtr -This method will be simpler and easier than tranditional method using cygwin. + +BUILDING FOR WINDOWS (TRADITIONAL METHOD) +=== If you prefer traditional method. Obtain Cygwin, see From dffd81b656a9df97a5f70ec44cf65d85c1242c12 Mon Sep 17 00:00:00 2001 From: Matt Kimball Date: Tue, 24 Oct 2023 03:02:43 +0100 Subject: [PATCH 32/33] Update Cygwin ICMP service thread for asynchronous pipes Recent versions of Cygwin implement pipe() using Windows' named pipes, and put the read end of the pipe in FILE_PIPE_COMPLETE_OPERATION mode, which doesn't allow overlapped I/O operations. For the relevant commit in the Cygwin repository, see 9e4d308cd592fe383dec58ea6523c1b436888ef8 The solution here is to maintain a Windows event object which is set only when any ICMP requests are pending. We can do an alertable wait on that event object, which will allow us to complete ICMP requests. Thanks to Adam Schultz for research into this issue and a first attempt at a fix. --- packet/probe_cygwin.c | 201 +++++++++++++++++++++--------------------- packet/probe_cygwin.h | 33 +++---- 2 files changed, 113 insertions(+), 121 deletions(-) diff --git a/packet/probe_cygwin.c b/packet/probe_cygwin.c index f41e5144..d6d2b7f2 100644 --- a/packet/probe_cygwin.c +++ b/packet/probe_cygwin.c @@ -136,8 +136,14 @@ void init_net_state( net_state->platform.thread_out_pipe_read = out_pipe[0]; net_state->platform.thread_out_pipe_write = out_pipe[1]; - net_state->platform.thread_in_pipe_read_handle = - (HANDLE)get_osfhandle(in_pipe[0]); + InitializeCriticalSection(&net_state->platform.pending_request_cs); + net_state->platform.pending_request_count = 0; + net_state->platform.pending_request_event = + CreateEvent(NULL, TRUE, FALSE, NULL); + + if (net_state->platform.pending_request_event == NULL) { + error(EXIT_FAILURE, errno, "Failure creating request event"); + } /* The read on the out pipe needs to be nonblocking because @@ -281,7 +287,7 @@ void WINAPI on_icmp_reply( remote_addr6->sin6_family = AF_INET6; remote_addr6->sin6_port = 0; remote_addr6->sin6_flowinfo = 0; - memcpy(&remote_addr6->sin6_addr, reply6->AddressBits, + memcpy(&remote_addr6->sin6_addr, reply6->Address.sin6_addr, sizeof(struct in6_addr)); remote_addr6->sin6_scope_id = 0; } @@ -468,109 +474,116 @@ void icmp_handle_probe_request(struct icmp_thread_request_t *request) } /* - The main loop of the ICMP service thread. The loop starts - an overlapped read on the incoming request pipe, then waits - in an alertable wait for that read to complete. Because - the wait is alertable, ICMP probes can complete through - APCs in that wait. + Write the next thread request to the request pipe. + Update the count of pending requests and set the event + indicating that requests are present. */ static -DWORD WINAPI icmp_service_thread(LPVOID param) { - struct net_state_t *net_state; - struct icmp_thread_request_t *request; - DWORD wait_status; - OVERLAPPED overlapped; - HANDLE event; - BOOL success; - bool read_pending; - DWORD read_count; - int err; +void send_thread_request( + struct net_state_t *net_state, + struct icmp_thread_request_t *request) +{ + int byte_count; + byte_count = write( + net_state->platform.thread_in_pipe_write, + &request, + sizeof(struct icmp_thread_request_t *)); - /* - We need an event to signal completion of reads from the request - pipe. - */ - event = CreateEvent(NULL, TRUE, FALSE, NULL); - if (event == NULL) { - error_win( - EXIT_FAILURE, GetLastError(), - "failure creating ICMP thread event"); + if (byte_count == -1) { + error( + EXIT_FAILURE, errno, + "failure writing to probe request queue"); } - net_state = (struct net_state_t *)param; - read_pending = false; - while (true) { - /* - Start a new read on the request pipe if none is - currently pending. - */ - if (!read_pending) { - request = NULL; - - ResetEvent(event); - - memset(&overlapped, 0, sizeof(OVERLAPPED)); - overlapped.hEvent = event; - - success = ReadFile( - net_state->platform.thread_in_pipe_read_handle, - &request, - sizeof(struct icmp_thread_request_t *), - NULL, - &overlapped); + EnterCriticalSection(&net_state->platform.pending_request_cs); + { + net_state->platform.pending_request_count++; + SetEvent(net_state->platform.pending_request_event); + } + LeaveCriticalSection(&net_state->platform.pending_request_cs); +} - if (!success) { - err = GetLastError(); +/* + Read the next thread request from the pipe, if any are pending. + If it is the last request in the queue, reset the pending + request event. - if (err != ERROR_IO_PENDING) { - error_win( - EXIT_FAILURE, err, - "failure starting overlapped thread pipe read"); - } + If no requests are pending, return NULL. +*/ +static +struct icmp_thread_request_t *receive_thread_request( + struct net_state_t *net_state) +{ + struct icmp_thread_request_t *request; + int byte_count; + bool pending_request; + + EnterCriticalSection(&net_state->platform.pending_request_cs); + { + if (net_state->platform.pending_request_count > 0) { + pending_request = true; + net_state->platform.pending_request_count--; + if (net_state->platform.pending_request_count == 0) { + ResetEvent(net_state->platform.pending_request_event); } - - read_pending = true; + } else { + pending_request = false; } + } + LeaveCriticalSection(&net_state->platform.pending_request_cs); - /* - Wait for either the request read to complete, or - an APC which completes an ICMP probe. - */ - wait_status = WaitForSingleObjectEx( - event, - INFINITE, - TRUE); + if (!pending_request) { + return NULL; + } - /* - If the event we waited on has been signalled, read - the request from the pipe. - */ - if (wait_status == WAIT_OBJECT_0) { - read_pending = false; - - success = GetOverlappedResult( - net_state->platform.thread_in_pipe_read_handle, - &overlapped, - &read_count, - FALSE); - - if (!success) { - error_win( - EXIT_FAILURE, GetLastError(), - "failure completing overlapped thread pipe read"); - } + byte_count = read( + net_state->platform.thread_in_pipe_read, + &request, + sizeof(struct icmp_thread_request_t *)); - if (read_count == 0) { - continue; - } + if (byte_count == -1) { + error( + EXIT_FAILURE, + errno, + "failure reading probe request queue"); + } + + assert(byte_count == sizeof(struct icmp_thread_request_t *)); + + return request; +} - assert( - read_count == sizeof(struct icmp_thread_request_t *)); +/* + The main loop of the ICMP service thread. The loop starts + an overlapped read on the incoming request pipe, then waits + in an alertable wait for that read to complete. Because + the wait is alertable, ICMP probes can complete through + APCs in that wait. +*/ +static +DWORD WINAPI icmp_service_thread(LPVOID param) { + struct net_state_t *net_state; + struct icmp_thread_request_t *request; + net_state = (struct net_state_t *)param; + while (true) { + request = receive_thread_request(net_state); + if (request != NULL) { /* Start the new probe from the request */ icmp_handle_probe_request(request); + } else { + /* + Wait for either a request to be queued or for + an APC which completes an ICMP probe. + */ + WaitForSingleObjectEx( + net_state->platform.pending_request_event, + INFINITE, + TRUE); } } + + return 0; } /* @@ -587,7 +600,6 @@ void queue_thread_request( struct sockaddr_storage *src_sockaddr) { struct icmp_thread_request_t *request; - int byte_count; request = malloc(sizeof(struct icmp_thread_request_t)); if (request == NULL) { @@ -610,16 +622,7 @@ void queue_thread_request( The ownership of the request is passed to the ICMP thread through the pipe. */ - byte_count = write( - net_state->platform.thread_in_pipe_write, - &request, - sizeof(struct icmp_thread_request_t *)); - - if (byte_count == -1) { - error( - EXIT_FAILURE, errno, - "failure writing to probe request queue"); - } + send_thread_request(net_state, request); } /* Decode the probe parameters and send a probe */ diff --git a/packet/probe_cygwin.h b/packet/probe_cygwin.h index eec001f5..1e9fde7b 100644 --- a/packet/probe_cygwin.h +++ b/packet/probe_cygwin.h @@ -19,32 +19,14 @@ #ifndef PROBE_CYGWIN_H #define PROBE_CYGWIN_H +#include +typedef struct in6_addr IN6_ADDR, *PIN6_ADDR, *LPIN6_ADDR; + #include #include #include #include -/* - This should be in the Windows headers, but is missing from - Cygwin's Windows headers. -*/ -typedef struct icmpv6_echo_reply_lh { - /* - Although Windows uses an IPV6_ADDRESS_EX here, we are using uint8_t - fields to avoid structure padding differences between gcc and - Visual C++. (gcc wants to align the flow info to a 4 byte boundary, - and Windows uses it unaligned.) - */ - uint8_t PortBits[2]; - uint8_t FlowInfoBits[4]; - uint8_t AddressBits[16]; - uint8_t ScopeIdBits[4]; - - ULONG Status; - unsigned int RoundTripTime; -} ICMPV6_ECHO_REPLY, -*PICMPV6_ECHO_REPLY; - /* Windows requires an echo reply structure for each in-flight ICMP probe. @@ -67,9 +49,16 @@ struct net_state_platform_t { bool ip4_socket_raw; bool ip6_socket_raw; - HANDLE thread_in_pipe_read_handle; int thread_in_pipe_read, thread_in_pipe_write; int thread_out_pipe_read, thread_out_pipe_write; + + CRITICAL_SECTION pending_request_cs; + + /* Guarded by the critical section. */ + unsigned int pending_request_count; + + /* Set when any requests are pending. */ + HANDLE pending_request_event; }; /* From b4c47a1e8284fb11a5c294e71a0e088d4c75c14a Mon Sep 17 00:00:00 2001 From: Denys Fedoryshchenko Date: Mon, 8 Jul 2024 01:00:46 +0300 Subject: [PATCH 33/33] Fix tiny typo in target Signed-off-by: Denys Fedoryshchenko --- ui/mtr.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui/mtr.c b/ui/mtr.c index 60f81671..49e9f9f1 100644 --- a/ui/mtr.c +++ b/ui/mtr.c @@ -108,7 +108,7 @@ static void __attribute__ ((__noreturn__)) usage(FILE * out) fputs(" -m, --max-ttl NUMBER maximum number of hops\n", out); fputs(" -U, --max-unknown NUMBER maximum unknown host\n", out); fputs(" -E, --max-display-path NUMBER maximum number of ECMP paths to display\n", out); - fputs(" -P, --port PORT arget port number for TCP, SCTP, or UDP\n", out); + fputs(" -P, --port PORT target port number for TCP, SCTP, or UDP\n", out); fputs(" -L, --localport LOCALPORT source port number for UDP\n", out); fputs(" -s, --psize PACKETSIZE set the packet size used for probing\n", out); fputs(" -B, --bitpattern NUMBER set bit pattern to use in payload\n", out);