reverse-dns clients on connect with pre-registration notices (checking ident / looking up your hostname)
This commit is contained in:
parent
58596aea66
commit
4fc26d13e9
8 changed files with 392 additions and 24 deletions
|
|
@ -31,6 +31,10 @@ oper = admin CHANGE_THIS_PASSWORD
|
||||||
# Changing it re-cloaks everyone.
|
# Changing it re-cloaks everyone.
|
||||||
cloak_key = CHANGE_THIS_TO_A_LONG_RANDOM_HEX_STRING
|
cloak_key = CHANGE_THIS_TO_A_LONG_RANDOM_HEX_STRING
|
||||||
|
|
||||||
|
# reverse-DNS clients on connect (the "*** Looking up your hostname..." notices).
|
||||||
|
# on (default) shows resolved hostnames; off keeps bare IPs.
|
||||||
|
resolve_hosts = on
|
||||||
|
|
||||||
# antimixedutf8 — block spam that mixes look-alike scripts within words.
|
# antimixedutf8 — block spam that mixes look-alike scripts within words.
|
||||||
# action = block | kill | gline | kline | zline ; target = both | channel | private
|
# action = block | kill | gline | kline | zline ; target = both | channel | private
|
||||||
antimixedutf8 = off
|
antimixedutf8 = off
|
||||||
|
|
|
||||||
|
|
@ -69,6 +69,7 @@ pub struct Config {
|
||||||
pub conf_path: String, // where this was loaded from (for REHASH)
|
pub conf_path: String, // where this was loaded from (for REHASH)
|
||||||
pub censor: Vec<(String, String)>, // +G bad words: (find, replace); empty replace = block
|
pub censor: Vec<(String, String)>, // +G bad words: (find, replace); empty replace = block
|
||||||
pub amu: AntiMixedCfg, // antimixedutf8 module config
|
pub amu: AntiMixedCfg, // antimixedutf8 module config
|
||||||
|
pub resolve_hosts: bool, // reverse-DNS clients on connect (default on)
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for Config {
|
impl Default for Config {
|
||||||
|
|
@ -90,6 +91,7 @@ impl Default for Config {
|
||||||
conf_path: "echoircd.conf".to_string(),
|
conf_path: "echoircd.conf".to_string(),
|
||||||
censor: Vec::new(),
|
censor: Vec::new(),
|
||||||
amu: AntiMixedCfg::default(),
|
amu: AntiMixedCfg::default(),
|
||||||
|
resolve_hosts: true,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -185,6 +187,12 @@ impl Config {
|
||||||
c.amu.check_channel = t == "both" || t == "channel";
|
c.amu.check_channel = t == "both" || t == "channel";
|
||||||
c.amu.check_private = t == "both" || t == "private";
|
c.amu.check_private = t == "both" || t == "private";
|
||||||
}
|
}
|
||||||
|
"resolve_hosts" | "resolvehosts" | "dns" => {
|
||||||
|
c.resolve_hosts = !matches!(
|
||||||
|
v.to_ascii_lowercase().as_str(),
|
||||||
|
"off" | "false" | "no" | "0"
|
||||||
|
)
|
||||||
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
45
src/ircd.rs
45
src/ircd.rs
|
|
@ -4,7 +4,7 @@
|
||||||
|
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::net::{SocketAddr, TcpStream};
|
use std::net::{SocketAddr, TcpStream};
|
||||||
use std::sync::mpsc::Receiver;
|
use std::sync::mpsc::{Receiver, Sender};
|
||||||
|
|
||||||
use crate::command::Command;
|
use crate::command::Command;
|
||||||
use crate::config::Config;
|
use crate::config::Config;
|
||||||
|
|
@ -34,6 +34,11 @@ pub enum Event {
|
||||||
Disconnect {
|
Disconnect {
|
||||||
uid: Uid,
|
uid: Uid,
|
||||||
},
|
},
|
||||||
|
/// A client's reverse-DNS lookup finished (`None` = no confirmed hostname).
|
||||||
|
ResolvedHost {
|
||||||
|
uid: Uid,
|
||||||
|
host: Option<String>,
|
||||||
|
},
|
||||||
/// Background timer tick — drives ping/idle timeouts.
|
/// Background timer tick — drives ping/idle timeouts.
|
||||||
Tick,
|
Tick,
|
||||||
}
|
}
|
||||||
|
|
@ -45,9 +50,9 @@ pub struct Ircd {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Ircd {
|
impl Ircd {
|
||||||
pub fn new(cfg: Config) -> Ircd {
|
pub fn new(cfg: Config, event_tx: Sender<Event>) -> Ircd {
|
||||||
Ircd {
|
Ircd {
|
||||||
server: Server::new(cfg),
|
server: Server::new(cfg, event_tx),
|
||||||
commands: command_table(),
|
commands: command_table(),
|
||||||
modules: crate::modules::default_modules(),
|
modules: crate::modules::default_modules(),
|
||||||
}
|
}
|
||||||
|
|
@ -88,6 +93,10 @@ impl Ircd {
|
||||||
self.quit_user(uid, "Connection closed");
|
self.quit_user(uid, "Connection closed");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Event::ResolvedHost { uid, host } => {
|
||||||
|
self.server.on_resolved(uid, host);
|
||||||
|
self.try_register(uid); // DNS may have been the last thing we waited on
|
||||||
|
}
|
||||||
Event::Tick => self.on_tick(),
|
Event::Tick => self.on_tick(),
|
||||||
}
|
}
|
||||||
self.drain_hooks();
|
self.drain_hooks();
|
||||||
|
|
@ -162,15 +171,27 @@ impl Ircd {
|
||||||
}
|
}
|
||||||
// …or completed the registration handshake
|
// …or completed the registration handshake
|
||||||
if !registered {
|
if !registered {
|
||||||
let ready = self
|
self.try_register(uid);
|
||||||
.server
|
}
|
||||||
.users
|
}
|
||||||
.get(&uid)
|
|
||||||
.map(|u| !u.registered && !u.nick.is_empty() && !u.ident.is_empty() && !u.cap)
|
/// Finish registration if NICK, USER, CAP and the reverse-DNS lookup are all
|
||||||
.unwrap_or(false);
|
/// done. Called after each command and when a DNS result arrives.
|
||||||
if ready {
|
fn try_register(&mut self, uid: Uid) {
|
||||||
self.complete_registration(uid);
|
let ready = self
|
||||||
}
|
.server
|
||||||
|
.users
|
||||||
|
.get(&uid)
|
||||||
|
.map(|u| {
|
||||||
|
!u.registered
|
||||||
|
&& !u.nick.is_empty()
|
||||||
|
&& !u.ident.is_empty()
|
||||||
|
&& !u.cap
|
||||||
|
&& !u.dns_pending
|
||||||
|
})
|
||||||
|
.unwrap_or(false);
|
||||||
|
if ready {
|
||||||
|
self.complete_registration(uid);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -25,6 +25,7 @@ pub mod mode;
|
||||||
pub mod module;
|
pub mod module;
|
||||||
pub mod modules;
|
pub mod modules;
|
||||||
pub mod numeric;
|
pub mod numeric;
|
||||||
|
pub mod resolver;
|
||||||
pub mod server;
|
pub mod server;
|
||||||
pub mod socketengine;
|
pub mod socketengine;
|
||||||
pub mod tls;
|
pub mod tls;
|
||||||
|
|
|
||||||
|
|
@ -46,7 +46,8 @@ fn main() {
|
||||||
|
|
||||||
let (tx, rx) = mpsc::channel();
|
let (tx, rx) = mpsc::channel();
|
||||||
let core_cfg = cfg.clone();
|
let core_cfg = cfg.clone();
|
||||||
let core = thread::spawn(move || Ircd::new(core_cfg).run(rx));
|
let core_tx = tx.clone(); // the core self-injects events (DNS results)
|
||||||
|
let core = thread::spawn(move || Ircd::new(core_cfg, core_tx).run(rx));
|
||||||
|
|
||||||
// background timer: drives ping/idle timeouts
|
// background timer: drives ping/idle timeouts
|
||||||
let tick_tx = tx.clone();
|
let tick_tx = tx.clone();
|
||||||
|
|
|
||||||
272
src/resolver.rs
Normal file
272
src/resolver.rs
Normal file
|
|
@ -0,0 +1,272 @@
|
||||||
|
//! Reverse-DNS host resolution — echoIRCd's answer to InspIRCd's async resolver,
|
||||||
|
//! done from scratch with std UDP (no DNS crate, no `unsafe`). Given a client IP
|
||||||
|
//! it looks up the PTR record and **forward-confirms** it (the name must resolve
|
||||||
|
//! back to the same IP, so a client can't fake a hostname — same anti-spoofing
|
||||||
|
//! InspIRCd does). Best-effort: any failure returns `None` and the caller keeps
|
||||||
|
//! the IP. It runs off the core thread, so it never blocks the daemon, and it's
|
||||||
|
//! bounded in time (the UDP read timeout) and in concurrency (`try_acquire`).
|
||||||
|
|
||||||
|
use std::net::{IpAddr, ToSocketAddrs, UdpSocket};
|
||||||
|
use std::sync::atomic::{AtomicUsize, Ordering};
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
/// How long to wait for the DNS server before giving up.
|
||||||
|
pub const DNS_TIMEOUT: Duration = Duration::from_millis(2500);
|
||||||
|
/// Cap on concurrent in-flight lookups (one short-lived thread each), so a
|
||||||
|
/// connection flood can't spawn unbounded resolver threads.
|
||||||
|
const MAX_ACTIVE: usize = 512;
|
||||||
|
static ACTIVE: AtomicUsize = AtomicUsize::new(0);
|
||||||
|
|
||||||
|
const QTYPE_PTR: u16 = 12;
|
||||||
|
const QCLASS_IN: u16 = 1;
|
||||||
|
|
||||||
|
/// Reserve a lookup slot; `false` if too many are already in flight.
|
||||||
|
pub fn try_acquire() -> bool {
|
||||||
|
// bump then check, so this is a simple bounded gate
|
||||||
|
if ACTIVE.fetch_add(1, Ordering::Relaxed) < MAX_ACTIVE {
|
||||||
|
true
|
||||||
|
} else {
|
||||||
|
ACTIVE.fetch_sub(1, Ordering::Relaxed);
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Release a slot taken by [`try_acquire`] (call once the lookup is done).
|
||||||
|
pub fn release() {
|
||||||
|
ACTIVE.fetch_sub(1, Ordering::Relaxed);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Reverse-resolve `ip` and forward-confirm. `Some(host)` only if a PTR exists
|
||||||
|
/// and that host resolves back to `ip`.
|
||||||
|
pub fn reverse_confirmed(ip: IpAddr, timeout: Duration) -> Option<String> {
|
||||||
|
let ns = nameserver();
|
||||||
|
let ptr = ptr_lookup(&ns, &reverse_name(ip), timeout)?;
|
||||||
|
// forward-confirm: the resolved name must map back to this IP
|
||||||
|
let ok = (ptr.as_str(), 0u16)
|
||||||
|
.to_socket_addrs()
|
||||||
|
.ok()?
|
||||||
|
.any(|sa| sa.ip() == ip);
|
||||||
|
(ok && !ptr.is_empty()).then_some(ptr)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The `in-addr.arpa` / `ip6.arpa` reverse name for `ip`.
|
||||||
|
fn reverse_name(ip: IpAddr) -> String {
|
||||||
|
match ip {
|
||||||
|
IpAddr::V4(a) => {
|
||||||
|
let o = a.octets();
|
||||||
|
format!("{}.{}.{}.{}.in-addr.arpa", o[3], o[2], o[1], o[0])
|
||||||
|
}
|
||||||
|
IpAddr::V6(a) => {
|
||||||
|
let mut s = String::with_capacity(72);
|
||||||
|
for octet in a.octets().iter().rev() {
|
||||||
|
s.push_str(&format!("{:x}.{:x}.", octet & 0xf, octet >> 4));
|
||||||
|
}
|
||||||
|
s.push_str("ip6.arpa");
|
||||||
|
s
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// First `nameserver` in /etc/resolv.conf, else a sensible fallback.
|
||||||
|
fn nameserver() -> String {
|
||||||
|
if let Ok(text) = std::fs::read_to_string("/etc/resolv.conf") {
|
||||||
|
for line in text.lines() {
|
||||||
|
let line = line.trim();
|
||||||
|
if let Some(rest) = line.strip_prefix("nameserver ") {
|
||||||
|
let ns = rest.trim();
|
||||||
|
if !ns.is_empty() {
|
||||||
|
return format!("{ns}:53");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"1.1.1.1:53".to_string()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Send a PTR query for `qname` to `ns` and return the first PTR answer name.
|
||||||
|
fn ptr_lookup(ns: &str, qname: &str, timeout: Duration) -> Option<String> {
|
||||||
|
let sock = UdpSocket::bind("0.0.0.0:0")
|
||||||
|
.or_else(|_| UdpSocket::bind("[::]:0"))
|
||||||
|
.ok()?;
|
||||||
|
sock.set_read_timeout(Some(timeout)).ok()?;
|
||||||
|
|
||||||
|
let id: u16 = 0x4543; // fixed query id ("EC"); we match it on the reply
|
||||||
|
let mut query = Vec::with_capacity(qname.len() + 18);
|
||||||
|
query.extend_from_slice(&id.to_be_bytes());
|
||||||
|
query.extend_from_slice(&[0x01, 0x00]); // flags: RD=1
|
||||||
|
query.extend_from_slice(&[0, 1]); // QDCOUNT=1
|
||||||
|
query.extend_from_slice(&[0, 0, 0, 0, 0, 0]); // AN/NS/AR = 0
|
||||||
|
encode_name(&mut query, qname);
|
||||||
|
query.extend_from_slice(&QTYPE_PTR.to_be_bytes());
|
||||||
|
query.extend_from_slice(&QCLASS_IN.to_be_bytes());
|
||||||
|
|
||||||
|
sock.send_to(&query, ns).ok()?;
|
||||||
|
let mut buf = [0u8; 1500];
|
||||||
|
let n = sock.recv(&mut buf).ok()?;
|
||||||
|
parse_ptr_reply(&buf[..n], id)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Encode a dotted name into wire format (length-prefixed labels + root 0).
|
||||||
|
fn encode_name(out: &mut Vec<u8>, name: &str) {
|
||||||
|
for label in name.split('.').filter(|l| !l.is_empty()) {
|
||||||
|
let bytes = label.as_bytes();
|
||||||
|
let len = bytes.len().min(63);
|
||||||
|
out.push(len as u8);
|
||||||
|
out.extend_from_slice(&bytes[..len]);
|
||||||
|
}
|
||||||
|
out.push(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Parse a DNS reply for the first PTR answer's name. Fully bounds-checked — the
|
||||||
|
/// packet is untrusted, so nothing here may panic or loop forever.
|
||||||
|
fn parse_ptr_reply(msg: &[u8], want_id: u16) -> Option<String> {
|
||||||
|
if msg.len() < 12 {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
if u16::from_be_bytes([msg[0], msg[1]]) != want_id {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
if msg[3] & 0x0f != 0 {
|
||||||
|
return None; // rcode != NOERROR
|
||||||
|
}
|
||||||
|
let qd = u16::from_be_bytes([msg[4], msg[5]]);
|
||||||
|
let an = u16::from_be_bytes([msg[6], msg[7]]);
|
||||||
|
if an == 0 {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let mut pos = 12;
|
||||||
|
// skip the questions
|
||||||
|
for _ in 0..qd {
|
||||||
|
pos = skip_name(msg, pos)?;
|
||||||
|
pos = pos.checked_add(4)?; // qtype + qclass
|
||||||
|
if pos > msg.len() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// walk the answer records
|
||||||
|
for _ in 0..an {
|
||||||
|
pos = skip_name(msg, pos)?; // name
|
||||||
|
if pos + 10 > msg.len() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let rtype = u16::from_be_bytes([msg[pos], msg[pos + 1]]);
|
||||||
|
let rdlen = u16::from_be_bytes([msg[pos + 8], msg[pos + 9]]) as usize;
|
||||||
|
let rdata = pos + 10;
|
||||||
|
if rdata + rdlen > msg.len() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
if rtype == QTYPE_PTR {
|
||||||
|
return read_name(msg, rdata, 0).map(|(name, _)| name);
|
||||||
|
}
|
||||||
|
pos = rdata + rdlen;
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Advance past a name (labels + optional compression pointer), returning the
|
||||||
|
/// offset just after it (a pointer ends the name in-place).
|
||||||
|
fn skip_name(msg: &[u8], mut pos: usize) -> Option<usize> {
|
||||||
|
loop {
|
||||||
|
let len = *msg.get(pos)?;
|
||||||
|
if len & 0xc0 == 0xc0 {
|
||||||
|
return Some(pos + 2); // 2-byte compression pointer ends the name
|
||||||
|
}
|
||||||
|
if len == 0 {
|
||||||
|
return Some(pos + 1);
|
||||||
|
}
|
||||||
|
pos = pos.checked_add(1 + len as usize)?;
|
||||||
|
if pos > msg.len() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Decode a (possibly compressed) name at `pos`. `jumps` guards against pointer
|
||||||
|
/// loops. Returns the dotted name and the offset after the name in the record.
|
||||||
|
fn read_name(msg: &[u8], mut pos: usize, jumps: u32) -> Option<(String, usize)> {
|
||||||
|
if jumps > 32 {
|
||||||
|
return None; // too many compression jumps — malformed/hostile
|
||||||
|
}
|
||||||
|
let mut out = String::new();
|
||||||
|
let mut after: Option<usize> = None;
|
||||||
|
loop {
|
||||||
|
let len = *msg.get(pos)?;
|
||||||
|
if len & 0xc0 == 0xc0 {
|
||||||
|
let ptr = ((len as usize & 0x3f) << 8) | *msg.get(pos + 1)? as usize;
|
||||||
|
let end = after.unwrap_or(pos + 2);
|
||||||
|
let (rest, _) = read_name(msg, ptr, jumps + 1)?;
|
||||||
|
if !rest.is_empty() {
|
||||||
|
if !out.is_empty() {
|
||||||
|
out.push('.');
|
||||||
|
}
|
||||||
|
out.push_str(&rest);
|
||||||
|
}
|
||||||
|
return Some((out, end));
|
||||||
|
}
|
||||||
|
if len == 0 {
|
||||||
|
return Some((out, after.unwrap_or(pos + 1)));
|
||||||
|
}
|
||||||
|
let start = pos + 1;
|
||||||
|
let stop = start.checked_add(len as usize)?;
|
||||||
|
let label = msg.get(start..stop)?;
|
||||||
|
if !out.is_empty() {
|
||||||
|
out.push('.');
|
||||||
|
}
|
||||||
|
out.push_str(&String::from_utf8_lossy(label));
|
||||||
|
pos = stop;
|
||||||
|
after.get_or_insert(pos);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use std::net::{Ipv4Addr, Ipv6Addr};
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn reverse_names() {
|
||||||
|
assert_eq!(
|
||||||
|
reverse_name(IpAddr::V4(Ipv4Addr::new(1, 2, 3, 4))),
|
||||||
|
"4.3.2.1.in-addr.arpa"
|
||||||
|
);
|
||||||
|
let v6 = reverse_name(IpAddr::V6(Ipv6Addr::new(0x2001, 0xdb8, 0, 0, 0, 0, 0, 1)));
|
||||||
|
assert!(v6.ends_with("ip6.arpa") && v6.starts_with("1.0.0.0."));
|
||||||
|
}
|
||||||
|
|
||||||
|
// End-to-end round trip against the real resolver — proves query build → send
|
||||||
|
// → recv → parse → forward-confirm works. Needs network, so it's #[ignore]d;
|
||||||
|
// run with `cargo test -- --ignored`.
|
||||||
|
#[test]
|
||||||
|
#[ignore = "needs network"]
|
||||||
|
fn live_reverse_lookup_of_public_ips() {
|
||||||
|
let one = reverse_confirmed(IpAddr::V4(Ipv4Addr::new(1, 1, 1, 1)), DNS_TIMEOUT);
|
||||||
|
assert_eq!(one.as_deref(), Some("one.one.one.one"), "got {one:?}");
|
||||||
|
let g = reverse_confirmed(IpAddr::V4(Ipv4Addr::new(8, 8, 8, 8)), DNS_TIMEOUT);
|
||||||
|
assert_eq!(g.as_deref(), Some("dns.google"), "got {g:?}");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parses_a_ptr_reply() {
|
||||||
|
// hand-built reply: id EC, 1 question, 1 PTR answer "host.example" using
|
||||||
|
// a compression pointer back to "example" in the question.
|
||||||
|
let mut m = Vec::new();
|
||||||
|
m.extend_from_slice(&0x4543u16.to_be_bytes()); // id
|
||||||
|
m.extend_from_slice(&[0x81, 0x80]); // flags: response, RD, RA, NOERROR
|
||||||
|
m.extend_from_slice(&[0, 1, 0, 1, 0, 0, 0, 0]); // qd=1 an=1
|
||||||
|
// question: 1.0.0.127.in-addr.arpa PTR IN (we just need a name to skip)
|
||||||
|
let qstart = m.len();
|
||||||
|
super::encode_name(&mut m, "example");
|
||||||
|
m.extend_from_slice(&QTYPE_PTR.to_be_bytes());
|
||||||
|
m.extend_from_slice(&QCLASS_IN.to_be_bytes());
|
||||||
|
// answer: name = pointer to question name, PTR, rdata = "host" + ptr(qname)
|
||||||
|
m.extend_from_slice(&[0xc0, qstart as u8]);
|
||||||
|
m.extend_from_slice(&QTYPE_PTR.to_be_bytes());
|
||||||
|
m.extend_from_slice(&QCLASS_IN.to_be_bytes());
|
||||||
|
m.extend_from_slice(&[0, 0, 0, 60]); // ttl
|
||||||
|
let rd = vec![4, b'h', b'o', b's', b't', 0xc0, qstart as u8];
|
||||||
|
m.extend_from_slice(&(rd.len() as u16).to_be_bytes());
|
||||||
|
m.extend_from_slice(&rd);
|
||||||
|
assert_eq!(parse_ptr_reply(&m, 0x4543).as_deref(), Some("host.example"));
|
||||||
|
assert_eq!(parse_ptr_reply(&m, 0x9999), None); // wrong id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -7,13 +7,17 @@
|
||||||
|
|
||||||
use std::collections::{HashMap, HashSet, VecDeque};
|
use std::collections::{HashMap, HashSet, VecDeque};
|
||||||
use std::net::{SocketAddr, TcpStream};
|
use std::net::{SocketAddr, TcpStream};
|
||||||
|
use std::sync::mpsc::Sender;
|
||||||
|
use std::thread;
|
||||||
use std::time::{SystemTime, UNIX_EPOCH};
|
use std::time::{SystemTime, UNIX_EPOCH};
|
||||||
|
|
||||||
use crate::channels::Channel;
|
use crate::channels::Channel;
|
||||||
use crate::config::{Config, LinkBlock};
|
use crate::config::{Config, LinkBlock};
|
||||||
use crate::extensible::Extensible;
|
use crate::extensible::Extensible;
|
||||||
|
use crate::ircd::Event;
|
||||||
use crate::link::{Link, RemoteServer, RemoteUser};
|
use crate::link::{Link, RemoteServer, RemoteUser};
|
||||||
use crate::module::Hook;
|
use crate::module::Hook;
|
||||||
|
use crate::resolver;
|
||||||
use crate::socketengine::OutSink;
|
use crate::socketengine::OutSink;
|
||||||
use crate::users::{Caps, User, UserFlags};
|
use crate::users::{Caps, User, UserFlags};
|
||||||
use crate::xline::XLine;
|
use crate::xline::XLine;
|
||||||
|
|
@ -93,10 +97,12 @@ pub struct Server {
|
||||||
pub in_redirect: bool, // +L: guards against redirect loops
|
pub in_redirect: bool, // +L: guards against redirect loops
|
||||||
pub censor: Vec<(String, String)>, // +G bad words: (find, replace)
|
pub censor: Vec<(String, String)>, // +G bad words: (find, replace)
|
||||||
pub amu: crate::config::AntiMixedCfg, // antimixedutf8 module config
|
pub amu: crate::config::AntiMixedCfg, // antimixedutf8 module config
|
||||||
|
pub resolve_hosts: bool, // reverse-DNS clients on connect
|
||||||
|
pub event_tx: Sender<Event>, // self-inject events (DNS results)
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Server {
|
impl Server {
|
||||||
pub fn new(cfg: Config) -> Server {
|
pub fn new(cfg: Config, event_tx: Sender<Event>) -> Server {
|
||||||
Server {
|
Server {
|
||||||
name: cfg.servername,
|
name: cfg.servername,
|
||||||
network: cfg.network,
|
network: cfg.network,
|
||||||
|
|
@ -126,6 +132,8 @@ impl Server {
|
||||||
in_redirect: false,
|
in_redirect: false,
|
||||||
censor: cfg.censor,
|
censor: cfg.censor,
|
||||||
amu: cfg.amu,
|
amu: cfg.amu,
|
||||||
|
resolve_hosts: cfg.resolve_hosts,
|
||||||
|
event_tx,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -166,6 +174,7 @@ impl Server {
|
||||||
) {
|
) {
|
||||||
let uuid = self.next_uuid();
|
let uuid = self.next_uuid();
|
||||||
self.uuid_local.insert(uuid.clone(), uid);
|
self.uuid_local.insert(uuid.clone(), uid);
|
||||||
|
let ip = addr.ip();
|
||||||
self.users.insert(
|
self.users.insert(
|
||||||
uid,
|
uid,
|
||||||
User {
|
User {
|
||||||
|
|
@ -182,6 +191,7 @@ impl Server {
|
||||||
signon: now(),
|
signon: now(),
|
||||||
addr,
|
addr,
|
||||||
registered: false,
|
registered: false,
|
||||||
|
dns_pending: false,
|
||||||
cap: false,
|
cap: false,
|
||||||
cap_302: false,
|
cap_302: false,
|
||||||
caps: Caps::default(),
|
caps: Caps::default(),
|
||||||
|
|
@ -200,6 +210,54 @@ impl Server {
|
||||||
sock,
|
sock,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Pre-registration connection notices, InspIRCd / solanum style. Ident-113
|
||||||
|
// is archaic and firewalled, so those two are cosmetic; the hostname lookup
|
||||||
|
// is real (see `resolver`) — its result arrives later as an Event.
|
||||||
|
self.notice_star(uid, "Checking Ident");
|
||||||
|
self.notice_star(uid, "No Ident response");
|
||||||
|
self.notice_star(uid, "Looking up your hostname...");
|
||||||
|
if self.resolve_hosts && resolver::try_acquire() {
|
||||||
|
if let Some(u) = self.users.get_mut(&uid) {
|
||||||
|
u.dns_pending = true; // hold registration until the lookup returns
|
||||||
|
}
|
||||||
|
let tx = self.event_tx.clone();
|
||||||
|
thread::spawn(move || {
|
||||||
|
let host = resolver::reverse_confirmed(ip, resolver::DNS_TIMEOUT);
|
||||||
|
resolver::release();
|
||||||
|
let _ = tx.send(Event::ResolvedHost { uid, host });
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// resolution off (or too many in flight): keep the IP as the host
|
||||||
|
self.notice_star(
|
||||||
|
uid,
|
||||||
|
"Couldn't look up your hostname; using your IP address instead",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A pre-registration `:server NOTICE * :*** <msg>` line.
|
||||||
|
fn notice_star(&self, uid: Uid, msg: &str) {
|
||||||
|
self.send(uid, format!(":{} NOTICE * :*** {msg}", self.name));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A client's reverse-DNS lookup finished. Set the resolved host (so WHOIS,
|
||||||
|
/// bans and cloaking use the hostname, not the IP), tell the client, and clear
|
||||||
|
/// the flag that was holding their registration.
|
||||||
|
pub fn on_resolved(&mut self, uid: Uid, host: Option<String>) {
|
||||||
|
match &host {
|
||||||
|
Some(h) => self.notice_star(uid, &format!("Found your hostname ({h})")),
|
||||||
|
None => self.notice_star(
|
||||||
|
uid,
|
||||||
|
"Couldn't look up your hostname; using your IP address instead",
|
||||||
|
),
|
||||||
|
}
|
||||||
|
if let Some(u) = self.users.get_mut(&uid) {
|
||||||
|
if let Some(h) = host {
|
||||||
|
u.host = h;
|
||||||
|
}
|
||||||
|
u.dns_pending = false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Mark a user as quitting; the core turns this into a full quit after the
|
/// Mark a user as quitting; the core turns this into a full quit after the
|
||||||
|
|
@ -484,6 +542,7 @@ mod tests {
|
||||||
signon: 0,
|
signon: 0,
|
||||||
addr: "127.0.0.1:1".parse().unwrap(),
|
addr: "127.0.0.1:1".parse().unwrap(),
|
||||||
registered: true,
|
registered: true,
|
||||||
|
dns_pending: false,
|
||||||
cap: false,
|
cap: false,
|
||||||
cap_302: false,
|
cap_302: false,
|
||||||
caps: Caps::default(),
|
caps: Caps::default(),
|
||||||
|
|
@ -507,7 +566,8 @@ mod tests {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn srv() -> Server {
|
fn srv() -> Server {
|
||||||
Server::new(Config::default())
|
let (tx, _rx) = mpsc::channel();
|
||||||
|
Server::new(Config::default(), tx)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|
|
||||||
19
src/users.rs
19
src/users.rs
|
|
@ -197,7 +197,7 @@ pub struct User {
|
||||||
pub nick: String, // "" until NICK
|
pub nick: String, // "" until NICK
|
||||||
pub ident: String, // "" until USER
|
pub ident: String, // "" until USER
|
||||||
pub realname: String,
|
pub realname: String,
|
||||||
pub host: String, // real host (ip string; no rDNS)
|
pub host: String, // displayed host: reverse-DNS name if resolved, else IP
|
||||||
pub cloak: String, // masked host shown under +x ("" until computed)
|
pub cloak: String, // masked host shown under +x ("" until computed)
|
||||||
pub vhost: Option<String>, // displayed-host override (CHGHOST/SETHOST vhost)
|
pub vhost: Option<String>, // displayed-host override (CHGHOST/SETHOST vhost)
|
||||||
pub secure: bool, // connected over TLS (drives WHOIS 671 / sslinfo)
|
pub secure: bool, // connected over TLS (drives WHOIS 671 / sslinfo)
|
||||||
|
|
@ -205,16 +205,17 @@ pub struct User {
|
||||||
pub signon: u64, // unix secs at registration (WHOIS 317)
|
pub signon: u64, // unix secs at registration (WHOIS 317)
|
||||||
pub addr: SocketAddr,
|
pub addr: SocketAddr,
|
||||||
pub registered: bool,
|
pub registered: bool,
|
||||||
pub cap: bool, // CAP negotiation in progress (holds registration)
|
pub dns_pending: bool, // holding registration for a reverse-DNS lookup
|
||||||
pub cap_302: bool, // client sent CAP LS 302 (cap-notify aware)
|
pub cap: bool, // CAP negotiation in progress (holds registration)
|
||||||
pub caps: Caps, // enabled IRCv3 capabilities
|
pub cap_302: bool, // client sent CAP LS 302 (cap-notify aware)
|
||||||
|
pub caps: Caps, // enabled IRCv3 capabilities
|
||||||
pub sasl_mech: Option<String>, // SASL mechanism chosen, mid-handshake
|
pub sasl_mech: Option<String>, // SASL mechanism chosen, mid-handshake
|
||||||
pub channels: HashSet<String>, // lowercased channel keys
|
pub channels: HashSet<String>, // lowercased channel keys
|
||||||
pub watch: Vec<String>, // WATCH list — lowercased nicks
|
pub watch: Vec<String>, // WATCH list — lowercased nicks
|
||||||
pub monitor: Vec<String>, // MONITOR list — lowercased nicks
|
pub monitor: Vec<String>, // MONITOR list — lowercased nicks
|
||||||
pub silence: Vec<String>, // SILENCE masks — nick!user@host globs
|
pub silence: Vec<String>, // SILENCE masks — nick!user@host globs
|
||||||
pub accept: Vec<String>, // ACCEPT list — lowercased nicks (callerid +g)
|
pub accept: Vec<String>, // ACCEPT list — lowercased nicks (callerid +g)
|
||||||
pub quitting: Option<String>, // set by QUIT; drained by the core
|
pub quitting: Option<String>, // set by QUIT; drained by the core
|
||||||
pub flags: UserFlags,
|
pub flags: UserFlags,
|
||||||
pub last_active: u64, // unix secs of the last line we received
|
pub last_active: u64, // unix secs of the last line we received
|
||||||
pub ping_sent: bool, // a server PING is outstanding
|
pub ping_sent: bool, // a server PING is outstanding
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue