Skip to main content

manycastr/worker/
bpf.rs

1use socket2::Socket;
2
3// cBPF opcode bits (linux/filter.h, linux/bpf_common.h)
4#[cfg(target_os = "linux")]
5mod op {
6    pub const LD: u16 = 0x00;
7    pub const LDX: u16 = 0x01;
8    pub const B: u16 = 0x10;
9    pub const H: u16 = 0x08;
10    pub const ABS: u16 = 0x20;
11    pub const IND: u16 = 0x40;
12    pub const MSH: u16 = 0xa0;
13    pub const ALU: u16 = 0x04;
14    pub const AND: u16 = 0x50;
15    pub const RSH: u16 = 0x70;
16    pub const JMP: u16 = 0x05;
17    pub const JEQ: u16 = 0x10;
18    pub const K: u16 = 0x00;
19    pub const RET: u16 = 0x06;
20}
21
22#[cfg(target_os = "linux")]
23const ACCEPT: u32 = 0xffff_ffff; // return value: keep the whole packet
24#[cfg(target_os = "linux")]
25const DROP: u32 = 0; // return value: drop the packet
26
27#[cfg(target_os = "linux")]
28#[inline]
29fn sf(code: u16, jt: u8, jf: u8, k: u32) -> libc::sock_filter {
30    libc::sock_filter { code, jt, jf, k }
31}
32
33/// Attach a cBPF program to a socket via `SO_ATTACH_FILTER`.
34/// The kernel copies the program during the call, so `prog` need only remain
35/// valid for the duration of this function.
36#[cfg(target_os = "linux")]
37fn attach(socket: &Socket, prog: &mut [libc::sock_filter]) -> std::io::Result<()> {
38    use std::os::fd::AsRawFd;
39
40    let fprog = libc::sock_fprog {
41        len: prog.len() as u16,
42        filter: prog.as_mut_ptr(),
43    };
44
45    let ret = unsafe {
46        // TODO unsafe
47        libc::setsockopt(
48            socket.as_raw_fd(),
49            libc::SOL_SOCKET,
50            libc::SO_ATTACH_FILTER,
51            &fprog as *const _ as *const libc::c_void,
52            std::mem::size_of::<libc::sock_fprog>() as libc::socklen_t,
53        )
54    };
55
56    if ret != 0 {
57        return Err(std::io::Error::last_os_error());
58    }
59    Ok(())
60}
61
62/// Attach a filter to a raw ICMP socket so the kernel only delivers ICMP echo
63/// replies whose identifier matches `icmp_id`, dropping all other ICMP traffic.
64///
65/// # Arguments
66/// * `socket` - the raw ICMP socket to attach the filter to
67/// * `icmp_id` - the ICMP identifier used for this measurement
68/// * `is_ipv6` - whether this is an IPv6 (ICMPv6) socket
69#[cfg(target_os = "linux")]
70pub(crate) fn attach_icmp_filter(
71    socket: &Socket,
72    icmp_id: u16,
73    is_ipv6: bool,
74) -> std::io::Result<()> {
75    use op::*;
76
77    const ICMP_ECHO_REPLY_V4: u32 = 0;
78    const ICMP_ECHO_REPLY_V6: u32 = 129;
79    let id = icmp_id as u32;
80
81    let mut prog: Vec<libc::sock_filter> = if !is_ipv6 {
82        vec![
83            sf(LDX | B | MSH, 0, 0, 0),                  // X = IP header length
84            sf(LD | B | IND, 0, 0, 0),                   // A = ICMP type
85            sf(JMP | JEQ | K, 0, 3, ICMP_ECHO_REPLY_V4), // type == 0 ? else -> drop
86            sf(LD | H | IND, 0, 0, 4),                   // A = ICMP identifier
87            sf(JMP | JEQ | K, 0, 1, id),                 // id == icmp_id ? else -> drop
88            sf(RET | K, 0, 0, ACCEPT),
89            sf(RET | K, 0, 0, DROP),
90        ]
91    } else {
92        vec![
93            sf(LD | B | ABS, 0, 0, 0),                   // A = ICMPv6 type
94            sf(JMP | JEQ | K, 0, 3, ICMP_ECHO_REPLY_V6), // type == 129 ? else -> drop
95            sf(LD | H | ABS, 0, 0, 4),                   // A = identifier
96            sf(JMP | JEQ | K, 0, 1, id),                 // id == icmp_id ? else -> drop
97            sf(RET | K, 0, 0, ACCEPT),
98            sf(RET | K, 0, 0, DROP),
99        ]
100    };
101
102    attach(socket, &mut prog)
103}
104
105/// Attach a filter to a raw ICMP socket for traceroute measurements: deliver
106/// ICMP Time Exceeded (intermediate hops), Destination Unreachable (destination
107/// reached for UDP traceroute), and Echo Reply (destination reached for ICMP
108/// traceroute), dropping all other ICMP traffic.
109///
110/// # Arguments
111/// * `socket` - the raw ICMP socket to attach the filter to
112/// * `is_ipv6` - whether this is an IPv6 (ICMPv6) socket
113#[cfg(target_os = "linux")]
114pub(crate) fn attach_traceroute_filter(socket: &Socket, is_ipv6: bool) -> std::io::Result<()> {
115    use op::*;
116
117    const ICMP_ECHO_REPLY_V4: u32 = 0;
118    const ICMP_DEST_UNREACHABLE_V4: u32 = 3;
119    const ICMP_TIME_EXCEEDED_V4: u32 = 11;
120    const ICMP_ECHO_REPLY_V6: u32 = 129;
121    const ICMP_DEST_UNREACHABLE_V6: u32 = 1;
122    const ICMP_TIME_EXCEEDED_V6: u32 = 3;
123
124    let mut prog: Vec<libc::sock_filter> = if !is_ipv6 {
125        vec![
126            sf(LDX | B | MSH, 0, 0, 0),                        // X = IP header length
127            sf(LD | B | IND, 0, 0, 0),                         // A = ICMP type
128            sf(JMP | JEQ | K, 2, 0, ICMP_TIME_EXCEEDED_V4),    // type == 11 -> accept
129            sf(JMP | JEQ | K, 1, 0, ICMP_DEST_UNREACHABLE_V4), // type == 3  -> accept
130            sf(JMP | JEQ | K, 0, 1, ICMP_ECHO_REPLY_V4),       // type == 0  -> accept, else drop
131            sf(RET | K, 0, 0, ACCEPT),
132            sf(RET | K, 0, 0, DROP),
133        ]
134    } else {
135        vec![
136            sf(LD | B | ABS, 0, 0, 0),                         // A = ICMPv6 type
137            sf(JMP | JEQ | K, 2, 0, ICMP_TIME_EXCEEDED_V6),    // type == 3   -> accept
138            sf(JMP | JEQ | K, 1, 0, ICMP_DEST_UNREACHABLE_V6), // type == 1   -> accept
139            sf(JMP | JEQ | K, 0, 1, ICMP_ECHO_REPLY_V6),       // type == 129 -> accept, else drop
140            sf(RET | K, 0, 0, ACCEPT),
141            sf(RET | K, 0, 0, DROP),
142        ]
143    };
144
145    attach(socket, &mut prog)
146}
147
148/// Attach a filter to a raw TCP socket so the kernel only delivers TCP segments
149/// with the RST flag set whose destination port matches `sport` (the worker's
150/// source port), dropping all other TCP traffic — which on a raw TCP socket
151/// includes a copy of every TCP segment on the host (SSH, the gRPC control
152/// connection to the orchestrator, etc.).
153///
154/// # Arguments
155/// * `socket` - the raw TCP socket to attach the filter to
156/// * `sport` - the worker's source port (TCP replies carry it as their dport)
157/// * `is_ipv6` - whether this is an IPv6 socket
158#[cfg(target_os = "linux")]
159pub(crate) fn attach_tcp_filter(socket: &Socket, sport: u16, is_ipv6: bool) -> std::io::Result<()> {
160    use op::*;
161
162    const TCP_RST: u32 = 0x04; // RST flag in the TCP flags byte (offset 13)
163    let dport = sport as u32;
164
165    let mut prog: Vec<libc::sock_filter> = if !is_ipv6 {
166        vec![
167            sf(LDX | B | MSH, 0, 0, 0),       // X = IP header length
168            sf(LD | H | IND, 0, 0, 2),        // A = TCP destination port
169            sf(JMP | JEQ | K, 0, 4, dport),   // dport == sport ? else -> drop
170            sf(LD | B | IND, 0, 0, 13),       // A = TCP flags byte
171            sf(ALU | AND | K, 0, 0, TCP_RST), // A = flags & RST
172            sf(JMP | JEQ | K, 0, 1, TCP_RST), // RST set ? else -> drop
173            sf(RET | K, 0, 0, ACCEPT),
174            sf(RET | K, 0, 0, DROP),
175        ]
176    } else {
177        vec![
178            sf(LD | H | ABS, 0, 0, 2),        // A = TCP destination port
179            sf(JMP | JEQ | K, 0, 4, dport),   // dport == sport ? else -> drop
180            sf(LD | B | ABS, 0, 0, 13),       // A = TCP flags byte
181            sf(ALU | AND | K, 0, 0, TCP_RST), // A = flags & RST
182            sf(JMP | JEQ | K, 0, 1, TCP_RST), // RST set ? else -> drop
183            sf(RET | K, 0, 0, ACCEPT),
184            sf(RET | K, 0, 0, DROP),
185        ]
186    };
187
188    attach(socket, &mut prog)
189}
190
191/// Attach a filter to a raw UDP socket so the kernel only delivers DNS replies
192/// destined to `sport` whose DNS transaction ID carries our 6-bit identifier,
193/// dropping all other UDP traffic (e.g. the host's own DNS resolution).
194///
195/// # Arguments
196/// * `socket` - the raw UDP socket to attach the filter to
197/// * `sport` - the worker's source port (DNS replies carry it as their dport)
198/// * `dns_identifier` - the 6-bit DNS identifier encoded in outgoing queries
199/// * `is_ipv6` - whether this is an IPv6 socket
200#[cfg(target_os = "linux")]
201pub(crate) fn attach_dns_filter(
202    socket: &Socket,
203    sport: u16,
204    dns_identifier: u8,
205    is_ipv6: bool,
206) -> std::io::Result<()> {
207    use op::*;
208
209    let dport = sport as u32;
210    let id = dns_identifier as u32;
211
212    let mut prog: Vec<libc::sock_filter> = if !is_ipv6 {
213        vec![
214            sf(LDX | B | MSH, 0, 0, 0),     // X = IP header length
215            sf(LD | H | IND, 0, 0, 2),      // A = UDP destination port
216            sf(JMP | JEQ | K, 0, 4, dport), // dport == sport ? else -> drop
217            sf(LD | B | IND, 0, 0, 8),      // A = first byte of DNS transaction ID
218            sf(ALU | RSH | K, 0, 0, 2),     // A = first_byte >> 2  (top 6 bits)
219            sf(JMP | JEQ | K, 0, 1, id),    // identifier matches ? else -> drop
220            sf(RET | K, 0, 0, ACCEPT),
221            sf(RET | K, 0, 0, DROP),
222        ]
223    } else {
224        vec![
225            sf(LD | H | ABS, 0, 0, 2),      // A = UDP destination port
226            sf(JMP | JEQ | K, 0, 4, dport), // dport == sport ? else -> drop
227            sf(LD | B | ABS, 0, 0, 8),      // A = first byte of DNS transaction ID
228            sf(ALU | RSH | K, 0, 0, 2),     // A = first_byte >> 2  (top 6 bits)
229            sf(JMP | JEQ | K, 0, 1, id),    // identifier matches ? else -> drop
230            sf(RET | K, 0, 0, ACCEPT),
231            sf(RET | K, 0, 0, DROP),
232        ]
233    };
234
235    attach(socket, &mut prog)
236}
237
238// Non-Linux stubs (cBPF socket filters are Linux-specific)
239
240#[cfg(not(target_os = "linux"))]
241pub(crate) fn attach_icmp_filter(
242    _socket: &Socket,
243    _icmp_id: u16,
244    _is_ipv6: bool,
245) -> std::io::Result<()> {
246    Ok(())
247}
248
249#[cfg(not(target_os = "linux"))]
250pub(crate) fn attach_traceroute_filter(_socket: &Socket, _is_ipv6: bool) -> std::io::Result<()> {
251    Ok(())
252}
253
254#[cfg(not(target_os = "linux"))]
255pub(crate) fn attach_tcp_filter(
256    _socket: &Socket,
257    _sport: u16,
258    _is_ipv6: bool,
259) -> std::io::Result<()> {
260    Ok(())
261}
262
263#[cfg(not(target_os = "linux"))]
264pub(crate) fn attach_dns_filter(
265    _socket: &Socket,
266    _sport: u16,
267    _dns_identifier: u8,
268    _is_ipv6: bool,
269) -> std::io::Result<()> {
270    Ok(())
271}