Skip to content
Open
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
11 changes: 8 additions & 3 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ endif

BIN = nsjail
LIBS = kafel/libkafel.a
TEST_BINS = tests/nstun_buffer_budget_test tests/nstun_policy_test tests/nstun_ip_test tests/nstun_udp_peer_test
TEST_BINS = tests/nstun_buffer_budget_test tests/nstun_policy_test tests/nstun_ip_test tests/nstun_ipparse_test tests/nstun_udp_peer_test

# If PASTA_BIN_PATH is not provided in env, dynamically search for it if EMBED_PASTA is requested
# or fallback to it naturally.
Expand All @@ -62,7 +62,7 @@ ifneq ($(PASTA_BIN_PATH),)
CXXFLAGS += -DPASTA_BIN_PATH='"$(PASTA_BIN_PATH)"'
endif

SRCS_CXX = caps.cc cgroup.cc cgroup2.cc cmdline.cc config.cc contain.cc cpu.cc logs.cc mnt.cc mnt_legacy.cc mnt_newapi.cc net.cc nsjail.cc pid.cc sandbox.cc subproc.cc uts.cc user.cc unotify/unotify.cc unotify/stats.cc unotify/syscall.cc util.cc nstun/nstun.cc nstun/policy.cc nstun/encap.cc nstun/iface.cc nstun/tun.cc nstun/ip.cc nstun/icmp.cc nstun/udp.cc nstun/tcp.cc
SRCS_CXX = caps.cc cgroup.cc cgroup2.cc cmdline.cc config.cc contain.cc cpu.cc logs.cc mnt.cc mnt_legacy.cc mnt_newapi.cc net.cc nsjail.cc pid.cc sandbox.cc subproc.cc uts.cc user.cc unotify/unotify.cc unotify/stats.cc unotify/syscall.cc util.cc nstun/nstun.cc nstun/policy.cc nstun/encap.cc nstun/iface.cc nstun/tun.cc nstun/ip.cc nstun/ipparse.cc nstun/icmp.cc nstun/udp.cc nstun/tcp.cc
SRCS_PROTO = config.proto unotify/unotify.proto

SRCS_PB_CXX = $(SRCS_PROTO:.proto=.pb.cc)
Expand Down Expand Up @@ -164,6 +164,7 @@ test: $(BIN) $(TEST_BINS) test-cmdline
$(call run_test, ./tests/nstun_buffer_budget_test, 0)
$(call run_test, ./tests/nstun_policy_test, 0)
$(call run_test, ./tests/nstun_ip_test, 0)
$(call run_test, ./tests/nstun_ipparse_test, 0)
$(call run_test, ./tests/nstun_udp_peer_test, 0)
# --- Basic sanity tests ---
$(call run_test, ./nsjail -q -Mo --chroot / --user 99999 --group 99999 -- /bin/true, 0)
Expand Down Expand Up @@ -272,6 +273,9 @@ tests/nstun_policy_test: tests/nstun_policy_test.cc nstun/policy.cc logs.cc util
tests/nstun_ip_test: tests/nstun_ip_test.cc nstun/net_defs.h
$(CXX) $(filter-out -c,$(CXXFLAGS)) tests/nstun_ip_test.cc -o $@

tests/nstun_ipparse_test: tests/nstun_ipparse_test.cc nstun/ipparse.cc nstun/ipparse.h
$(CXX) $(filter-out -c,$(CXXFLAGS)) tests/nstun_ipparse_test.cc nstun/ipparse.cc -o $@

tests/nstun_udp_peer_test: tests/nstun_udp_peer_test.cc nstun/udp_peer.h nstun/core.h
$(CXX) $(filter-out -c,$(CXXFLAGS)) tests/nstun_udp_peer_test.cc -o $@

Expand Down Expand Up @@ -326,7 +330,8 @@ nstun/iface.o: config.pb.h nstun/nstun.h
nstun/tun.o: nstun/tun.h nstun/core.h nstun/buffer_budget.h nstun/net_defs.h
nstun/tun.o: nstun/nstun.h nstun/icmp.h nstun/ip.h logs.h
nstun/ip.o: nstun/ip.h nstun/core.h nstun/buffer_budget.h nstun/net_defs.h
nstun/ip.o: nstun/nstun.h nstun/icmp.h logs.h nstun/tcp.h nstun/udp.h
nstun/ip.o: nstun/nstun.h nstun/icmp.h nstun/ipparse.h logs.h nstun/tcp.h nstun/udp.h
nstun/ipparse.o: nstun/ipparse.h
nstun/icmp.o: nstun/icmp.h nstun/core.h nstun/buffer_budget.h
nstun/icmp.o: nstun/net_defs.h nstun/nstun.h logs.h macros.h nstun/policy.h
nstun/icmp.o: nstun/tun.h
Expand Down
212 changes: 88 additions & 124 deletions nstun/ip.cc
Original file line number Diff line number Diff line change
Expand Up @@ -5,99 +5,48 @@

#include "core.h"
#include "icmp.h"
#include "ipparse.h"
#include "logs.h"
#include "tcp.h"
#include "udp.h"

namespace nstun {

/*
* Skip IPv6 extension headers to locate the first L4 header.
*
* Returns the L4 protocol number (e.g. IPPROTO_TCP) on success,
* or -1 if the chain is malformed, too deep, or contains a fragment
* (which nstun does not reassemble).
*
* `ptr` and `rem` are advanced past each extension header so the
* caller can read the L4 header directly from `ptr`.
*/
static int skip_ipv6_ext_headers(int next_header, const uint8_t*& ptr, size_t& rem) {
constexpr int MAX_EXT = 8; /* Defense: cap chain depth */
for (int i = 0; i < MAX_EXT; ++i) {
ssize_t ext_len;
switch (next_header) {
case IPPROTO_HOPOPTS: /* Hop-by-Hop Options */
case IPPROTO_DSTOPTS: /* Destination Options */
case IPPROTO_ROUTING: /* Routing */
case 139: /* Host Identity Protocol */
case 140: { /* Shim6 */
if (rem < 2) return -1;
size_t len = ((size_t)ptr[1] + 1) * 8;
ext_len = (len <= rem) ? (ssize_t)len : -1;
break;
}
case IPPROTO_FRAGMENT:
/* nstun does not reassemble fragments. Drop unconditionally
* to match IPv4 behavior and prevent L4 port-based rule
* bypass via non-first fragments. */
return -1;
case IPPROTO_AH: { /* Authentication Header */
if (rem < 2) return -1;
size_t len = ((size_t)ptr[1] + 2) * 4;
ext_len = (len <= rem) ? (ssize_t)len : -1;
break;
}
default:
ext_len = 0; /* Not an extension header: this is the L4 protocol */
break;
}

if (ext_len < 0) return -1; /* Malformed or unsupported */
if (ext_len == 0) return next_header; /* Reached L4 */

next_header = ptr[0]; /* Next Header field is first byte of ext header */
ptr += ext_len;
rem -= ext_len;
}
return -1; /* Extension header chain too deep */
}

void handle_ip4(Context* ctx, std::span<const uint8_t> payload) {
if (payload.size() < sizeof(ip4_hdr)) {
ByteReader reader(payload);
ip4_hdr ip = {};
if (!reader.read_struct(&ip)) {
return;
}

const ip4_hdr* ip = reinterpret_cast<const ip4_hdr*>(payload.data());
uint8_t ihl = ip4_ihl(ip) * 4;

size_t ihl = static_cast<size_t>(ip4_ihl(&ip)) * 4;
if (ihl < sizeof(ip4_hdr) || ihl > payload.size()) {
LOG_D("Invalid IPv4 IHL");
return;
}

uint16_t tot_len = ntohs(ip->tot_len);
uint16_t tot_len = ntohs(ip.tot_len);
if (tot_len < ihl || tot_len > payload.size()) {
LOG_D("Invalid IPv4 tot_len");
return;
}

const uint8_t* l4_payload = payload.data() + ihl;
size_t l4_len = tot_len - ihl;
auto l4_payload = payload.subspan(ihl, static_cast<size_t>(tot_len) - ihl);

/* Drop IP fragments: nstun does not reassemble, and non-first
* fragments have no L4 header - parsing them would bypass rules */
if (ntohs(ip->frag_off) & 0x3FFF) {
if (ntohs(ip.frag_off) & 0x3FFF) {
LOG_W("Dropping IPv4 fragment");
return;
}

/* Validate IPv4 header checksum */
if (compute_checksum(ip, ihl) != 0) {
if (compute_checksum(payload.data(), ihl) != 0) {
LOG_W("Invalid IPv4 header checksum, dropping");
return;
}

if (ip->saddr != ctx->guest_ip4 && ip->saddr != 0) {
if (ip.saddr != ctx->guest_ip4 && ip.saddr != 0) {
LOG_W("Dropping packet with invalid source IP");
return;
}
Expand All @@ -106,72 +55,80 @@ void handle_ip4(Context* ctx, std::span<const uint8_t> payload) {
* This is the single authoritative check - L4 handlers rely on this
* and do NOT duplicate it. Redirect rules in policy may still target
* loopback intentionally (admin-controlled). */
if (IN_LOOPBACK(ntohl(ip->daddr)) || ip4_is_link_local(ip->daddr) ||
ip->daddr == htonl(INADDR_ANY) || ip->daddr == htonl(INADDR_BROADCAST)) {
if (IN_LOOPBACK(ntohl(ip.daddr)) || ip4_is_link_local(ip.daddr) ||
ip.daddr == htonl(INADDR_ANY) || ip.daddr == htonl(INADDR_BROADCAST)) {
LOG_W("Dropping packet destined to loopback, link-local, ANY, or broadcast: %s",
ip4_to_string(ip->daddr).c_str());
ip4_to_string(ip.daddr).c_str());
return;
}
uint16_t src_port = 0, dest_port = 0;
auto l4_span = payload.subspan(ihl, l4_len);
if (ip->protocol == IPPROTO_TCP && l4_span.size() >= sizeof(tcp_hdr)) {
const tcp_hdr* tcp = reinterpret_cast<const tcp_hdr*>(l4_span.data());
src_port = ntohs(tcp->source);
dest_port = ntohs(tcp->dest);
} else if (ip->protocol == IPPROTO_UDP && l4_span.size() >= sizeof(udp_hdr)) {
const udp_hdr* udp = reinterpret_cast<const udp_hdr*>(l4_span.data());
src_port = ntohs(udp->source);
dest_port = ntohs(udp->dest);
ByteReader l4_reader(l4_payload);
if (ip.protocol == IPPROTO_TCP) {
tcp_hdr tcp = {};
if (l4_reader.read_struct(&tcp)) {
src_port = ntohs(tcp.source);
dest_port = ntohs(tcp.dest);
}
} else if (ip.protocol == IPPROTO_UDP) {
udp_hdr udp = {};
if (l4_reader.read_struct(&udp)) {
src_port = ntohs(udp.source);
dest_port = ntohs(udp.dest);
}
}

if (src_port != 0 && dest_port != 0) {
LOG_D("IP packet: proto=%u, %s:%u -> %s:%u, len=%zu", ip->protocol,
ip4_to_string(ip->saddr).c_str(), src_port, ip4_to_string(ip->daddr).c_str(),
dest_port, l4_len);
LOG_D("IP packet: proto=%u, %s:%u -> %s:%u, len=%zu", ip.protocol,
ip4_to_string(ip.saddr).c_str(), src_port, ip4_to_string(ip.daddr).c_str(),
dest_port, l4_payload.size());
} else {
LOG_D("IP packet: proto=%u, %s -> %s, len=%zu", ip->protocol,
ip4_to_string(ip->saddr).c_str(), ip4_to_string(ip->daddr).c_str(), l4_len);
LOG_D("IP packet: proto=%u, %s -> %s, len=%zu", ip.protocol,
ip4_to_string(ip.saddr).c_str(), ip4_to_string(ip.daddr).c_str(),
l4_payload.size());
}

switch (ip->protocol) {
/* The handlers may copy the original header into an ICMP error, so keep
* the pointer into the packet rather than the parsed copy above. */
const ip4_hdr* ip_header = reinterpret_cast<const ip4_hdr*>(payload.data());
switch (ip.protocol) {
case IPPROTO_ICMP:
handle_icmp4(ctx, ip, payload.subspan(ihl, l4_len));
handle_icmp4(ctx, ip_header, l4_payload);
break;
case IPPROTO_UDP:
handle_udp4(ctx, ip, payload.subspan(ihl, l4_len));
handle_udp4(ctx, ip_header, l4_payload);
break;
case IPPROTO_TCP:
handle_tcp4(ctx, ip, payload.subspan(ihl, l4_len));
handle_tcp4(ctx, ip_header, l4_payload);
break;
default:
LOG_D("Unknown IPv4 protocol: %u", ip->protocol);
LOG_D("Unknown IPv4 protocol: %u", ip.protocol);
break;
}
}

void handle_ip6(Context* ctx, std::span<const uint8_t> payload) {
if (payload.size() < sizeof(ip6_hdr)) {
ByteReader reader(payload);
ip6_hdr ip6 = {};
if (!reader.read_struct(&ip6)) {
return;
}

const ip6_hdr* ip6 = reinterpret_cast<const ip6_hdr*>(payload.data());
uint16_t payload_len = ntohs(ip6->payload_len);

if (payload_len + sizeof(ip6_hdr) > payload.size()) {
uint16_t payload_len = ntohs(ip6.payload_len);
if (payload_len > reader.remaining()) {
LOG_D("Invalid IPv6 payload_len");
return;
}

/* Source IP filtering */
if (memcmp(ip6->saddr, ctx->guest_ip6, IPV6_ADDR_LEN) != 0) {
if (IN6_IS_ADDR_LINKLOCAL((const struct in6_addr*)ip6->saddr) ||
IN6_IS_ADDR_SITELOCAL((const struct in6_addr*)ip6->saddr)) {
if (memcmp(ip6.saddr, ctx->guest_ip6, IPV6_ADDR_LEN) != 0) {
if (IN6_IS_ADDR_LINKLOCAL((const struct in6_addr*)ip6.saddr) ||
IN6_IS_ADDR_SITELOCAL((const struct in6_addr*)ip6.saddr)) {
LOG_D("Dropping IPv6 packet with link/site-local source address: %s",
ip6_to_string(ip6->saddr).c_str());
ip6_to_string(ip6.saddr).c_str());
return;
} else {
LOG_W("Dropping IPv6 packet with spoofed source address: %s",
ip6_to_string(ip6->saddr).c_str());
ip6_to_string(ip6.saddr).c_str());
return;
}
}
Expand All @@ -181,40 +138,40 @@ void handle_ip6(Context* ctx, std::span<const uint8_t> payload) {
* This is the single authoritative check - L4 handlers rely on this
* and do NOT duplicate it. Redirect rules in policy may still target
* ::1 intentionally (admin-controlled). */
if (IN6_IS_ADDR_UNSPECIFIED((const struct in6_addr*)ip6->daddr)) {
if (IN6_IS_ADDR_UNSPECIFIED((const struct in6_addr*)ip6.daddr)) {
/* connect() to :: reaches ::1 on Linux, exactly as connect() to 0.0.0.0
* reaches 127.0.0.1. Without this the guest would get at host loopback
* services; handle_ip4() rejects INADDR_ANY for the same reason. */
LOG_W("Dropping IPv6 packet to the unspecified address: %s",
ip6_to_string(ip6->daddr).c_str());
ip6_to_string(ip6.daddr).c_str());
return;
}
if (IN6_IS_ADDR_LOOPBACK((const struct in6_addr*)ip6->daddr)) {
LOG_D("Dropping IPv6 packet to loopback: %s", ip6_to_string(ip6->daddr).c_str());
if (IN6_IS_ADDR_LOOPBACK((const struct in6_addr*)ip6.daddr)) {
LOG_D("Dropping IPv6 packet to loopback: %s", ip6_to_string(ip6.daddr).c_str());
return;
}
if (IN6_IS_ADDR_V4MAPPED((const struct in6_addr*)ip6->daddr)) {
if (IN6_IS_ADDR_V4MAPPED((const struct in6_addr*)ip6.daddr)) {
LOG_D("Dropping IPv6 packet to v4-mapped address (use IPv4 directly): %s",
ip6_to_string(ip6->daddr).c_str());
ip6_to_string(ip6.daddr).c_str());
return;
}
if (IN6_IS_ADDR_V4COMPAT((const struct in6_addr*)ip6->daddr)) {
if (IN6_IS_ADDR_V4COMPAT((const struct in6_addr*)ip6.daddr)) {
LOG_D("Dropping IPv6 packet to v4-compatible address (deprecated): %s",
ip6_to_string(ip6->daddr).c_str());
ip6_to_string(ip6.daddr).c_str());
return;
}
if (IN6_IS_ADDR_LINKLOCAL((const struct in6_addr*)ip6->daddr) ||
IN6_IS_ADDR_SITELOCAL((const struct in6_addr*)ip6->daddr) ||
ip6_is_aws_local_service(ip6->daddr)) {
if (IN6_IS_ADDR_LINKLOCAL((const struct in6_addr*)ip6.daddr) ||
IN6_IS_ADDR_SITELOCAL((const struct in6_addr*)ip6.daddr) ||
ip6_is_aws_local_service(ip6.daddr)) {
LOG_D("Dropping IPv6 packet to link/site-local or AWS local-service address: %s",
ip6_to_string(ip6->daddr).c_str());
ip6_to_string(ip6.daddr).c_str());
return;
}
if (IN6_IS_ADDR_MULTICAST((const struct in6_addr*)ip6->daddr)) {
if (IN6_IS_ADDR_MULTICAST((const struct in6_addr*)ip6.daddr)) {
/* nstun is a TUN (L3) device and joins no groups; forwarding these
* would emit guest multicast onto the host's network instead. */
LOG_D("Dropping IPv6 packet to multicast address: %s",
ip6_to_string(ip6->daddr).c_str());
ip6_to_string(ip6.daddr).c_str());
return;
}
/*
Expand All @@ -224,45 +181,52 @@ void handle_ip6(Context* ctx, std::span<const uint8_t> payload) {
* Each header's "Next Header" field identifies what follows.
* We only need to find the L4 header, not process the extensions.
*/
const uint8_t* l4_payload = payload.data() + sizeof(ip6_hdr);
size_t remaining = payload_len;

int l4_proto = skip_ipv6_ext_headers(ip6->next_header, l4_payload, remaining);
std::span<const uint8_t> l4_area = reader.rest().first(payload_len);
ByteReader l4_reader(l4_area);
int l4_proto = skip_ipv6_ext_headers(ip6.next_header, &l4_reader);
if (l4_proto < 0) {
LOG_D("Failed to parse IPv6 extension headers");
return;
}

uint16_t src_port = 0, dest_port = 0;
if (l4_proto == IPPROTO_TCP && remaining >= sizeof(tcp_hdr)) {
const tcp_hdr* tcp = reinterpret_cast<const tcp_hdr*>(l4_payload);
src_port = ntohs(tcp->source);
dest_port = ntohs(tcp->dest);
} else if (l4_proto == IPPROTO_UDP && remaining >= sizeof(udp_hdr)) {
const udp_hdr* udp = reinterpret_cast<const udp_hdr*>(l4_payload);
src_port = ntohs(udp->source);
dest_port = ntohs(udp->dest);
ByteReader l4_peek = l4_reader;
if (l4_proto == IPPROTO_TCP) {
tcp_hdr tcp = {};
if (l4_peek.read_struct(&tcp)) {
src_port = ntohs(tcp.source);
dest_port = ntohs(tcp.dest);
}
} else if (l4_proto == IPPROTO_UDP) {
udp_hdr udp = {};
if (l4_peek.read_struct(&udp)) {
src_port = ntohs(udp.source);
dest_port = ntohs(udp.dest);
}
}

if (src_port != 0 && dest_port != 0) {
LOG_D("IPv6 packet: next_header=%u, %s:%u -> %s:%u, len=%u", l4_proto,
ip6_to_string(ip6->saddr).c_str(), src_port, ip6_to_string(ip6->daddr).c_str(),
ip6_to_string(ip6.saddr).c_str(), src_port, ip6_to_string(ip6.daddr).c_str(),
dest_port, payload_len);
} else {
LOG_D("IPv6 packet: next_header=%u, %s -> %s, len=%u", l4_proto,
ip6_to_string(ip6->saddr).c_str(), ip6_to_string(ip6->daddr).c_str(),
ip6_to_string(ip6.saddr).c_str(), ip6_to_string(ip6.daddr).c_str(),
payload_len);
}

/* The handlers may copy the original header into an ICMP error, so keep
* the pointer into the packet rather than the parsed copy above. */
const ip6_hdr* ip6_header = reinterpret_cast<const ip6_hdr*>(payload.data());
switch (l4_proto) {
case IPPROTO_ICMPV6:
handle_icmp6(ctx, ip6, std::span<const uint8_t>(l4_payload, remaining));
handle_icmp6(ctx, ip6_header, l4_reader.rest());
break;
case IPPROTO_UDP:
handle_udp6(ctx, ip6, std::span<const uint8_t>(l4_payload, remaining));
handle_udp6(ctx, ip6_header, l4_reader.rest());
break;
case IPPROTO_TCP:
handle_tcp6(ctx, ip6, std::span<const uint8_t>(l4_payload, remaining));
handle_tcp6(ctx, ip6_header, l4_reader.rest());
break;
default:
LOG_D("Unknown IPv6 next_header: %u", l4_proto);
Expand Down
Loading
Loading