Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions Makefile.am
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,13 @@ EXTRA_DIST = \

sbin_PROGRAMS = mtr mtr-packet
TESTS = \
test/capability-drop.py \
test/cmdparse.py \
test/param.py \
test/probe.py

TEST_FILES = \
test/capability-drop.py \
test/cmdparse.py \
test/mtrpacket.py \
test/param.py \
Expand Down Expand Up @@ -60,6 +62,7 @@ mtr_SOURCES = ui/mtr.c ui/mtr.h \
ui/select.c ui/select.h \
ui/utils.c ui/utils.h \
packet/cmdparse.c packet/cmdparse.h \
packet/ports.h \
packet/sockaddr.c packet/sockaddr.h \
ui/mtr-curses.h \
img/mtr_icon.xpm \
Expand Down Expand Up @@ -106,6 +109,7 @@ mtr_packet_SOURCES = \
packet/packet.c \
packet/cmdparse.c packet/cmdparse.h \
packet/command.c packet/command.h \
packet/ports.h \
packet/platform.h \
packet/probe.c packet/probe.h \
packet/utils.h \
Expand Down
4 changes: 4 additions & 0 deletions man/mtr.8.in
Original file line number Diff line number Diff line change
Expand Up @@ -479,6 +479,10 @@ rotating privileged ports into the high end of the UDP port range unless
.B \-\-port
was given; in that case, UDP traces encode the probe sequence number in
the source port instead.
Valid port numbers are 1 through 65535. Ports below 1024 can be used
only when the operating system allows the packet helper to bind
privileged local ports; otherwise the probe fails with a permission
error.
.TP
.B \-Z \fISECONDS\fR, \fB\-\-timeout \fISECONDS
The number of seconds to keep probe sockets open before giving up on
Expand Down
6 changes: 1 addition & 5 deletions packet/command.c
Original file line number Diff line number Diff line change
Expand Up @@ -226,11 +226,7 @@ bool decode_probe_argument(
return false;
}

/*
Don't allow using a local port which requires
privileged binding.
*/
if (param->local_port < 1024) {
if (!MTR_IS_VALID_PORT(param->local_port)) {
param->local_port = 0;
return false;
}
Expand Down
154 changes: 61 additions & 93 deletions packet/construct_unix.c
Original file line number Diff line number Diff line change
Expand Up @@ -34,12 +34,7 @@
#define SOL_IP IPPROTO_IP
#endif

#ifdef HAVE_LIBCAP
#include <sys/capability.h>
#endif

#define MIN_UNPRIVILEGED_PORT 1024
#define UDP_PORT_RANGE 65536
#include "ports.h"

/* A source of data for computing a checksum */
struct checksum_source_t {
Expand All @@ -52,8 +47,8 @@ uint16_t udp_source_port_from_pid(void)
{
uint16_t port = getpid() & 0xffff;

if (port < MIN_UNPRIVILEGED_PORT) {
port += UDP_PORT_RANGE - MIN_UNPRIVILEGED_PORT;
if (port < MTR_UNPRIVILEGED_PORT_MIN) {
port += MTR_UDP_PORT_RANGE - MTR_UNPRIVILEGED_PORT_MIN;
}

return port;
Expand Down Expand Up @@ -103,6 +98,39 @@ void construct_addr_port(
*sockaddr_port_offset(addr_with_port) = htons(port);
}

static
int check_udp_bind_allowed(
const struct sockaddr_storage *addr,
socklen_t addr_len)
{
int saved_errno;
int udp_socket;

udp_socket = socket(addr->ss_family, SOCK_DGRAM, IPPROTO_UDP);
if (udp_socket == -1) {
return -1;
}

/*
Raw sockets can put any UDP source port in the packet header. For
privileged ports, first prove that this process can bind the same port
after capability dropping, so raw packet construction cannot bypass the
kernel's local-port permission policy.
*/
if (bind(udp_socket, (const struct sockaddr *) addr, addr_len)) {
saved_errno = errno;
close(udp_socket);
errno = saved_errno;
return -1;
}

if (close(udp_socket)) {
return -1;
}

return 0;
}

/* Construct an ICMP header for IPv4 */
static
int construct_icmp4_packet(
Expand Down Expand Up @@ -298,99 +326,25 @@ int construct_udp6_packet(
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);
int set_socket_mark(
int socket,
unsigned int mark)
{
return setsockopt(socket, SOL_SOCKET, SO_MARK, &mark, sizeof(mark));
}
#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);
int set_bind_to_device(
int socket,
char const *device)
{
return setsockopt(socket, SOL_SOCKET, SO_BINDTODEVICE, device,
strlen(device));
}
#endif /* ifdef SO_BINDTODEVICE */

Expand Down Expand Up @@ -670,6 +624,13 @@ int construct_ip4_packet(
(net_state, probe, packet_buffer, packet_size, param)) {
return -1;
}

if (net_state->platform.ip4_socket_raw &&
MTR_IS_PRIVILEGED_PORT(param->local_port) &&
check_udp_bind_allowed(&probe->local_addr,
sizeof(struct sockaddr_in))) {
return -1;
}
} else {
errno = EINVAL;
return -1;
Expand Down Expand Up @@ -823,6 +784,13 @@ int construct_ip6_packet(
(net_state, probe, packet_buffer, packet_size, param)) {
return -1;
}

if (net_state->platform.ip6_socket_raw &&
MTR_IS_PRIVILEGED_PORT(param->local_port) &&
check_udp_bind_allowed(&probe->local_addr,
sizeof(struct sockaddr_in6))) {
return -1;
}
} else {
errno = EINVAL;
return -1;
Expand Down
70 changes: 14 additions & 56 deletions packet/packet.c
Original file line number Diff line number Diff line change
Expand Up @@ -42,78 +42,36 @@

#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();
void drop_all_capabilities()
{
cap_t wanted_cap = cap_get_proc();

if(!current_cap || !wanted_cap) {
if (!wanted_cap) {
goto pcap_error;
}

// Clear all capabilities from the 'wanted_cap' set
if(cap_clear(wanted_cap)) {
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)) {
/*
mtr-packet opens any sockets that need elevated privileges before this
point. Do not keep capabilities in the permitted set for later
re-enabling: once privilege is dropped, later packet handling must not be
able to regain it.
*/
if (cap_set_proc(wanted_cap)) {
goto pcap_error;
}

if(cap_free(current_cap) || cap_free(wanted_cap)) {
if (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");
}
Expand All @@ -134,10 +92,10 @@ int drop_elevated_permissions(
}

/*
Drop all process capabilities.
Drop all process capabilities permanently.
*/
#ifdef HAVE_LIBCAP
drop_excess_capabilities();
drop_all_capabilities();
#endif

return 0;
Expand Down
33 changes: 33 additions & 0 deletions packet/ports.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
/*
mtr -- a network diagnostic tool
Copyright (C) 2026 Darafei Praliaskouski

This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License version 2 as
published by the Free Software Foundation.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License along
with this program; if not, write to the Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
*/

#ifndef PORTS_H
#define PORTS_H

#define MTR_PORT_MIN 1
#define MTR_PORT_MAX 65535
#define MTR_UNPRIVILEGED_PORT_MIN 1024
#define MTR_UDP_PORT_RANGE 65536

#define MTR_IS_VALID_PORT(port) \
((port) >= MTR_PORT_MIN && (port) <= MTR_PORT_MAX)

#define MTR_IS_PRIVILEGED_PORT(port) \
((port) >= MTR_PORT_MIN && (port) < MTR_UNPRIVILEGED_PORT_MIN)

#endif
Loading
Loading