asn: core ASN lookup module (native MMDB); match connect classes + security groups on origin AS

This commit is contained in:
Jean Chevronnet 2026-08-28 11:53:55 +00:00
parent cc13ce02c7
commit 068924de86
No known key found for this signature in database
GPG key ID: 439666D63A9477E4
5 changed files with 94 additions and 6 deletions

View file

@ -7,7 +7,7 @@
//!
//! ```text
//! connectclass = <name> allow=<mask[,mask]> [parent=<name>] [deny=yes]
//! [requiressl=yes|trusted] [password=<pw>] [hash=<algo>] [port=<p[,p]>]
//! [requiressl=yes|trusted] [password=<pw>] [hash=<algo>] [port=<p[,p]>] [asn=<n[,n]>]
//! [localmax=<n>] [globalmax=<n>] [limit=<n>] [maxchans=<n>] [pingfreq=<secs>]
//! [timeout=<secs>] [modes=<+modes>] [recvq=<bytes>] [hardsendq=<bytes>]
//! [softsendq=<bytes>] [fakelag=yes|no] [penaltythreshold=<n>] [commandrate=<secs>]
@ -35,6 +35,7 @@ pub struct ConnClass {
pub ssl_trusted: bool, // require a TLS client certificate (requiressl=trusted)
pub password: Option<String>, // PASS credential (plain or hashed; verify auto-detects)
pub ports: Vec<u16>, // restrict to these listener ports (empty = any)
pub asn: Vec<u32>, // restrict to these origin AS numbers (empty = any)
pub localmax: Option<usize>, // max local connections per IP in this class
pub globalmax: Option<usize>, // max network-wide connections per IP
pub limit: Option<usize>, // max total local users in this class
@ -75,6 +76,7 @@ fn apply(c: &mut ConnClass, k: &str, v: &str) {
// regardless of whether it appears before or after `password=`.
"hash" => {}
"port" => c.ports.extend(list(v).filter_map(|p| p.parse::<u16>().ok())),
"asn" => c.asn.extend(crate::modules::asn::parse_list(v)),
"localmax" => c.localmax = v.parse().ok(),
"globalmax" => c.globalmax = v.parse().ok(),
"limit" => c.limit = v.parse().ok(),
@ -269,6 +271,7 @@ fn pick(
secure: bool,
has_cert: bool,
port: u16,
asn: Option<u32>,
) -> Pick {
for c in all(s) {
if !c.allow.iter().any(|m| mask_match(m, ip, host)) {
@ -283,6 +286,9 @@ fn pick(
if !c.ports.is_empty() && !c.ports.contains(&port) {
continue;
}
if !c.asn.is_empty() && !asn.is_some_and(|a| c.asn.contains(&a)) {
continue;
}
if c.deny {
return Pick::Deny(c.name);
}
@ -343,7 +349,8 @@ pub fn assign(s: &mut Server, uid: Uid) -> Option<String> {
u.port,
)
};
let class = match pick(s, uid, &ip, "", secure, has_cert, port) {
let asn = crate::modules::asn::of(s, uid);
let class = match pick(s, uid, &ip, "", secure, has_cert, port, asn) {
Pick::Deny(name) => {
return Some(format!("Connection class {name} denies your address"));
}
@ -409,7 +416,8 @@ pub fn on_register(s: &mut Server, uid: Uid) -> AuthOutcome {
}) else {
return AuthOutcome::Proceed;
};
match pick(s, uid, &ip, &host, secure, has_cert, port) {
let asn = crate::modules::asn::of(s, uid);
match pick(s, uid, &ip, &host, secure, has_cert, port, asn) {
Pick::Deny(name) => {
return AuthOutcome::Reject(format!("Connection class {name} denies your address"));
}
@ -558,4 +566,12 @@ mod tests {
assert!(mask_match("192.0.2.*", "192.0.2.7", ""));
assert!(!mask_match("nomatch/33", "1.2.3.4", "")); // unparseable → no match
}
#[test]
fn asn_param_parses() {
let mut c = ConnClass::default();
apply(&mut c, "asn", "3215,15169");
apply(&mut c, "asn", "AS16276");
assert_eq!(c.asn, vec![3215, 15169, 16276]);
}
}