diff --git a/CHANGELOG.md b/CHANGELOG.md index c4e9314..53c8c30 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ This file records each release. The format follows [Keep a Changelog](https://ke ### Added - The eBPF verifier gives the result of kernel/bpf/verifier.c of Linux for all programs of bpf/test_verifier of the socket filter and cgroup program types. It prunes the paths with equal states at the jump targets and refuses a loop that does not end, it refines the bounds of a register after `BPF_JSET` and after a `BPF_JMP32` that compares as 64 bits, and it checks the alignment of the pointers and the size of each context field as Linux, with the errors and the log messages of Linux in the same order. `bpf(BPF_PROG_TEST_RUN)` runs a program on a packet or a context from user space. The kselftest boot test runs bpf/test_verifier, and tools/kselftest/bpf.md gives the result: 281 passed and 0 failed (#537). +- The stateful expressions of the sets of nf_tables, as `nft_set_elem_expr_alloc` of Linux: `NFTA_SET_EXPR` and `NFTA_SET_EXPRESSIONS` of a set, `NFTA_SET_ELEM_EXPR` and `NFTA_SET_ELEM_EXPRESSIONS` of an element, and `NFTA_DYNSET_EXPR` and `NFTA_DYNSET_EXPRESSIONS` of `dynset`. The expressions are `counter`, `quota`, `limit` and `connlimit`, two at most. Each element has its own state: a copy of the expressions of the set, or the ones that it gives with the same types. A `lookup` and a `dynset` run the expressions of the element that they find. The `connlimit` expression of nft_connlimit.c counts the connections that connection tracking still has (#542). ## [0.5.22] - 2026-11-12 diff --git a/net/rk-net/src/netfilter/nf_tables_api.rs b/net/rk-net/src/netfilter/nf_tables_api.rs index 37a3f95..e99a735 100644 --- a/net/rk-net/src/netfilter/nf_tables_api.rs +++ b/net/rk-net/src/netfilter/nf_tables_api.rs @@ -21,10 +21,11 @@ pub(crate) use self::nf_tables_core::{NftLog, nft_log_eval}; use self::nf_tables_core::{ NFT_OBJECT_MAX, NFTA_FLOWTABLE_MAX, NFTA_OBJ_MAX, NftDynset, NftExpr, NftFlowtable, NftObj, - NftProg, nf_tables_dump_flowtable, nf_tables_dump_obj, nf_tables_expr_parse, + NftObjState, NftProg, nf_tables_dump_flowtable, nf_tables_dump_obj, nf_tables_expr_parse, nf_tables_getflowtable, nf_tables_getobj, nft_check_loops, nft_do_chain, nft_expr_dump, nft_expr_needs_validate, nft_expr_targets, nft_expr_validate, nft_obj_commit, nft_prog, - nft_rule_needs_ct, nft_setelem_parse_obj, nft_table_validate, + nft_rule_needs_ct, nft_set_exprs_dump, nft_set_exprs_parse, nft_setelem_parse_obj, + nft_table_validate, }; use crate::buf::NetBuf; use crate::netfilter::nf_conntrack::{nf_ct_netns_get, nf_ct_netns_put}; @@ -378,6 +379,8 @@ struct NftElem { timeout: u64, /// The time of the clock in nanoseconds when the element expires, or 0. expires: u64, + /// The stateful expressions of the element, as `NFT_SET_EXT_EXPRESSIONS` of Linux. + exprs: Vec, } impl NftElem { @@ -476,6 +479,8 @@ struct NftSet { timeout: u64, /// `NFTA_SET_GC_INTERVAL`, kept for the dump. gc_int: u32, + /// The stateful expressions of the set, that each new element gets a copy of. + exprs: Vec, elems: BTreeMap, /// The elements of `dynset` since the last commit. dynamic: DynElems, @@ -488,6 +493,14 @@ struct NftSet { } impl NftSet { + /// The set or one of its elements has `connlimit`, that needs connection tracking. + fn needs_ct(&self) -> bool { + self.exprs + .iter() + .chain(self.elems.values().flat_map(|e| &e.exprs)) + .any(NftObjState::is_connlimit) + } + /// The type of the data of the elements, as `nft_set_datatype` of Linux. fn datatype(&self) -> u32 { if self.flags & NFT_SET_MAP != 0 { self.dtype } else { NFT_DATA_VALUE } @@ -1096,6 +1109,7 @@ fn nf_tables_fill_set( b.end_nest(concat); } b.end_nest(nest); + nft_set_exprs_dump(&mut b, (NFTA_SET_EXPR, NFTA_SET_EXPRESSIONS), &set.exprs, false); b.finish() } @@ -1142,6 +1156,7 @@ fn nf_tables_fill_setelem( Some(d) => nft_data_dump(b, NFTA_SET_ELEM_DATA, d, table), None => {} } + nft_set_exprs_dump(b, (NFTA_SET_ELEM_EXPR, NFTA_SET_ELEM_EXPRESSIONS), &elem.exprs, false); if elem.flags != 0 { b.put(NFTA_SET_ELEM_FLAGS as u16, &elem.flags.to_be_bytes()); } @@ -2365,10 +2380,6 @@ impl NftBatch { { return Err(Errno::EOPNOTSUPP); } - // The expressions of the sets come later. - if flags & NFT_SET_EXPR != 0 || nla.has(NFTA_SET_EXPR) || nla.has(NFTA_SET_EXPRESSIONS) { - return Err(Errno::EOPNOTSUPP); - } let (mut dtype, mut dlen) = (0, 0); if let Some(d) = be32(nla, NFTA_SET_DATA_TYPE)? { if flags & NFT_SET_MAP == 0 { @@ -2417,6 +2428,17 @@ impl NftBatch { let t = nft_table_lookup(&self.tables, table_name, ctx.family, ctx.portid) .map_err(|e| bad(info, nla, NFTA_SET_TABLE, e))?; let table = &self.tables[t]; + // The stateful expressions, as `nft_set_expr_alloc` of Linux. A list needs + // `NFT_SET_EXPR`. + let exprs = nft_set_exprs_parse( + table, + flags, + nla, + (NFTA_SET_EXPR, NFTA_SET_EXPRESSIONS), + flags & NFT_SET_EXPR != 0, + &[], + )? + .unwrap_or_default(); match nft_set_lookup(table, Some(name)) { Ok(s) => { if ctx.has(NLM_F_EXCL) { @@ -2440,6 +2462,8 @@ impl NftBatch { || set.field_len != field_len || set.timeout != timeout || set.gc_int != gc_int + || set.exprs.len() != exprs.len() + || set.exprs.iter().zip(&exprs).any(|(a, b)| a.name() != b.name()) { return Err(bad(info, nla, NFTA_SET_NAME, Errno::EEXIST)); } @@ -2470,6 +2494,7 @@ impl NftBatch { udata, timeout, gc_int, + exprs, elems: BTreeMap::new(), dynamic: DynElems::default(), touched: BTreeSet::new(), @@ -2588,9 +2613,19 @@ impl NftBatch { }; let expires = if timeout == 0 { 0 } else { nft_now_ns().saturating_add(expiration * 1_000_000) }; - if ea.has(NFTA_SET_ELEM_EXPR) || ea.has(NFTA_SET_ELEM_EXPRESSIONS) { - return Err(Errno::EOPNOTSUPP); - } + // The stateful expressions: the ones of the element, or a copy of the ones of the set. + let exprs = match nft_set_exprs_parse( + table, + set.flags, + &ea, + (NFTA_SET_ELEM_EXPR, NFTA_SET_ELEM_EXPRESSIONS), + true, + &set.exprs, + )? { + Some(e) => e, + None if end => Vec::new(), + None => set.exprs.iter().map(NftObjState::fresh).collect(), + }; let key_end = match ea.get(NFTA_SET_ELEM_KEY_END) { Some(k) => Some(nft_value_init(k, set.klen)?), None => None, @@ -2632,7 +2667,7 @@ impl NftBatch { if matches!(data, Some(NftData::Verdict { chain: Some(_), .. })) { self.validate = true; } - let elem = NftElem { key_end, data, flags, udata, timeout, expires }; + let elem = NftElem { key_end, data, flags, udata, timeout, expires, exprs }; // The elements that have expired go, as the garbage collection of Linux. let now = nft_now_ns(); let set = &mut self.tables[t].sets[s]; @@ -2767,7 +2802,8 @@ fn nft_sync_ct(net: &mut NftablesPernet, tables: &[NftTable]) -> Result<()> { for t in tables { // A rule with a `ct` expression or NAT, and a chain of NAT. let uses = t.chains.iter().flat_map(|c| &c.rules).any(|r| nft_rule_needs_ct(&r.parsed)) - || t.chains.iter().any(|c| c.base.as_ref().is_some_and(|b| b.ty.nat)); + || t.chains.iter().any(|c| c.base.as_ref().is_some_and(|b| b.ty.nat)) + || t.sets.iter().any(NftSet::needs_ct); if uses && !want.contains(&t.family) { want.push(t.family); } @@ -2964,6 +3000,7 @@ mod tests { udata: None, timeout: 0, gc_int: 0, + exprs: Vec::new(), elems: BTreeMap::new(), dynamic: DynElems::default(), touched: BTreeSet::new(), @@ -3090,6 +3127,7 @@ mod tests { udata: None, timeout: 0, expires: 0, + exprs: Vec::new(), }; m.elems.insert(key, elem); t.sets.push(m); @@ -3151,6 +3189,7 @@ mod tests { udata: None, timeout: 0, expires: 0, + exprs: Vec::new(), }; m.elems.insert(key, elem); t.sets.push(m); @@ -3192,6 +3231,7 @@ mod tests { udata: Some(alloc::vec![1]), timeout: if expires == 0 { 0 } else { 60_000 }, expires, + exprs: Vec::new(), }; let mut s = set(b"d"); s.elems.insert(k(1), elem(0)); diff --git a/net/rk-net/src/netfilter/nf_tables_core.rs b/net/rk-net/src/netfilter/nf_tables_core.rs index e108874..87281d1 100644 --- a/net/rk-net/src/netfilter/nf_tables_core.rs +++ b/net/rk-net/src/netfilter/nf_tables_core.rs @@ -67,12 +67,13 @@ pub(super) use self::nft_flowtable::{ NFTA_FLOWTABLE_MAX, NftFlowtable, nf_tables_dump_flowtable, nf_tables_getflowtable, }; pub(super) use self::nft_objects::{ - NFT_OBJECT_MAX, NFTA_OBJ_MAX, NftObj, nf_tables_dump_obj, nf_tables_getobj, nft_obj_commit, - nft_setelem_parse_obj, + NFT_OBJECT_MAX, NFTA_OBJ_MAX, NftObj, NftObjState, nf_tables_dump_obj, nf_tables_getobj, + nft_obj_commit, nft_set_exprs_dump, nft_set_exprs_parse, nft_setelem_parse_obj, }; use self::nft_objects::{ - NFTA_OBJREF_SET_ID, NFTA_OBJREF_SET_NAME, NftObjState, NftQuota, nft_obj_eval, nft_objref_init, - nft_quota_eval, nft_quota_init, + NFTA_OBJREF_SET_ID, NFTA_OBJREF_SET_NAME, NftConnlimit, NftQuota, nft_connlimit_eval, + nft_connlimit_init, nft_obj_eval, nft_objref_init, nft_quota_eval, nft_quota_init, + nft_set_elem_update_expr, }; // The attributes of an expression, `NFTA_EXPR_*`. @@ -282,6 +283,8 @@ pub(super) enum NftExpr { Limit(Arc), /// `quota`, as nft_quota.c. The copies of the rule share the bytes. Quota(Arc), + /// `connlimit`, as nft_connlimit.c. The copies of the rule share the connections. + Connlimit(Arc), /// `objref` with the object `obj` (a handle), as nft_objref.c. Objref { obj: u64 }, /// `flow_offload`, as nft_flow_offload.c, with the handle of its flowtable. @@ -313,10 +316,12 @@ pub(super) struct NftDynset { /// The timeout in milliseconds of the new elements, or 0 for the one of the set. timeout: u64, invert: bool, + /// The expressions that a new element gets a copy of: the ones of `dynset`, or else the + /// ones of the set. + exprs: Vec, } -/// Reads a `dynset` expression, as `nft_dynset_init` of Linux. The expressions of the -/// elements come later. +/// Reads a `dynset` expression, as `nft_dynset_init` of Linux. fn nft_dynset_init(table: &NftTable, data: &[u8]) -> Result { let a = Attrs::parse(data, NFTA_DYNSET_MAX); let name = a.str(NFTA_DYNSET_SET_NAME).map(|n| n.split(|&c| c == 0).next().unwrap_or(n)); @@ -357,12 +362,24 @@ fn nft_dynset_init(table: &NftTable, data: &[u8]) -> Result { } else { None }; - if a.has(NFTA_DYNSET_EXPR) || a.has(NFTA_DYNSET_EXPRESSIONS) { - if set.flags & NFT_SET_EVAL == 0 { - return Err(Errno::EINVAL); - } - return Err(Errno::EOPNOTSUPP); + if (a.has(NFTA_DYNSET_EXPR) || a.has(NFTA_DYNSET_EXPRESSIONS)) && set.flags & NFT_SET_EVAL == 0 + { + return Err(Errno::EINVAL); } + // The expressions of the new elements, as `nft_dynset_expr_alloc` of Linux. One expression + // must be the first of the set, and the set must have no other one. + let exprs = match nft_set_exprs_parse( + table, + set.flags, + &a, + (NFTA_DYNSET_EXPR, NFTA_DYNSET_EXPRESSIONS), + flags & NFT_DYNSET_F_EXPR != 0, + &set.exprs, + )? { + Some(e) if e.len() == 1 && set.exprs.len() > 1 => return Err(Errno::EOPNOTSUPP), + Some(e) => e, + None => set.exprs.iter().map(NftObjState::fresh).collect(), + }; Ok(NftExpr::Dynset(NftDynset { set: set.handle, op, @@ -371,6 +388,7 @@ fn nft_dynset_init(table: &NftTable, data: &[u8]) -> Result { sreg_data, timeout, invert: flags & NFT_DYNSET_F_INV != 0, + exprs, })) } @@ -661,6 +679,7 @@ pub(super) fn nf_tables_expr_parse( b"range" => Ok(NftExpr::Range(nft_range_init(data)?)), b"limit" => Ok(NftExpr::Limit(nft_limit_init(data)?)), b"quota" => Ok(NftExpr::Quota(nft_quota_init(data)?)), + b"connlimit" => nft_connlimit_expr_init(table, data), b"objref" => nft_objref_init(table, data), b"flow_offload" => nft_flow_offload_init(table, data), b"log" => Ok(NftExpr::Log(nft_log_init(data)?)), @@ -674,6 +693,15 @@ pub(super) fn nf_tables_expr_parse( } } +/// Reads a `connlimit` expression, as `nft_connlimit_init` of Linux: it needs connection +/// tracking. +fn nft_connlimit_expr_init(table: &NftTable, data: &[u8]) -> Result { + if !matches!(table.family, NFPROTO_IPV4 | NFPROTO_IPV6 | NFPROTO_INET) { + return Err(Errno::EOPNOTSUPP); + } + Ok(NftExpr::Connlimit(nft_connlimit_init(data)?)) +} + /// Reads a `ct` expression, as `nft_ct_get_init` and `nft_ct_set_init` of Linux. The keys that /// need a direction are the ones of a tuple. fn nft_ct_init(table: &NftTable, data: &[u8]) -> Result { @@ -922,7 +950,9 @@ pub(super) fn nft_rule_needs_ct(exprs: &[NftExpr]) -> bool { | NftExpr::Masq { .. } | NftExpr::Redir { .. } | NftExpr::FlowOffload { .. } + | NftExpr::Connlimit(_) ) || matches!(e, NftExpr::Xt(x) if x.needs_ct()) + || matches!(e, NftExpr::Dynset(d) if d.exprs.iter().any(NftObjState::is_connlimit)) }) } @@ -1055,6 +1085,10 @@ struct ProgSet { ranges: Vec<(Vec, Vec, bool, ProgData)>, /// The catch-all element, for a key that no other element has. catchall: Option, + /// The stateful expressions of the elements that have them, by the start of their key, + /// and of the catch-all element. + exprs: BTreeMap, Vec>, + catchall_exprs: Vec, /// The time in nanoseconds when the elements with a timeout expire, by the start of their /// key, and of the catch-all element. Empty for a set with no timeouts. expires: BTreeMap, u64>, @@ -1083,6 +1117,16 @@ impl ProgSet { /// The data of the element of `key`, as the `lookup` of the backends of sets of Linux. An /// element that has expired does not match. fn find(&self, key: &[u8]) -> Option { + self.find_elem(key).map(|(d, _)| d) + } + + /// The expressions of the element with the start `start`. + fn exprs_of(&self, start: &[u8]) -> Vec { + self.exprs.get(start).cloned().unwrap_or_default() + } + + /// The data and the stateful expressions of the element of `key`. + fn find_elem(&self, key: &[u8]) -> Option<(ProgData, Vec)> { let now = if self.expires.is_empty() && self.catchall_expires == 0 && self.dynamic.is_none() { 0 @@ -1093,27 +1137,29 @@ impl ProgSet { self.catchall .clone() .filter(|_| self.catchall_expires == 0 || now < self.catchall_expires) + .map(|d| (d, self.catchall_exprs.clone())) }; + let committed = |k: &[u8]| self.keys.get(k).map(|d| (d.clone(), self.exprs_of(k))); if let Some(d) = &self.dynamic { match d.0.lock().elems.get(key) { - Some(DynElem::Add(e)) if !e.expired(now) => return Some(prog_data(e)), + Some(DynElem::Add(e)) if !e.expired(now) => { + return Some((prog_data(e), e.exprs.clone())); + } Some(&DynElem::Refresh(x)) if now < x => { - return self.keys.get(key).cloned().or_else(catchall); + return committed(key).or_else(catchall); } Some(_) => return catchall(), None => {} } } let live = |k: &[u8]| self.expires.get(k).is_none_or(|&t| now < t); - self.keys - .get(key) + committed(key) .filter(|_| live(key)) - .cloned() .or_else(|| { self.ranges.iter().find_map(|(s, e, with_end, d)| { let inside = s.as_slice() <= key && (key < e.as_slice() || (*with_end && key == e.as_slice())); - (inside && live(s)).then(|| d.clone()) + (inside && live(s)).then(|| (d.clone(), self.exprs_of(s))) }) }) .or_else(catchall) @@ -1123,9 +1169,9 @@ impl ProgSet { /// Runs `dynset` on the set `set`, as `nft_dynset_eval` of Linux: adds the key of the /// registers, updates the expiration of its element, or removes it. A key that is not there /// and does not fit in the set ends the rule, and with `NFT_DYNSET_F_INV` a key that is there -/// or that fits. -fn nft_dynset_eval(d: &NftDynset, set: &ProgSet, regs: &mut NftRegs) { - let Some(dy) = &set.dynamic else { return }; +/// or that fits. Gives the stateful expressions of the element that the packet must run. +fn nft_dynset_eval(d: &NftDynset, set: &ProgSet, regs: &mut NftRegs) -> Vec { + let Some(dy) = &set.dynamic else { return Vec::new() }; let key = regs.data[d.sreg_key..d.sreg_key + d.klen].to_vec(); let now = super::nft_now_ns(); let mut g = dy.0.lock(); @@ -1147,10 +1193,15 @@ fn nft_dynset_eval(d: &NftDynset, set: &ProgSet, regs: &mut NftRegs) { g.elems.insert(key, DynElem::Del); g.delta -= 1; } - return; + return Vec::new(); } + let mut exprs = Vec::new(); let ok = match present { Some(t) => { + exprs = match g.elems.get(&key) { + Some(DynElem::Add(e)) => e.exprs.clone(), + _ => set.exprs_of(&key), + }; // An update starts the timeout of the element again. if d.op == NFT_DYNSET_OP_UPDATE && t != 0 { let t = if d.timeout != 0 { d.timeout } else { t }; @@ -1168,6 +1219,7 @@ fn nft_dynset_eval(d: &NftDynset, set: &ProgSet, regs: &mut NftRegs) { None => { let timeout = if d.timeout != 0 { d.timeout } else { set.timeout }; let data = d.sreg_data.map(|(r, l)| NftData::Value(regs.data[r..r + l].to_vec())); + exprs = d.exprs.iter().map(NftObjState::fresh).collect(); let elem = super::NftElem { key_end: None, data, @@ -1175,6 +1227,7 @@ fn nft_dynset_eval(d: &NftDynset, set: &ProgSet, regs: &mut NftRegs) { udata: None, timeout, expires: if timeout == 0 { 0 } else { now.saturating_add(timeout * 1_000_000) }, + exprs: exprs.clone(), }; // An element of `dynset` that expired has its place in the count. if !matches!(g.elems.get(&key), Some(DynElem::Add(_))) { @@ -1188,6 +1241,7 @@ fn nft_dynset_eval(d: &NftDynset, set: &ProgSet, regs: &mut NftRegs) { if ok == d.invert { regs.verdict = NFT_BREAK; } + exprs } /// The program of a base chain: the chains that it can reach, the first is the base chain. @@ -1251,6 +1305,13 @@ fn prog_set(table: &NftTable, s: usize, chains: &mut ProgChains) -> ProgSet { p.expires.insert(k.key.clone(), e.expires); } } + for (k, e) in set.elems.iter().filter(|(_, e)| !e.exprs.is_empty()) { + if k.catchall { + p.catchall_exprs = e.exprs.clone(); + } else if !k.end { + p.exprs.insert(k.key.clone(), e.exprs.clone()); + } + } let mut data = |e: &super::NftElem| match &e.data { Some(NftData::Value(v)) => ProgData::Value(v.clone()), Some(NftData::Verdict { code, chain }) => { @@ -1378,6 +1439,21 @@ impl NftRegs { } } +/// Runs the stateful expressions of the element that a `lookup` or a `dynset` found, after +/// the verdict that it gave, as `nft_set_elem_update_expr` of Linux. An expression that ends +/// the rule changes the verdict. +fn nft_elem_exprs_eval(exprs: &[NftObjState], regs: &mut NftRegs, skb: &NetBuf) { + if exprs.is_empty() { + return; + } + let verdict = regs.verdict; + regs.verdict = NFT_CONTINUE; + nft_set_elem_update_expr(exprs, regs, skb); + if regs.verdict != NFT_BREAK { + regs.verdict = verdict; + } +} + /// Runs an expression. fn nft_eval( e: &ProgExpr, @@ -1393,21 +1469,26 @@ fn nft_eval( regs.chain = *chain; } ProgExpr::Lookup { set, sreg, len, invert, dreg } => { - let found = prog.sets[*set].find(®s.data[*sreg..*sreg + *len]); + let found = prog.sets[*set].find_elem(®s.data[*sreg..*sreg + *len]); if found.is_some() == *invert { regs.verdict = NFT_BREAK; return; } - match (found, dreg) { - (Some(ProgData::Value(v)), Some(dreg)) => regs.store(*dreg, &v), - (Some(ProgData::Verdict { code, chain }), Some(_)) => { + let Some((data, exprs)) = found else { return }; + match (data, dreg) { + (ProgData::Value(v), Some(dreg)) => regs.store(*dreg, &v), + (ProgData::Verdict { code, chain }, Some(_)) => { regs.verdict = code; regs.chain = chain; } _ => {} } + nft_elem_exprs_eval(&exprs, regs, skb); + } + ProgExpr::Dynset { set, d } => { + let exprs = nft_dynset_eval(d, &prog.sets[*set], regs); + nft_elem_exprs_eval(&exprs, regs, skb); } - ProgExpr::Dynset { set, d } => nft_dynset_eval(d, &prog.sets[*set], regs), ProgExpr::Obj(o) => nft_obj_eval(o, regs, skb), // As `nft_objref_map_eval` of Linux: a key with no element ends the rule. ProgExpr::ObjMap { set, sreg, len } => { @@ -1580,6 +1661,7 @@ fn nft_eval( NftExpr::Range(r) => nft_range_eval(r, regs), NftExpr::Limit(l) => nft_limit_eval(l, regs, skb), NftExpr::Quota(q) => nft_quota_eval(q, regs, skb), + NftExpr::Connlimit(c) => nft_connlimit_eval(c, regs, skb), NftExpr::Log(l) => nft_log_eval(l, skb, state), // A chain that a jump reaches from a hook with no `reject` does not reject. NftExpr::Reject(r) => { @@ -1968,6 +2050,7 @@ mod tests { sreg_data: None, timeout, invert: false, + exprs: Vec::new(), }; let mut regs = NftRegs { verdict: NFT_CONTINUE, chain: 0, data: [0; NFT_REGS_LEN] }; regs.data[R1..R1 + 2].copy_from_slice(&k(5)); @@ -1995,7 +2078,16 @@ mod tests { e(NftExpr::Payload { base: NFT_PAYLOAD_TRANSPORT_HEADER, offset: 2, len: 2, dreg: R1 }); let dynset = |set, op, invert| ProgExpr::Dynset { set, - d: NftDynset { set: 0, op, sreg_key: R1, klen: 2, sreg_data: None, timeout: 0, invert }, + d: NftDynset { + set: 0, + op, + sreg_key: R1, + klen: 2, + sreg_data: None, + timeout: 0, + invert, + exprs: Vec::new(), + }, }; let mut r0 = vec![load, lookup(0)]; r0.push(verdict(NF_DROP, 0)); diff --git a/net/rk-net/src/netfilter/nft_objects.rs b/net/rk-net/src/netfilter/nft_objects.rs index 1db6f5c..961e297 100644 --- a/net/rk-net/src/netfilter/nft_objects.rs +++ b/net/rk-net/src/netfilter/nft_objects.rs @@ -5,25 +5,34 @@ //! different types can have the same name. A rule uses an object with the expression `objref`: //! by its name, or by the key of the packet in a map of objects. The copies of a table in a //! batch share the state of each object, and a packet changes it with no lock on the tables. +//! +//! The same states are the stateful expressions of a set and of its elements, as +//! `nft_set_elem_expr_alloc` of Linux, with `connlimit` of nft_connlimit.c. Each element has +//! its own state: a copy of the expressions of the set, or the ones that it gives. use super::super::{ - NFT, NFT_MSG_DELOBJ, NFT_MSG_DESTROYOBJ, NFT_MSG_GETOBJ_RESET, NFT_MSG_NEWOBJ, NFT_SET_OBJECT, - NFTA_SET_ELEM_OBJREF, NftBatch, NftCtx, NftData, NftTable, bad, be32, be64, cstr, family_match, - nfnl_msg_put, nfnl_msg_type, nft_set_lookup_global, nft_table_lookup, nft_type, nla_name, - nla_udata, + NF_DROP, NFT, NFT_MSG_DELOBJ, NFT_MSG_DESTROYOBJ, NFT_MSG_GETOBJ_RESET, NFT_MSG_NEWOBJ, + NFT_SET_OBJECT, NFT_SET_TIMEOUT, NFTA_LIST_ELEM, NFTA_SET_ELEM_OBJREF, NftBatch, NftCtx, + NftData, NftTable, bad, be32, be64, cstr, family_match, nfnl_msg_put, nfnl_msg_type, + nft_set_lookup_global, nft_table_lookup, nft_type, nla_name, nla_udata, }; use super::{ - NFT_BREAK, NFTA_COUNTER_BYTES, NFTA_COUNTER_PACKETS, NftExpr, NftLimit, NftRegs, - nft_limit_eval, nft_limit_init, nft_parse_register, nla_raw, + NFT_BREAK, NFTA_COUNTER_BYTES, NFTA_COUNTER_PACKETS, NFTA_EXPR_DATA, NFTA_EXPR_MAX, + NFTA_EXPR_NAME, NftExpr, NftLimit, NftRegs, nf_tables_expr_parse, nft_limit_eval, + nft_limit_init, nft_parse_register, nla_raw, }; use crate::buf::NetBuf; -use crate::netlink::msg::{Attrs, Builder}; +use crate::netfilter::nf_conntrack::{NfConntrackTuple, nf_conntrack_find_get, nf_ct_get_tuple}; +use crate::netfilter::{NFPROTO_IPV4, NFPROTO_IPV6}; +use crate::netlink::msg::{self, Attrs, Builder}; use crate::netlink::nfnetlink::{NFNETLINK_V0, NfnlInfo}; use alloc::sync::Arc; use alloc::vec::Vec; +use core::fmt; use core::sync::atomic::{AtomicU32, AtomicU64, Ordering}; use rk_abi::Errno; use rk_abi::native::netlink::{NLM_F_EXCL, NLM_F_MULTI, NLM_F_REPLACE}; +use rk_frame::sync::SpinLock; use rk_vfs::inode::Result; // The attributes of an object, `enum nft_object_attributes`. @@ -76,13 +85,248 @@ pub(crate) struct NftQuota { flags: AtomicU32, } -/// The state of an object. The copies of the object share it. +// The attributes of `connlimit`, `enum nft_connlimit_attributes`. +const NFTA_CONNLIMIT_COUNT: u32 = 1; +const NFTA_CONNLIMIT_FLAGS: u32 = 2; +const NFT_CONNLIMIT_F_INV: u32 = 1; + +/// The most expressions of a set or of an element, `NFT_SET_EXPR_MAX`. +const NFT_SET_EXPR_MAX: usize = 2; + +/// `connlimit`, as nft_connlimit.c: the most connections, and the connections that the +/// packets had, as the list of nf_conncount.c of Linux. +pub(crate) struct NftConnlimit { + limit: u32, + invert: bool, + conns: SpinLock>, +} + +impl fmt::Debug for NftConnlimit { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("NftConnlimit") + .field("limit", &self.limit) + .field("invert", &self.invert) + .finish() + } +} + +/// Reads a `connlimit`, as `nft_connlimit_do_init` of Linux. +pub(super) fn nft_connlimit_init(data: &[u8]) -> Result> { + let a = Attrs::parse(data, 2); + let Some(limit) = be32(&a, NFTA_CONNLIMIT_COUNT)? else { return Err(Errno::EINVAL) }; + let flags = be32(&a, NFTA_CONNLIMIT_FLAGS)?.unwrap_or(0); + if flags & !NFT_CONNLIMIT_F_INV != 0 { + return Err(Errno::EOPNOTSUPP); + } + Ok(Arc::new(NftConnlimit { + limit, + invert: flags & NFT_CONNLIMIT_F_INV != 0, + conns: SpinLock::new(Vec::new()), + })) +} + +/// Counts the connection of the packet, as `nft_connlimit_do_eval` and `nf_conncount_add` of +/// Linux. The list keeps only the connections that connection tracking still has. A packet +/// with no tuple is dropped. A count over the limit ends the rule, or a count that is not over +/// it with `NFT_CONNLIMIT_F_INV`. +pub(super) fn nft_connlimit_eval(c: &NftConnlimit, regs: &mut NftRegs, skb: &NetBuf) { + let tuple = match skb.nfct.get() { + Some((ct, _)) => ct.tuple(0), + None => { + let d = skb.data(); + let pf = + if d.first().is_some_and(|v| v >> 4 == 6) { NFPROTO_IPV6 } else { NFPROTO_IPV4 }; + match nf_ct_get_tuple(pf, d) { + Some((t, _)) => t, + None => { + regs.verdict = NF_DROP; + return; + } + } + } + }; + let count = { + let mut conns = c.conns.lock(); + conns.retain(|t| *t == tuple || nf_conntrack_find_get(t).is_some()); + if !conns.contains(&tuple) { + conns.push(tuple); + } + conns.len() + }; + if (count > c.limit as usize) != c.invert { + regs.verdict = NFT_BREAK; + } +} + +/// The state of an object, or of a stateful expression of a set or of an element. The copies +/// of the object share it. #[derive(Clone, Debug)] pub(crate) enum NftObjState { Counter(Arc), Quota(Arc), /// A `limit`, with the attributes that made it, for the dump. Limit(Arc, Vec), + /// A `connlimit`, only as an expression. + Connlimit(Arc), +} + +impl NftObjState { + /// The name of the expression of the state. + pub(crate) fn name(&self) -> &'static [u8] { + match self { + NftObjState::Counter(_) => b"counter", + NftObjState::Quota(_) => b"quota", + NftObjState::Limit(..) => b"limit", + NftObjState::Connlimit(_) => b"connlimit", + } + } + + /// The state is a `connlimit`, that needs connection tracking. + pub(crate) fn is_connlimit(&self) -> bool { + matches!(self, NftObjState::Connlimit(_)) + } + + /// A new state with the values of this one, as `nft_expr_clone` of Linux. A counter and + /// a quota keep their counts, a limit gets a full bucket, and a `connlimit` an empty list. + pub(crate) fn fresh(&self) -> NftObjState { + match self { + NftObjState::Counter(c) => NftObjState::Counter(Arc::new(NftCounter { + packets: AtomicU64::new(c.packets.load(Ordering::Relaxed)), + bytes: AtomicU64::new(c.bytes.load(Ordering::Relaxed)), + })), + NftObjState::Quota(q) => NftObjState::Quota(Arc::new(NftQuota { + quota: AtomicU64::new(q.quota.load(Ordering::Relaxed)), + consumed: AtomicU64::new(q.consumed.load(Ordering::Relaxed)), + flags: AtomicU32::new(q.flags.load(Ordering::Relaxed)), + })), + NftObjState::Limit(l, data) => match nft_limit_init(data) { + Ok(n) => NftObjState::Limit(n, data.clone()), + Err(_) => NftObjState::Limit(l.clone(), data.clone()), + }, + NftObjState::Connlimit(c) => NftObjState::Connlimit(Arc::new(NftConnlimit { + limit: c.limit, + invert: c.invert, + conns: SpinLock::new(Vec::new()), + })), + } + } +} + +/// Reads a stateful expression of the set with the flags `set_flags`, as +/// `nft_set_elem_expr_alloc` of Linux. An expression that is not stateful gives `EOPNOTSUPP`, +/// and `connlimit` in a set with timeouts too: its list has no garbage collection there. +pub(crate) fn nft_set_elem_expr_alloc( + table: &NftTable, + set_flags: u32, + nla: &[u8], +) -> Result { + let tb = Attrs::parse(nla, NFTA_EXPR_MAX); + let Some(name) = tb.str(NFTA_EXPR_NAME) else { return Err(Errno::EINVAL) }; + let name = name.split(|&c| c == 0).next().unwrap_or(name); + let data = tb.get(NFTA_EXPR_DATA).unwrap_or(&[]); + match name { + b"counter" => nft_obj_state_init(NFT_OBJECT_COUNTER, data), + b"quota" => nft_obj_state_init(NFT_OBJECT_QUOTA, data), + b"limit" => nft_obj_state_init(NFT_OBJECT_LIMIT, data), + b"connlimit" => { + let c = nft_connlimit_init(data)?; + if set_flags & NFT_SET_TIMEOUT != 0 { + return Err(Errno::EOPNOTSUPP); + } + Ok(NftObjState::Connlimit(c)) + } + // An expression that Linux knows, but that has no state. + _ => match nf_tables_expr_parse(table, nla, None) { + Ok(_) => Err(Errno::EOPNOTSUPP), + Err(e) => Err(e), + }, + } +} + +/// Reads the expressions of a set, of an element or of `dynset`: one in the attribute +/// `single`, or a list in `list`. The list needs `list_ok`, the flag that allows it, or gives +/// `EINVAL`. `want` is the list of expressions that the set has: the expressions must be of the +/// same types, in the same order, or they give `EOPNOTSUPP`, as `nft_add_set_elem` and +/// `nft_dynset_init` of Linux. A list with more than `NFT_SET_EXPR_MAX` gives `E2BIG`. +pub(crate) fn nft_set_exprs_parse( + table: &NftTable, + set_flags: u32, + a: &Attrs<'_>, + (single, list): (u32, u32), + list_ok: bool, + want: &[NftObjState], +) -> Result>> { + if let Some(e) = a.get(single) { + let s = nft_set_elem_expr_alloc(table, set_flags, e)?; + if want.first().is_some_and(|w| w.name() != s.name()) { + return Err(Errno::EOPNOTSUPP); + } + return Ok(Some(alloc::vec![s])); + } + let Some(l) = a.get(list) else { return Ok(None) }; + if !list_ok { + return Err(Errno::EINVAL); + } + let mut out = Vec::new(); + for (ty, e) in msg::attrs(l) { + if out.len() == NFT_SET_EXPR_MAX || (!want.is_empty() && out.len() == want.len()) { + return Err(Errno::E2BIG); + } + if ty != NFTA_LIST_ELEM { + return Err(Errno::EINVAL); + } + let s = nft_set_elem_expr_alloc(table, set_flags, e)?; + if want.get(out.len()).is_some_and(|w| w.name() != s.name()) { + return Err(Errno::EOPNOTSUPP); + } + out.push(s); + } + if !want.is_empty() && want.len() != out.len() { + return Err(Errno::EOPNOTSUPP); + } + Ok(Some(out)) +} + +/// Adds one expression in the attribute `attr`, as `nft_expr_dump` of Linux. +fn nft_state_expr_dump(b: &mut Builder, attr: u16, s: &NftObjState, reset: bool) { + let nest = b.nest(attr); + b.put(NFTA_EXPR_NAME as u16, &cstr(s.name())); + let data = b.nest(NFTA_EXPR_DATA as u16); + nft_state_dump(s, b, reset, None); + b.end_nest(data); + b.end_nest(nest); +} + +/// Adds the expressions of a set, of an element or of `dynset`: one in `single`, more in a +/// list in `list`, as `nft_set_elem_expr_dump` of Linux. +pub(crate) fn nft_set_exprs_dump( + b: &mut Builder, + (single, list): (u32, u32), + exprs: &[NftObjState], + reset: bool, +) { + match exprs { + [] => {} + [s] => nft_state_expr_dump(b, single as u16, s, reset), + _ => { + let nest = b.nest(list as u16); + for s in exprs { + nft_state_expr_dump(b, NFTA_LIST_ELEM, s, reset); + } + b.end_nest(nest); + } + } +} + +/// Runs the expressions of an element on a packet, as `nft_set_elem_update_expr` of Linux: +/// an expression that ends the rule stops the others. +pub(super) fn nft_set_elem_update_expr(exprs: &[NftObjState], regs: &mut NftRegs, skb: &NetBuf) { + for s in exprs { + nft_obj_eval(s, regs, skb); + if regs.verdict == NFT_BREAK { + return; + } + } } /// An object of a table, as `struct nft_object` of Linux. @@ -147,6 +391,7 @@ pub(super) fn nft_obj_eval(s: &NftObjState, regs: &mut NftRegs, skb: &NetBuf) { } NftObjState::Quota(q) => nft_quota_eval(q, regs, skb), NftObjState::Limit(l, _) => nft_limit_eval(l, regs, skb), + NftObjState::Connlimit(c) => nft_connlimit_eval(c, regs, skb), } } @@ -173,44 +418,54 @@ impl NftObj { &self.state } - /// Adds the attributes of the state, as the `dump` of the types of objects of Linux. A - /// reset sets the counter or the bytes of the quota back to 0. + /// Adds the attributes of the state, as the `dump` of the types of objects of Linux. fn dump_data(&self, b: &mut Builder, reset: bool) { - match &self.state { - NftObjState::Counter(c) => { - let bytes = c.bytes.load(Ordering::Relaxed); - let packets = c.packets.load(Ordering::Relaxed); - b.put(NFTA_COUNTER_BYTES as u16, &bytes.to_be_bytes()) - .put(NFTA_COUNTER_PACKETS as u16, &packets.to_be_bytes()); - if reset { - c.bytes.fetch_sub(bytes, Ordering::Relaxed); - c.packets.fetch_sub(packets, Ordering::Relaxed); - } + nft_state_dump(&self.state, b, reset, self.update); + } +} + +/// Adds the attributes of a state, as the `dump` of the types of objects and of the stateful +/// expressions of Linux. A reset sets the counter or the bytes of the quota back to 0. +/// `update` is the quota and the flags that a batch gives a quota object. +fn nft_state_dump(s: &NftObjState, b: &mut Builder, reset: bool, update: Option<(u64, u32)>) { + match s { + NftObjState::Counter(c) => { + let bytes = c.bytes.load(Ordering::Relaxed); + let packets = c.packets.load(Ordering::Relaxed); + b.put(NFTA_COUNTER_BYTES as u16, &bytes.to_be_bytes()) + .put(NFTA_COUNTER_PACKETS as u16, &packets.to_be_bytes()); + if reset { + c.bytes.fetch_sub(bytes, Ordering::Relaxed); + c.packets.fetch_sub(packets, Ordering::Relaxed); } - NftObjState::Quota(q) => { - let (quota, mut flags) = self - .update - .unwrap_or((q.quota.load(Ordering::Relaxed), q.flags.load(Ordering::Relaxed))); - let consumed = q.consumed.load(Ordering::Relaxed); - let shown = if consumed >= quota { - flags |= NFT_QUOTA_F_DEPLETED; - quota - } else { - consumed - }; - b.put(NFTA_QUOTA_BYTES as u16, "a.to_be_bytes()) - .put(NFTA_QUOTA_CONSUMED as u16, &shown.to_be_bytes()) - .put(NFTA_QUOTA_FLAGS as u16, &flags.to_be_bytes()); - if reset { - q.consumed.fetch_sub(consumed, Ordering::Relaxed); - } + } + NftObjState::Quota(q) => { + let (quota, mut flags) = update + .unwrap_or((q.quota.load(Ordering::Relaxed), q.flags.load(Ordering::Relaxed))); + let consumed = q.consumed.load(Ordering::Relaxed); + let shown = if consumed >= quota { + flags |= NFT_QUOTA_F_DEPLETED; + quota + } else { + consumed + }; + b.put(NFTA_QUOTA_BYTES as u16, "a.to_be_bytes()) + .put(NFTA_QUOTA_CONSUMED as u16, &shown.to_be_bytes()) + .put(NFTA_QUOTA_FLAGS as u16, &flags.to_be_bytes()); + if reset { + q.consumed.fetch_sub(consumed, Ordering::Relaxed); } - NftObjState::Limit(_, data) => { - for (ty, v) in nla_raw(data) { - b.put(ty, v); - } + } + NftObjState::Limit(_, data) => { + for (ty, v) in nla_raw(data) { + b.put(ty, v); } } + NftObjState::Connlimit(c) => { + let flags = if c.invert { NFT_CONNLIMIT_F_INV } else { 0 }; + b.put(NFTA_CONNLIMIT_COUNT as u16, &c.limit.to_be_bytes()) + .put(NFTA_CONNLIMIT_FLAGS as u16, &flags.to_be_bytes()); + } } } @@ -506,6 +761,83 @@ mod tests { assert_eq!(nft_quota_parse(&d).unwrap_err(), Errno::EINVAL); } + fn table() -> NftTable { + NftTable { + name: b"t".to_vec(), + family: NFPROTO_IPV4, + flags: 0, + handle: 1, + udata: None, + nlpid: 0, + chains: Vec::new(), + sets: Vec::new(), + objs: Vec::new(), + flowtables: Vec::new(), + hgenerator: 1, + } + } + + /// An expression with its name and its data, in the attribute `ty`. + fn expr(ty: u32, name: &[u8], data: &[u8]) -> Vec { + let mut e = attr(NFTA_EXPR_NAME, name); + e.extend(attr(NFTA_EXPR_DATA, data)); + attr(ty, &e) + } + + #[test] + fn a_set_takes_only_stateful_expressions_of_the_types_of_the_set() { + let t = table(); + let counter = || expr(NFTA_LIST_ELEM.into(), b"counter\0", &[]); + let limit = attr(1, &1u64.to_be_bytes()); + let limit = [limit, attr(2, &1u64.to_be_bytes())].concat(); + let limit = expr(NFTA_LIST_ELEM.into(), b"limit\0", &limit); + let parse = |l: &[u8], ok: bool, want: &[NftObjState]| { + let a = attr(2, l); + nft_set_exprs_parse(&t, 0, &Attrs::parse(&a, 2), (1, 2), ok, want) + }; + let two = [limit.clone(), counter()].concat(); + let set = parse(&two, true, &[]).unwrap().unwrap(); + assert_eq!( + set.iter().map(NftObjState::name).collect::>(), + [&b"limit"[..], b"counter"] + ); + // A list needs its flag, has at most two expressions, and has the ones of the set. + assert_eq!(parse(&two, false, &[]).unwrap_err(), Errno::EINVAL); + assert_eq!(parse(&[two.clone(), counter()].concat(), true, &[]).unwrap_err(), Errno::E2BIG); + assert_eq!(parse(&counter(), true, &set).unwrap_err(), Errno::EOPNOTSUPP); + assert_eq!(parse(&[counter(), limit].concat(), true, &set).unwrap_err(), Errno::EOPNOTSUPP); + assert_eq!(parse(&two, true, &set).unwrap().unwrap().len(), 2); + // An expression with no state. + let imm = expr(NFTA_LIST_ELEM.into(), b"immediate\0", &[]); + assert!(parse(&imm, true, &[]).is_err()); + // A connlimit has no place in a set with timeouts. + let c = expr(1, b"connlimit\0", &attr(NFTA_CONNLIMIT_COUNT, &20u32.to_be_bytes())); + let a = Attrs::parse(&c, 2); + assert!(nft_set_exprs_parse(&t, 0, &a, (1, 2), false, &[]).is_ok()); + let e = nft_set_exprs_parse(&t, NFT_SET_TIMEOUT, &a, (1, 2), false, &[]); + assert_eq!(e.unwrap_err(), Errno::EOPNOTSUPP); + } + + #[test] + fn a_copy_of_a_state_has_its_own_counts() { + let c = nft_obj_state_init( + NFT_OBJECT_COUNTER, + &attr(NFTA_COUNTER_PACKETS, &9u64.to_be_bytes()), + ) + .unwrap(); + let copy = c.fresh(); + let (NftObjState::Counter(a), NftObjState::Counter(b)) = (&c, ©) else { panic!() }; + assert_eq!(b.packets.load(Ordering::Relaxed), 9); + a.packets.store(1, Ordering::Relaxed); + assert_eq!(b.packets.load(Ordering::Relaxed), 9); + let mut b = Builder::new(0, 0, 0, 0); + nft_set_exprs_dump(&mut b, (1, 2), &[copy], false); + let d = b.finish()[crate::netlink::msg::HDRLEN..].to_vec(); + let e = Attrs::parse(&d, 2); + let x = Attrs::parse(e.get(1).unwrap(), NFTA_EXPR_MAX); + assert_eq!(x.get(NFTA_EXPR_NAME), Some(&b"counter\0"[..])); + } + #[test] fn an_unknown_type_of_object_gives_enoent() { assert_eq!(nft_obj_state_init(3, &[]).unwrap_err(), Errno::ENOENT);