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
9 changes: 7 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -147,8 +147,13 @@ OLD_EF := --experimental_mnt=old
NEW_EF := --experimental_mnt=new
UID := $(shell id -u)

.PHONY: test
test: $(BIN)
.PHONY: test nstun_ip_test
nstun_ip_test:
$(CXX) -std=c++20 -Wall -Wextra -Werror -I. \
tests/nstun_ip_test.cc -o /tmp/nsjail_nstun_ip_test
/tmp/nsjail_nstun_ip_test

test: $(BIN) nstun_ip_test
# --- Basic sanity tests ---
$(call run_test, ./nsjail -q -Mo --chroot / --user 99999 --group 99999 -- /bin/true, 0)
$(call run_test, ./nsjail -q -Mo --chroot / --user 99999 --group 99999 -- /bin/false, 1)
Expand Down
16 changes: 9 additions & 7 deletions nstun/ip.cc
Original file line number Diff line number Diff line change
Expand Up @@ -102,13 +102,13 @@ void handle_ip4(Context* ctx, std::span<const uint8_t> payload) {
return;
}

/* SSRF gate: reject packets to loopback, broadcast, or INADDR_ANY.
/* SSRF gate: reject packets to loopback, link-local, broadcast, or INADDR_ANY.
* 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)) || ip->daddr == htonl(INADDR_ANY) ||
ip->daddr == htonl(INADDR_BROADCAST)) {
LOG_W("Dropping packet destined to loopback, ANY, or broadcast: %s",
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());
return;
}
Expand Down Expand Up @@ -176,7 +176,8 @@ void handle_ip6(Context* ctx, std::span<const uint8_t> payload) {
}
}

/* SSRF gate: reject packets to loopback, v4-mapped, link-local, or site-local.
/* SSRF gate: reject packets to loopback, v4-mapped, local-service,
* link-local, or site-local destinations.
* 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). */
Expand All @@ -195,8 +196,9 @@ void handle_ip6(Context* ctx, std::span<const uint8_t> payload) {
return;
}
if (IN6_IS_ADDR_LINKLOCAL((const struct in6_addr*)ip6->daddr) ||
IN6_IS_ADDR_SITELOCAL((const struct in6_addr*)ip6->daddr)) {
LOG_D("Dropping IPv6 packet to link/site-local address: %s",
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());
return;
}
Expand Down
10 changes: 10 additions & 0 deletions nstun/net_defs.h
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,16 @@ constexpr size_t NSTUN_MTU = ((1024 * 64) - 1024);
constexpr size_t IPV4_ADDR_LEN = sizeof(in_addr);
constexpr size_t IPV6_ADDR_LEN = sizeof(in6_addr);

inline bool ip4_is_link_local(uint32_t addr_nbo) {
return (ntohl(addr_nbo) & 0xFFFF0000U) == 0xA9FE0000U;
}

inline bool ip6_is_aws_local_service(const uint8_t addr[16]) {
/* AWS reserves fd00:ec2::/32 for instance-local services, including IMDS. */
constexpr uint8_t prefix[] = {0xFD, 0x00, 0x0E, 0xC2};
return memcmp(addr, prefix, sizeof(prefix)) == 0;
}

struct __attribute__((packed)) ip4_hdr {
uint8_t ihl_version; /* version << 4 | ihl */
uint8_t tos;
Expand Down
39 changes: 39 additions & 0 deletions tests/nstun_ip_test.cc
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
#include <assert.h>

#include <arpa/inet.h>
#include <stdint.h>

#include <array>

#include "nstun/net_defs.h"

static uint32_t parse_ip4(const char* str) {
uint32_t addr;
assert(inet_pton(AF_INET, str, &addr) == 1);
return addr;
}

static bool ip6_is_aws_local_service(const char* str) {
std::array<uint8_t, 16> addr = {};
assert(inet_pton(AF_INET6, str, addr.data()) == 1);
return nstun::ip6_is_aws_local_service(addr.data());
}

int main() {
assert(!nstun::ip4_is_link_local(parse_ip4("169.253.255.255")));
assert(nstun::ip4_is_link_local(parse_ip4("169.254.0.0")));
assert(nstun::ip4_is_link_local(parse_ip4("169.254.169.254")));
assert(nstun::ip4_is_link_local(parse_ip4("169.254.255.255")));
assert(!nstun::ip4_is_link_local(parse_ip4("169.255.0.0")));
assert(!nstun::ip4_is_link_local(parse_ip4("127.0.0.1")));
assert(!nstun::ip4_is_link_local(parse_ip4("10.0.0.1")));

assert(!ip6_is_aws_local_service("fd00:ec1:ffff:ffff:ffff:ffff:ffff:ffff"));
assert(ip6_is_aws_local_service("fd00:ec2::"));
assert(ip6_is_aws_local_service("fd00:ec2::254"));
assert(ip6_is_aws_local_service("fd00:ec2:ffff:ffff:ffff:ffff:ffff:ffff"));
assert(!ip6_is_aws_local_service("fd00:ec3::"));
assert(!ip6_is_aws_local_service("fc00::1"));
assert(!ip6_is_aws_local_service("fe80::1"));
return 0;
}