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/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 diff --git a/README.md b/README.md index 9e4ebbb5..b3fab52a 100644 --- a/README.md +++ b/README.md @@ -70,12 +70,29 @@ to the "trusted" directory.) Building on MacOS should not require any special steps. -BUILDING FOR WINDOWS +USING MTR ON WINDOWS === -Building for Windows requires Cygwin. To obtain Cygwin, see +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). + +After complete initial process, +simple as: + + sudo apt-get -y install mtr + + + +BUILDING FOR WINDOWS (TRADITIONAL METHOD) +=== + +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 +106,8 @@ Finally, install the built binaries: make install + + WHERE CAN I GET THE LATEST VERSION OR MORE INFORMATION? === diff --git a/configure.ac b/configure.ac index 3175d56b..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 @@ -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], diff --git a/man/mtr.8.in b/man/mtr.8.in index 80c47902..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 @@ -92,6 +98,9 @@ mtr \- a network diagnostic tool .BI \-U \ MAX\-UNKNOWN\c ] [\c +.BI \-E \ MAX\-DISPLAY\-PATH\c +] +[\c .B \-\-udp\c ] [\c @@ -347,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 @@ -407,6 +424,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/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 644536d8..95fefbaa 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 @@ -32,6 +34,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; @@ -71,19 +77,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 +88,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 +98,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 +197,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 +207,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 +237,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 */ @@ -306,21 +262,124 @@ 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; } +/* + 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 + +#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) + +#endif /* ifdef HAVE_LIBCAP */ + +UNUSED static +int set_privileged_socket_opt(int socket, int option_name, + void const * option_value, socklen_t option_len, + UNUSED mayadd_cap_value_t required_cap) { + + int result = -1; + + // Add CAP_NET_ADMIN to the effective set if libcap is present +#ifdef HAVE_LIBCAP + static cap_value_t cap_add[1]; + cap_add[0] = required_cap; + + // Get the capabilities of the current process + 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, N_ENTRIES(cap_add), 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_LIBCAP */ + + // Set the socket mark + 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 + + // Clear the CAP_NET_ADMIN capability flag + if (cap_set_flag(cap, CAP_EFFECTIVE, N_ENTRIES(cap_add), 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_LIBCAP */ + + if(!set_sock_err) { + result = 0; // Success + } + +#ifdef HAVE_LIBCAP +cleanup_and_exit: + cap_free(cap); +#endif /* ifdef HAVE_LIBCAP */ + + 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 the packet parameters. @@ -384,8 +443,15 @@ 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; + } + } +#endif + +#ifdef SO_BINDTODEVICE + if (param->local_device) { + if (set_bind_to_device(stream_socket, param->local_device)) { return -1; } } @@ -394,6 +460,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. @@ -545,10 +612,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; @@ -558,23 +625,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, @@ -600,62 +678,66 @@ 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; } } #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. - */ - 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)) { +#ifdef SO_BINDTODEVICE + if (param->local_device) { + if(set_bind_to_device(send_socket, param->local_device)) { return -1; } - struct sockaddr_in *sin_cur = - (struct sockaddr_in *) ¤t_sockaddr; + } +#endif - /* avoid double bind */ - if (sin_cur->sin_port) { - bind_send_socket = false; + /* + 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. + */ + 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; + + 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; } @@ -767,9 +849,17 @@ int construct_ip6_packet( } #ifdef SO_MARK if (param->routing_mark) { + if (set_socket_mark(send_socket, param->routing_mark)) { + return -1; + } + } +#endif + +#ifdef SO_BINDTODEVICE + if (param->local_device) { if (setsockopt(send_socket, - SOL_SOCKET, SO_MARK, ¶m->routing_mark, - sizeof(int))) { + SOL_SOCKET, SO_BINDTODEVICE, param->local_device, + strlen(param->local_device))) { return -1; } } diff --git a/packet/packet.c b/packet/packet.c index 3821d91b..5f3a39b4 100644 --- a/packet/packet.c +++ b/packet/packet.c @@ -33,17 +33,94 @@ #include #endif +#include "utils.h" + #include "wait.h" +#ifdef HAVE_LIBCAP +static +void drop_excess_capabilities() { + + /* + By default, the root user has all capabilities, which poses a security risk. + + Some capabilities must be retained in the permitted set so that it can be added + to the effective set when needed. + */ + 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(); + 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 +132,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; diff --git a/packet/probe.h b/packet/probe.h index e7b8beea..5a5dc3af 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; @@ -66,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; 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; }; /* diff --git a/packet/probe_unix.c b/packet/probe_unix.c index f7f393fc..0c885a58 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; @@ -543,6 +544,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); } diff --git a/packet/probe_unix.h b/packet/probe_unix.h index f217a66e..2ba2fac6 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 */ @@ -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; 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 diff --git a/test/linux/netem.py b/test/linux/netem.py new file mode 100644 index 00000000..fb744680 --- /dev/null +++ b/test/linux/netem.py @@ -0,0 +1,700 @@ +''' +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, 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 + +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 # noqa: S101 + 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" # noqa: S101 + + + 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" # noqa: S101 + + + 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 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. + + 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(f'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) + + ip_cmd = cast(str, find_ip_command()) + + # Add host namespaces + host : Host + for host in net._hosts.values(): + cmd([ ip_cmd, '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_cmd, '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_cmd, '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_cmd, 'addr', 'add', addr, 'dev', intf_name]) + + # Activate the interface + cmd([ip_cmd, '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_cmd, 'route', 'add', *route_spec(route)]) + + # Add the host's policy-database rules + for rule in host._rules: + cmd([ip_cmd, '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. + ''' + + ip_cmd = cast(str, find_ip_command()) + + host : Host + for host in net._hosts.values(): + 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_cmd, '-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() 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''' diff --git a/ui/asn.c b/ui/asn.c index 3f424e08..111c3942 100644 --- a/ui/asn.c +++ b/ui/asn.c @@ -21,6 +21,7 @@ #include #include #include +#include #include #ifdef HAVE_ERROR_H #include @@ -65,7 +66,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 */ @@ -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.origin6.asn.cymru.com", key) - >= 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 @@ -243,7 +264,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/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); diff --git a/ui/curses.c b/ui/curses.c index 3b295f22..1c41c467 100644 --- a/ui/curses.c +++ b/ui/curses.c @@ -424,8 +424,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); @@ -474,7 +474,7 @@ static void mtr_curses_hosts( } /* Multi path */ - for (i = 0; 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..49e9f9f1 100644 --- a/ui/mtr.c +++ b/ui/mtr.c @@ -94,84 +94,63 @@ 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); + 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 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); + fputs(" --ipinfo_provider4 provider for IPv4 AS lookups\n", out); #ifdef ENABLE_IPV6 - fputs(" -6 use IPv6 only\n", out); + fputs(" --ipinfo_provider6 provider for IPv6 AS lookups\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); +#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); @@ -319,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 */ @@ -356,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'}, @@ -369,6 +356,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 @@ -400,7 +388,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) @@ -521,6 +508,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) { @@ -628,6 +621,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': @@ -728,6 +729,7 @@ int main( int argc, char **argv) { + int exit_val = EXIT_SUCCESS; names_t *names_head = NULL; names_t *names_walk; @@ -747,9 +749,17 @@ 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; +#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); /* @@ -801,6 +811,7 @@ int main( if (ctl.Interactive) exit(EXIT_FAILURE); else { + exit_val = EXIT_FAILURE; names_walk = names_walk->next; continue; } @@ -811,6 +822,7 @@ int main( if (ctl.Interactive) exit(EXIT_FAILURE); else { + exit_val = EXIT_FAILURE; names_walk = names_walk->next; continue; } @@ -846,5 +858,5 @@ int main( item = NULL; } - return 0; + return exit_val; } diff --git a/ui/mtr.h b/ui/mtr.h index 252d9541..7b4482b3 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 */ @@ -117,6 +118,12 @@ struct mtr_ctl { use_dns:1, show_ips:1, enablempls:1, dns:1, reportwide:1, Interactive:1, DisplayMode:5, CompactLayout:1; +#ifdef HAVE_IPINFO +#ifdef ENABLE_IPV6 + char *ipinfo_provider6; +#endif + char *ipinfo_provider4; +#endif }; /* dynamic field drawing */ 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); } } diff --git a/ui/report.c b/ui/report.c index a63ed552..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; @@ -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); 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: