Skip to content

Commit 1cd6ba6

Browse files
authored
Merge pull request #128 from multikernel/refactor/net-parse-decide-execute
refactor(net): restructure the network module into parse/decide/execute phases
2 parents 5c9bafe + 4f2b45d commit 1cd6ba6

9 files changed

Lines changed: 3578 additions & 3151 deletions

File tree

crates/sandlock-core/src/network.rs

Lines changed: 0 additions & 3151 deletions
This file was deleted.

crates/sandlock-core/src/network/connect.rs

Lines changed: 436 additions & 0 deletions
Large diffs are not rendered by default.

crates/sandlock-core/src/network/materialize.rs

Lines changed: 451 additions & 0 deletions
Large diffs are not rendered by default.
Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
// Network policy enforcement via seccomp notification: connect/sendto/
2+
// sendmsg/sendmmsg are intercepted and either passed through, denied, or
3+
// performed on-behalf against a copy of the child's arguments.
4+
//
5+
// The module is organized around three phases, each enforced by a boundary:
6+
//
7+
// materialize parse phase: every byte-level reader of child-controlled
8+
// memory; produces owned values (ChildMsghdr, MaterializedMsg)
9+
// so later phases never re-read child state (TOCTOU-safe).
10+
// verdict decide phase: pure policy verdicts over materialized values;
11+
// no I/O, no locks, unit-testable.
12+
// send_engine execute phase: the only code that performs sends, including
13+
// the blocking/defer state machine for the notification loop.
14+
//
15+
// The per-syscall handlers chain those phases:
16+
//
17+
// connect IP connect (verdict + HTTP-ACL redirect / port-remap plan).
18+
// send IP sendto/sendmsg/sendmmsg.
19+
// unix the named AF_UNIX gate shared by connect and send.
20+
// rules --net-allow/--net-deny parsing, DNS resolution, /etc/hosts.
21+
//
22+
// This file keeps the socket probes and the handle_net dispatch.
23+
24+
use std::os::unix::io::RawFd;
25+
use std::sync::Arc;
26+
27+
use crate::seccomp::ctx::SupervisorCtx;
28+
use crate::seccomp::notif::NotifAction;
29+
use crate::sys::structs::SeccompNotif;
30+
31+
mod connect;
32+
mod materialize;
33+
mod rules;
34+
mod send;
35+
mod send_engine;
36+
mod unix;
37+
mod verdict;
38+
39+
// `network` is pub(crate), so this re-export is the crate-internal path for
40+
// the rule types. The two resolved-set types are named only in re-exported
41+
// signatures (callers bind them by inference), which trips unused_imports.
42+
#[allow(unused_imports)]
43+
pub use rules::{
44+
compose_virtual_etc_hosts, resolve_net_allow, resolve_net_deny, IpCidr, NetAllow, NetDeny,
45+
NetRule, NetTarget, Protocol, ResolvedNetAllow, ResolvedNetAllowSet, ResolvedNetDenySet,
46+
};
47+
48+
use connect::connect_on_behalf;
49+
use send::{sendmmsg_on_behalf, sendmsg_on_behalf, sendto_on_behalf};
50+
51+
// ============================================================
52+
// query_socket_protocol — derive the rule Protocol from a fd via getsockopt
53+
// ============================================================
54+
55+
/// Query `SO_PROTOCOL` on a dup'd socket fd to learn whether to route
56+
/// the on-behalf check through the TCP, UDP, or ICMP policy.
57+
///
58+
/// Returns `None` for protocols sandlock does not gate via `net_allow`
59+
/// (raw, SCTP, etc.) — the handler treats those as "no rule applies"
60+
/// which collapses to the default-deny path.
61+
pub(crate) fn query_socket_protocol(fd: RawFd) -> Option<Protocol> {
62+
let mut proto: libc::c_int = 0;
63+
let mut len: libc::socklen_t = std::mem::size_of::<libc::c_int>() as libc::socklen_t;
64+
let rc = unsafe {
65+
libc::getsockopt(
66+
fd,
67+
libc::SOL_SOCKET,
68+
libc::SO_PROTOCOL,
69+
&mut proto as *mut _ as *mut libc::c_void,
70+
&mut len,
71+
)
72+
};
73+
if rc != 0 {
74+
return None;
75+
}
76+
match proto {
77+
libc::IPPROTO_TCP => Some(Protocol::Tcp),
78+
libc::IPPROTO_UDP => Some(Protocol::Udp),
79+
// IPPROTO_ICMP and IPPROTO_ICMPV6 both route to the ICMP policy
80+
// (the policy doesn't distinguish IP versions; the rule's
81+
// resolved IP set already covers both via DNS).
82+
libc::IPPROTO_ICMP | libc::IPPROTO_ICMPV6 => Some(Protocol::Icmp),
83+
_ => None,
84+
}
85+
}
86+
87+
/// True iff `fd` is an `AF_UNIX` socket, probed via `SO_DOMAIN`. `SCM_RIGHTS`
88+
/// and `SCM_CREDENTIALS` are unix-only, so control rewriting/gating is applied
89+
/// only to unix sockets — an IP socket's control (e.g. `IP_PKTINFO`) carries no
90+
/// fds or credentials and passes through untouched.
91+
fn socket_is_unix(fd: RawFd) -> bool {
92+
let mut domain: libc::c_int = 0;
93+
let mut len = std::mem::size_of::<libc::c_int>() as libc::socklen_t;
94+
let rc = unsafe {
95+
libc::getsockopt(
96+
fd,
97+
libc::SOL_SOCKET,
98+
libc::SO_DOMAIN,
99+
&mut domain as *mut _ as *mut libc::c_void,
100+
&mut len,
101+
)
102+
};
103+
rc == 0 && domain == libc::AF_UNIX
104+
}
105+
106+
// ============================================================
107+
// handle_net — main handler for connect/sendto/sendmsg
108+
// ============================================================
109+
110+
/// Handle network-related notifications (connect, sendto, sendmsg).
111+
///
112+
/// All three are handled on-behalf (TOCTOU-safe): the supervisor copies data
113+
/// from child memory, validates the destination, duplicates the socket via
114+
/// pidfd_getfd, and performs the syscall itself. The child's memory is never
115+
/// re-read by the kernel after validation.
116+
///
117+
/// Continue safety (issue #27): the on-behalf paths don't return Continue
118+
/// at all (they return ReturnValue/Errno after performing the syscall in
119+
/// the supervisor). The Continue cases in this module are:
120+
/// 1. Non-IP families (AF_UNIX etc.) — the IP allowlist doesn't apply;
121+
/// Landlock IPC scoping is the enforcement boundary.
122+
/// 2. Connected sockets with addr_ptr == 0 — the address was already
123+
/// validated at connect time, so the kernel re-read of (nothing) is
124+
/// moot.
125+
/// 3. The fall-through case below — only reachable if the BPF filter
126+
/// mis-routes a syscall; the kernel handles it normally.
127+
/// In sendmsg_on_behalf, the msghdr read failure path returns
128+
/// Errno(EFAULT) rather than Continue: a racing thread that briefly
129+
/// unmaps the msghdr could otherwise force a fall-through that lets the
130+
/// kernel execute sendmsg without the allowlist check. Sub-buffer read
131+
/// failures (sockaddr/iovec/control) already return Errno(EIO) and so
132+
/// don't bypass the check either.
133+
pub(crate) async fn handle_net(
134+
notif: &SeccompNotif,
135+
ctx: &Arc<SupervisorCtx>,
136+
notif_fd: RawFd,
137+
) -> NotifAction {
138+
let nr = notif.data.nr as i64;
139+
140+
if nr == libc::SYS_connect {
141+
connect_on_behalf(notif, ctx, notif_fd).await
142+
} else if nr == libc::SYS_sendto {
143+
sendto_on_behalf(notif, ctx, notif_fd).await
144+
} else if nr == libc::SYS_sendmsg {
145+
sendmsg_on_behalf(notif, ctx, notif_fd).await
146+
} else if nr == libc::SYS_sendmmsg {
147+
sendmmsg_on_behalf(notif, ctx, notif_fd).await
148+
} else {
149+
NotifAction::Continue
150+
}
151+
}
152+

0 commit comments

Comments
 (0)